一 、背景
前几日投递简历,要按照要求才有面试机会,趁着不忙,也做了个简单demo
1.1 前端演示
二、项目要求
三、项目设计
3.1 数据库设计
3.1.1 需求分析
3.1.1.1 表格页面
3.1.1.2 详情页面
3.1.1.3 城市列表页面
3.1.1.4 分析
主要分析的难点是一个地区包含多个城市,而一个城市又包含多个地址
属于连续的两个一对多
关系
根据以上关系,建立了两张表(理论上建立三张)
3.1.1.4.1 主表area地区表
存储了编号、区域名称、区域下城市的id、创建时间
对应关系如下:
3.1.1.4.2 从表area_detail
存储了区域下的城市、城市下的地址,这儿稍微偷了点懒,把城市下的地址按照数组的形式存入一列,少建立的一张表
3.1.1.4.3 城市表city
这个结构就很简单了,就不细说了
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
自动生成就好了
其他类似的模板可以参考: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 存储查询历史
用户在进行搜索时,会有提示框出现,可避免重复输入
相关逻辑为:
提示框最多可存储五条历史搜索记录,
若该条记录存在,则更新存储时间为现在
若该条记录不存在,分为两种情况:
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相关操作可参考:juejin.cn/post/724382…
WebSoket 双向连接
刚刚说完es,关键词检索要求用户每次输入后,服务器都需要快速响应,
此时可以使用WebSoket 双向连接:持续的长连接
例如我在前端输入地名后
会自动检索,并将检索后的结果集合传给socket
前端收到socket传递的消息后,会在控制台打印相关信息
关键代码:在上一节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简单做了个即时通讯页面
详细操作可参考:juejin.cn/post/725296…
Apache POI 实现报表导出
在项目要求的功能之外,我还添加了数据报表导出功能
默认会导出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…
面试结局
没有面试的面试
限时两天,以上开发耗时三天,对方以超时为由,失去面试机会,唉