记一次没有面试的面试

4,485 阅读8分钟

一 、背景

前几日投递简历,要按照要求才有面试机会,趁着不忙,也做了个简单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

其他类似的模板可以参考:Java SpringBoot模板 - 掘金 (juejin.cn)

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

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

相关逻辑为:

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

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

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

    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<Word> list = mongoTemplate.find(query1, Word.class);


        // 现在都是数据不存在的情况
        if(list==null || list.size()<5){
            // 直接保存
            mongoTemplate.save(word);
        }else {
            // 替代最久远的记录
            Word last = list.get(list.size() - 1);
            mongoTemplate.findAndReplace(Query.query(
                    Criteria.where("id").is(last.getId())
            ), word);
        }

        return ResponseResult.okResult(HttpCodeEnum.SUCCESS);
    }

附: mogodb在 Java中基本语法:mongodb基本操作 - 掘金 (juejin.cn)

若对语法感兴趣,可参考:juejin.cn/post/725479…

es 进行地址查询

用户在进行地址搜索时候,会动态出现与输入有关的关键词

image.png image.png

高亮设置,相关代码如下:

 @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Override
    public ResponseResult findPathList(String path) {

        if(StringUtils.isBlank(path)){
            return ResponseResult.errorResult(HttpCodeEnum.AREA_EMPTY);
        }

        //2.设置查询条件
        SearchRequest searchRequest = new SearchRequest("area_address");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        //布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        //关键字的分词之后查询
        QueryStringQueryBuilder queryStringQueryBuilder =
                QueryBuilders
                        .queryStringQuery(path)
                        .field("areaAddress");
        boolQueryBuilder.must(queryStringQueryBuilder);

        //设置高亮  title
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("areaAddress");
        highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
        highlightBuilder.postTags("</font>");
        searchSourceBuilder.highlighter(highlightBuilder);

        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);

        SearchResponse searchResponse = null;
        try {
            searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            System.out.println(e);
        }

        //3.结果封装返回

        List<Map> list = new ArrayList<>();

        SearchHit[] hits = searchResponse.getHits().getHits();
        for (SearchHit hit : hits) {
            String json = hit.getSourceAsString();
            Map map = JSON.parseObject(json, Map.class);

            //处理高亮
            if(hit.getHighlightFields() != null && hit.getHighlightFields().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<AddressDoc> allPath = areaDetailMapper.getAllPath();
        System.out.println("查询的信息为"+ allPath);

        /* 去除重复的地址 */
        HashMap<String, Long> map = new HashMap<>();
        for (AddressDoc addressDoc : allPath) {
            Long id = addressDoc.getId();
            String address = addressDoc.getAreaAddress();

            List<String> converted = Convert.convert(List.class, address);
            for (String path : converted) {
                map.put(path,id);
            }
        }

        System.out.println("转化后的数据为:"+map);

        /* 序列化为addressDoc格式的数据 */
        for(Map.Entry<String,Long> 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());
    }

小结:

  1. 更新文档的操作,理论上应该发送在用户编辑、添加、删除地址内容后,但是偷了点懒,使用了定时任务,每天自动更新一次。
  2. 从数据库中查询到的地址信息可能存在重复现象,所以在插入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<AreaResponse> list = (List<AreaResponse>) 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<String> areaCityList = areaResponse.getAreaCityList();
                String str = Convert.toStr(areaCityList);
                String area = str.substring(1, str.length()-1);
                row.getCell(3).setCellValue(area);
                // 地址
                List<String> 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<AreaItemDto> 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<AreaDto> 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<AreaItemDto> 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<String> 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包:Idea 导出为jar包 - 掘金 (juejin.cn)

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

注意:

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

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

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

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

四、 更多

访问地址(不定时关闭)

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

项目地址

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

面试结局

没有面试的面试

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