记一次没有面试的面试

2023年 10月 3日 25.3k 0

一 、背景

前几日投递简历,要按照要求才有面试机会,趁着不忙,也做了个简单demo

image.png

1.1 前端演示

二、项目要求

2023区域管理功能.jpg

三、项目设计

3.1 数据库设计

3.1.1 需求分析

3.1.1.1 表格页面

image.png

3.1.1.2 详情页面

image.png

3.1.1.3 城市列表页面

image.png

3.1.1.4 分析

主要分析的难点是一个地区包含多个城市,而一个城市又包含多个地址

属于连续的两个一对多关系

根据以上关系,建立了两张表(理论上建立三张)

3.1.1.4.1 主表area地区表

存储了编号、区域名称、区域下城市的id、创建时间

image.png

对应关系如下:

image.png

3.1.1.4.2 从表area_detail

存储了区域下的城市、城市下的地址,这儿稍微偷了点懒,把城市下的地址按照数组的形式存入一列,少建立的一张表

image.png

3.1.1.4.3 城市表city

这个结构就很简单了,就不细说了

image.png

3.1.1.4 注意

连接两张表的是position_id,需要全局唯一,且在执行sql前就指定数值,

这里我使用了redis生成为唯一id:

一共64位(Long),

1位符号位,固定为0

31位存储时间戳,如果一天一个数值,理论上可以存储68年的数据

32位存储自增编号,一天最多可以生成2^32个编号

代码参考:juejin.cn/post/724192…

3.2 Java部分

3.2.1 实体类生成

实体类直接用MybatisPlusX自动生成就好了

image.png

image.png

image.png

其他类似的模板可以参考:juejin.cn/post/724818…

主要查询就两张表,比较简单,直接说易错点和难点了

3.2.2 项目易错点

主键无法自增

参考链接:juejin.cn/post/725514…

加载分页插件

参考链接:juejin.cn/post/725521…

修改redis序列化

参考链接:juejin.cn/post/725596…

LocalDataTime 在sql中不能和空比较

参考链接:juejin.cn/post/725559…

数据库编码错误

参考链接:juejin.cn/post/725514…

开启sql日志

参考链接:juejin.cn/post/725514…

前端接收excel

参考地址: juejin.cn/post/725696…

缓存方法失败

参考链接:juejin.cn/post/725595…

minio 端口用错

参考地址: juejin.cn/post/725704…

导入 websocket 后测试类失效

参考地址:juejin.cn/post/725704…

3.2.3 项目亮点

redis缓存常用方法

操作流程:juejin.cn/post/724069…

mongodb 存储查询历史

用户在进行搜索时,会有提示框出现,可避免重复输入

image.png

相关逻辑为:

  • 提示框最多可存储五条历史搜索记录,

  • 若该条记录存在,则更新存储时间为现在

  • 若该条记录不存在,分为两种情况:

    3.1 若超出五条,则最新搜索的记录会替代最久远的记录

    3.2 若没有超出五条,则直接保存

  • 相关代码如下:

      @Override
        public ResponseResult saveWord(Word word) {
            // 获取类别
            String keyWord = word.getWord();
            if(StringUtils.isBlank(keyWord)){
                return ResponseResult.errorResult(HttpCodeEnum.IS_BRANK);
            }
            Integer kind = word.getKind();
    
            // 封装准备存入的数据
           word.setCreatedAt(new Date());
    
            Query query = Query.query(Criteria.where("kind").is(word.getKind().intValue())
                    .and("word").is(word.getWord()));;
            Word one = mongoTemplate.findOne(query, Word.class);
    
            log.info("收到的消息为{}",String.valueOf(one));
    
            // 数据已经存在,只需要更新时间
            if(one!=null){
                Query query2 = new Query(Criteria.where("kind").is(kind)
                        .and("word").is(word));
    
                Update update = new Update().set("createdAt",new Date());
                mongoTemplate.updateFirst(query2,update,Word.class);
                return ResponseResult.okResult(HttpCodeEnum.SUCCESS);
            }
    
            // 查询数据库中该种类关键词的数量
            Query query1 = Query.query(Criteria.where("kind").is(kind));
            query1.with(Sort.by(Sort.Direction.DESC,"createdAt"));
            List list = mongoTemplate.find(query1, Word.class);
    
    
            // 现在都是数据不存在的情况
            if(list==null || list.size() 0){
                    Text[] titles = hit.getHighlightFields().get("areaAddress").getFragments();
                    String title = org.apache.commons.lang3.StringUtils.join(titles);
                    //高亮标题
                    map.put("title",title);
                }else {
                    //原始标题
                    map.put("title",map.get("title"));
                }
                list.add(map);
            }
    
            return ResponseResult.okResult(list);
        }
    

    定时更新文档,相关代码如下:

      @Autowired
        private RestHighLevelClient client;
        // 定时更新索引库的内容
        @Scheduled(cron = "0 0 0 * * ? ")
        public void updateSearchIndex() throws IOException {
    
            List allPath = areaDetailMapper.getAllPath();
            System.out.println("查询的信息为"+ allPath);
    
            /* 去除重复的地址 */
            HashMap map = new HashMap();
            for (AddressDoc addressDoc : allPath) {
                Long id = addressDoc.getId();
                String address = addressDoc.getAreaAddress();
    
                List converted = Convert.convert(List.class, address);
                for (String path : converted) {
                    map.put(path,id);
                }
            }
    
            System.out.println("转化后的数据为:"+map);
    
            /* 序列化为addressDoc格式的数据 */
            for(Map.Entry entry:map.entrySet()){
                String address = entry.getKey();
                Long id = entry.getValue();
    
                List pathList = Convert.convert(List.class, address);
    
    
                AddressDoc addressDoc = new AddressDoc();
                addressDoc.setId(id).setAreaAddress(address);
    
                String jsonStr = JSONUtil.toJsonStr(addressDoc);
                System.out.println(jsonStr);
    
    
                // 准备request对象
                IndexRequest request = new IndexRequest("area_address").id(id.toString());
                // 准备json对象
                request.source(jsonStr, XContentType.JSON);
                // 发生请求
                client.index(request,RequestOptions.DEFAULT);
            }
    
            log.info("定时任务开始执行:{}", new Date());
        }
    

    小结:

  • 更新文档的操作,理论上应该发送在用户编辑、添加、删除地址内容后,但是偷了点懒,使用了定时任务,每天自动更新一次。
  • 从数据库中查询到的地址信息可能存在重复现象,所以在插入es之前,我先按照key为地址,value为编号存入map数组,实现地址去重功能。
  • es相关操作可参考:juejin.cn/post/724382…

    WebSoket 双向连接

    刚刚说完es,关键词检索要求用户每次输入后,服务器都需要快速响应,

    此时可以使用WebSoket 双向连接:持续的长连接

    例如我在前端输入地名后

    image.png

    会自动检索,并将检索后的结果集合传给socket

    image.png

    前端收到socket传递的消息后,会在控制台打印相关信息

    image.png

    关键代码:在上一节es的基础上,直接将数据传递给socket处理

    @Override
    public ResponseResult findPathList(String path) {
    
        xxxxx
        // 第二个参数为用户id,由于没做登录,默认为1
        webSocket.sendMessage(list,"1");
        return ResponseResult.okResult(HttpCodeEnum.SUCCESS);
    }
    
    

    此外,如果各位git上已经下载了项目,可以访问http://localhost:9876/webSocket.html

    利用socket简单做了个即时通讯页面

    image.png

    详细操作可参考:juejin.cn/post/725296…

    Apache POI 实现报表导出

    在项目要求的功能之外,我还添加了数据报表导出功能

    image.png

    image.png

    默认会导出7天内数据明细,且数量不能超过20条

    相关代码如下:

     @Override
        public ResponseResult exportData(HttpServletResponse response) {
    
            //1. 查询数据库,获取数据---查询最近7天的数据
            LocalDate dateBegin = LocalDate.now().minusDays(7);
            LocalDate dateEnd = LocalDate.now().minusDays(1);
    
            SearchDto searchDto = new SearchDto();
            searchDto.setFrom(dateBegin.atStartOfDay())
                    .setTo(dateEnd.atStartOfDay());
            ResponseResult result = search(searchDto);
            List list = (List) result.getData();
    
            // 通过POI将数据写入到Excel文件中
            InputStream in = this.getClass().getClassLoader().getResourceAsStream("static/报表模板.xlsx");
    
            try {
                //基于模板文件创建一个新的Excel文件
                XSSFWorkbook excel = new XSSFWorkbook(in);
    
                //获取表格文件的Sheet页
                XSSFSheet sheet = excel.getSheet("Sheet1");
    
                //填充数据--时间
                sheet.getRow(3).getCell(4).setCellValue("时间:" + dateBegin + "至" + dateEnd);
    
                //填充数据--统计量
                sheet.getRow(3).getCell(2).setCellValue(list.size());
    
                int start = 0;
                //填充明细数据
                for (AreaResponse areaResponse : list) {
                    if(start>20){
                        break;
                    }
    
                    //获得某一行
                    XSSFRow row = sheet.getRow(7 + start++);
                    // 编号
                    row.getCell(1).setCellValue(areaResponse.getId());
                    // 区域名称
                    row.getCell(2).setCellValue(areaResponse.getAreaName());
                    // 区域城市
                    List areaCityList = areaResponse.getAreaCityList();
                    String str = Convert.toStr(areaCityList);
                    String area = str.substring(1, str.length()-1);
                    row.getCell(3).setCellValue(area);
                    // 地址
                    List PathList = areaResponse.getPathList();
                    String pathStr = Convert.toStr(PathList);
                    pathStr = pathStr.substring(1, pathStr.length()-1);
    
                    row.getCell(4).setCellValue(pathStr);
                    //  创建时间
                    LocalDateTime createdTime = areaResponse.getCreatedTime();
                    String time = createdTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    row.getCell(5).setCellValue(time);
                }
    
                //3. 通过输出流将Excel文件下载到客户端浏览器
                ServletOutputStream out = response.getOutputStream();
                excel.write(out);
    
    
                //通过输出流将内存中的Excel文件写入到磁盘
                FileOutputStream out1 = new FileOutputStream(new File("E:\桌面\learning-files\mongo\02_代码\Demo\src\main\resources\static\mode.xlsx"));
                excel.write(out1);
    
                //关闭资源
                out.close();
    
                excel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return ResponseResult.okResult(HttpCodeEnum.SUCCESS);
        }
    
    

    POI 相关操作可参考:juejin.cn/post/725335…

    前端接收可参考:juejin.cn/post/725696…

    缓存队列

    若同一时刻大量用户对数据库进行添加操作,会给服务器带来巨大压力,

    可将数据存入缓存队列,由并发操作转化为串行

    相关代码如下:

    服务器校验格式无误后,直接返回响应,数据库操作扔给缓存队列操作

    @Override
    public ResponseResult saveArea(AreaDto areaDto) {
        // 获取区域名称
        String areaName = areaDto.getAreaName();
        if(StringUtils.isBlank(areaName)){
            return ResponseResult.errorResult(HttpCodeEnum.AREA_NAME_IS_BLANK);
        }
        // 获取区域城市
        List cityList = areaDto.getAreaCityList();
        if (cityList==null||cityList.size()==0){
            return ResponseResult.errorResult(HttpCodeEnum.CITY_LIST_IS_NULL);
        }
        // 扔入缓存队列
        blockTasks.add(areaDto);
    
        return ResponseResult.okResult(HttpCodeEnum.SUCCESS);
    }
    

    缓存队列初始化及数据处理:

    // 阻塞队列
    private BlockingQueue blockTasks =  new ArrayBlockingQueue(1024 * 1024);
    //线程池
    private static final ExecutorService EXCUTOR_SERVIC = Executors.newSingleThreadExecutor();
    // 一初始化就提交任务
    @PostConstruct
    private void init() {
        EXCUTOR_SERVIC.submit(new SaveHandler() );
    }
    
    private class SaveHandler implements Runnable {
        @Override
        public void run() {
            while(true) {
                try {
                    // 获取队列中的信息
                    AreaDto areaDto = blockTasks.take();
                    // 创建订单
                    handleSave(areaDto);
                } catch (Exception e) {
                    log.info("数据插入异常");
                }
    
            }
        }
    }
    
    public void handleSave(AreaDto areaDto){
        // 获取区域名称
        String areaName = areaDto.getAreaName();
        // 获取区域城市
        List cityList = areaDto.getAreaCityList();
    
        // 获取区域的基本信息
        Area area = new Area();
        long positionId = idWorker.nextId("area_id");
        // 插入到主表
        area.setAreaName(areaName)
                .setAreaPositionId(positionId)
                .setCreatedTime(LocalDateTime.now());
        save(area);
        // 插入到从表
        for (AreaItemDto itemDto : cityList) {
            //  获取城市列表
            List list = itemDto.getPositionList();
            // 获取区域城市
            String areaCity = itemDto.getAreaCity();
            // 转化列表为字符串
            String pathList = Convert.toStr(list);
    
            AreaDetail areaDetail = new AreaDetail();
            areaDetail.setAreaCity(areaCity)
                    .setAreaAddress(pathList)
                    .setPositionId(positionId);
            areaDetailService.save(areaDetail);
        }
    }
    
    

    相关操作可参考: juejin.cn/post/724231…

    minio 存储文件

    开发中~

    参考地址: juejin.cn/post/725707…

    nginx 前端部署

    开发中~

    参考地址:juejin.cn/post/725288…

    3.3 项目部署

    导出为jar包:juejin.cn/post/725349…

    宝塔部署: blog.csdn.net/bakelFF/art…

    注意:

  • 若小白部署在服务器,请限制访问ip,否则将会面临安全问题

    参考地址:juejin.cn/post/725178…

  • 若没有服务器,却想公网展示,可使用内网穿透

    参考地址:juejin.cn/post/725295…

  • 四、 更多

    访问地址(不定时关闭)

    地址链接:42efe32f.r3.cpolar.top/index.html

    项目地址

    项目地址: gitee.com/gitee-enter…

    面试结局

    没有面试的面试

    限时两天,以上开发耗时三天,对方以超时为由,失去面试机会,唉

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论