Compare commits

..

25 Commits

Author SHA1 Message Date
7d3130c4fe feat: 修改隐患排查计划列表待审核人数统计逻辑,改为只统计审核中(status=2,5)的数量 2026-07-28 16:47:10 +08:00
96d7f8d803 feat(隐患排查计划): 新增待审核人数统计字段及计算逻辑 2026-07-28 15:56:27 +08:00
0adba6f5cb feat(photo-evidence): 实现图片水印异常修复全流程功能
抽离并重构原有图片水印生成逻辑,统一输出jpeg格式,避免因异常文件后缀产生坏数据
新增异常水印图片的分页查询、预览确认、单条修复、批量异步修复及任务状态查询接口
新增对应的控制器、业务服务类与数据传输对象
补充图片水印异常修复的完整方案文档
2026-07-01 23:02:00 +08:00
668ae04530 feat: 百度api key更新 2026-06-25 20:23:43 +08:00
116dca0543 修复(测试配置): 更新测试环境的服务IP地址
将application-test.yml中MySQL、Redis及Gotenberg的服务地址从192.168.129.64替换为172.21.143.43
2026-06-25 16:49:21 +08:00
c54de034d2 chore(开发配置): 更新开发环境数据库连接配置
调整数据库连接地址为本地192.168.24.254:50001,数据库名称改为color_20260608,同时更新对应的访问密码
2026-06-25 14:07:27 +08:00
efab0eeb18 refactor(vehicleThreeInspect): 优化批量审核与隐患治理流程,完善日志与异常处理
- 为车辆三检批量审核流程添加多阶段详细日志便于排查问题
- 重构批量审核筛选逻辑,新增各跳过原因统计及无候选数据提前抛出异常
- 修复隐患治理流程重复创建问题,新增重复记录检查逻辑
- 优化隐患治理流程的参数处理与日志输出,新增审核结果翻译工具方法
- 更新流程回调日志,补充实例ID、流程编码等上下文信息
2026-06-23 00:16:13 +08:00
b1913abab5 车辆筛选条件修改 2026-06-17 17:30:04 +08:00
f21c57943c 车辆三检批量审核 2026-06-17 10:36:39 +08:00
c737dd5571 排序 2026-06-08 17:28:09 +08:00
7a40f887a2 fix(security/HotVehicleThreeInspect): 修正热点车辆三检审核状态计算逻辑
移除原硬编码的3个审核阶段限制,改为通过非空审核阶段数与已通过数相等判断审核完成状态
2026-06-08 15:01:07 +08:00
5133b20cca docs: 文档提交 2026-06-06 16:45:12 +08:00
15f20d9e28 docs: add 驾驶员默认角色缺失排查与修复文档 2026-06-06 15:45:43 +08:00
bcc39277b0 chore: 去掉无效编码 2026-06-05 12:57:28 +08:00
98a83bc79c feat(system-maintenance): 新增媒体资源元数据修复功能
实现完整的媒体资源元数据修复业务模块,提供提交修复任务与查询任务状态的REST接口,支持修复媒体资源的大小和时长信息,适配普通文件和HLS流媒体,内置域名白名单、下载限制及任务缓存清理能力。
2026-06-05 12:35:19 +08:00
5b87e2f7ca feat: 报表模板同步功能 2026-06-04 09:54:10 +08:00
e76b3a35aa fix(securityManagement): 修复移动端巡检列表分页查询SQL逻辑
重构该Mapper的分页查询SQL,调整表关联与过滤条件,优化状态计算、搜索匹配及排序规则,修复原查询的数据统计与展示问题。
2026-06-03 21:07:53 +08:00
977bd0a2bb feat: 隐患排查补查功能 2026-06-03 17:45:53 +08:00
38b393cafa feat(hiddenDangerPlan): 新增隐患排查计划的完成未完成数量统计
为隐患排查计划VO新增已完成数、未完成数统计字段,编写自定义XML SQL统计各计划的完成情况,更新Mapper和Service使用新的查询逻辑
2026-06-03 10:41:40 +08:00
041bdf1a32 feat(企业安全管理): 新增唯一性校验,修复多记录查询问题并优化业务逻辑
- 修复角色查询方法,处理多条有效安全管理员记录场景,改为获取最新记录并添加警告日志
- 在安全管理人员新增/更新时新增唯一性校验,阻止同一企业下重复手机号和用户的有效记录
- 重构企业负责人业务逻辑,拆分通用方法,优化现有记录查找逻辑,新增旧负责人数据清理功
2026-06-02 17:33:31 +08:00
d9ce694835 三检历史经纬度定位 2026-06-02 15:27:47 +08:00
c29596337f 新增车辆三检的三检历史记录列表 2026-06-02 10:02:09 +08:00
71558b072d 隐患排查加计划id筛选条件 2026-06-01 16:39:02 +08:00
afaa25489d Merge remote-tracking branch 'origin/main' 2026-06-01 14:38:37 +08:00
9ada784a17 驾驶证,行驶证,从业资格证
改为到期前30天提醒并发消息到管理员账号,单个驾驶员账号也要收到自己证件到期的消息
2026-06-01 14:38:23 +08:00
121 changed files with 5396 additions and 607 deletions

View File

@@ -111,3 +111,200 @@ ADD COLUMN reject_reason varchar(500) NULL COMMENT '审核不通过原因';
- 详情接口、列表接口、导出接口会返回 `rejectReason` 字段。
- 审核驳回通知优先展示 `rejectReason`,没有时才回退展示 `auditResult`
## 6. 三检历史记录接口
### 6.1 新增字段
- `inspectNo`:三检记录编号,规则为 `SJ + 三检创建日期(yyyyMMdd) + 车牌号后3位 + 4位随机数`
- `inspectLocation`:检查地点,出车前、行车中、收车后三个阶段都支持保存
### 6.2 历史列表接口
- 路径:`GET /securityManagement/vehicleThreeInspect/history/list`
- 说明:按 `inspectId` 聚合查询一轮三检历史记录,支持分页
返回核心字段:
- `inspectId`三检流程ID
- `inspectNo`:三检记录编号
- `plateNumber`:车牌号
- `inspectorName`:检查人
- `startTime`:检查开始时间(出车提交时间)
- `endTime`:检查结束时间(收车提交时间)
- `auditorName`:审核人
- `outInspectLocation`:出车前检查地点
- `drivingInspectLocation`:行车中检查地点
- `backInspectLocation`:收车后检查地点
- `outCheck` / `drivingCheck` / `backCheck`:三个阶段详情
### 6.3 历史详情接口
- 路径:`GET /securityManagement/vehicleThreeInspect/history/{inspectId}`
- 说明:返回指定 `inspectId` 下三个阶段的车辆三检详情
### 6.4 数据库脚本
新增脚本:
- `sql/alter_hot_vehicle_three_inspect_history_fields_20260601.sql`
脚本内容包含:
-`hot_vehicle_three_inspect` 增加 `inspect_no``inspect_location` 字段
- 对历史数据回填 `inspect_no`
## 7. 多车辆批量审核接口
### 7.1 可批量审核列表接口
- 路径:`GET /securityManagement/vehicleThreeInspect/multiVehicleBatchAudit/list`
- 说明:分页查询当前登录人员可进行多车辆批量审核的车辆三检列表
### 7.2 查询参数
- `pageNum`:页码
- `pageSize`:每页条数,前端默认 `20`
- `plateNumber`:车牌号,支持筛选
- `companyId`企业ID可选不传时后端默认取当前登录企业
请求示例:
```http
GET /securityManagement/vehicleThreeInspect/multiVehicleBatchAudit/list?pageNum=1&pageSize=20&plateNumber=川A
```
### 7.3 列表筛选规则
后端只会返回同时满足以下条件的三检流程:
1. 当前三检流程已经存在`收车后检查`记录。
2. 当前三检流程下存在`审核人是当前登录账号`的未审核记录。
3. 数据属于当前登录企业。
不满足上述条件的数据不会出现在列表中。
### 7.4 返回字段
后端返回类型为 `HotVehicleThreeInspectSummaryVo`,前端当前主要使用以下字段:
- `inspectId`三检流程ID
- `vehicleId`车辆ID
- `plateNumber`:车牌号
- `inspectorName`:检查人姓名
- `auditorName`:审核人姓名
- `startTime`:开始时间
- `endTime`:结束时间
- `outCheck` / `drivingCheck` / `backCheck`:三个阶段详情,查看详情时可直接复用
返回示例:
```json
{
"code": 200,
"msg": "操作成功",
"rows": [
{
"inspectId": 1001,
"vehicleId": 501,
"plateNumber": "川A12345",
"inspectorName": "张三",
"auditorName": "李四",
"startTime": "2026-06-17 08:00:00",
"endTime": "2026-06-17 18:00:00"
}
],
"total": 1
}
```
## 8. 多车辆批量审核提交接口
### 8.1 接口信息
- 路径:`POST /securityManagement/vehicleThreeInspect/multiVehicleBatchAudit`
- 说明:对多辆车的车辆三检进行批量审核
- 处理方式:新接口只负责组装批量审核数据,底层仍复用原有 `batchAudit``doAudit` 逻辑
### 8.2 请求体
```json
{
"inspectIds": [1001, 1002, 1003],
"hasHiddenDanger": 1,
"evaluatorId": 2001,
"auditTime": "2026-06-17 18:30:00",
"auditResult": "存在隐患,需整改",
"rejectReason": "轮胎磨损严重",
"auditorSignImgUrl": "https://example.com/sign.png"
}
```
### 8.3 请求字段说明
- `inspectIds`三检流程ID集合必填
- `hasHiddenDanger`:是否存在隐患,必填;`0=否``1=是`
- `evaluatorId`评估人ID`hasHiddenDanger = 1` 时必填
- `auditTime`:审核时间;不传时后端默认取当前时间
- `auditResult`:审核结论;不传时后端自动生成
- `rejectReason`:审核不通过原因;有隐患时建议传入
- `auditorSignImgUrl`:审核人员签名图片地址,必填
### 8.4 自动生成规则
-`auditTime` 为空时,后端自动使用当前时间。
-`auditResult` 为空时:
- `hasHiddenDanger = 0`:自动生成 `通过`
- `hasHiddenDanger = 1` 且传了 `rejectReason`:自动生成 `不通过:{rejectReason}`
- `hasHiddenDanger = 1` 且未传 `rejectReason`:自动生成 `不通过`
### 8.5 批量审核校验规则
后端会对每个 `inspectId` 依次校验:
1. 三检流程必须存在。
2. 当前流程必须已完成收车检查,否则报错:`{车牌号}尚未完成收车检查,不可批量审核`
3. 当前流程中必须存在`审核人是当前登录人`的未审核数据,否则报错:`{车牌号}没有当前账号可审核的未审核数据`
4. 只有当前登录企业下的数据才会参与组装。
5. 组装后的明细仍会走原有单条审核校验,包括:
- 流程任务ID不能为空
- 三检记录ID不能为空
- 审核时间不能早于检查时间
- 审核签名校验
- 流程审批人校验
### 8.6 实际审核范围
每个 `inspectId` 并不是只审核一条数据,而是会自动筛出该三检流程下所有满足以下条件的阶段记录一起审核:
- 审核人等于当前登录账号
- 当前记录尚未审核完成
- 当前记录存在流程任务 `taskId`
因此一次多车辆批量审核,最终可能会展开成多条实际审核明细。
### 8.7 有隐患时的处理
-`hasHiddenDanger = 1` 时,审核流程按不通过处理。
- 流程结束后,会按每一条三检明细分别创建隐患治理数据。
- 如果本次批量审核展开后共有 N 条三检明细,则会生成 N 条隐患治理数据,不会合并成 1 条。
### 8.8 返回结果
成功返回:
```json
{
"code": 200,
"msg": "操作成功",
"data": null
}
```
失败时返回业务异常,例如:
- `三检流程ID不能为空`
- `存在隐患时评估人不能为空`
- `未查询到可批量审核的车辆三检数据`
- `{车牌号}尚未完成收车检查,不可批量审核`
- `{车牌号}没有当前账号可审核的未审核数据`

View File

@@ -0,0 +1,421 @@
# 图片水印后缀异常兜底与补救方案
## 1. 背景
现有图片取证水印流程位于 [PhotoEvidenceServiceImpl.java:L51-L149](file:///g:/code/shw/hot/wucaixing-full/wucaixing-backend/src/main/java/com/hotwj/platform/common/photoEvidence/service/impl/PhotoEvidenceServiceImpl.java#L51-L149)。
当前已确认的问题链路如下:
- 前端上传 OSS 时,部分原始图片没有正确的文件后缀名。
- 原始 `sys_oss.file_suffix` 被写成了 `2``3``.2``.3` 这一类异常值。
- 后端在生成水印图时,直接使用原始 `file_suffix` 作为 `ImageIO.write` 的输出格式。
- 生成的临时文件可能为空文件,最终上传到 OSS 后变成 `0B`
- 新生成的 `sys_oss` 记录中,`ext1.fileSize=0``contentType=null`,页面预览和下载均异常。
示例数据可见 [data.json:L1-L35](file:///g:/code/shw/hot/wucaixing-full/data.json#L1-L35)
- 原图记录 `file_suffix=3`,但 `ext1.contentType=image/jpeg`
- 水印图记录 `file_suffix=.3``ext1.fileSize=0``contentType=null`
当前风险点位于 [PhotoEvidenceServiceImpl.java:L137-L149](file:///g:/code/shw/hot/wucaixing-full/wucaixing-backend/src/main/java/com/hotwj/platform/common/photoEvidence/service/impl/PhotoEvidenceServiceImpl.java#L137-L149)
,源码中也已经有 `TODO` 提示需要判断 `mime`
## 2. 根因分析
本问题的根因不是“没有后缀名”本身,而是“后端把不可信的后缀名继续当成图片编码格式使用”。
具体来说:
- 原始图片是否可读,真正应该依据的是二进制内容和 MIME不应该只依赖文件名后缀。
- 水印图不是前端上传文件,而是后端重新生成的系统产物,其输出格式应由后端统一决定。
- 只要继续信任 `sys_oss.file_suffix`,就会持续受到历史脏数据和前端上传异常的影响。
因此,兜底思路必须从“猜正确后缀”改成“后端固定输出规范”。
## 3. 线上兜底方案
### 3.1 目标
阻断新增坏数据,确保后续所有水印图都能稳定生成、上传、展示和下载。
### 3.2 方案原则
- 原图校验看内容,不看后缀。
- 水印图输出格式由后端统一指定,不继承原图后缀。
- 上传时必须带明确的 `contentType`
- 入库前必须校验结果文件非空。
### 3.3 推荐方案
建议将“图片取证水印图”统一输出为 `jpg/jpeg`
原因:
-
当前水印结果图在 [PhotoEvidenceServiceImpl.java:L261-L283](file:///g:/code/shw/hot/wucaixing-full/wucaixing-backend/src/main/java/com/hotwj/platform/common/photoEvidence/service/impl/PhotoEvidenceServiceImpl.java#L261-L283)
中已经固定为 `BufferedImage.TYPE_INT_RGB`,天然适合输出为 `JPEG`
- 业务场景是现场拍照取证,统一输出 `jpg` 与实际使用习惯一致。
- 可以彻底摆脱 `2``3``.2``.3` 这一类脏后缀的影响。
- 后续历史修复和排障标准会非常统一。
### 3.4 兜底规则
建议按以下规则处理:
1. 下载原始 OSS 文件后,先尝试按图片内容解析。
2. 只要原图字节能正常解析成图片,就允许继续加水印。
3. 水印结果图统一编码为 `jpeg`
4. 临时文件统一使用 `.jpg` 后缀。
5. 上传 OSS 时统一使用 `contentType=image/jpeg`
6. 上传前必须校验:
- `ImageIO.write` 返回成功
- 临时文件存在
- 临时文件大小大于 `0`
7. 任一校验失败,直接报错,不创建 `HotPhotoEvidence` 记录,不创建新的水印 `sys_oss` 记录。
### 3.5 兜底后的收益
- 前端即使继续上传无后缀文件,后端仍可稳定产出标准水印图。
- 生成的水印图始终具备标准后缀和标准 `contentType`
- 不再出现 `.2``.3` 这类系统生成的异常文件。
- 新增数据和历史补救数据将收敛到同一规范。
## 4. 历史补救总原则
### 4.1 必须满足的约束
本次历史补救必须满足以下硬约束:
- 必须替换原有坏掉的 `sys_oss` 记录内容。
- 必须保持原 `oss_id` 不变。
- 不能只改 `HotPhotoEvidence` 表中的 `watermarked_oss_id` 指向。
- 不能新增一条新的 `sys_oss` 记录再让业务表改引用。
原因:
- 这个 `oss_id` 不仅存在于 `HotPhotoEvidence`,还可能被其他业务表、日志记录、导出记录、接口缓存、前端页面配置等位置复用。
- 如果改成新 `oss_id`,补救范围会迅速扩散到未知引用方,风险不可控。
- 保持原 `oss_id` 不变,才能把历史补救范围收敛在“文件对象替换 + 原记录修补”这一层。
### 4.2 正确的补救思路
正确思路不是“替换原始图的 `sys_oss` 记录”,而是:
-`HotPhotoEvidence` 为主表定位坏掉的水印记录。
- 通过 `original_oss_id` 找回原始图片。
- 重新生成一张标准的水印图。
- 上传到 OSS 获得新的对象文件。
- 然后覆盖原 `watermarked_oss_id` 对应的 `sys_oss` 记录内容。
- `oss_id` 保持不变,只更新该记录指向的对象信息和元数据。
换句话说:
- 替换的是“坏水印图对应的 `sys_oss` 内容”
- 保留的是“该 `sys_oss` 的主键 `oss_id`
## 5. 历史补救识别范围
建议先做数据筛查,再执行修补。
### 5.1 强特征
以下记录可直接视为高风险坏数据:
- `sys_oss.ext1.fileSize = 0`
- `sys_oss.file_suffix` 为纯数字,如 `2``3`
- `sys_oss.file_suffix``.数字`,如 `.2``.3`
- `sys_oss.original_name``photo_evidence_xxx.2``photo_evidence_xxx.3`
- `sys_oss.ext1.contentType is null`
### 5.2 建议以 `HotPhotoEvidence` 为主表驱动
原因:
- 该表已经保留了 `originalOssId``watermarkedOssId`
- 已经保留了 `captureTimeClient``captureTimeServer`
- 已经保留了 `latitude``longitude`
- 已经保留了 `addressTextServer``addressTextClient``addressSource`
这些字段足够支撑“按历史状态重建水印图”,不需要重新依赖前端数据。
## 6. 历史补救执行方案
### 6.1 核心目标
在不改变 `watermarked_oss_id``oss_id` 的前提下,修复坏掉的水印图对象和其 `sys_oss` 元数据。
### 6.2 推荐执行步骤
1. 根据 `HotPhotoEvidence.watermarked_oss_id` 关联 `sys_oss`,筛出疑似坏水印记录。
2. 按记录读取 `original_oss_id` 对应的原始图。
3. 使用历史记录中的 `captureTimeClient` 作为水印时间。
4. 水印地址按以下优先级确定:
-`addressSource=SERVER`,取 `addressTextServer`
- 否则取 `addressTextClient`
5. 不重新调用逆地理接口,不重算地址。
6. 按统一规范重新生成 `jpg` 水印图。
7. 先上传到 OSS 新对象路径,确认对象可访问且大小正常。
8. 用新对象信息覆盖旧的 `sys_oss` 记录:
- `file_name`
- `url`
- `file_suffix`
- `original_name`
- `ext1.fileSize`
- `ext1.contentType`
- 如有必要同步更新时间和更新人
9. `oss_id` 保持不变。
10. 数据更新成功后,再删除旧的坏对象文件,或先保留待统一清理。
### 6.3 必须覆盖的字段
覆盖原 `sys_oss` 记录时,建议至少同步修正以下内容:
- `file_name`:替换为新上传对象键
- `url`:替换为新对象地址
- `file_suffix`:统一改为 `.jpg`
- `original_name`:统一规范化,例如 `photo_evidence_{ossId}.jpg`
- `ext1.fileSize`:更新为真实大小
- `ext1.contentType`:更新为 `image/jpeg`
如有以下字段被业务使用,也建议同步补齐:
- `ext1.source`
- `ext1.remark`
- `update_time`
- `update_by`
### 6.4 为什么不要重新生成新的 `sys_oss` 记录
- 会引入新的 `oss_id`
- 会导致其他业务表引用失效
- 会让修复脚本必须跨全库改引用
- 很难保证没有漏网表
- 回滚和审计也会变复杂
因此,历史补救必须采用“新对象文件 + 原记录覆盖”的模式。
## 7. 前端可视化补救方案
为了让补救方案易于操作,建议将历史补救放到后台管理界面,而不是只做一次性脚本。
### 7.1 推荐入口
建议新增后台管理页面,例如:
- 菜单名称:`水印图片修复`
- 所属模块:`系统管理``附件管理`
- 页面定位:面向运维、实施、研发,不对普通业务用户开放
前端落点建议明确放到维护模块中,参考现有维护页 [index.vue](file:///g:/code/shw/hot/wucaixing-full/wucaixing-frontend/src/views/system/maintenance/index.vue)
的维护项列表能力。
建议方案:
- 在前端维护模块 `wucaixing-frontend/src/views/system/maintenance/index.vue` 中新增一个维护项,例如 `图片水印修复`
- 点击维护项后进入专门的修复弹窗或修复页面
- 该页面承载异常筛选、预览比对、勾选记录、单条修复、批量修复、任务进度和结果查看
这样可以与现有 `媒体资源元数据修复` 的运维入口保持一致,便于统一权限、统一操作习惯、统一审计。
### 7.2 列表页建议字段
列表页建议展示以下信息,方便快速识别和人工核验:
- `evidenceId`
- `watermarkedOssId`
- `originalOssId`
- 原始图片缩略图
- 当前水印图缩略图
- `sys_oss.file_suffix`
- `ext1.fileSize`
- `contentType`
- `captureTimeClient`
- `addressSource`
- 地址文本
- 公司 / 业务类型 / 业务 ID
- 创建时间
- 修复状态
- 修复结果说明
### 7.3 列表页筛选条件
建议支持以下筛选项:
- 仅看异常记录
-`watermarkedOssId`
-`originalOssId`
- 按公司
- 按业务类型
- 按时间范围
-`fileSize=0`
-`contentType为空`
-`file_suffix` 异常
- 按修复状态
### 7.4 页面操作按钮
建议至少提供以下操作:
- `预览原图`
- `预览当前水印图`
- `生成修复预览`
- `勾选当前记录`
- `批量勾选筛选结果`
- `清空勾选`
- `执行单条修复`
- `批量修复已勾选记录`
- `导出异常清单`
- `查看修复日志`
补充要求:
- 不允许用户在未查看修复前后效果的情况下直接发起批量修复
- 批量修复应以“用户已勾选且已完成预览确认的记录”为执行范围,而不是直接对整个筛选结果无差别执行
### 7.5 单条修复建议流程
单条修复推荐流程如下:
1. 用户点开一条异常记录。
2. 页面展示:
- 原图预览
- 当前坏水印图预览
- 拟修复后的新水印预览
- 修复前后元数据对比
3. 用户点击 `确认修复`
4. 后端执行:
- 读取原图
- 按历史时间与地址重新加水印
- 上传新 OSS 对象
- 覆盖原 `sys_oss` 记录
- 记录操作日志
5. 页面回显修复结果。
这个流程适合先做试点修复和人工验证。
### 7.6 批量修复建议流程
批量修复推荐流程如下:
1. 用户先通过筛选条件锁定异常数据范围。
2. 页面展示总数、可修复数、疑似失败风险数,并支持逐条勾选待修复记录。
3. 用户先对准备修复的记录执行预览,页面必须展示:
- 原图
- 当前坏水印图
- 拟修复后的新水印图
- 修复前后元数据差异
4. 用户确认预览效果无误后,勾选本次真正要修复的记录。
5. 用户点击 `批量修复已勾选记录` 前,系统弹出二次确认,并明确显示本次修复条数。
6. 后端采用任务方式异步执行,页面可轮询查看进度。
7. 每条记录输出修复结果:
- 成功
- 原图不存在
- 原图无法解析
- 新水印生成失败
- OSS 上传失败
-`sys_oss` 覆盖失败
8. 任务完成后支持导出结果明细。
这里的关键要求是:
- 批量修复之前必须先有“预览确认”步骤
- 批量修复的对象必须是“用户主动勾选的记录”
- 不能仅凭筛选条件直接全量执行修复任务
### 7.7 为什么前端界面比一次性脚本更适合
- 更适合先小批量验证,再逐步扩大范围
- 操作人员无需直接跑脚本和改 SQL
- 每条记录可预览、可追踪、可重试
- 更方便做权限控制和操作审计
- 后续若再出现新脏数据,也可以复用同一页面处理
## 8. 后端接口建议
为支撑前端可视化操作,建议至少提供以下几类接口:
### 8.1 异常列表接口
用途:
- 按筛选条件查询疑似异常水印记录
- 返回分页列表和异常原因标签
### 8.2 预览接口
用途:
- 根据 `evidenceId``watermarkedOssId` 生成“拟修复水印图”的临时预览
- 不落库,不覆盖 `sys_oss`
### 8.3 单条修复接口
用途:
- 对单条异常记录执行正式修复
- 修复成功后覆盖原 `sys_oss`
### 8.4 批量修复任务接口
用途:
- 创建批量修复任务
- 入参必须是前端勾选确认后的记录 ID 列表,而不是只有筛选条件
- 后台异步执行
- 支持查询进度、成功数、失败数、失败原因
### 8.5 操作日志接口
用途:
- 记录谁在什么时间修复了哪条记录
- 保留修复前后对象信息,便于追溯
## 9. 操作审计与回滚建议
### 9.1 建议记录的审计信息
每次修复建议记录:
- 操作人
- 操作时间
- `evidenceId`
- `watermarkedOssId`
-`file_name`
-`file_name`
-`url`
-`url`
-`ext1`
-`ext1`
- 修复结果
- 错误信息
### 9.2 回滚方式
由于要求保持原 `oss_id` 不变,回滚方案建议也按“对象替换”处理:
1. 修复前先记录旧对象信息。
2. 如修复后发现异常,可重新把旧对象重新上传或恢复。
3. 再次覆盖同一个 `sys_oss` 记录,使其回到修复前状态。
也就是说,回滚不是改回旧 `oss_id`,而是把同一个 `oss_id` 再次指回旧对象。
## 10. 推荐实施顺序
建议按以下顺序落地:
1. 先完成线上兜底,阻断新增坏数据。
2. 再开发后台可视化补救页面和补救接口。
3. 先选少量样本做单条修复验证。
4. 确认预览、下载、页面展示全部正常后,再进行批量修复。
5. 批量修复完成后,统一清理被替换掉的坏 OSS 对象。
## 11. 最终结论
本问题的正确处理方式是:
- 线上新增场景:后端不再信任原始上传后缀,水印图统一输出为 `jpg/jpeg`
- 历史补救场景:以 `HotPhotoEvidence` 为主表重建水印图,但不能改 `oss_id`,必须覆盖原坏水印图对应的 `sys_oss` 记录。
- 落地形式:优先做成后台管理可视化能力,支持异常筛选、预览、单条修复、批量修复、日志追踪。
该方案既能快速止血,也能在不扩散引用风险的前提下,稳妥完成历史数据修复。

View File

@@ -0,0 +1,63 @@
```sql
-- 传入你的 taskId
set @taskId = 'f5e730e3bfaf57ce00af7c0f7228958a';
start transaction;
-- 1. 归档到历史表
insert into sys_flow_task_his (his_id,
task_id,
instance_id,
node_code,
node_name,
approver_id,
status,
comment,
audit_time,
company_id,
is_read,
create_by,
create_time,
update_by,
update_time)
select replace(uuid(), '-', '') as his_id,
t.task_id,
t.instance_id,
t.node_code,
t.node_name,
t.approver_id,
case
when i.status = 1 then 1
when i.status in (2, 9) then 2
else 2
end as status,
'手工归档待办' as comment,
coalesce(i.finish_time, t.update_time, now()) as audit_time,
t.company_id,
t.is_read,
t.create_by,
t.create_time,
t.update_by,
t.update_time
from sys_flow_task t
left join sys_flow_instance i
on i.instance_id = t.instance_id
where t.task_id = @taskId
and not exists (select 1
from sys_flow_task_his th
where th.task_id = t.task_id
and th.instance_id = t.instance_id);
-- 2. 删除当前待办
delete
from sys_flow_task
where task_id = @taskId
and exists (select 1
from sys_flow_task_his th
where th.task_id = @taskId
and th.instance_id = sys_flow_task.instance_id);
commit;
```

View File

@@ -0,0 +1,14 @@
ALTER TABLE hot_vehicle_three_inspect
ADD COLUMN inspect_no varchar(32) NULL COMMENT '三检记录编号',
ADD COLUMN inspect_location varchar(255) NULL COMMENT '检查地点';
UPDATE hot_vehicle_three_inspect t
LEFT JOIN hot_vehicle v ON v.id = t.vehicle_id
SET t.inspect_no = CONCAT(
'SJ',
DATE_FORMAT(COALESCE(t.inspect_time, t.create_time), '%Y%m%d'),
RIGHT(COALESCE(v.plate_number, '000'), 3),
LPAD(MOD(COALESCE(t.inspect_id, t.id), 10000), 4, '0')
)
WHERE (t.inspect_no IS NULL OR t.inspect_no = '')
AND t.is_deleted = 0;

View File

@@ -61,7 +61,7 @@ public final class DriverLoginContextHelper {
lqw.eq(HotDriver::getId, userId);
lqw.eq(HotDriver::getStatus, 1L);
lqw.eq(HotDriver::getAuditStatus, 1);
lqw.orderByDesc(HotDriver::getCreateTime);
lqw.orderByDesc(HotDriver::getCreateTime, HotDriver::getId);
lqw.last("limit 1");
HotDriverMapper hotDriverMapper = SpringUtils.getBean(HotDriverMapper.class);
HotDriver driver = hotDriverMapper.selectOne(lqw);

View File

@@ -2,9 +2,22 @@ package com.hotwj.platform.common.photoEvidence.mapper;
import com.hotwj.platform.common.photoEvidence.domain.HotPhotoEvidence;
import com.hotwj.platform.common.photoEvidence.domain.vo.PhotoEvidenceVo;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairQueryBo;
import com.hotwj.platform.system.maintenance.domain.vo.PhotoEvidenceRepairCandidateVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.List;
@Mapper
public interface HotPhotoEvidenceMapper extends BaseMapperPlus<HotPhotoEvidence, PhotoEvidenceVo> {
long countRepairCandidates(@Param("query") PhotoEvidenceRepairQueryBo query);
List<PhotoEvidenceRepairCandidateVo> selectRepairCandidates(@Param("query") PhotoEvidenceRepairQueryBo query,
@Param("offset") long offset,
@Param("pageSize") long pageSize);
PhotoEvidenceRepairCandidateVo selectRepairCandidateByEvidenceId(@Param("evidenceId") Long evidenceId);
}

View File

@@ -6,6 +6,7 @@ import com.hotwj.platform.common.photoEvidence.domain.bo.PhotoEvidenceCreateBo;
import com.hotwj.platform.common.photoEvidence.domain.vo.PhotoEvidenceVo;
import com.hotwj.platform.common.photoEvidence.mapper.HotPhotoEvidenceMapper;
import com.hotwj.platform.common.photoEvidence.service.IPhotoEvidenceService;
import com.hotwj.platform.common.photoEvidence.support.PhotoEvidenceWatermarkSupport;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.FormatsType;
@@ -22,16 +23,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
@@ -43,6 +40,9 @@ public class PhotoEvidenceServiceImpl implements IPhotoEvidenceService {
@Resource
private ISysOssService ossService;
@Resource
private PhotoEvidenceWatermarkSupport watermarkSupport;
@Value("${map.gd.key:}")
private String gdKey;
@@ -129,30 +129,19 @@ public class PhotoEvidenceServiceImpl implements IPhotoEvidenceService {
Date captureTimeClient,
String address) {
byte[] originalBytes = downloadOssFile(originalOss);
BufferedImage originalImage = readImage(originalBytes);
BufferedImage watermarked = addWatermark(originalImage, captureTimeClient, address);
File tempFile = null;
PhotoEvidenceWatermarkSupport.RenderedWatermark renderedWatermark = null;
try {
String suffix = originalOss.getFileSuffix();
if (StringUtils.isBlank(suffix)) {
suffix = "jpg";
}
if (suffix.startsWith(".")) {
suffix = suffix.substring(1);
}
tempFile = File.createTempFile("photo_evidence_", "." + suffix);
try (OutputStream os = new FileOutputStream(tempFile)) {
ImageIO.write(watermarked, suffix, os);
}
return ossService.upload(tempFile).getOssId();
} catch (IOException e) {
throw new ServiceException("水印图片生成失败");
renderedWatermark = watermarkSupport.renderToTempFile(originalBytes, captureTimeClient, address);
OssClient storage = OssFactory.instance();
String objectKey = storage.getPath("photo-evidence", PhotoEvidenceWatermarkSupport.OUTPUT_SUFFIX);
return ossService.upload(
renderedWatermark.getFile(),
objectKey,
PhotoEvidenceWatermarkSupport.OUTPUT_CONTENT_TYPE
).getOssId();
} finally {
if (tempFile != null && tempFile.exists()) {
if (!tempFile.delete()) {
log.warn("临时文件删除失败: {}", tempFile.getAbsolutePath());
}
if (renderedWatermark != null) {
watermarkSupport.deleteQuietly(renderedWatermark.getFile());
}
}
}
@@ -167,124 +156,6 @@ public class PhotoEvidenceServiceImpl implements IPhotoEvidenceService {
}
}
private BufferedImage readImage(byte[] bytes) {
try (InputStream is = new ByteArrayInputStream(bytes)) {
BufferedImage image = ImageIO.read(is);
if (image == null) {
String hex = "";
if (bytes.length >= 4) {
hex = String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]);
}
log.error("ImageIO.read returned null. Header: {}", hex);
throw new ServiceException("原始图片格式不支持 (Header: " + hex + ")");
}
return image;
} catch (IOException e) {
log.error("图片解析失败", e);
throw new ServiceException("原始图片解析失败: " + e.getMessage());
}
}
private BufferedImage addWatermark(BufferedImage original,
Date captureTimeClient,
String address) {
int width = original.getWidth();
int height = original.getHeight();
// 强制最小画布宽度,防止文字区域过窄
int minWidth = 600;
if (width < minWidth) {
int newWidth = minWidth;
int newHeight = (int) Math.round((double) height * newWidth / width);
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D gResize = resized.createGraphics();
gResize.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
gResize.drawImage(original, 0, 0, newWidth, newHeight, null);
gResize.dispose();
original = resized;
width = newWidth;
height = newHeight;
}
// 字体大小随宽度自适应(设置最小值)
int fontSize = Math.max(20, width / 30);
int padding = Math.max(fontSize, 16);
String timeText = DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, captureTimeClient);
String addressText = "地址:" + (address != null ? address : "");
// 构建字体并测量
Font font;
try (InputStream is = getClass().getResourceAsStream("/fonts/simsun.ttc")) {
if (is == null) {
font = new Font("SansSerif", Font.PLAIN, fontSize);
} else {
font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(Font.PLAIN, fontSize);
}
} catch (Exception e) {
log.warn("Failed to load simsun font, fallback to SansSerif", e);
font = new Font("SansSerif", Font.PLAIN, fontSize);
}
// 使用临时图形上下文计算文字宽度与行高
BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D gMeasure = tmp.createGraphics();
gMeasure.setFont(font);
FontMetrics fm = gMeasure.getFontMetrics();
int contentWidth = Math.max(1, width - padding * 2);
// 构造文本行:第一行时间,后续为地址按宽度换行
List<String> lines = new ArrayList<>();
lines.add(timeText);
if (fm.stringWidth(addressText) <= contentWidth) {
lines.add(addressText);
} else {
StringBuilder current = new StringBuilder();
for (char c : addressText.toCharArray()) {
if (fm.stringWidth(current.toString() + c) > contentWidth) {
lines.add(current.toString());
current = new StringBuilder();
}
current.append(c);
}
if (current.length() > 0) {
lines.add(current.toString());
}
}
int lineHeight = fm.getHeight() + fontSize / 4;
gMeasure.dispose();
// 计算白条高度,确保所有行完整显示
int barHeight = Math.max(lineHeight + padding * 2, lines.size() * lineHeight + padding * 2);
// 输出结果图
BufferedImage result = new BufferedImage(width, height + barHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = result.createGraphics();
try {
// 绘制原图
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(original, 0, 0, null);
// 绘制底部白条
g.setColor(Color.WHITE);
g.fillRect(0, height, width, barHeight);
// 绘制文本
g.setFont(font);
g.setColor(new Color(51, 51, 51));
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int y = height + padding + fm.getAscent();
for (String line : lines) {
g.drawString(line, padding, y);
y += lineHeight;
}
return result;
} finally {
g.dispose();
}
}
private String reverseGeocode(Double latitude, Double longitude) {
if (latitude == null || longitude == null) {
return null;

View File

@@ -0,0 +1,209 @@
package com.hotwj.platform.common.photoEvidence.support;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.FormatsType;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.DateUtils;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 照片取证水印图统一生成组件。
* 无论原图文件名或后缀是否异常,系统产出的水印图都固定为 jpg/jpeg。
*/
@Slf4j
@Component
public class PhotoEvidenceWatermarkSupport {
public static final String OUTPUT_FORMAT = "jpeg";
public static final String OUTPUT_SUFFIX = ".jpg";
public static final String OUTPUT_CONTENT_TYPE = "image/jpeg";
public RenderedWatermark renderToTempFile(byte[] originalBytes, Date captureTimeClient, String address) {
BufferedImage watermarked = renderImage(originalBytes, captureTimeClient, address);
File tempFile = null;
try {
tempFile = File.createTempFile("photo_evidence_", OUTPUT_SUFFIX);
boolean writeSuccess;
try (OutputStream os = new FileOutputStream(tempFile)) {
writeSuccess = ImageIO.write(watermarked, OUTPUT_FORMAT, os);
}
validateWriteResult(writeSuccess, tempFile);
return new RenderedWatermark(tempFile, tempFile.length());
} catch (IOException ex) {
deleteQuietly(tempFile);
throw new ServiceException("水印图片生成失败: " + ex.getMessage());
}
}
public byte[] renderToBytes(byte[] originalBytes, Date captureTimeClient, String address) {
BufferedImage watermarked = renderImage(originalBytes, captureTimeClient, address);
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
boolean writeSuccess = ImageIO.write(watermarked, OUTPUT_FORMAT, outputStream);
if (!writeSuccess || outputStream.size() <= 0) {
throw new ServiceException("水印图片生成失败");
}
return outputStream.toByteArray();
} catch (IOException ex) {
throw new ServiceException("水印图片生成失败: " + ex.getMessage());
}
}
public String buildStandardOriginalName(Long ossId) {
if (ossId == null) {
return "photo_evidence" + OUTPUT_SUFFIX;
}
return "photo_evidence_" + ossId + OUTPUT_SUFFIX;
}
private BufferedImage renderImage(byte[] originalBytes, Date captureTimeClient, String address) {
BufferedImage originalImage = readImage(originalBytes);
return addWatermark(originalImage, captureTimeClient, address);
}
private void validateWriteResult(boolean writeSuccess, File tempFile) {
if (!writeSuccess) {
throw new ServiceException("水印图片生成失败: 未找到可用的 JPEG 编码器");
}
if (tempFile == null || !tempFile.exists()) {
throw new ServiceException("水印图片生成失败: 临时文件不存在");
}
if (tempFile.length() <= 0) {
throw new ServiceException("水印图片生成失败: 临时文件为空");
}
}
private BufferedImage readImage(byte[] bytes) {
try (InputStream is = new ByteArrayInputStream(bytes)) {
BufferedImage image = ImageIO.read(is);
if (image == null) {
String hex = "";
if (bytes.length >= 4) {
hex = String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]);
}
log.error("ImageIO.read returned null. Header: {}", hex);
throw new ServiceException("原始图片格式不支持 (Header: " + hex + ")");
}
return image;
} catch (IOException e) {
log.error("图片解析失败", e);
throw new ServiceException("原始图片解析失败: " + e.getMessage());
}
}
private BufferedImage addWatermark(BufferedImage original, Date captureTimeClient, String address) {
int width = original.getWidth();
int height = original.getHeight();
int minWidth = 600;
if (width < minWidth) {
int newWidth = minWidth;
int newHeight = (int) Math.round((double) height * newWidth / width);
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D gResize = resized.createGraphics();
gResize.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
gResize.drawImage(original, 0, 0, newWidth, newHeight, null);
gResize.dispose();
original = resized;
width = newWidth;
height = newHeight;
}
int fontSize = Math.max(20, width / 30);
int padding = Math.max(fontSize, 16);
String timeText = DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, captureTimeClient);
String addressText = "地址:" + (address != null ? address : "");
Font font;
try (InputStream is = getClass().getResourceAsStream("/fonts/simsun.ttc")) {
if (is == null) {
font = new Font("SansSerif", Font.PLAIN, fontSize);
} else {
font = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(Font.PLAIN, fontSize);
}
} catch (Exception e) {
log.warn("Failed to load simsun font, fallback to SansSerif", e);
font = new Font("SansSerif", Font.PLAIN, fontSize);
}
BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D gMeasure = tmp.createGraphics();
gMeasure.setFont(font);
FontMetrics fm = gMeasure.getFontMetrics();
int contentWidth = Math.max(1, width - padding * 2);
List<String> lines = new ArrayList<>();
lines.add(timeText);
if (fm.stringWidth(addressText) <= contentWidth) {
lines.add(addressText);
} else {
StringBuilder current = new StringBuilder();
for (char c : addressText.toCharArray()) {
if (fm.stringWidth(current.toString() + c) > contentWidth) {
lines.add(current.toString());
current = new StringBuilder();
}
current.append(c);
}
if (current.length() > 0) {
lines.add(current.toString());
}
}
int lineHeight = fm.getHeight() + fontSize / 4;
gMeasure.dispose();
int barHeight = Math.max(lineHeight + padding * 2, lines.size() * lineHeight + padding * 2);
BufferedImage result = new BufferedImage(width, height + barHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = result.createGraphics();
try {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(original, 0, 0, null);
g.setColor(Color.WHITE);
g.fillRect(0, height, width, barHeight);
g.setFont(font);
g.setColor(new Color(51, 51, 51));
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int y = height + padding + fm.getAscent();
for (String line : lines) {
g.drawString(line, padding, y);
y += lineHeight;
}
return result;
} finally {
g.dispose();
}
}
public void deleteQuietly(File file) {
if (file == null || !file.exists()) {
return;
}
if (!file.delete()) {
log.warn("临时文件删除失败: {}", file.getAbsolutePath());
}
}
@Getter
public static class RenderedWatermark {
private final File file;
private final long fileSize;
public RenderedWatermark(File file, long fileSize) {
this.file = file;
this.fileSize = fileSize;
}
}
}

View File

@@ -73,7 +73,7 @@ public class HotRiskLevelConfigServiceImpl implements IHotRiskLevelConfigService
private LambdaQueryWrapper<HotRiskLevelConfig> buildQueryWrapper(HotRiskLevelConfigBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotRiskLevelConfig> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotRiskLevelConfig::getLevelValue);
lqw.orderByDesc(HotRiskLevelConfig::getLevelValue, HotRiskLevelConfig::getId);
lqw.eq(bo.getLevelValue() != null, HotRiskLevelConfig::getLevelValue, bo.getLevelValue());
lqw.like(StringUtils.isNotBlank(bo.getLevelName()), HotRiskLevelConfig::getLevelName, bo.getLevelName());
lqw.eq(StringUtils.isNotBlank(bo.getDescription()), HotRiskLevelConfig::getDescription, bo.getDescription());

View File

@@ -1,5 +1,6 @@
package com.hotwj.platform.config.vehicleThreeInspect.callback;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hotwj.platform.config.vehicleThreeInspect.domain.HotVehicleThreeInspect;
import com.hotwj.platform.config.vehicleThreeInspect.mapper.HotVehicleThreeInspectMapper;
import com.hotwj.platform.flow.callback.IFlowCallback;
@@ -10,8 +11,8 @@ import com.hotwj.platform.securityManagement.hiddenDanger.domain.HotHiddenDanger
import com.hotwj.platform.securityManagement.hiddenDanger.domain.bo.HotHiddenDangerBo;
import com.hotwj.platform.securityManagement.hiddenDanger.mapper.HotHiddenDangerMapper;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@@ -48,7 +49,7 @@ public class VehicleThreeInspectCallback implements IFlowCallback {
@Override
public void onProcessEnd(String instanceId, String businessId, String flowCode, boolean success) {
log.info("车辆三检流程结束回调: businessId={}, success={}", businessId, success);
log.info("车辆三检流程结束回调: instanceId={}, businessId={}, flowCode={}, success={}", instanceId, businessId, flowCode, success);
Long id = Long.parseLong(businessId);
// 去掉try catch防止事务无法正常回滚
HotVehicleThreeInspect inspect = inspectMapper.selectById(id);
@@ -74,19 +75,41 @@ public class VehicleThreeInspectCallback implements IFlowCallback {
* 创建隐患治理流程
*/
private void createHiddenDangerFlow(HotVehicleThreeInspect inspect) {
if (inspect.getHasHiddenDanger() != null && inspect.getHasHiddenDanger() == 1L) {
log.info("车辆三检存在隐患自动创建隐患治理流程业务ID: {}", inspect.getId());
if (!Long.valueOf(1L).equals(inspect.getHasHiddenDanger())) {
log.info("车辆三检未标记隐患,跳过隐患治理创建: recordId={}, inspectId={}", inspect.getId(), inspect.getInspectId());
return;
}
String sourceId = String.valueOf(inspect.getInspectId() != null ? inspect.getInspectId() : inspect.getId());
log.info("车辆三检存在隐患,准备创建隐患治理流程: recordId={}, inspectId={}, sourceId={}, auditorId={}, evaluatorId={}",
inspect.getId(), inspect.getInspectId(), sourceId, inspect.getAuditorId(), inspect.getEvaluatorId());
HotHiddenDanger existingDanger = hiddenDangerMapper.selectOne(
Wrappers.<HotHiddenDanger>lambdaQuery()
.eq(HotHiddenDanger::getSourceType, 2L)
.eq(HotHiddenDanger::getDataSource, 2L)
.eq(HotHiddenDanger::getSourceId, sourceId)
.eq(HotHiddenDanger::getIsDeleted, 0L)
.last("LIMIT 1")
);
if (existingDanger != null) {
log.warn("车辆三检隐患治理已存在,跳过重复创建: recordId={}, inspectId={}, sourceId={}, existingDangerId={}, existingInstanceId={}",
inspect.getId(), inspect.getInspectId(), sourceId, existingDanger.getId(), existingDanger.getInstanceId());
return;
}
HotHiddenDangerBo hiddenDangerBo = new HotHiddenDangerBo();
hiddenDangerBo.setCompanyId(inspect.getCompanyId());
hiddenDangerBo.setCheckTime(new Date());
hiddenDangerBo.setCheckerId(String.valueOf(inspect.getAuditorId()));
hiddenDangerBo.setCheckerName(inspect.getAuditorName());
hiddenDangerBo.setDescription(inspect.getAuditResult());
hiddenDangerBo.setDescription(StringUtils.defaultIfBlank(
StringUtils.trimToNull(inspect.getRejectReason()),
resolveReadableAuditResult(inspect.getAuditResult())
));
hiddenDangerBo.setEvidenceUrls(inspect.getAttachImgUrl());
hiddenDangerBo.setDangerNo("YH-" + System.currentTimeMillis() / 1000);
hiddenDangerBo.setSourceName("车辆");
hiddenDangerBo.setSourceId(String.valueOf(inspect.getId()));
hiddenDangerBo.setSourceId(sourceId);
hiddenDangerBo.setSourceType(2L);
hiddenDangerBo.setDataSource(2L);
hiddenDangerBo.setDangerName("车辆三检治理");
@@ -94,6 +117,8 @@ public class VehicleThreeInspectCallback implements IFlowCallback {
HotHiddenDanger danger = MapstructUtils.convert(hiddenDangerBo, HotHiddenDanger.class);
hiddenDangerMapper.insert(danger);
log.info("车辆三检隐患治理数据已创建: recordId={}, inspectId={}, sourceId={}, dangerId={}",
inspect.getId(), inspect.getInspectId(), sourceId, danger.getId());
// 启动隐患治理流程
// 发起人为审核人(发现隐患的人)
@@ -112,7 +137,8 @@ public class VehicleThreeInspectCallback implements IFlowCallback {
danger.setEvaluatorId(inspect.getEvaluatorId());
danger.setFlowStatus("SUBMITTED");
hiddenDangerMapper.updateById(danger);
}
log.info("车辆三检隐患治理流程已启动: recordId={}, inspectId={}, sourceId={}, dangerId={}, hiddenDangerInstanceId={}",
inspect.getId(), inspect.getInspectId(), sourceId, danger.getId(), instanceId);
}
private void sendDriverAuditNotification(HotVehicleThreeInspect inspect, boolean success) {
@@ -139,13 +165,27 @@ public class VehicleThreeInspectCallback implements IFlowCallback {
if (StringUtils.isNotBlank(rejectReason)) {
return "您的车辆三检【" + phaseName + "】未通过,不通过原因:" + rejectReason;
}
String auditResult = StringUtils.trimToEmpty(inspect.getAuditResult());
String auditResult = StringUtils.trimToEmpty(resolveReadableAuditResult(inspect.getAuditResult()));
if (StringUtils.isNotBlank(auditResult)) {
return "您的车辆三检【" + phaseName + "】未通过,审核意见:" + auditResult;
}
return "您的车辆三检【" + phaseName + "】未通过,请及时查看审核意见。";
}
private String resolveReadableAuditResult(String auditResult) {
String value = StringUtils.trimToNull(auditResult);
if (value == null) {
return null;
}
if ("1".equals(value)) {
return "通过";
}
if ("2".equals(value)) {
return "不通过";
}
return value;
}
private String getPhaseName(Long phase) {
if (phase == null) {
return "未知阶段";

View File

@@ -2,6 +2,7 @@ package com.hotwj.platform.config.vehicleThreeInspect.controller;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectBatchAuditBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectMultiVehicleBatchAuditBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
@@ -223,4 +224,45 @@ public class HotVehicleThreeInspectController extends BaseController {
public TableDataInfo<HotVehicleThreeInspectSummaryVo> listSummary(HotVehicleThreeInspectBo bo, PageQuery pageQuery) {
return hotVehicleThreeInspectService.querySummaryPageList(bo, pageQuery);
}
/**
* 查询当前人员可进行多车辆批量审核的车辆三检列表
*/
@GetMapping("/multiVehicleBatchAudit/list")
@Operation(summary = "分页查询当前人员可进行多车辆批量审核的车辆三检列表")
public TableDataInfo<HotVehicleThreeInspectSummaryVo> listMultiVehicleBatchAudit(HotVehicleThreeInspectVehicleQueryBo bo, PageQuery pageQuery) {
return hotVehicleThreeInspectService.queryBatchAuditVehiclePageList(bo, pageQuery);
}
/**
* 多车辆批量审核车辆三检
*/
@Log(title = "车辆三检多车辆批量审核", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/multiVehicleBatchAudit")
@Operation(summary = "多车辆批量审核车辆三检")
public R<Void> multiVehicleBatchAudit(@Validated @RequestBody HotVehicleThreeInspectMultiVehicleBatchAuditBo bo) {
hotVehicleThreeInspectService.multiVehicleBatchAudit(bo);
return R.ok();
}
/**
* 查询车辆三检历史记录列表
*/
@GetMapping("/history/list")
@Operation(summary = "分页查询车辆三检历史记录列表")
public TableDataInfo<HotVehicleThreeInspectSummaryVo> listHistory(HotVehicleThreeInspectBo bo, PageQuery pageQuery) {
return hotVehicleThreeInspectService.queryHistoryPageList(bo, pageQuery);
}
/**
* 查询车辆三检历史记录详情
*/
@GetMapping("/history/{inspectId}")
@Operation(summary = "查询车辆三检历史记录详情")
public R<HotVehicleThreeInspectSummaryVo> getHistoryDetail(@NotNull(message = "三检流程ID不能为空")
@Parameter(name = "inspectId", description = "三检流程ID", required = true, example = "1")
@PathVariable Long inspectId) {
return R.ok(hotVehicleThreeInspectService.queryHistoryDetail(inspectId));
}
}

View File

@@ -60,11 +60,21 @@ public class HotVehicleThreeInspect extends BaseEntity {
*/
private Long inspectId;
/**
* 三检记录编号
*/
private String inspectNo;
/**
* 检查时间
*/
private Date inspectTime;
/**
* 检查地点
*/
private String inspectLocation;
/**
* 审核人ID
*/

View File

@@ -63,12 +63,32 @@ public class HotVehicleThreeInspectBo extends BaseEntity {
*/
private Long inspectId;
/**
* 三检记录编号
*/
private String inspectNo;
/**
* 检查时间
*/
@NotNull(message = "检查时间不能为空", groups = {AddGroup.class, EditGroup.class})
private Date inspectTime;
/**
* 检查地点
*/
private String inspectLocation;
/**
* 纬度
*/
private Double latitude;
/**
* 经度
*/
private Double longitude;
/**
* 审核人ID
*/

View File

@@ -0,0 +1,57 @@
package com.hotwj.platform.config.vehicleThreeInspect.domain.bo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 车辆三检多车辆批量审核业务对象
*
* @author shihongwei
* @date 2026-06-17
*/
@Data
public class HotVehicleThreeInspectMultiVehicleBatchAuditBo {
/**
* 三检流程ID集合
*/
@NotEmpty(message = "三检流程ID不能为空")
private List<Long> inspectIds;
/**
* 是否存在隐患 0=否, 1=是
*/
@NotNull(message = "是否存在隐患不能为空")
private Long hasHiddenDanger;
/**
* 评估人ID
*/
private Long evaluatorId;
/**
* 审核时间
*/
private Date auditTime;
/**
* 审核结论
*/
private String auditResult;
/**
* 审核不通过原因
*/
private String rejectReason;
/**
* 审核人员签名图片url
*/
@NotBlank(message = "审核人员签名不能为空")
private String auditorSignImgUrl;
}

View File

@@ -13,50 +13,48 @@ public class HotVehicleThreeInspectPrintVo {
*/
private Long inspectId;
/**
* 三检记录编号
*/
private String inspectNo;
/**
* 车牌号
*/
private String plateNumber;
// --- 出车前 Phase 1 ---
/**
* 出车前-检查时间
* 检查人
*/
private String startPhaseTime;
/**
* 出车前-状态
*/
private String startPhaseStatus;
/**
* 出车前-隐患
*/
private String startPhaseHiddenDanger;
private String inspectorName;
// --- 行车中 Phase 2 ---
/**
* 行车中-检查时间
* 审核人
*/
private String runningPhaseTime;
/**
* 行车中-状态
*/
private String runningPhaseStatus;
/**
* 行车中-隐患
*/
private String runningPhaseHiddenDanger;
private String auditorName;
// --- 收车后 Phase 3 ---
/**
* 收车后-检查时间
* 检查开始时间
*/
private String endPhaseTime;
private String startTime;
/**
* 收车后-状态
* 检查结束时间
*/
private String endPhaseStatus;
private String endTime;
/**
* 收车后-隐患
* 出车前检查地点
*/
private String endPhaseHiddenDanger;
private String outInspectLocation;
/**
* 行车中检查地点
*/
private String drivingInspectLocation;
/**
* 收车后检查地点
*/
private String backInspectLocation;
}

View File

@@ -23,6 +23,11 @@ public class HotVehicleThreeInspectSummaryVo implements Serializable {
*/
private Long inspectId;
/**
* 三检记录编号
*/
private String inspectNo;
/**
* 公司ID
*/
@@ -48,6 +53,16 @@ public class HotVehicleThreeInspectSummaryVo implements Serializable {
*/
private String driverName;
/**
* 检查人
*/
private String inspectorName;
/**
* 审核人
*/
private String auditorName;
/**
* 开始时间
*/
@@ -83,6 +98,21 @@ public class HotVehicleThreeInspectSummaryVo implements Serializable {
*/
private Boolean auditCompleted;
/**
* 出车前检查地点
*/
private String outInspectLocation;
/**
* 行车中检查地点
*/
private String drivingInspectLocation;
/**
* 收车后检查地点
*/
private String backInspectLocation;
/**
* 出车检查
*/

View File

@@ -66,12 +66,24 @@ public class HotVehicleThreeInspectVo implements Serializable {
*/
private Long inspectId;
/**
* 三检记录编号
*/
@ExcelProperty(value = "三检记录编号")
private String inspectNo;
/**
* 检查时间
*/
@ExcelProperty(value = "检查时间")
private Date inspectTime;
/**
* 检查地点
*/
@ExcelProperty(value = "检查地点")
private String inspectLocation;
/**
* 审核人ID
*/

View File

@@ -57,6 +57,28 @@ public interface HotVehicleThreeInspectMapper extends BaseMapperPlus<HotVehicleT
@Param("bo") HotVehicleThreeInspectBo bo
);
/**
* 分页查询当前人员可进行多车辆批量审核的车辆三检聚合列表
*
* @param page 分页参数
* @param bo 查询条件
* @param auditorId 当前审核人ID
* @return 聚合分页列表
*/
Page<HotVehicleThreeInspectSummaryVo> queryBatchAuditVehiclePageList(
Page<HotVehicleThreeInspectSummaryVo> page,
@Param("bo") HotVehicleThreeInspectVehicleQueryBo bo,
@Param("auditorId") Long auditorId
);
/**
* 按流程ID查询车辆三检聚合详情
*
* @param inspectId 流程ID
* @return 聚合详情
*/
HotVehicleThreeInspectSummaryVo querySummaryByInspectId(@Param("inspectId") Long inspectId);
/**
* 按流程ID集合查询三检明细
*

View File

@@ -2,6 +2,7 @@ package com.hotwj.platform.config.vehicleThreeInspect.service;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectBatchAuditBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectMultiVehicleBatchAuditBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
@@ -82,6 +83,32 @@ public interface IHotVehicleThreeInspectService {
*/
TableDataInfo<HotVehicleThreeInspectSummaryVo> querySummaryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
/**
* 分页查询当前人员可进行多车辆批量审核的三检流程列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 聚合分页列表
*/
TableDataInfo<HotVehicleThreeInspectSummaryVo> queryBatchAuditVehiclePageList(HotVehicleThreeInspectVehicleQueryBo bo, PageQuery pageQuery);
/**
* 查询车辆三检历史记录分页列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 历史记录分页列表
*/
TableDataInfo<HotVehicleThreeInspectSummaryVo> queryHistoryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
/**
* 按三检流程ID查询历史详情
*
* @param inspectId 三检流程ID
* @return 历史详情
*/
HotVehicleThreeInspectSummaryVo queryHistoryDetail(Long inspectId);
/**
* 查询符合条件的车辆三检列表
*
@@ -164,4 +191,11 @@ public interface IHotVehicleThreeInspectService {
* @param bo 批量审核业务对象
*/
void batchAudit(HotVehicleThreeInspectBatchAuditBo bo);
/**
* 多车辆批量审核车辆三检
*
* @param bo 多车辆批量审核业务对象
*/
void multiVehicleBatchAudit(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo);
}

View File

@@ -1,23 +1,22 @@
package com.hotwj.platform.config.vehicleThreeInspect.service.impl;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.hotwj.platform.common.helper.DriverLoginContextHelper;
import com.hotwj.platform.config.vehicleThreeInspect.domain.HotVehicleThreeInspect;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectBatchAuditBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectMultiVehicleBatchAuditBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectSummaryVo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
import com.hotwj.platform.config.vehicleThreeInspectConfig.domain.HotVehicleThreeInspectConfig;
import com.hotwj.platform.config.vehicleThreeInspectConfig.mapper.HotVehicleThreeInspectConfigMapper;
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.*;
import com.hotwj.platform.config.vehicleThreeInspect.mapper.HotVehicleThreeInspectMapper;
import com.hotwj.platform.config.vehicleThreeInspect.service.IHotVehicleThreeInspectService;
import com.hotwj.platform.config.vehicleThreeInspectConfig.domain.HotVehicleThreeInspectConfig;
import com.hotwj.platform.config.vehicleThreeInspectConfig.mapper.HotVehicleThreeInspectConfigMapper;
import com.hotwj.platform.flow.service.ISysFlowService;
import com.hotwj.platform.integration.ocr.SignatureVerifyService;
import com.hotwj.platform.noticeManagerment.systemNotification.domain.bo.HotSystemNotificationGroupBo;
@@ -30,6 +29,7 @@ import com.hotwj.platform.securityManagement.vehicleInoutCheck.domain.HotVehicle
import com.hotwj.platform.securityManagement.vehicleInoutCheck.mapper.HotVehicleInoutCheckMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
@@ -37,15 +37,16 @@ import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
/**
* 车辆三检Service业务层处理
@@ -67,6 +68,9 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
private final SignatureVerifyService signatureVerifyService;
private final HotVehicleThreeInspectConfigMapper hotVehicleThreeInspectConfigMapper;
@Value("${map.gd.key:}")
private String gdKey;
/**
* 查询可进行三检的车辆列表(无历史记录或上一轮已完结)
*
@@ -208,35 +212,42 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
@Override
public TableDataInfo<HotVehicleThreeInspectSummaryVo> querySummaryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery) {
Page<HotVehicleThreeInspectSummaryVo> result = baseMapper.querySummaryPageList(pageQuery.build(), bo);
List<HotVehicleThreeInspectSummaryVo> summaryList = result.getRecords();
if (summaryList == null || summaryList.isEmpty()) {
enrichSummaryList(result.getRecords());
return TableDataInfo.build(result);
}
List<Long> inspectIds = summaryList.stream()
.map(HotVehicleThreeInspectSummaryVo::getInspectId)
.filter(Objects::nonNull)
.toList();
if (inspectIds.isEmpty()) {
@Override
public TableDataInfo<HotVehicleThreeInspectSummaryVo> queryBatchAuditVehiclePageList(HotVehicleThreeInspectVehicleQueryBo bo, PageQuery pageQuery) {
Long currentAuditorId = resolveCurrentAuditorId();
LoginUser loginUser = LoginHelper.getLoginUser();
if (bo == null) {
bo = new HotVehicleThreeInspectVehicleQueryBo();
}
if (bo.getCompanyId() == null && loginUser != null) {
bo.setCompanyId(loginUser.getCompanyId());
}
Page<HotVehicleThreeInspectSummaryVo> result = baseMapper.queryBatchAuditVehiclePageList(pageQuery.build(), bo, currentAuditorId);
return TableDataInfo.build(result);
}
List<HotVehicleThreeInspectVo> detailList = baseMapper.querySummaryDetailList(inspectIds);
fillTaskId(detailList);
Map<Long, HotVehicleThreeInspectSummaryVo> summaryMap = new HashMap<>(summaryList.size());
for (HotVehicleThreeInspectSummaryVo summary : summaryList) {
summaryMap.put(summary.getInspectId(), summary);
@Override
public TableDataInfo<HotVehicleThreeInspectSummaryVo> queryHistoryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery) {
return querySummaryPageList(bo, pageQuery);
}
for (HotVehicleThreeInspectVo detail : detailList) {
HotVehicleThreeInspectSummaryVo summary = summaryMap.get(detail.getInspectId());
@Override
public HotVehicleThreeInspectSummaryVo queryHistoryDetail(Long inspectId) {
HotVehicleThreeInspectSummaryVo summary = baseMapper.querySummaryByInspectId(inspectId);
if (summary == null) {
continue;
throw new ServiceException("车辆三检历史记录不存在");
}
List<HotVehicleThreeInspectVo> detailList = baseMapper.querySummaryDetailList(Collections.singletonList(inspectId));
fillTaskId(detailList);
for (HotVehicleThreeInspectVo detail : detailList) {
attachPhaseDetail(summary, detail);
}
return TableDataInfo.build(result);
refreshSummary(summary);
return summary;
}
/**
@@ -255,20 +266,33 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
if (summary == null || detail == null || detail.getInspectPhase() == null) {
return;
}
if (detail.getInspectPhase() == 1L) {
if (StringUtils.isBlank(summary.getInspectNo()) && StringUtils.isNotBlank(detail.getInspectNo())) {
summary.setInspectNo(detail.getInspectNo());
}
if (StringUtils.isBlank(summary.getInspectorName()) && StringUtils.isNotBlank(detail.getDriverName())) {
summary.setInspectorName(detail.getDriverName());
}
if (StringUtils.isBlank(summary.getAuditorName()) && StringUtils.isNotBlank(detail.getAuditorName())) {
summary.setAuditorName(detail.getAuditorName());
}
Long inspectPhase = detail.getInspectPhase();
if (Objects.equals(inspectPhase, 1L)) {
if (summary.getOutCheck() == null) {
summary.setOutCheck(detail);
summary.setOutInspectLocation(detail.getInspectLocation());
}
return;
}
if (detail.getInspectPhase() == 2L) {
if (Objects.equals(inspectPhase, 2L)) {
if (summary.getDrivingCheck() == null) {
summary.setDrivingCheck(detail);
summary.setDrivingInspectLocation(detail.getInspectLocation());
}
return;
}
if (detail.getInspectPhase() == 3L && summary.getBackCheck() == null) {
if (Objects.equals(inspectPhase, 3L) && summary.getBackCheck() == null) {
summary.setBackCheck(detail);
summary.setBackInspectLocation(detail.getInspectLocation());
}
}
@@ -287,7 +311,7 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
private LambdaQueryWrapper<HotVehicleThreeInspect> buildQueryWrapper(HotVehicleThreeInspectBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicleThreeInspect> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotVehicleThreeInspect::getCreateTime);
lqw.orderByDesc(HotVehicleThreeInspect::getCreateTime, HotVehicleThreeInspect::getId);
Object beginTime = params == null ? null : params.get("beginTime");
Object endTime = params == null ? null : params.get("endTime");
if (beginTime != null && endTime != null) {
@@ -320,6 +344,8 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
lqw.eq(bo.getInspectPhase() != null, HotVehicleThreeInspect::getInspectPhase, bo.getInspectPhase());
lqw.eq(bo.getInspectTime() != null, HotVehicleThreeInspect::getInspectTime, bo.getInspectTime());
lqw.eq(bo.getInspectId() != null, HotVehicleThreeInspect::getInspectId, bo.getInspectId());
lqw.eq(StringUtils.isNotBlank(bo.getInspectNo()), HotVehicleThreeInspect::getInspectNo, bo.getInspectNo());
lqw.like(StringUtils.isNotBlank(bo.getInspectLocation()), HotVehicleThreeInspect::getInspectLocation, bo.getInspectLocation());
lqw.eq(bo.getAuditorId() != null, HotVehicleThreeInspect::getAuditorId, bo.getAuditorId());
lqw.like(StringUtils.isNotBlank(bo.getAuditorName()), HotVehicleThreeInspect::getAuditorName, bo.getAuditorName());
lqw.eq(StringUtils.isNotBlank(bo.getInspectResultJson()), HotVehicleThreeInspect::getInspectResultJson, bo.getInspectResultJson());
@@ -337,6 +363,8 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
*/
@Override
public Boolean insertByBo(HotVehicleThreeInspectBo bo) {
normalizeInspectLocation(bo);
prepareInspectIdentity(bo);
validateAttachImgUrlRequiredWhenAbnormal(bo);
HotVehicleThreeInspect add = MapstructUtils.convert(bo, HotVehicleThreeInspect.class);
validEntityBeforeSave(add);
@@ -401,6 +429,7 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
patchNewRoundInspectIdentity(add, bo);
bo.setId(add.getId());
// 开启车辆三检的流程
@@ -542,6 +571,7 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(HotVehicleThreeInspectBo bo) {
normalizeInspectLocation(bo);
validateAttachImgUrlRequiredWhenAbnormal(bo);
HotVehicleThreeInspect update = MapstructUtils.convert(bo, HotVehicleThreeInspect.class);
validEntityBeforeSave(update);
@@ -677,6 +707,15 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void multiVehicleBatchAudit(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo) {
validateMultiVehicleBatchAuditRequest(bo);
HotVehicleThreeInspectBatchAuditBo batchAuditBo = new HotVehicleThreeInspectBatchAuditBo();
batchAuditBo.setAuditItems(buildMultiVehicleBatchAuditItems(bo));
batchAudit(batchAuditBo);
}
private void validateBatchAuditAuditor(HotVehicleThreeInspectBatchAuditBo bo) {
Long expectedAuditorId = null;
String expectedAuditorName = null;
@@ -794,8 +833,409 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
Wrappers.<HotVehicleThreeInspect>lambdaQuery()
.eq(HotVehicleThreeInspect::getVehicleId, vehicleId)
.eq(HotVehicleThreeInspect::getIsDeleted, 0L)
.orderByDesc(HotVehicleThreeInspect::getCreateTime)
.orderByDesc(HotVehicleThreeInspect::getCreateTime, HotVehicleThreeInspect::getId)
.last("LIMIT 1")
);
}
private void enrichSummaryList(List<HotVehicleThreeInspectSummaryVo> summaryList) {
if (summaryList == null || summaryList.isEmpty()) {
return;
}
List<Long> inspectIds = summaryList.stream()
.map(HotVehicleThreeInspectSummaryVo::getInspectId)
.filter(Objects::nonNull)
.toList();
if (inspectIds.isEmpty()) {
return;
}
List<HotVehicleThreeInspectVo> detailList = baseMapper.querySummaryDetailList(inspectIds);
fillTaskId(detailList);
Map<Long, HotVehicleThreeInspectSummaryVo> summaryMap = new HashMap<>(summaryList.size());
for (HotVehicleThreeInspectSummaryVo summary : summaryList) {
summaryMap.put(summary.getInspectId(), summary);
}
for (HotVehicleThreeInspectVo detail : detailList) {
HotVehicleThreeInspectSummaryVo summary = summaryMap.get(detail.getInspectId());
if (summary == null) {
continue;
}
attachPhaseDetail(summary, detail);
}
for (HotVehicleThreeInspectSummaryVo summary : summaryList) {
refreshSummary(summary);
}
}
private void validateMultiVehicleBatchAuditRequest(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo) {
if (bo == null || bo.getInspectIds() == null || bo.getInspectIds().isEmpty()) {
throw new ServiceException("三检流程ID不能为空");
}
if (Objects.equals(bo.getHasHiddenDanger(), 1L) && bo.getEvaluatorId() == null) {
throw new ServiceException("存在隐患时评估人不能为空");
}
}
private List<HotVehicleThreeInspectBo> buildMultiVehicleBatchAuditItems(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo) {
Long currentAuditorId = resolveCurrentAuditorId();
Long currentCompanyId = resolveCurrentCompanyId();
LinkedHashSet<Long> inspectIdSet = new LinkedHashSet<>();
for (Long inspectId : bo.getInspectIds()) {
if (inspectId != null) {
inspectIdSet.add(inspectId);
}
}
if (inspectIdSet.isEmpty()) {
throw new ServiceException("三检流程ID不能为空");
}
log.info("车辆三检批量审核开始: inspectIds={}, currentAuditorId={}, currentCompanyId={}, hasHiddenDanger={}, evaluatorId={}",
inspectIdSet, currentAuditorId, currentCompanyId, bo.getHasHiddenDanger(), bo.getEvaluatorId());
List<HotVehicleThreeInspectVo> detailList = baseMapper.querySummaryDetailList(new ArrayList<>(inspectIdSet));
if (detailList == null || detailList.isEmpty()) {
throw new ServiceException("未查询到可批量审核的车辆三检数据");
}
fillTaskId(detailList);
log.info("车辆三检批量审核明细加载完成: detailCount={}, inspectCount={}", detailList.size(), inspectIdSet.size());
Map<Long, List<HotVehicleThreeInspectVo>> detailMap = new LinkedHashMap<>();
for (HotVehicleThreeInspectVo detail : detailList) {
if (detail == null || detail.getInspectId() == null) {
continue;
}
detailMap.computeIfAbsent(detail.getInspectId(), key -> new ArrayList<>()).add(detail);
}
Date auditTime = resolveBatchAuditTime(bo);
String auditResult = buildBatchAuditResult(bo);
List<HotVehicleThreeInspectBo> auditItems = new ArrayList<>();
for (Long inspectId : inspectIdSet) {
List<HotVehicleThreeInspectVo> currentDetails = detailMap.get(inspectId);
if (currentDetails == null || currentDetails.isEmpty()) {
throw new ServiceException("三检流程不存在inspectId=" + inspectId);
}
appendPendingAuditItems(bo, currentDetails, currentAuditorId, currentCompanyId, auditTime, auditResult, auditItems);
}
if (auditItems.isEmpty()) {
throw new ServiceException("没有符合批量审核条件的数据");
}
log.info("车辆三检批量审核审核项构建完成: inspectCount={}, auditItemCount={}, auditItemRecordIds={}",
inspectIdSet.size(),
auditItems.size(),
auditItems.stream().map(HotVehicleThreeInspectBo::getId).filter(Objects::nonNull).toList());
return auditItems;
}
private void appendPendingAuditItems(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo,
List<HotVehicleThreeInspectVo> currentDetails,
Long currentAuditorId,
Long currentCompanyId,
Date auditTime,
String auditResult,
List<HotVehicleThreeInspectBo> auditItems) {
boolean hasBackCheck = false;
String plateNumber = null;
Long inspectId = null;
for (HotVehicleThreeInspectVo detail : currentDetails) {
if (detail == null) {
continue;
}
if (inspectId == null) {
inspectId = detail.getInspectId();
}
if (Objects.equals(detail.getInspectPhase(), 3L)) {
hasBackCheck = true;
}
if (StringUtils.isBlank(plateNumber)) {
plateNumber = resolvePlateNumber(detail.getVehicleId());
}
}
if (!hasBackCheck) {
throw new ServiceException(buildBatchAuditVehicleMessage(plateNumber, "尚未完成收车检查,不可批量审核"));
}
List<HotVehicleThreeInspectVo> candidateDetails = new ArrayList<>();
int skippedByCompany = 0;
int skippedByAuditor = 0;
int skippedByStatus = 0;
int skippedByTask = 0;
for (HotVehicleThreeInspectVo detail : currentDetails) {
if (detail == null) {
continue;
}
if (currentCompanyId != null && !Objects.equals(currentCompanyId, detail.getCompanyId())) {
skippedByCompany++;
continue;
}
if (!Objects.equals(currentAuditorId, detail.getAuditorId())) {
skippedByAuditor++;
continue;
}
if (!isPendingAuditRecord(detail)) {
skippedByStatus++;
continue;
}
if (StringUtils.isBlank(detail.getTaskId())) {
skippedByTask++;
continue;
}
candidateDetails.add(detail);
}
log.info("车辆三检批量审核候选明细筛选: inspectId={}, plateNumber={}, totalDetails={}, candidateCount={}, skippedByCompany={}, skippedByAuditor={}, skippedByStatus={}, skippedByTask={}, candidateRecordIds={}",
inspectId,
plateNumber,
currentDetails.size(),
candidateDetails.size(),
skippedByCompany,
skippedByAuditor,
skippedByStatus,
skippedByTask,
candidateDetails.stream().map(HotVehicleThreeInspectVo::getId).filter(Objects::nonNull).toList());
if (candidateDetails.isEmpty()) {
throw new ServiceException(buildBatchAuditVehicleMessage(plateNumber, "没有当前账号可审核的未审核数据"));
}
for (HotVehicleThreeInspectVo detail : candidateDetails) {
HotVehicleThreeInspectBo item = new HotVehicleThreeInspectBo();
item.setId(detail.getId());
item.setTaskId(detail.getTaskId());
item.setAuditTime(auditTime);
item.setHasHiddenDanger(bo.getHasHiddenDanger());
item.setAuditResult(auditResult);
item.setRejectReason(bo.getRejectReason());
item.setAuditorSignImgUrl(bo.getAuditorSignImgUrl());
item.setAuditorId(detail.getAuditorId());
item.setAuditorName(detail.getAuditorName());
item.setEvaluatorId(bo.getEvaluatorId());
auditItems.add(item);
}
log.info("车辆三检批量审核候选明细全部入队: inspectId={}, plateNumber={}, queuedCount={}, queuedRecordIds={}",
inspectId,
plateNumber,
candidateDetails.size(),
candidateDetails.stream().map(HotVehicleThreeInspectVo::getId).filter(Objects::nonNull).toList());
}
private boolean isPendingAuditRecord(HotVehicleThreeInspectVo detail) {
if (detail == null) {
return false;
}
return !"APPROVED".equals(detail.getFlowStatus()) && !"REJECTED".equals(detail.getFlowStatus());
}
private Date resolveBatchAuditTime(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo) {
return bo.getAuditTime() != null ? bo.getAuditTime() : new Date();
}
private String buildBatchAuditResult(HotVehicleThreeInspectMultiVehicleBatchAuditBo bo) {
String auditResult = StringUtils.trimToNull(bo.getAuditResult());
if (auditResult != null) {
return auditResult;
}
if (Objects.equals(bo.getHasHiddenDanger(), 1L)) {
String rejectReason = StringUtils.trimToNull(bo.getRejectReason());
return rejectReason != null ? "不通过:" + rejectReason : "不通过";
}
return "通过";
}
private String buildBatchAuditVehicleMessage(String plateNumber, String suffix) {
String vehicleName = StringUtils.defaultIfBlank(StringUtils.trimToNull(plateNumber), "当前车辆");
return vehicleName + suffix;
}
private Long resolveCurrentAuditorId() {
String businessUserId = LoginHelper.getBusinessUserId();
if (StringUtils.isBlank(businessUserId)) {
throw new ServiceException("当前登录人员不存在");
}
try {
return Long.valueOf(businessUserId);
} catch (NumberFormatException e) {
throw new ServiceException("当前登录人员信息异常");
}
}
private Long resolveCurrentCompanyId() {
LoginUser loginUser = LoginHelper.getLoginUser();
return loginUser != null ? loginUser.getCompanyId() : null;
}
private void refreshSummary(HotVehicleThreeInspectSummaryVo summary) {
if (summary == null) {
return;
}
if (summary.getOutCheck() != null) {
summary.setStartTime(summary.getOutCheck().getInspectTime());
if (StringUtils.isBlank(summary.getInspectNo())) {
summary.setInspectNo(summary.getOutCheck().getInspectNo());
}
}
if (summary.getBackCheck() != null) {
summary.setEndTime(summary.getBackCheck().getInspectTime());
}
if (StringUtils.isBlank(summary.getInspectorName())) {
summary.setInspectorName(summary.getDriverName());
}
summary.setInspectTime(buildInspectTimeRange(summary.getStartTime(), summary.getEndTime()));
}
private String buildInspectTimeRange(Date startTime, Date endTime) {
if (startTime == null && endTime == null) {
return null;
}
if (startTime == null) {
return DateUtil.formatDateTime(endTime);
}
if (endTime == null || Objects.equals(startTime, endTime)) {
return DateUtil.formatDateTime(startTime);
}
return DateUtil.formatDateTime(startTime) + " ~ " + DateUtil.formatDateTime(endTime);
}
private void normalizeInspectLocation(HotVehicleThreeInspectBo bo) {
if (bo == null) {
return;
}
String inspectLocation = StringUtils.trimToNull(bo.getInspectLocation());
String reverseAddress = reverseGeocode(bo.getLatitude(), bo.getLongitude());
bo.setInspectLocation(StringUtils.defaultIfBlank(reverseAddress, inspectLocation));
}
private void prepareInspectIdentity(HotVehicleThreeInspectBo bo) {
if (bo == null || bo.getVehicleId() == null || bo.getInspectPhase() == null) {
return;
}
HotVehicleThreeInspect latestInspect = selectLatestInspect(bo.getVehicleId());
if (Objects.equals(bo.getInspectPhase(), 1L)) {
if (StringUtils.isBlank(bo.getInspectNo())) {
bo.setInspectNo(generateInspectNo(bo.getInspectTime(), bo.getVehicleId()));
}
return;
}
if (latestInspect == null) {
return;
}
if (bo.getInspectId() == null) {
bo.setInspectId(latestInspect.getInspectId() != null ? latestInspect.getInspectId() : latestInspect.getId());
}
if (StringUtils.isBlank(bo.getInspectNo())) {
bo.setInspectNo(latestInspect.getInspectNo());
}
}
private void patchNewRoundInspectIdentity(HotVehicleThreeInspect add, HotVehicleThreeInspectBo bo) {
if (add == null || !Objects.equals(add.getInspectPhase(), 1L)) {
return;
}
boolean changed = false;
if (add.getInspectId() == null) {
add.setInspectId(add.getId());
changed = true;
}
if (StringUtils.isBlank(add.getInspectNo())) {
add.setInspectNo(generateInspectNo(add.getInspectTime(), add.getVehicleId()));
changed = true;
}
if (!changed) {
return;
}
baseMapper.updateById(add);
bo.setInspectId(add.getInspectId());
bo.setInspectNo(add.getInspectNo());
}
private String generateInspectNo(Date inspectTime, Long vehicleId) {
Date recordDate = inspectTime == null ? new Date() : inspectTime;
String dateText = DateUtil.format(recordDate, "yyyyMMdd");
String plateSuffix = resolvePlateSuffix(vehicleId);
for (int i = 0; i < 10; i++) {
String candidate = "SJ" + dateText + plateSuffix + randomFourDigits();
Long count = baseMapper.selectCount(
Wrappers.<HotVehicleThreeInspect>lambdaQuery()
.eq(HotVehicleThreeInspect::getInspectNo, candidate)
);
if (count == null || count == 0L) {
return candidate;
}
}
return "SJ" + dateText + plateSuffix + String.format("%04d", System.currentTimeMillis() % 10000);
}
private String resolvePlateSuffix(Long vehicleId) {
if (vehicleId == null) {
return "000";
}
HotVehicle vehicle = hotVehicleMapper.selectById(String.valueOf(vehicleId));
String plateNumber = vehicle == null ? null : StringUtils.trimToNull(vehicle.getPlateNumber());
if (plateNumber == null) {
return "000";
}
String normalized = plateNumber.replace(" ", "");
if (normalized.length() <= 3) {
return normalized;
}
return normalized.substring(normalized.length() - 3);
}
private String resolvePlateNumber(Long vehicleId) {
if (vehicleId == null) {
return null;
}
HotVehicle vehicle = hotVehicleMapper.selectById(String.valueOf(vehicleId));
return vehicle == null ? null : StringUtils.trimToNull(vehicle.getPlateNumber());
}
private String randomFourDigits() {
return String.format("%04d", ThreadLocalRandom.current().nextInt(1000, 10000));
}
private String reverseGeocode(Double latitude, Double longitude) {
if (latitude == null || longitude == null) {
return null;
}
if (StringUtils.isBlank(gdKey)) {
return null;
}
String location = longitude + "," + latitude;
String url = "https://restapi.amap.com/v3/geocode/regeo?output=JSON&location="
+ location + "&key=" + gdKey + "&radius=1000&extensions=base";
HttpURLConnection connection = null;
try {
URL u = new URL(url);
connection = (HttpURLConnection) u.openConnection();
connection.setConnectTimeout(3000);
connection.setReadTimeout(3000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
return null;
}
try (InputStream is = connection.getInputStream()) {
String body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
JsonNode root = JsonUtils.getObjectMapper().readTree(body);
if (root == null) {
return null;
}
if (!"1".equals(root.path("status").asText())) {
return null;
}
String formatted = root.path("regeocode").path("formatted_address").asText();
return StringUtils.trimToNull(formatted);
}
} catch (Exception e) {
log.warn("车辆三检逆地理解析失败: {},{}", latitude, longitude);
return null;
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}

View File

@@ -72,7 +72,7 @@ public class HotVehicleThreeInspectItemServiceImpl implements IHotVehicleThreeIn
private LambdaQueryWrapper<HotVehicleThreeInspectItem> buildQueryWrapper(HotVehicleThreeInspectItemBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicleThreeInspectItem> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(HotVehicleThreeInspectItem::getSortNo);
lqw.orderByAsc(HotVehicleThreeInspectItem::getSortNo, HotVehicleThreeInspectItem::getId);
lqw.eq(bo.getCompanyId() != null, HotVehicleThreeInspectItem::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getInspectId() != null, HotVehicleThreeInspectItem::getInspectId, bo.getInspectId());
lqw.eq(bo.getInspectType() != null, HotVehicleThreeInspectItem::getInspectType, bo.getInspectType());

View File

@@ -270,7 +270,7 @@ public class HotDriverServiceImpl implements IHotDriverService {
private LambdaQueryWrapper<HotDriver> buildQueryWrapper(HotDriverBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotDriver> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotDriver::getCreateTime);
lqw.orderByDesc(HotDriver::getCreateTime, HotDriver::getId);
lqw.in(CollUtil.isNotEmpty(bo.getIds()), HotDriver::getId, bo.getIds());
lqw.eq(bo.getCompanyId() != null, HotDriver::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getEntryDate() != null, HotDriver::getEntryDate, bo.getEntryDate());

View File

@@ -76,7 +76,7 @@ public class HotDriverAnnualAssessmentServiceImpl implements IHotDriverAnnualAss
private LambdaQueryWrapper<HotDriverAnnualAssessment> buildQueryWrapper(HotDriverAnnualAssessmentBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotDriverAnnualAssessment> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotDriverAnnualAssessment::getAssessmentTime);
lqw.orderByDesc(HotDriverAnnualAssessment::getAssessmentTime, HotDriverAnnualAssessment::getId);
lqw.eq(bo.getCompanyId() != null, HotDriverAnnualAssessment::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getDriverId() != null, HotDriverAnnualAssessment::getDriverId, bo.getDriverId());
lqw.eq(StringUtils.isNotBlank(bo.getAssessmentContent()), HotDriverAnnualAssessment::getAssessmentContent, bo.getAssessmentContent());

View File

@@ -74,7 +74,7 @@ public class HotDriverHealthReportServiceImpl implements IHotDriverHealthReportS
private LambdaQueryWrapper<HotDriverHealthReport> buildQueryWrapper(HotDriverHealthReportBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotDriverHealthReport> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotDriverHealthReport::getReportDate);
lqw.orderByDesc(HotDriverHealthReport::getReportDate, HotDriverHealthReport::getId);
lqw.eq(bo.getCompanyId() != null, HotDriverHealthReport::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getDriverId() != null, HotDriverHealthReport::getDriverId, bo.getDriverId());
lqw.eq(bo.getReportDate() != null, HotDriverHealthReport::getReportDate, bo.getReportDate());

View File

@@ -249,7 +249,7 @@ public class SysFlowServiceImpl implements ISysFlowService {
if (StrUtil.isNotBlank(isRead)) {
lqw.eq(SysFlowTask::getIsRead, Integer.valueOf(isRead));
}
lqw.orderByDesc(SysFlowTask::getCreateTime);
lqw.orderByDesc(SysFlowTask::getCreateTime, SysFlowTask::getTaskId);
List<SysFlowTask> records = taskMapper.selectList(lqw);
fillFlowInfo(records);
filterInvalidTrainingStudyTodos(records);
@@ -264,7 +264,7 @@ public class SysFlowServiceImpl implements ISysFlowService {
if (loginUser != null && cn.hutool.core.util.StrUtil.isNotBlank(loginUser.getBusinessUserId())) {
lqw.ne(SysFlowTask::getApproverId, loginUser.getBusinessUserId());
}
lqw.orderByDesc(SysFlowTask::getCreateTime);
lqw.orderByDesc(SysFlowTask::getCreateTime, SysFlowTask::getTaskId);
List<SysFlowTask> records = taskMapper.selectList(lqw);
fillFlowInfo(records);
filterInvalidTrainingStudyTodos(records);
@@ -634,7 +634,7 @@ public class SysFlowServiceImpl implements ISysFlowService {
public List<SysFlowTaskHis> getHistory(String instanceId) {
LambdaQueryWrapper<SysFlowTaskHis> lqw = new LambdaQueryWrapper<>();
lqw.eq(SysFlowTaskHis::getInstanceId, instanceId);
lqw.orderByAsc(SysFlowTaskHis::getAuditTime);
lqw.orderByAsc(SysFlowTaskHis::getAuditTime, SysFlowTaskHis::getHisId);
return taskHisMapper.selectList(lqw);
}
@@ -757,7 +757,7 @@ public class SysFlowServiceImpl implements ISysFlowService {
SysFlowTask task = taskMapper.selectOne(
Wrappers.<SysFlowTask>lambdaQuery()
.in(SysFlowTask::getInstanceId, instanceIds)
.orderByDesc(SysFlowTask::getCreateTime)
.orderByDesc(SysFlowTask::getCreateTime, SysFlowTask::getTaskId)
.last("limit 1")
);
return task != null ? task.getTaskId() : null;

View File

@@ -91,7 +91,7 @@ public class HotCompanyNoticeServiceImpl implements IHotCompanyNoticeService {
lqw.eq(HotCompanyNotice::getIsPublished, 1L);
lqw.apply("NOT EXISTS (SELECT 1 FROM hot_company_notice_read r WHERE r.notice_id = hot_company_notice.id AND r.reader_id = {0} AND r.is_deleted = 0)", readerId);
lqw.orderByDesc(HotCompanyNotice::getIsTop);
lqw.orderByDesc(HotCompanyNotice::getCreateTime);
lqw.orderByDesc(HotCompanyNotice::getCreateTime, HotCompanyNotice::getId);
lqw.orderByDesc(HotCompanyNotice::getId);
lqw.last("limit 1");
HotCompanyNotice notice = baseMapper.selectOne(lqw);
@@ -104,7 +104,7 @@ public class HotCompanyNoticeServiceImpl implements IHotCompanyNoticeService {
private LambdaQueryWrapper<HotCompanyNotice> buildQueryWrapper(HotCompanyNoticeBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotCompanyNotice> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotCompanyNotice::getCreateTime);
lqw.orderByDesc(HotCompanyNotice::getCreateTime, HotCompanyNotice::getId);
Object beginTime = params.get("beginTime");
Object endTime = params.get("endTime");
if (beginTime != null && endTime != null) {
@@ -140,7 +140,7 @@ public class HotCompanyNoticeServiceImpl implements IHotCompanyNoticeService {
HotCompanyNotice maxSortNotice = baseMapper.selectOne(new LambdaQueryWrapper<HotCompanyNotice>()
.select(HotCompanyNotice::getSortNo)
.eq(HotCompanyNotice::getCompanyId, bo.getCompanyId())
.orderByDesc(HotCompanyNotice::getSortNo)
.orderByDesc(HotCompanyNotice::getSortNo, HotCompanyNotice::getId)
.last("limit 1"));
long maxSort = (maxSortNotice != null && maxSortNotice.getSortNo() != null) ? maxSortNotice.getSortNo() : 0L;
add.setSortNo(maxSort + 1);

View File

@@ -121,7 +121,7 @@ public class HotFileTypeConfigServiceImpl implements IHotFileTypeConfigService {
private LambdaQueryWrapper<HotFileTypeConfig> buildQueryWrapper(HotFileTypeConfigBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotFileTypeConfig> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(HotFileTypeConfig::getSortOrder).orderByDesc(BaseEntity::getCreateTime);
lqw.orderByAsc(HotFileTypeConfig::getSortOrder).orderByDesc(BaseEntity::getCreateTime).orderByDesc(HotFileTypeConfig::getId);
lqw.eq(bo.getCompanyId() != null, HotFileTypeConfig::getCompanyId, bo.getCompanyId());
lqw.like(StringUtils.isNotBlank(bo.getName()), HotFileTypeConfig::getName, bo.getName());
lqw.eq(StringUtils.isNotBlank(bo.getValue()), HotFileTypeConfig::getValue, bo.getValue());

View File

@@ -96,7 +96,7 @@ public class HotLawRegulationServiceImpl implements IHotLawRegulationService {
HotLawRegulation maxEntity = baseMapper.selectOne(Wrappers.<HotLawRegulation>lambdaQuery()
.select(HotLawRegulation::getSortno)
.eq(add.getCompanyId() != null, HotLawRegulation::getCompanyId, add.getCompanyId())
.orderByDesc(HotLawRegulation::getSortno)
.orderByDesc(HotLawRegulation::getSortno, HotLawRegulation::getId)
.last("LIMIT 1"));
Long maxSort = (maxEntity != null && maxEntity.getSortno() != null) ? maxEntity.getSortno() : 0L;
add.setSortno(maxSort + 1);

View File

@@ -359,7 +359,7 @@ public class HotNoticeReplyServiceImpl implements IHotNoticeReplyService {
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.eq(HotCompanySafetyManager::getIsResigned, 0L)
.eq(HotCompanySafetyManager::getStatus, 1L)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime, HotCompanySafetyManager::getId)
);
if (heads != null && !heads.isEmpty()) {
return String.valueOf(heads.get(0).getId());
@@ -370,7 +370,7 @@ public class HotNoticeReplyServiceImpl implements IHotNoticeReplyService {
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.eq(HotCompanySafetyManager::getIsResigned, 0L)
.eq(HotCompanySafetyManager::getStatus, 1L)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime, HotCompanySafetyManager::getId)
);
if (managers == null || managers.isEmpty()) {
return null;

View File

@@ -305,7 +305,7 @@ public class HotNotificationIssueController extends BaseController {
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.eq(HotCompanySafetyManager::getIsResigned, 0L)
.eq(HotCompanySafetyManager::getStatus, 1L)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime, HotCompanySafetyManager::getId)
);
if (heads != null && !heads.isEmpty()) {
return String.valueOf(heads.get(0).getId());
@@ -316,7 +316,7 @@ public class HotNotificationIssueController extends BaseController {
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.eq(HotCompanySafetyManager::getIsResigned, 0L)
.eq(HotCompanySafetyManager::getStatus, 1L)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime, HotCompanySafetyManager::getId)
);
if (managers == null || managers.isEmpty()) {
return null;

View File

@@ -127,7 +127,7 @@ public class HotSystemNotificationServiceImpl implements IHotSystemNotificationS
private LambdaQueryWrapper<HotSystemNotification> buildQueryWrapper(HotSystemNotificationBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotSystemNotification> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotSystemNotification::getSendTime);
lqw.orderByDesc(HotSystemNotification::getSendTime, HotSystemNotification::getId);
Object beginTime = params == null ? null : params.get("beginTime");
Object endTime = params == null ? null : params.get("endTime");
if (beginTime != null && endTime != null) {

View File

@@ -325,13 +325,13 @@ public class PayRechargeOpenServiceImpl implements PayRechargeOpenService {
.in(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus(), PayOrderStatusEnum.PAYING.getStatus())
.gt(PayOrder::getExpireTime, now)
.ge(PayOrder::getCreateTime, DateUtil.offsetMinute(now, -ORDER_EXPIRE_MINUTES))
.orderByAsc(PayOrder::getCreateTime)
.orderByAsc(PayOrder::getCreateTime, PayOrder::getId)
.last("limit " + COMPENSATE_BATCH_SIZE));
List<PayOrder> expiredOrders = payOrderMapper.selectList(Wrappers.<PayOrder>lambdaQuery()
.in(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus(), PayOrderStatusEnum.PAYING.getStatus())
.isNotNull(PayOrder::getExpireTime)
.le(PayOrder::getExpireTime, now)
.orderByAsc(PayOrder::getExpireTime)
.orderByAsc(PayOrder::getExpireTime, PayOrder::getId)
.last("limit " + COMPENSATE_BATCH_SIZE));
int queried = 0;

View File

@@ -109,7 +109,7 @@ public class HotPolicyRevisionServiceImpl implements IHotPolicyRevisionService {
private LambdaQueryWrapper<HotPolicyRevision> buildQueryWrapper(HotPolicyRevisionBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotPolicyRevision> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotPolicyRevision::getCreatedTime);
lqw.orderByDesc(HotPolicyRevision::getCreatedTime, HotPolicyRevision::getId);
lqw.eq(bo.getCompanyId() != null, HotPolicyRevision::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getPolicyId() != null, HotPolicyRevision::getPolicyId, bo.getPolicyId());
lqw.eq(bo.getVersionNo() != null, HotPolicyRevision::getVersionNo, bo.getVersionNo());
@@ -185,7 +185,7 @@ public class HotPolicyRevisionServiceImpl implements IHotPolicyRevisionService {
public Long nextVersionNo(Long policyId) {
LambdaQueryWrapper<HotPolicyRevision> lqw = Wrappers.lambdaQuery();
lqw.eq(HotPolicyRevision::getPolicyId, policyId);
lqw.orderByDesc(HotPolicyRevision::getVersionNo);
lqw.orderByDesc(HotPolicyRevision::getVersionNo, HotPolicyRevision::getId);
lqw.last("limit 1");
HotPolicyRevision last = baseMapper.selectOne(lqw);
Long lastNo = last != null ? last.getVersionNo() : 0L;

View File

@@ -1,7 +1,9 @@
package com.hotwj.platform.reportStatistics.controller;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateBo;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateSyncBo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateVo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateSyncCompareVo;
import com.hotwj.platform.reportStatistics.service.ISysReportTemplateService;
import com.hotwj.platform.reportStatistics.service.ReportRenderService;
import jakarta.servlet.http.HttpServletResponse;
@@ -97,6 +99,26 @@ public class SysReportTemplateController extends BaseController {
return R.ok();
}
/**
* 查询模板同步差异
*/
//@SaCheckPermission("report:template:sync")
@GetMapping("/sync/diff")
public R<List<SysReportTemplateSyncCompareVo>> syncDiff() {
return R.ok(sysReportTemplateService.querySyncDiffList());
}
/**
* 按选中项同步模板
*/
//@SaCheckPermission("report:template:sync")
@Log(title = "按项同步模版", businessType = BusinessType.UPDATE)
@PostMapping("/sync/selective")
public R<Void> selectiveSync(@Validated @RequestBody SysReportTemplateSyncBo bo) {
sysReportTemplateService.syncTemplatesFromClasspath(bo.getTemplateCodes());
return R.ok();
}
/**
* 删除报表模板
*/

View File

@@ -0,0 +1,19 @@
package com.hotwj.platform.reportStatistics.domain.bo;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* 报表模板按项同步请求
*/
@Data
public class SysReportTemplateSyncBo {
/**
* 需要同步的模板编码集合
*/
@NotEmpty(message = "请选择需要同步的模板")
private List<String> templateCodes;
}

View File

@@ -0,0 +1,45 @@
package com.hotwj.platform.reportStatistics.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 报表模板同步差异视图对象
*/
@Data
public class SysReportTemplateSyncCompareVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String templateCode;
private Long dbId;
private String localTemplateName;
private String dbTemplateName;
private String localContent;
private String dbContent;
private String localMockData;
private String dbMockData;
private String localRemark;
private String dbRemark;
private Boolean existsInLocal;
private Boolean existsInDb;
private Boolean syncable;
private List<String> diffTypes;
}

View File

@@ -2,6 +2,7 @@ package com.hotwj.platform.reportStatistics.service;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateBo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateVo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateSyncCompareVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -56,6 +57,16 @@ public interface ISysReportTemplateService {
*/
void syncTemplatesFromClasspath();
/**
* 查询本地模板与数据库模板的差异
*/
List<SysReportTemplateSyncCompareVo> querySyncDiffList();
/**
* 按模板编码同步本地模板到数据库
*/
void syncTemplatesFromClasspath(Collection<String> templateCodes);
/**
* 根据模板编码查找对应的 Provider
*/

View File

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hotwj.platform.reportStatistics.domain.SysReportTemplate;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateBo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateSyncCompareVo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateVo;
import com.hotwj.platform.reportStatistics.mapper.SysReportTemplateMapper;
import com.hotwj.platform.reportStatistics.service.IReportDataProvider;
@@ -16,6 +17,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.core.io.ClassPathResource;
@@ -27,9 +29,15 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 报表模板Service业务层处理
@@ -42,6 +50,9 @@ import java.util.Map;
@Service
public class SysReportTemplateServiceImpl implements ISysReportTemplateService {
private static final String CLASSPATH_SCAN_REMARK = "Synced from classpath scan";
private static final String PROVIDER_REMARK_PREFIX = "Synced from Provider: ";
private final SysReportTemplateMapper baseMapper;
private final List<IReportDataProvider> reportDataProviders;
@@ -153,114 +164,257 @@ public class SysReportTemplateServiceImpl implements ISysReportTemplateService {
@Override
@Transactional(rollbackFor = Exception.class)
public void syncTemplatesFromClasspath() {
// 1. Sync from Providers (Source of truth for Title and MockData)
syncTemplatesFromClasspath(null);
}
@Override
public List<SysReportTemplateSyncCompareVo> querySyncDiffList() {
LinkedHashMap<String, LocalTemplateSnapshot> localTemplates = loadLocalTemplateSnapshots();
Map<String, SysReportTemplateVo> dbTemplates = queryExistingTemplates(null);
Set<String> allCodes = new LinkedHashSet<>();
allCodes.addAll(localTemplates.keySet());
allCodes.addAll(dbTemplates.keySet());
return allCodes.stream()
.map(code -> buildCompareVo(code, localTemplates.get(code), dbTemplates.get(code)))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void syncTemplatesFromClasspath(Collection<String> templateCodes) {
LinkedHashMap<String, LocalTemplateSnapshot> localTemplates = loadLocalTemplateSnapshots();
Set<String> targetCodes = normalizeTemplateCodes(templateCodes);
if (targetCodes != null && targetCodes.isEmpty()) {
throw new ServiceException("请选择需要同步的模板");
}
if (targetCodes != null) {
List<String> missingCodes = targetCodes.stream()
.filter(code -> !localTemplates.containsKey(code))
.collect(Collectors.toList());
if (!missingCodes.isEmpty()) {
throw new ServiceException("以下模板在本地不存在,无法同步: " + String.join(", ", missingCodes));
}
}
Map<String, SysReportTemplateVo> dbTemplates = queryExistingTemplates(targetCodes);
for (LocalTemplateSnapshot snapshot : localTemplates.values()) {
if (targetCodes != null && !targetCodes.contains(snapshot.getTemplateCode())) {
continue;
}
SysReportTemplateVo existing = dbTemplates.get(snapshot.getTemplateCode());
if (existing == null) {
SysReportTemplate insert = new SysReportTemplate();
insert.setTemplateCode(snapshot.getTemplateCode());
insert.setTemplateName(snapshot.getTemplateName());
insert.setContent(snapshot.getContent());
insert.setMockData(snapshot.getMockData());
insert.setVersion(1);
insert.setRemark(snapshot.getRemark());
baseMapper.insert(insert);
log.info("Inserted template from local source: {}", snapshot.getTemplateCode());
continue;
}
if (!hasDiff(snapshot, existing)) {
continue;
}
SysReportTemplate update = new SysReportTemplate();
update.setId(existing.getId());
update.setTemplateCode(snapshot.getTemplateCode());
update.setTemplateName(snapshot.getTemplateName());
update.setContent(snapshot.getContent());
update.setMockData(snapshot.getMockData());
update.setRemark(snapshot.getRemark());
update.setVersion(existing.getVersion() == null ? 1 : existing.getVersion() + 1);
baseMapper.updateById(update);
log.info("Updated template from local source: {}", snapshot.getTemplateCode());
}
}
private SysReportTemplateSyncCompareVo buildCompareVo(String code, LocalTemplateSnapshot local, SysReportTemplateVo db) {
List<String> diffTypes = buildDiffTypes(local, db);
if (diffTypes.isEmpty()) {
return null;
}
SysReportTemplateSyncCompareVo vo = new SysReportTemplateSyncCompareVo();
vo.setTemplateCode(code);
vo.setDbId(db == null ? null : db.getId());
vo.setLocalTemplateName(local == null ? "" : local.getTemplateName());
vo.setDbTemplateName(db == null ? "" : db.getTemplateName());
vo.setLocalContent(local == null ? "" : local.getContent());
vo.setDbContent(db == null ? "" : normalizeText(db.getContent()));
vo.setLocalMockData(toJsonText(local == null ? null : local.getMockData()));
vo.setDbMockData(toJsonText(db == null ? null : db.getMockData()));
vo.setLocalRemark(local == null ? "" : local.getRemark());
vo.setDbRemark(db == null ? "" : normalizeText(db.getRemark()));
vo.setExistsInLocal(local != null);
vo.setExistsInDb(db != null);
vo.setSyncable(local != null);
vo.setDiffTypes(diffTypes);
return vo;
}
private boolean hasDiff(LocalTemplateSnapshot local, SysReportTemplateVo db) {
return !buildDiffTypes(local, db).isEmpty();
}
private List<String> buildDiffTypes(LocalTemplateSnapshot local, SysReportTemplateVo db) {
List<String> diffTypes = new ArrayList<>();
if (local == null && db != null) {
diffTypes.add("仅数据库");
return diffTypes;
}
if (local != null && db == null) {
diffTypes.add("仅本地");
return diffTypes;
}
if (local == null) {
return diffTypes;
}
if (!Objects.equals(normalizeText(local.getTemplateName()), normalizeText(db.getTemplateName()))) {
diffTypes.add("名称");
}
if (!Objects.equals(normalizeText(local.getContent()), normalizeText(db.getContent()))) {
diffTypes.add("内容");
}
if (!Objects.equals(toJsonText(local.getMockData()), toJsonText(db.getMockData()))) {
diffTypes.add("模拟数据");
}
if (!Objects.equals(normalizeText(local.getRemark()), normalizeText(db.getRemark()))) {
diffTypes.add("备注");
}
return diffTypes;
}
private Map<String, SysReportTemplateVo> queryExistingTemplates(Set<String> templateCodes) {
LambdaQueryWrapper<SysReportTemplate> queryWrapper = Wrappers.lambdaQuery();
if (templateCodes != null && !templateCodes.isEmpty()) {
queryWrapper.in(SysReportTemplate::getTemplateCode, templateCodes);
}
return baseMapper.selectVoList(queryWrapper).stream()
.collect(Collectors.toMap(SysReportTemplateVo::getTemplateCode, item -> item, (left, right) -> left, LinkedHashMap::new));
}
private LinkedHashMap<String, LocalTemplateSnapshot> loadLocalTemplateSnapshots() {
LinkedHashMap<String, LocalTemplateSnapshot> templateMap = new LinkedHashMap<>();
if (reportDataProviders != null) {
for (IReportDataProvider provider : reportDataProviders) {
try {
String code = provider.getTemplateCode();
if (StringUtils.isBlank(code)) continue;
SysReportTemplateVo existing = queryByCode(code);
// Read content
String content = null;
try {
ClassPathResource resource = new ClassPathResource(code);
if (resource.exists()) {
content = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
}
} catch (Exception e) {
log.warn("Template file not found for provider: {}", provider.getClass().getSimpleName());
if (StringUtils.isBlank(code)) {
continue;
}
if (existing == null) {
String content = readTemplateContent(code);
if (StringUtils.isBlank(content)) {
log.warn("Skipping provider {} because template file {} not found", provider.getClass().getSimpleName(), code);
continue;
}
SysReportTemplate template = new SysReportTemplate();
template.setTemplateCode(code);
template.setTemplateName(provider.getReportTitle());
template.setContent(content);
template.setMockData(provider.getMockData());
template.setVersion(1);
template.setRemark("Synced from Provider: " + provider.getClass().getSimpleName());
baseMapper.insert(template);
log.info("Inserted template from provider: {}", code);
} else {
// Update metadata
SysReportTemplate update = new SysReportTemplate();
update.setId(existing.getId());
boolean changed = false;
// Sync Title
String title = provider.getReportTitle();
if (StringUtils.isNotBlank(title) && !title.equals(existing.getTemplateName())) {
update.setTemplateName(title);
changed = true;
}
// Sync MockData if provided
Map<String, Object> mockData = provider.getMockData();
if (mockData != null && !mockData.isEmpty()) {
update.setMockData(mockData);
changed = true;
}
// Optional: Sync content if it was empty in DB (unlikely)
if (StringUtils.isBlank(existing.getContent()) && StringUtils.isNotBlank(content)) {
update.setContent(content);
changed = true;
}
if (changed) {
baseMapper.updateById(update);
log.info("Updated template metadata from provider: {}", code);
}
}
LocalTemplateSnapshot snapshot = new LocalTemplateSnapshot();
snapshot.setTemplateCode(code);
snapshot.setTemplateName(provider.getReportTitle());
snapshot.setContent(content);
snapshot.setMockData(provider.getMockData());
snapshot.setRemark(PROVIDER_REMARK_PREFIX + provider.getClass().getSimpleName());
templateMap.put(code, snapshot);
} catch (Exception e) {
log.error("Error syncing provider template: " + provider.getClass().getSimpleName(), e);
log.error("Error loading provider template: {}", provider.getClass().getSimpleName(), e);
}
}
}
// 2. Scan remaining files (for layouts and fragments)
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
// Find all .vm files in templates directory
Resource[] resources = resolver.getResources("classpath*:templates/**/*.vm");
for (Resource resource : resources) {
try {
String path = resource.getURL().getPath();
// Normalize path separators
path = path.replace("\\", "/");
int index = path.lastIndexOf("templates/");
if (index > -1) {
String code = path.substring(index); // Include "templates/" in the code
// Check if exists
SysReportTemplateVo exist = queryByCode(code);
if (exist == null) {
String content = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
SysReportTemplate template = new SysReportTemplate();
template.setTemplateCode(code);
// Use filename as name
template.setTemplateName(FileUtil.mainName(resource.getFilename()));
template.setContent(content);
template.setVersion(1);
template.setRemark("Synced from classpath scan");
baseMapper.insert(template);
log.info("Synced template from scan: {}", code);
}
String code = extractTemplateCode(resource);
if (StringUtils.isBlank(code) || templateMap.containsKey(code)) {
continue;
}
LocalTemplateSnapshot snapshot = new LocalTemplateSnapshot();
snapshot.setTemplateCode(code);
snapshot.setTemplateName(FileUtil.mainName(resource.getFilename()));
snapshot.setContent(IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8));
snapshot.setRemark(CLASSPATH_SCAN_REMARK);
templateMap.put(code, snapshot);
} catch (Exception e) {
log.error("Error processing resource: " + resource, e);
log.error("Error processing resource: {}", resource, e);
}
}
} catch (IOException e) {
log.error("Failed to sync templates", e);
throw new ServiceException("同步模版失败");
log.error("Failed to load local templates", e);
throw new ServiceException("读取本地模板失败");
}
return templateMap;
}
private String extractTemplateCode(Resource resource) throws IOException {
String path = resource.getURL().getPath().replace("\\", "/");
int index = path.lastIndexOf("templates/");
if (index < 0) {
return null;
}
return path.substring(index);
}
private String readTemplateContent(String code) {
try {
ClassPathResource resource = new ClassPathResource(code);
if (!resource.exists()) {
return null;
}
return IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (Exception e) {
log.warn("Template file not found: {}", code, e);
return null;
}
}
private Set<String> normalizeTemplateCodes(Collection<String> templateCodes) {
if (templateCodes == null) {
return null;
}
return templateCodes.stream()
.filter(StringUtils::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private String normalizeText(String value) {
return value == null ? "" : value;
}
private String toJsonText(Object value) {
if (value == null) {
return "";
}
return JsonUtils.toJsonString(value);
}
@lombok.Data
private static class LocalTemplateSnapshot {
private String templateCode;
private String templateName;
private String content;
private Object mockData;
private String remark;
}
}

View File

@@ -75,7 +75,7 @@ public class SysCompanyRoleServiceImpl implements ISysCompanyRoleService {
private LambdaQueryWrapper<SysCompanyRole> buildQueryWrapper(SysCompanyRoleBo bo) {
LambdaQueryWrapper<SysCompanyRole> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(SysCompanyRole::getCompanyId).orderByAsc(SysCompanyRole::getRoleSort);
lqw.orderByAsc(SysCompanyRole::getCompanyId).orderByAsc(SysCompanyRole::getRoleSort).orderByAsc(SysCompanyRole::getId);
lqw.eq(bo.getCompanyId() != null, SysCompanyRole::getCompanyId, bo.getCompanyId());
lqw.like(StringUtils.isNotBlank(bo.getRoleName()), SysCompanyRole::getRoleName, bo.getRoleName());
lqw.eq(StringUtils.isNotBlank(bo.getRoleKey()), SysCompanyRole::getRoleKey, bo.getRoleKey());
@@ -171,15 +171,29 @@ public class SysCompanyRoleServiceImpl implements ISysCompanyRoleService {
@Override
public Set<String> selectRoleKeys(Long userId, Long companyId) {
// Query HotCompanySafetyManager to get role ID
HotCompanySafetyManager manager = safetyManagerMapper.selectOne(Wrappers.<HotCompanySafetyManager>lambdaQuery()
.select(HotCompanySafetyManager::getPermissionRoleId)
// hot_company_safety_manager 可能存在重复有效数据,按最近一条记录兜底,避免登录鉴权直接失败
List<HotCompanySafetyManager> managers = safetyManagerMapper.selectList(Wrappers.<HotCompanySafetyManager>lambdaQuery()
.select(HotCompanySafetyManager::getId, HotCompanySafetyManager::getPermissionRoleId,
HotCompanySafetyManager::getUpdateTime, HotCompanySafetyManager::getCreateTime)
.eq(HotCompanySafetyManager::getUserId, userId)
.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getIsDeleted, 0)
.eq(HotCompanySafetyManager::getStatus, 1)
.and(w -> w.eq(HotCompanySafetyManager::getIsResigned, 0).or().isNull(HotCompanySafetyManager::getIsResigned))
);
.orderByDesc(HotCompanySafetyManager::getUpdateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getId));
if (managers == null || managers.isEmpty()) {
return Collections.emptySet();
}
if (managers.size() > 1) {
log.warn("查询企业角色标识命中多条安全管理人员记录,按最新一条兜底返回 userId={}, companyId={}, count={}",
userId, companyId, managers.size());
}
HotCompanySafetyManager manager = managers.get(0);
if (manager == null || manager.getPermissionRoleId() == null) {
return Collections.emptySet();

View File

@@ -163,7 +163,7 @@ public class SysCompanyServiceImpl implements ISysCompanyService {
private LambdaQueryWrapper<SysCompany> buildQueryWrapper(SysCompanyBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<SysCompany> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(SysCompany::getCreateTime);
lqw.orderByDesc(SysCompany::getCreateTime, SysCompany::getId);
lqw.like(StringUtils.isNotBlank(bo.getCompanyName()), SysCompany::getCompanyName, bo.getCompanyName());
lqw.like(StringUtils.isNotBlank(bo.getCompanyShortName()), SysCompany::getCompanyShortName, bo.getCompanyShortName());
lqw.eq(StringUtils.isNotBlank(bo.getCompanyType()), SysCompany::getCompanyType, bo.getCompanyType());
@@ -378,40 +378,61 @@ public class SysCompanyServiceImpl implements ISysCompanyService {
// 确保有企业管理员角色
Long roleId = createEnterpriseAdminRole(companyId, userId, responsibleName);
// 查找是否已存在该人员记录
LambdaQueryWrapper<HotCompanySafetyManager> smWrapper = new LambdaQueryWrapper<>();
smWrapper.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getPhone, responsibleMobile);
HotCompanySafetyManager safetyManager = hotCompanySafetyManagerMapper.selectOne(smWrapper);
HotCompanySafetyManager safetyManager = resolveResponsibleSafetyManager(companyId, userId, responsibleMobile);
if (safetyManager == null) {
safetyManager = new HotCompanySafetyManager();
fillResponsibleSafetyManager(safetyManager, companyId, responsibleName, responsibleMobile, userId, roleId);
hotCompanySafetyManagerMapper.insert(safetyManager);
log.info("[企业负责人] 创建安全管理人员记录 companyId={}, userId={}, managerId={}", companyId, userId, safetyManager.getId());
} else {
fillResponsibleSafetyManager(safetyManager, companyId, responsibleName, responsibleMobile, userId, roleId);
hotCompanySafetyManagerMapper.updateById(safetyManager);
log.info("[企业负责人] 更新安全管理人员记录 companyId={}, userId={}, managerId={}", companyId, userId, safetyManager.getId());
}
}
private HotCompanySafetyManager resolveResponsibleSafetyManager(Long companyId, Long userId, String responsibleMobile) {
List<HotCompanySafetyManager> matches = hotCompanySafetyManagerMapper.selectList(Wrappers.<HotCompanySafetyManager>lambdaQuery()
.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getUserId, userId)
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.orderByDesc(HotCompanySafetyManager::getCompanyHead)
.orderByDesc(HotCompanySafetyManager::getUpdateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getId));
if (CollUtil.isEmpty(matches) && StringUtils.isNotBlank(responsibleMobile)) {
matches = hotCompanySafetyManagerMapper.selectList(Wrappers.<HotCompanySafetyManager>lambdaQuery()
.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getPhone, responsibleMobile)
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.orderByDesc(HotCompanySafetyManager::getCompanyHead)
.orderByDesc(HotCompanySafetyManager::getUpdateTime)
.orderByDesc(HotCompanySafetyManager::getCreateTime)
.orderByDesc(HotCompanySafetyManager::getId));
}
if (CollUtil.isEmpty(matches)) {
return null;
}
if (matches.size() > 1) {
log.warn("[企业负责人] 命中多条安全管理人员记录,按最新一条更新 companyId={}, userId={}, phone={}, count={}",
companyId, userId, responsibleMobile, matches.size());
}
return matches.get(0);
}
private void fillResponsibleSafetyManager(HotCompanySafetyManager safetyManager, Long companyId, String responsibleName,
String responsibleMobile, Long userId, Long roleId) {
safetyManager.setCompanyId(companyId);
safetyManager.setName(responsibleName);
safetyManager.setPhone(responsibleMobile);
safetyManager.setUserId(userId);
// 设置为企业负责人
safetyManager.setCompanyHead(1L);
// 企业负责人权限变更
safetyManager.setPermissionType("company_admin");
safetyManager.setPermissionRoleId(roleId);
safetyManager.setIsResigned(0L);
safetyManager.setStatus(1L);
hotCompanySafetyManagerMapper.insert(safetyManager);
log.info("[企业负责人] 创建安全管理人员记录 companyId={}, userId={}, managerId={}", companyId, userId, safetyManager.getId());
} else {
// 更新现有记录
safetyManager.setName(responsibleName);
safetyManager.setUserId(userId);
safetyManager.setCompanyHead(1L);
safetyManager.setPermissionType("company_admin");
safetyManager.setPermissionRoleId(roleId);
safetyManager.setIsDeleted(0L);
safetyManager.setIsResigned(0L);
safetyManager.setStatus(1L);
hotCompanySafetyManagerMapper.updateById(safetyManager);
log.info("[企业负责人] 更新安全管理人员记录 companyId={}, userId={}, managerId={}", companyId, userId, safetyManager.getId());
}
}
public Long createEnterpriseAdminRole(Long companyId, Long userId, String username) {
@@ -486,23 +507,7 @@ public class SysCompanyServiceImpl implements ISysCompanyService {
// 1. 处理旧负责人:移除相关权限,标记离职(仅当负责人手机号变更时)
String oldMobile = oldCompany.getResponsibleMobile();
if (StrUtil.isNotBlank(oldMobile) && !StrUtil.equals(oldMobile, bo.getResponsibleMobile())) {
SysUser oldUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, oldMobile));
if (oldUser != null) {
Long oldUserId = oldUser.getUserId();
// 移除该用户在当前企业的角色关联
sysUserRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
.eq(SysUserRole::getUserId, oldUserId)
.eq(SysUserRole::getLoginPort, ENTERPRISE_PORT)
.eq(SysUserRole::getCompanyId, bo.getId()));
// 移除该用户与企业的关联关系
sysUserLoginPortMapper.delete(new LambdaQueryWrapper<SysUserLoginPort>()
.eq(SysUserLoginPort::getUserId, oldUserId)
.eq(SysUserLoginPort::getLoginPort, ENTERPRISE_PORT)
.eq(SysUserLoginPort::getCompanyId, bo.getId()));
hotCompanySafetyManagerMapper.deleteCompanyHeadByPhone(bo.getId(), oldMobile);
}
cleanupOldResponsibleData(bo.getId(), oldMobile);
}
// 2. 添加新负责人:创建用户(如果不存在)、赋予权限、添加安全管理人员记录
@@ -512,6 +517,39 @@ public class SysCompanyServiceImpl implements ISysCompanyService {
return flag;
}
private void cleanupOldResponsibleData(Long companyId, String oldMobile) {
if (companyId == null || StrUtil.isBlank(oldMobile)) {
return;
}
SysUser oldUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, oldMobile));
Long oldUserId = oldUser != null ? oldUser.getUserId() : null;
if (oldUserId != null) {
sysUserRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
.eq(SysUserRole::getUserId, oldUserId)
.eq(SysUserRole::getLoginPort, ENTERPRISE_PORT)
.eq(SysUserRole::getCompanyId, companyId));
sysUserLoginPortMapper.delete(new LambdaQueryWrapper<SysUserLoginPort>()
.eq(SysUserLoginPort::getUserId, oldUserId)
.eq(SysUserLoginPort::getLoginPort, ENTERPRISE_PORT)
.eq(SysUserLoginPort::getCompanyId, companyId));
}
LambdaQueryWrapper<HotCompanySafetyManager> wrapper = Wrappers.<HotCompanySafetyManager>lambdaQuery()
.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getCompanyHead, 1L)
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.and(w -> {
w.eq(HotCompanySafetyManager::getPhone, oldMobile);
if (oldUserId != null) {
w.or().eq(HotCompanySafetyManager::getUserId, oldUserId);
}
});
int deleted = hotCompanySafetyManagerMapper.delete(wrapper);
if (deleted > 0) {
log.info("[企业负责人] 清理旧负责人安全管理人员记录 companyId={}, oldMobile={}, oldUserId={}, deleted={}",
companyId, oldMobile, oldUserId, deleted);
}
}
/**
* 保存前的数据校验
*/

View File

@@ -142,7 +142,7 @@ public class HotCompanyChangeHistoryServiceImpl implements IHotCompanyChangeHist
lqw.like(StringUtils.isNotBlank(bo.getName()), HotCompanyChangeHistory::getName, bo.getName());
lqw.like(StringUtils.isNotBlank(bo.getPhone()), HotCompanyChangeHistory::getPhone, bo.getPhone());
lqw.eq(StringUtils.isNotBlank(bo.getChangeType()), HotCompanyChangeHistory::getChangeType, bo.getChangeType());
lqw.orderByDesc(HotCompanyChangeHistory::getChangeTime);
lqw.orderByDesc(HotCompanyChangeHistory::getChangeTime, HotCompanyChangeHistory::getId);
return lqw;
}

View File

@@ -176,7 +176,7 @@ public class HotCompanyPermissionGrantServiceImpl implements IHotCompanyPermissi
lqw.eq(StringUtils.isNotBlank(bo.getPermissionCode()), HotCompanyPermissionGrant::getPermissionCode, bo.getPermissionCode());
lqw.eq(bo.getStatus() != null, HotCompanyPermissionGrant::getStatus, bo.getStatus());
lqw.eq(HotCompanyPermissionGrant::getIsDeleted, 0L);
lqw.orderByDesc(HotCompanyPermissionGrant::getCreateTime);
lqw.orderByDesc(HotCompanyPermissionGrant::getCreateTime, HotCompanyPermissionGrant::getId);
return lqw;
}

View File

@@ -107,7 +107,7 @@ public class HotCompanyPolicyServiceImpl implements IHotCompanyPolicyService {
.select(HotCompanyPolicy::getOrderNum)
.eq(HotCompanyPolicy::getCompanyId, bo.getCompanyId())
.eq(HotCompanyPolicy::getPolicyType, bo.getPolicyType())
.orderByDesc(HotCompanyPolicy::getOrderNum)
.orderByDesc(HotCompanyPolicy::getOrderNum, HotCompanyPolicy::getId)
.last("limit 1"));
long maxOrder = (maxOrderPolicy != null && maxOrderPolicy.getOrderNum() != null) ? maxOrderPolicy.getOrderNum() : 0L;
add.setOrderNum(maxOrder + 1);

View File

@@ -451,6 +451,8 @@ public class HotCompanySafetyManagerServiceImpl implements IHotCompanySafetyMana
userId = sysUser.getUserId();
}
validateManagerUniqueness(add.getCompanyId(), add.getPhone(), userId, null);
// 初始化或更新子密码(特定企业的登录密码)
String subPassword;
if (StringUtils.isNotBlank(add.getLoginPassword())) {
@@ -521,6 +523,9 @@ public class HotCompanySafetyManagerServiceImpl implements IHotCompanySafetyMana
if (before == null) {
throw new ServiceException("安全管理人员不存在");
}
Long finalCompanyId = update.getCompanyId() != null ? update.getCompanyId() : before.getCompanyId();
String finalPhone = StringUtils.isNotBlank(update.getPhone()) ? update.getPhone() : before.getPhone();
validateManagerUniqueness(finalCompanyId, finalPhone, before.getUserId(), update.getId());
// 参考新增:编辑时如果更换部门,且目标部门暂无负责人、当前人员又不是负责人,则不允许变更
boolean deptChanged = update.getDeptId() != null && !Objects.equals(update.getDeptId(), before.getDeptId());
Long finalIsDepartmentHead = update.getIsDepartmentHead() != null ? update.getIsDepartmentHead() : before.getIsDepartmentHead();
@@ -575,6 +580,39 @@ public class HotCompanySafetyManagerServiceImpl implements IHotCompanySafetyMana
);
}
private void validateManagerUniqueness(Long companyId, String phone, Long userId, Long excludeId) {
if (companyId == null) {
return;
}
if (StringUtils.isNotBlank(phone)) {
LambdaQueryWrapper<HotCompanySafetyManager> phoneWrapper = Wrappers.<HotCompanySafetyManager>lambdaQuery()
.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getPhone, phone)
.eq(HotCompanySafetyManager::getIsDeleted, 0L);
if (excludeId != null) {
phoneWrapper.ne(HotCompanySafetyManager::getId, excludeId);
}
if (baseMapper.selectCount(phoneWrapper) > 0) {
throw new ServiceException("同一公司下手机号不允许重复");
}
}
if (userId == null) {
return;
}
LambdaQueryWrapper<HotCompanySafetyManager> userWrapper = Wrappers.<HotCompanySafetyManager>lambdaQuery()
.eq(HotCompanySafetyManager::getCompanyId, companyId)
.eq(HotCompanySafetyManager::getUserId, userId)
.eq(HotCompanySafetyManager::getIsDeleted, 0L)
.eq(HotCompanySafetyManager::getStatus, 1L)
.and(w -> w.eq(HotCompanySafetyManager::getIsResigned, 0L).or().isNull(HotCompanySafetyManager::getIsResigned));
if (excludeId != null) {
userWrapper.ne(HotCompanySafetyManager::getId, excludeId);
}
if (baseMapper.selectCount(userWrapper) > 0) {
throw new ServiceException("同一公司下该用户已存在有效的安全管理人员记录");
}
}
private void syncUserAndRecordHistory(HotCompanySafetyManager update, HotCompanySafetyManager before, String changeReason, String beforeBusiness) {
SysUser sysUser = findUserByPhone(before.getPhone());
if (sysUser == null) {

View File

@@ -88,7 +88,7 @@ public class HotVehicleAnnualReviewServiceImpl implements IHotVehicleAnnualRevie
return baseMapper.selectVoOne(new LambdaQueryWrapper<HotVehicleAnnualReview>()
.eq(HotVehicleAnnualReview::getVehicleId, vehicleId)
.eq(HotVehicleAnnualReview::getIsDeleted, 0L)
.orderByDesc(HotVehicleAnnualReview::getReviewDate)
.orderByDesc(HotVehicleAnnualReview::getReviewDate, HotVehicleAnnualReview::getId)
.last("LIMIT 1"));
}
@@ -97,7 +97,7 @@ public class HotVehicleAnnualReviewServiceImpl implements IHotVehicleAnnualRevie
LambdaQueryWrapper<HotVehicleAnnualReview> lqw = new LambdaQueryWrapper<HotVehicleAnnualReview>()
.eq(HotVehicleAnnualReview::getVehicleId, vehicleId)
.eq(HotVehicleAnnualReview::getIsDeleted, 0L)
.orderByDesc(HotVehicleAnnualReview::getReviewDate)
.orderByDesc(HotVehicleAnnualReview::getReviewDate, HotVehicleAnnualReview::getId)
.last("LIMIT 1");
if (startDate != null) {
lqw.ge(HotVehicleAnnualReview::getReviewDate, startDate);

View File

@@ -82,7 +82,7 @@ public class HotVehicleChangeServiceImpl implements IHotVehicleChangeService {
private LambdaQueryWrapper<HotVehicleChange> buildQueryWrapper(HotVehicleChangeBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicleChange> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotVehicleChange::getCreateTime);
lqw.orderByDesc(HotVehicleChange::getCreateTime, HotVehicleChange::getId);
lqw.eq(bo.getCompanyId() != null, HotVehicleChange::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getVehicleId() != null, HotVehicleChange::getVehicleId, bo.getVehicleId());
lqw.like(StringUtils.isNotBlank(bo.getPlateNumber()), HotVehicleChange::getPlateNumber, bo.getPlateNumber());

View File

@@ -226,7 +226,7 @@ public class HotVehicleClaimServiceImpl implements IHotVehicleClaimService {
public HotVehicleClaimVo queryLatestByVehicleId(String vehicleId) {
return baseMapper.selectVoOne(new LambdaQueryWrapper<HotVehicleClaim>()
.eq(HotVehicleClaim::getVehicleId, vehicleId)
.orderByDesc(HotVehicleClaim::getIncidentTime)
.orderByDesc(HotVehicleClaim::getIncidentTime, HotVehicleClaim::getId)
.last("LIMIT 1"));
}
}

View File

@@ -81,7 +81,7 @@ public class HotVehicleMaintenanceServiceImpl implements IHotVehicleMaintenanceS
return baseMapper.selectVoOne(new LambdaQueryWrapper<HotVehicleMaintenance>()
.eq(HotVehicleMaintenance::getVehicleId, vehicleId)
.eq(HotVehicleMaintenance::getIsDeleted, 0L)
.orderByDesc(HotVehicleMaintenance::getFinishTime)
.orderByDesc(HotVehicleMaintenance::getFinishTime, HotVehicleMaintenance::getId)
.last("LIMIT 1"));
}

View File

@@ -85,6 +85,16 @@ public class HotVehicleController extends BaseController {
return hotVehicleService.queryPageListByStatus(bo, pageQuery);
}
/**
* 查询公司车辆信息列表(状态为数组,不过滤已绑定驾驶员)
*/
//@SaCheckPermission("resourceManagement:vehicle:list")
@GetMapping("/listByStatusAll")
@Operation(summary = "查询公司车辆信息列表(状态为数组,不过滤已绑定驾驶员)")
public TableDataInfo<HotVehicleVo> listByStatusAll(HotVehicleStatusBo bo, PageQuery pageQuery) {
return hotVehicleService.queryPageListByStatusAll(bo, pageQuery);
}
/**
* 查询当前企业可绑定的车辆列表
*/

View File

@@ -64,6 +64,15 @@ public interface IHotVehicleService {
*/
TableDataInfo<HotVehicleVo> queryPageListByStatus(HotVehicleStatusBo bo, PageQuery pageQuery);
/**
* 分页查询公司车辆信息列表(不过滤已绑定驾驶员)
*
* @param bo 查询条件 车辆状态为字符串逗号拼接
* @param pageQuery 分页参数
* @return 公司车辆信息分页列表
*/
TableDataInfo<HotVehicleVo> queryPageListByStatusAll(HotVehicleStatusBo bo, PageQuery pageQuery);
/**
* 查询当前企业可绑定的车辆列表
*

View File

@@ -195,7 +195,15 @@ public class HotVehicleServiceImpl implements IHotVehicleService {
*/
@Override
public TableDataInfo<HotVehicleVo> queryPageListByStatus(HotVehicleStatusBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<HotVehicle> lqw = buildQueryWrapperByStatus(bo);
LambdaQueryWrapper<HotVehicle> lqw = buildQueryWrapperByStatus(bo, true);
Page<HotVehicleVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
fillCurrentBindingPersonName(result.getRecords());
return TableDataInfo.build(result);
}
@Override
public TableDataInfo<HotVehicleVo> queryPageListByStatusAll(HotVehicleStatusBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<HotVehicle> lqw = buildQueryWrapperByStatus(bo, false);
Page<HotVehicleVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
fillCurrentBindingPersonName(result.getRecords());
return TableDataInfo.build(result);
@@ -212,6 +220,7 @@ public class HotVehicleServiceImpl implements IHotVehicleService {
LambdaQueryWrapper<HotVehicle> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(HotVehicle::getArchiveNo)
.orderByAsc(HotVehicle::getPlateNumber)
.orderByAsc(HotVehicle::getId)
.eq(bo.getCompanyId() != null, HotVehicle::getCompanyId, bo.getCompanyId())
.eq(HotVehicle::getVehicleStatus, 1L)
.eq(HotVehicle::getOperationStatus, 1L)
@@ -238,7 +247,8 @@ public class HotVehicleServiceImpl implements IHotVehicleService {
.eq(HotVehicle::getCompanyId, companyId)
.eq(HotVehicle::getIsDeleted, 0L)
.orderByAsc(HotVehicle::getArchiveNo)
.orderByAsc(HotVehicle::getPlateNumber));
.orderByAsc(HotVehicle::getPlateNumber)
.orderByAsc(HotVehicle::getId));
List<DriverBindingVehicleItemVo> boundVehicleList = new ArrayList<>();
List<DriverBindingVehicleItemVo> bindableVehicleList = new ArrayList<>();
@@ -266,7 +276,8 @@ public class HotVehicleServiceImpl implements IHotVehicleService {
.eq(HotVehicle::getCompanyId, companyId)
.eq(HotVehicle::getIsDeleted, 0L)
.orderByAsc(HotVehicle::getArchiveNo)
.orderByAsc(HotVehicle::getPlateNumber));
.orderByAsc(HotVehicle::getPlateNumber)
.orderByAsc(HotVehicle::getId));
List<DriverBindingVehicleItemVo> boundVehicleList = new ArrayList<>();
for (HotVehicle vehicle : companyVehicles) {
if (splitCsv(vehicle.getCurrentDriver()).contains(driverId)) {
@@ -330,7 +341,8 @@ public class HotVehicleServiceImpl implements IHotVehicleService {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicle> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(HotVehicle::getOperationStatus)
.orderByAsc(HotVehicle::getArchiveNo);
.orderByAsc(HotVehicle::getArchiveNo)
.orderByAsc(HotVehicle::getId);
lqw.eq(bo.getCompanyId() != null, HotVehicle::getCompanyId, bo.getCompanyId());
lqw.in(CollUtil.isNotEmpty(bo.getIds()), HotVehicle::getId, bo.getIds());
lqw.eq(StringUtils.isNotBlank(bo.getGpsParams()), HotVehicle::getGpsParams, bo.getGpsParams());
@@ -413,18 +425,20 @@ public class HotVehicleServiceImpl implements IHotVehicleService {
return lqw;
}
private LambdaQueryWrapper<HotVehicle> buildQueryWrapperByStatus(HotVehicleStatusBo bo) {
private LambdaQueryWrapper<HotVehicle> buildQueryWrapperByStatus(HotVehicleStatusBo bo, boolean onlyUnboundDriver) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicle> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotVehicle::getCreateTime);
lqw.orderByDesc(HotVehicle::getCreateTime, HotVehicle::getId);
lqw.eq(bo.getCompanyId() != null, HotVehicle::getCompanyId, bo.getCompanyId());
lqw.eq(StringUtils.isNotBlank(bo.getGpsParams()), HotVehicle::getGpsParams, bo.getGpsParams());
lqw.eq(bo.getIsTrailer() != null, HotVehicle::getIsTrailer, bo.getIsTrailer());
lqw.eq(bo.getVehicleStatus() != null, HotVehicle::getVehicleStatus, bo.getVehicleStatus());
lqw.in(HotVehicle::getOperationStatus, Arrays.asList(bo.getOperationStatus().split(",")));
if (onlyUnboundDriver) {
lqw.and(wrapper -> wrapper.isNull(HotVehicle::getCurrentDriver)
.or()
.eq(HotVehicle::getCurrentDriver, ""));
}
lqw.eq(StringUtils.isNotBlank(bo.getBodyImageUrls()), HotVehicle::getBodyImageUrls, bo.getBodyImageUrls());
lqw.eq(StringUtils.isNotBlank(bo.getOtherAttachmentUrls()), HotVehicle::getOtherAttachmentUrls, bo.getOtherAttachmentUrls());
lqw.eq(StringUtils.isNotBlank(bo.getColor()), HotVehicle::getColor, bo.getColor());

View File

@@ -129,7 +129,7 @@ public class HotVehicleMileageServiceImpl implements IHotVehicleMileageService {
public List<HotVehicleMileageVo> queryListByVehicleId(String vehicleId) {
LambdaQueryWrapper<HotVehicleMileage> lqw = Wrappers.lambdaQuery();
lqw.eq(HotVehicleMileage::getVehicleId, vehicleId);
lqw.orderByDesc(HotVehicleMileage::getStartDate);
lqw.orderByDesc(HotVehicleMileage::getStartDate, HotVehicleMileage::getId);
return baseMapper.selectVoList(lqw);
}

View File

@@ -87,7 +87,7 @@ public class HotVehicleRepairServiceImpl implements IHotVehicleRepairService {
public HotVehicleRepairVo queryLatestByVehicleId(String vehicleId) {
return baseMapper.selectVoOne(new LambdaQueryWrapper<HotVehicleRepair>()
.eq(HotVehicleRepair::getVehicleId, vehicleId)
.orderByDesc(HotVehicleRepair::getFinishDate)
.orderByDesc(HotVehicleRepair::getFinishDate, HotVehicleRepair::getId)
.last("LIMIT 1"));
}

View File

@@ -100,7 +100,7 @@ public class HotAccidentArchiveServiceImpl implements IHotAccidentArchiveService
private LambdaQueryWrapper<HotAccidentArchive> buildQueryWrapper(HotAccidentArchiveBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotAccidentArchive> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotAccidentArchive::getAccidentTime);
lqw.orderByDesc(HotAccidentArchive::getAccidentTime, HotAccidentArchive::getId);
if (bo.getAccidentTime() != null) {
java.util.Calendar calendarStart = java.util.Calendar.getInstance();
calendarStart.setTime(bo.getAccidentTime());

View File

@@ -122,7 +122,7 @@ public class HotHiddenDangerServiceImpl implements IHotHiddenDangerService {
} else if (hasEndTime) {
lqw.le(HotHiddenDanger::getCheckTime, endTime);
}
lqw.orderByDesc(HotHiddenDanger::getCreateTime);
lqw.orderByDesc(HotHiddenDanger::getCreateTime, HotHiddenDanger::getId);
lqw.eq(bo.getCompanyId() != null, HotHiddenDanger::getCompanyId, bo.getCompanyId());
lqw.eq(StringUtils.isNotBlank(bo.getDangerNo()), HotHiddenDanger::getDangerNo, bo.getDangerNo());
lqw.like(StringUtils.isNotBlank(bo.getDangerName()), HotHiddenDanger::getDangerName, bo.getDangerName());

View File

@@ -131,11 +131,12 @@ public class HotHiddenDangerInspectionController extends BaseController {
@GetMapping("/hidden-danger-plans/page")
@Operation(
summary = "移动端分页查询隐患排查计划列表",
description = "权限说明:驾驶员仅可查看本人及所属车辆当月全部计划和历史未做计划;管理员可查看企业当月全部计划和历史未做计划。"
description = "权限说明:驾驶员仅可查看当前处于原始周期或补查周期内、且属于本人及所属车辆的计划;管理员可查看企业内当前处于有效时间范围的计划。"
)
public TableDataInfo<HotHiddenDangerInspectionVo> hiddenDangerPlanPage(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "planId", required = false) Long planId,
PageQuery pageQuery) {
return hotHiddenDangerInspectionService.queryMobilePlanPage(keyword, pageQuery);
return hotHiddenDangerInspectionService.queryMobilePlanPage(keyword, planId, pageQuery);
}
}

View File

@@ -80,6 +80,16 @@ public class HotHiddenDangerInspection extends BaseEntity {
*/
private Date checkDate;
/**
* 是否补查期间处理0否 1是
*/
private Long isRecheckProcessed;
/**
* 补查处理时间
*/
private Date recheckProcessedTime;
/**
* 评估人id
*/

View File

@@ -86,6 +86,16 @@ public class HotHiddenDangerInspectionBo extends BaseEntity {
// @NotNull(message = "排查日期不能为空", groups = {AddGroup.class, EditGroup.class})
private Date checkDate;
/**
* 是否补查期间处理0否 1是
*/
private Long isRecheckProcessed;
/**
* 补查处理时间
*/
private Date recheckProcessedTime;
/**
* 评估人id
*/

View File

@@ -94,6 +94,18 @@ public class HotHiddenDangerInspectionVo implements Serializable {
@ExcelProperty(value = "排查日期")
private Date checkDate;
/**
* 是否补查期间处理0否 1是
*/
@ExcelProperty(value = "是否补查期间处理")
private Long isRecheckProcessed;
/**
* 补查处理时间
*/
@ExcelProperty(value = "补查处理时间")
private Date recheckProcessedTime;
/**
* 评估人id
*/

View File

@@ -23,6 +23,6 @@ public interface HotHiddenDangerInspectionMapper extends BaseMapperPlus<HotHidde
@Param("isAdmin") Boolean isAdmin,
@Param("driverId") String driverId,
@Param("vehicleId") String vehicleId,
@Param("keyword") String keyword,
@Param("currentMonth") String currentMonth);
@Param("planId") Long planId,
@Param("keyword") String keyword);
}

View File

@@ -33,7 +33,7 @@ public interface IHotHiddenDangerInspectionService {
*/
TableDataInfo<HotHiddenDangerInspectionVo> queryPageList(HotHiddenDangerInspectionBo bo, PageQuery pageQuery);
TableDataInfo<HotHiddenDangerInspectionVo> queryMobilePlanPage(String keyword, PageQuery pageQuery);
TableDataInfo<HotHiddenDangerInspectionVo> queryMobilePlanPage(String keyword, Long planId, PageQuery pageQuery);
/**
* 查询符合条件的隐患排查-排查项目列表

View File

@@ -17,6 +17,9 @@ import com.hotwj.platform.securityManagement.hiddenDangerInspection.domain.vo.Ho
import com.hotwj.platform.securityManagement.hiddenDangerInspection.domain.vo.HotHiddenDangerInspectionVo;
import com.hotwj.platform.securityManagement.hiddenDangerInspection.mapper.HotHiddenDangerInspectionAuditHistoryMapper;
import com.hotwj.platform.securityManagement.hiddenDangerInspection.mapper.HotHiddenDangerInspectionMapper;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.HotHiddenDangerPlan;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.mapper.HotHiddenDangerPlanMapper;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.service.HiddenDangerPlanTimeWindowHelper;
import com.hotwj.platform.securityManagement.hiddenDangerInspection.service.IHotHiddenDangerInspectionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,7 +32,6 @@ import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.YearMonth;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
@@ -52,6 +54,8 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
private final ISysFlowService flowService;
private final IHotSystemNotificationService notificationService;
private final SignatureVerifyService signatureVerifyService;
private final HotHiddenDangerPlanMapper planMapper;
private final HiddenDangerPlanTimeWindowHelper timeWindowHelper;
/**
* 查询隐患排查-排查项目
@@ -88,7 +92,7 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
}
@Override
public TableDataInfo<HotHiddenDangerInspectionVo> queryMobilePlanPage(String keyword, PageQuery pageQuery) {
public TableDataInfo<HotHiddenDangerInspectionVo> queryMobilePlanPage(String keyword, Long planId, PageQuery pageQuery) {
Long companyId = LoginHelper.getCompanyId();
if (companyId == null) {
throw new ServiceException("当前登录用户无公司信息");
@@ -101,7 +105,6 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
driverId = driver.getId();
vehicleId = driver.getVehicleId();
}
String currentMonth = YearMonth.now().toString();
Page<HotHiddenDangerInspectionVo> page = pageQuery.build();
List<HotHiddenDangerInspectionVo> rows = baseMapper.selectMobilePlanPage(
page,
@@ -109,8 +112,8 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
!isDriver,
driverId,
vehicleId,
StringUtils.trimToNull(keyword),
currentMonth
planId,
StringUtils.trimToNull(keyword)
);
page.setRecords(rows);
fillFlowExtras(rows);
@@ -209,8 +212,14 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
if (original == null) {
throw new ServiceException("排查记录不存在");
}
HotHiddenDangerPlan plan = getPlanByInspectionOrThrow(original);
Date now = new Date();
if (!timeWindowHelper.canProcess(now, plan)) {
throw new ServiceException("当前计划未处于可处理时间范围内");
}
HotHiddenDangerInspection update = MapstructUtils.convert(bo, HotHiddenDangerInspection.class);
validEntityBeforeSave(update);
markRecheckProcessIfNeeded(update, original, plan, now);
String signImgUrl = bo.getSignImgUrl();
if (DriverLoginContextHelper.isDriverPort() && StringUtils.isNotBlank(signImgUrl)) {
String checkerName = bo.getCheckerName();
@@ -274,6 +283,25 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
}
return flag;
}
/**
* 标记是否需要补查处理
*/
private void markRecheckProcessIfNeeded(HotHiddenDangerInspection update,
HotHiddenDangerInspection original,
HotHiddenDangerPlan plan,
Date now) {
if (update == null || original == null || plan == null || now == null) {
return;
}
if (!timeWindowHelper.isInRecheckWindow(now, plan)) {
return;
}
if (Long.valueOf(1L).equals(original.getIsRecheckProcessed())) {
return;
}
update.setIsRecheckProcessed(1L);
update.setRecheckProcessedTime(now);
}
private Long parseSignatureOssId(String signImgUrl) {
String first = signImgUrl.split(",")[0].trim();
@@ -518,6 +546,7 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
.eq(HotHiddenDangerInspectionAuditHistory::getIsDeleted, 0L)
.orderByDesc(HotHiddenDangerInspectionAuditHistory::getAuditRound)
.orderByDesc(HotHiddenDangerInspectionAuditHistory::getCreateTime)
.orderByDesc(HotHiddenDangerInspectionAuditHistory::getId)
);
}
@@ -538,6 +567,22 @@ public class HotHiddenDangerInspectionServiceImpl implements IHotHiddenDangerIns
vo.setTaskId(taskId);
}
private HotHiddenDangerPlan getPlanByInspectionOrThrow(HotHiddenDangerInspection inspection) {
if (inspection == null || inspection.getPlanId() == null) {
throw new ServiceException("排查计划不存在");
}
HotHiddenDangerPlan plan = planMapper.selectOne(
Wrappers.<HotHiddenDangerPlan>lambdaQuery()
.eq(HotHiddenDangerPlan::getId, inspection.getPlanId())
.eq(HotHiddenDangerPlan::getIsDeleted, 0L)
.last("LIMIT 1")
);
if (plan == null) {
throw new ServiceException("排查计划不存在");
}
return plan;
}
/**
* 保存前的数据校验
*/

View File

@@ -2,6 +2,8 @@ package com.hotwj.platform.securityManagement.hiddenDangerPlan.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.bo.HotHiddenDangerPlanBo;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.bo.HotHiddenDangerPlanRecheckBo;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.bo.HotHiddenDangerPlanRecheckCloseBo;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.vo.HotHiddenDangerPlanVo;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.service.IHotHiddenDangerPlanService;
import io.swagger.v3.oas.annotations.Operation;
@@ -130,6 +132,39 @@ public class HotHiddenDangerPlanController extends BaseController {
return toAjax(hotHiddenDangerPlanService.updateByBo(bo));
}
/**
* 开启补查
*/
@Log(title = "隐患排查补查", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/recheck/open")
@Operation(summary = "开启隐患排查补查")
public R<Void> openRecheck(@Validated @RequestBody HotHiddenDangerPlanRecheckBo bo) {
return toAjax(hotHiddenDangerPlanService.openRecheck(bo.getPlanId(), bo.getRecheckStartTime(), bo.getRecheckEndTime()));
}
/**
* 修改补查
*/
@Log(title = "隐患排查补查", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/recheck/update")
@Operation(summary = "修改隐患排查补查")
public R<Void> updateRecheck(@Validated @RequestBody HotHiddenDangerPlanRecheckBo bo) {
return toAjax(hotHiddenDangerPlanService.updateRecheck(bo.getPlanId(), bo.getRecheckStartTime(), bo.getRecheckEndTime()));
}
/**
* 关闭补查
*/
@Log(title = "隐患排查补查", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/recheck/close")
@Operation(summary = "关闭隐患排查补查")
public R<Void> closeRecheck(@Validated @RequestBody HotHiddenDangerPlanRecheckCloseBo bo) {
return toAjax(hotHiddenDangerPlanService.closeRecheck(bo.getPlanId()));
}
/**
* 删除隐患排查
*

View File

@@ -60,6 +60,21 @@ public class HotHiddenDangerPlan extends BaseEntity {
*/
private Date endDate;
/**
* 是否开启补查0否 1是
*/
private Long isRecheck;
/**
* 补查开始时间
*/
private Date recheckStartTime;
/**
* 补查结束时间
*/
private Date recheckEndTime;
/**
* 进度 0=待处理 1=审核中 2=评估 3=治理 4=复查 5=完结
*/

View File

@@ -63,6 +63,21 @@ public class HotHiddenDangerPlanBo extends BaseEntity {
@NotNull(message = "结束日期不能为空", groups = {AddGroup.class, EditGroup.class})
private Date endDate;
/**
* 是否开启补查0否 1是
*/
private Long isRecheck;
/**
* 补查开始时间
*/
private Date recheckStartTime;
/**
* 补查结束时间
*/
private Date recheckEndTime;
/**
* 进度 0=待处理 1=审核中 2=评估 3=治理 4=复查 5=完结
*/

View File

@@ -0,0 +1,37 @@
package com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Date;
/**
* 隐患排查补查开启/修改请求对象
*
* @author shihongwei
* @date 2026-06-03
*/
@Data
public class HotHiddenDangerPlanRecheckBo {
/**
* 计划ID
*/
@NotNull(message = "计划ID不能为空")
private Long planId;
/**
* 补查开始时间
*/
@NotNull(message = "补查开始时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date recheckStartTime;
/**
* 补查结束时间
*/
@NotNull(message = "补查结束时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date recheckEndTime;
}

View File

@@ -0,0 +1,20 @@
package com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 隐患排查补查关闭请求对象
*
* @author shihongwei
* @date 2026-06-03
*/
@Data
public class HotHiddenDangerPlanRecheckCloseBo {
/**
* 计划ID
*/
@NotNull(message = "计划ID不能为空")
private Long planId;
}

View File

@@ -67,6 +67,24 @@ public class HotHiddenDangerPlanVo implements Serializable {
@ExcelProperty(value = "结束日期")
private Date endDate;
/**
* 是否开启补查0否 1是
*/
@ExcelProperty(value = "是否开启补查")
private Long isRecheck;
/**
* 补查开始时间
*/
@ExcelProperty(value = "补查开始时间")
private Date recheckStartTime;
/**
* 补查结束时间
*/
@ExcelProperty(value = "补查结束时间")
private Date recheckEndTime;
/**
* 进度 0=待处理 1=审核中 2=评估 3=治理 4=复查 5=完结
*/
@@ -97,5 +115,23 @@ public class HotHiddenDangerPlanVo implements Serializable {
@ExcelProperty(value = "计划周期Key")
private String planDateKey;
/**
* 已完成数
*/
@ExcelProperty(value = "已完成数")
private Long completedCount;
/**
* 未完成数
*/
@ExcelProperty(value = "未完成数")
private Long pendingCount;
/**
* 待审核人数
*/
@ExcelProperty(value = "待审核人数")
private Long pendingAuditCount;
}

View File

@@ -2,9 +2,15 @@ package com.hotwj.platform.securityManagement.hiddenDangerPlan.mapper;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.HotHiddenDangerPlan;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.vo.HotHiddenDangerPlanVo;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.List;
/**
* 隐患排查Mapper接口
*
@@ -14,4 +20,8 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
@Mapper
public interface HotHiddenDangerPlanMapper extends BaseMapperPlus<HotHiddenDangerPlan, HotHiddenDangerPlanVo> {
Page<HotHiddenDangerPlanVo> queryPageList(Page<HotHiddenDangerPlanVo> page,
@Param(Constants.WRAPPER) Wrapper<HotHiddenDangerPlan> queryWrapper);
List<HotHiddenDangerPlanVo> queryList(@Param(Constants.WRAPPER) Wrapper<HotHiddenDangerPlan> queryWrapper);
}

View File

@@ -0,0 +1,63 @@
package com.hotwj.platform.securityManagement.hiddenDangerPlan.service;
import com.hotwj.platform.securityManagement.hiddenDangerPlan.domain.HotHiddenDangerPlan;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 隐患排查计划时间窗口判定
*
* @author shihongwei
* @date 2026-06-03
*/
@Component
public class HiddenDangerPlanTimeWindowHelper {
/**
* 是否在原始时间窗口内
*/
public boolean isInOriginalWindow(Date now, HotHiddenDangerPlan plan) {
if (now == null || plan == null || plan.getStartDate() == null || plan.getEndDate() == null) {
return false;
}
return !now.before(plan.getStartDate()) && !now.after(plan.getEndDate());
}
/**
* 是否在补查时间窗口内
*/
public boolean isInRecheckWindow(Date now, HotHiddenDangerPlan plan) {
if (now == null || plan == null) {
return false;
}
if (!Long.valueOf(1L).equals(plan.getIsRecheck())) {
return false;
}
if (plan.getRecheckStartTime() == null || plan.getRecheckEndTime() == null) {
return false;
}
return !now.before(plan.getRecheckStartTime()) && !now.after(plan.getRecheckEndTime());
}
/**
* 是否在有效时间窗口内
*/
public boolean isInEffectiveWindow(Date now, HotHiddenDangerPlan plan) {
return isInOriginalWindow(now, plan) || isInRecheckWindow(now, plan);
}
/**
* 是否可以在有效时间窗口内处理
*/
public boolean canProcess(Date now, HotHiddenDangerPlan plan) {
return isInEffectiveWindow(now, plan);
}
/**
* 是否应该在司机端展示
*/
public boolean shouldDisplayToDriver(Date now, HotHiddenDangerPlan plan) {
return isInEffectiveWindow(now, plan);
}
}

View File

@@ -69,6 +69,34 @@ public interface IHotHiddenDangerPlanService {
*/
void generatePlan(Long companyId);
/**
* 开启补查
*
* @param planId 计划ID
* @param recheckStartTime 补查开始时间
* @param recheckEndTime 补查结束时间
* @return 是否成功
*/
Boolean openRecheck(Long planId, java.util.Date recheckStartTime, java.util.Date recheckEndTime);
/**
* 修改补查
*
* @param planId 计划ID
* @param recheckStartTime 补查开始时间
* @param recheckEndTime 补查结束时间
* @return 是否成功
*/
Boolean updateRecheck(Long planId, java.util.Date recheckStartTime, java.util.Date recheckEndTime);
/**
* 关闭补查
*
* @param planId 计划ID
* @return 是否成功
*/
Boolean closeRecheck(Long planId);
/**
* 校验并批量删除隐患排查信息
*

View File

@@ -13,6 +13,7 @@ import com.hotwj.platform.config.hiddenDangerCheckConfig.service.IHotHiddenDange
import com.hotwj.platform.driverManagement.driver.domain.bo.HotDriverBo;
import com.hotwj.platform.driverManagement.driver.domain.vo.HotDriverVo;
import com.hotwj.platform.driverManagement.driver.service.IHotDriverService;
import com.hotwj.platform.common.helper.DriverLoginContextHelper;
import com.hotwj.platform.noticeManagerment.systemNotification.domain.bo.HotSystemNotificationGroupBo;
import com.hotwj.platform.noticeManagerment.systemNotification.service.IHotSystemNotificationService;
import com.hotwj.platform.resourceManagement.company.domain.vo.SysCompanyVo;
@@ -33,6 +34,7 @@ import com.hotwj.platform.securityManagement.hiddenDangerPlan.mapper.HotHiddenDa
import com.hotwj.platform.securityManagement.hiddenDangerPlan.service.IHotHiddenDangerPlanService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
@@ -43,6 +45,7 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.IsoFields;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -90,7 +93,7 @@ public class HotHiddenDangerPlanServiceImpl implements IHotHiddenDangerPlanServi
@Override
public TableDataInfo<HotHiddenDangerPlanVo> queryPageList(HotHiddenDangerPlanBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<HotHiddenDangerPlan> lqw = buildQueryWrapper(bo);
Page<HotHiddenDangerPlanVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
Page<HotHiddenDangerPlanVo> result = baseMapper.queryPageList(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@@ -103,7 +106,7 @@ public class HotHiddenDangerPlanServiceImpl implements IHotHiddenDangerPlanServi
@Override
public List<HotHiddenDangerPlanVo> queryList(HotHiddenDangerPlanBo bo) {
LambdaQueryWrapper<HotHiddenDangerPlan> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
return baseMapper.queryList(lqw);
}
/**
@@ -183,6 +186,45 @@ public class HotHiddenDangerPlanServiceImpl implements IHotHiddenDangerPlanServi
processCompanyPlan(companyConfig, checkConfigsMap);
}
@Override
public Boolean openRecheck(Long planId, Date recheckStartTime, Date recheckEndTime) {
HotHiddenDangerPlan plan = getPlanOrThrow(planId);
validateRecheckTime(recheckStartTime, recheckEndTime);
return baseMapper.update(
null,
Wrappers.<HotHiddenDangerPlan>lambdaUpdate()
.eq(HotHiddenDangerPlan::getId, plan.getId())
.set(HotHiddenDangerPlan::getIsRecheck, 1L)
.set(HotHiddenDangerPlan::getRecheckStartTime, recheckStartTime)
.set(HotHiddenDangerPlan::getRecheckEndTime, recheckEndTime)
) > 0;
}
@Override
public Boolean updateRecheck(Long planId, Date recheckStartTime, Date recheckEndTime) {
HotHiddenDangerPlan plan = getPlanOrThrow(planId);
validateRecheckTime(recheckStartTime, recheckEndTime);
return baseMapper.update(
null,
Wrappers.<HotHiddenDangerPlan>lambdaUpdate()
.eq(HotHiddenDangerPlan::getId, plan.getId())
.set(HotHiddenDangerPlan::getIsRecheck, 1L)
.set(HotHiddenDangerPlan::getRecheckStartTime, recheckStartTime)
.set(HotHiddenDangerPlan::getRecheckEndTime, recheckEndTime)
) > 0;
}
@Override
public Boolean closeRecheck(Long planId) {
HotHiddenDangerPlan plan = getPlanOrThrow(planId);
return baseMapper.update(
null,
Wrappers.<HotHiddenDangerPlan>lambdaUpdate()
.eq(HotHiddenDangerPlan::getId, plan.getId())
.set(HotHiddenDangerPlan::getIsRecheck, 0L)
) > 0;
}
/**
* 处理单个公司的排查计划
*/
@@ -299,7 +341,7 @@ public class HotHiddenDangerPlanServiceImpl implements IHotHiddenDangerPlanServi
lqw.eq(HotHiddenDangerPlan::getCompanyId, companyId);
lqw.eq(HotHiddenDangerPlan::getIsDeleted, 0L);
lqw.eq(HotHiddenDangerPlan::getPlanDateKey, timeInfo.getPlanDateKey());
lqw.orderByDesc(HotHiddenDangerPlan::getCreateTime);
lqw.orderByDesc(HotHiddenDangerPlan::getCreateTime, HotHiddenDangerPlan::getId);
lqw.last("LIMIT 1");
return baseMapper.selectOne(lqw);
}
@@ -511,19 +553,39 @@ public class HotHiddenDangerPlanServiceImpl implements IHotHiddenDangerPlanServi
}
private LambdaQueryWrapper<HotHiddenDangerPlan> buildQueryWrapper(HotHiddenDangerPlanBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotHiddenDangerPlan> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotHiddenDangerPlan::getCreateTime);
lqw.orderByDesc(HotHiddenDangerPlan::getCreateTime, HotHiddenDangerPlan::getId);
lqw.eq(bo.getCompanyId() != null, HotHiddenDangerPlan::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getInspectId() != null, HotHiddenDangerPlan::getInspectId, bo.getInspectId());
lqw.like(StringUtils.isNotBlank(bo.getPlanName()), HotHiddenDangerPlan::getPlanName, bo.getPlanName());
lqw.eq(bo.getStartDate() != null, HotHiddenDangerPlan::getStartDate, bo.getStartDate());
lqw.eq(bo.getEndDate() != null, HotHiddenDangerPlan::getEndDate, bo.getEndDate());
lqw.eq(bo.getIsRecheck() != null, HotHiddenDangerPlan::getIsRecheck, bo.getIsRecheck());
lqw.eq(bo.getProgress() != null, HotHiddenDangerPlan::getProgress, bo.getProgress());
lqw.eq(bo.getIsDeleted() != null, HotHiddenDangerPlan::getIsDeleted, bo.getIsDeleted());
if (shouldLimitToEffectiveWindow()) {
Date now = new Date();
lqw.and(wrapper -> wrapper
.and(original -> original
.le(HotHiddenDangerPlan::getStartDate, now)
.ge(HotHiddenDangerPlan::getEndDate, now))
.or(recheck -> recheck
.eq(HotHiddenDangerPlan::getIsRecheck, 1L)
.le(HotHiddenDangerPlan::getRecheckStartTime, now)
.ge(HotHiddenDangerPlan::getRecheckEndTime, now))
);
}
return lqw;
}
private boolean shouldLimitToEffectiveWindow() {
try {
return DriverLoginContextHelper.isDriverPort();
} catch (Exception e) {
return false;
}
}
/**
* 新增隐患排查
*
@@ -626,6 +688,34 @@ public class HotHiddenDangerPlanServiceImpl implements IHotHiddenDangerPlanServi
//TODO 做一些数据校验,如唯一约束
}
private HotHiddenDangerPlan getPlanOrThrow(Long planId) {
if (planId == null) {
throw new ServiceException("计划ID不能为空");
}
HotHiddenDangerPlan plan = baseMapper.selectOne(
Wrappers.<HotHiddenDangerPlan>lambdaQuery()
.eq(HotHiddenDangerPlan::getId, planId)
.eq(HotHiddenDangerPlan::getIsDeleted, 0L)
.last("LIMIT 1")
);
if (plan == null) {
throw new ServiceException("排查计划不存在");
}
return plan;
}
private void validateRecheckTime(Date recheckStartTime, Date recheckEndTime) {
if (recheckStartTime == null) {
throw new ServiceException("补查开始时间不能为空");
}
if (recheckEndTime == null) {
throw new ServiceException("补查结束时间不能为空");
}
if (!recheckEndTime.after(recheckStartTime)) {
throw new ServiceException("补查结束时间必须晚于补查开始时间");
}
}
/**
* 校验并批量删除隐患排查信息
*

View File

@@ -88,7 +88,7 @@ public class HotTrainingServiceImpl implements IHotTrainingService {
private LambdaQueryWrapper<HotTraining> buildQueryWrapper(HotTrainingBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotTraining> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotTraining::getCreateTime);
lqw.orderByDesc(HotTraining::getCreateTime, HotTraining::getId);
lqw.eq(bo.getCompanyId() != null, HotTraining::getCompanyId, bo.getCompanyId());
lqw.like(StringUtils.isNotBlank(bo.getPlanName()), HotTraining::getPlanName, bo.getPlanName());
lqw.eq(StringUtils.isNotBlank(bo.getPersonType()), HotTraining::getPersonType, bo.getPersonType());

View File

@@ -79,7 +79,7 @@ public class HotTrainingCourseConfigServiceImpl implements IHotTrainingCourseCon
private LambdaQueryWrapper<HotTrainingCourseConfig> buildQueryWrapper(HotTrainingCourseConfigBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotTrainingCourseConfig> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(HotTrainingCourseConfig::getSortNo);
lqw.orderByAsc(HotTrainingCourseConfig::getSortNo, HotTrainingCourseConfig::getId);
lqw.eq(bo.getCompanyId() != null, HotTrainingCourseConfig::getCompanyId, bo.getCompanyId());
lqw.like(StringUtils.isNotBlank(bo.getCourseName()), HotTrainingCourseConfig::getCourseName, bo.getCourseName());
lqw.eq(bo.getCourseId() != null, HotTrainingCourseConfig::getCourseId, bo.getCourseId());
@@ -107,7 +107,7 @@ public class HotTrainingCourseConfigServiceImpl implements IHotTrainingCourseCon
.select(HotTrainingCourseConfig::getSortNo)
.eq(HotTrainingCourseConfig::getCompanyId, bo.getCompanyId())
.eq(bo.getTrainingId() != null, HotTrainingCourseConfig::getTrainingId, bo.getTrainingId())
.orderByDesc(HotTrainingCourseConfig::getSortNo)
.orderByDesc(HotTrainingCourseConfig::getSortNo, HotTrainingCourseConfig::getId)
.last("limit 1")
);
long max = (maxSort != null && maxSort.getSortNo() != null) ? maxSort.getSortNo() : 0L;

View File

@@ -110,6 +110,7 @@ public class HotTrainingCourseRecordServiceImpl implements IHotTrainingCourseRec
lqw.eq(bo.getTrainingCourseId() != null, HotTrainingCourseRecord::getTrainingCourseId, bo.getTrainingCourseId());
lqw.orderByDesc(HotTrainingCourseRecord::getCompleteTime);
lqw.orderByDesc(HotTrainingCourseRecord::getCreateTime);
lqw.orderByDesc(HotTrainingCourseRecord::getId);
Page<HotTrainingCourseRecordVo> page = baseMapper.selectVoPage(new Page<>(1, 1), lqw);
List<HotTrainingCourseRecordVo> records = page.getRecords();
if (records == null || records.isEmpty()) {
@@ -126,7 +127,7 @@ public class HotTrainingCourseRecordServiceImpl implements IHotTrainingCourseRec
private LambdaQueryWrapper<HotTrainingCourseRecord> buildQueryWrapper(HotTrainingCourseRecordBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotTrainingCourseRecord> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotTrainingCourseRecord::getCreateTime);
lqw.orderByDesc(HotTrainingCourseRecord::getCreateTime, HotTrainingCourseRecord::getId);
lqw.eq(bo.getCompanyId() != null, HotTrainingCourseRecord::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getTrainingId() != null, HotTrainingCourseRecord::getTrainingId, bo.getTrainingId());
lqw.eq(StringUtils.isNotBlank(bo.getUserId()), HotTrainingCourseRecord::getUserId, bo.getUserId());

View File

@@ -162,7 +162,7 @@ public class TrainingProgressService {
.eq(HotTrainingCourseRecord::getUserId, userId)
.in(HotTrainingCourseRecord::getTrainingCourseId, trainingCourseIds)
.eq(HotTrainingCourseRecord::getIsDeleted, 0L)
.orderByDesc(HotTrainingCourseRecord::getCreateTime)
.orderByDesc(HotTrainingCourseRecord::getCreateTime, HotTrainingCourseRecord::getId)
);
if (records == null || records.isEmpty()) {
return Map.of();

View File

@@ -87,7 +87,7 @@ public class HotVehicleEntryExitServiceImpl implements IHotVehicleEntryExitServi
private LambdaQueryWrapper<HotVehicleEntryExit> buildQueryWrapper(HotVehicleEntryExitBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicleEntryExit> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotVehicleEntryExit::getCreateTime);
lqw.orderByDesc(HotVehicleEntryExit::getCreateTime, HotVehicleEntryExit::getId);
lqw.eq(bo.getCompanyId() != null, HotVehicleEntryExit::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getCheckTime() != null, HotVehicleEntryExit::getCheckTime, bo.getCheckTime());
lqw.like(StringUtils.isNotBlank(bo.getCreateByName()), HotVehicleEntryExit::getCreateByName, bo.getCreateByName());

View File

@@ -90,7 +90,7 @@ public class HotVehicleInoutCheckServiceImpl implements IHotVehicleInoutCheckSer
private LambdaQueryWrapper<HotVehicleInoutCheck> buildQueryWrapper(HotVehicleInoutCheckBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicleInoutCheck> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotVehicleInoutCheck::getCheckDate);
lqw.orderByDesc(HotVehicleInoutCheck::getCheckDate, HotVehicleInoutCheck::getId);
lqw.eq(bo.getRecordId() != null, HotVehicleInoutCheck::getRecordId, bo.getRecordId());
lqw.eq(bo.getCompanyId() != null, HotVehicleInoutCheck::getCompanyId, bo.getCompanyId());
lqw.eq(bo.getVehicleId() != null, HotVehicleInoutCheck::getVehicleId, bo.getVehicleId());

View File

@@ -350,7 +350,7 @@ public class HotViolationInfoServiceImpl implements IHotViolationInfoService {
private LambdaQueryWrapper<HotViolationInfo> buildQueryWrapper(HotViolationInfoBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotViolationInfo> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(HotViolationInfo::getOccurTime);
lqw.orderByDesc(HotViolationInfo::getOccurTime, HotViolationInfo::getId);
if (bo.getOccurTime() != null) {
java.util.Calendar calendarStart = java.util.Calendar.getInstance();
calendarStart.setTime(bo.getOccurTime());

View File

@@ -0,0 +1,129 @@
package com.hotwj.platform.system.maintenance.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* 媒体资源元数据修复配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "maintenance.media-resource-repair")
public class MediaResourceMetadataRepairProperties {
/**
* 任务缓存key前缀
*/
private String taskKeyPrefix = "maintenance:media-resource-repair:";
/**
* 临时目录根路径
*/
private String workDir = System.getProperty("java.io.tmpdir") + File.separator + "media-resource-repair";
/**
* ffprobe 命令
*/
private String ffprobeCommand = "ffprobe";
/**
* ffmpeg 命令
*/
private String ffmpegCommand = "ffmpeg";
/**
* 任务状态保留小时数
*/
private Integer taskExpireHours = 24;
/**
* 默认单次处理上限
*/
private Integer defaultLimit = 200;
/**
* 最大单次处理上限
*/
private Integer maxLimit = 500;
/**
* 默认批处理条数
*/
private Integer defaultBatchSize = 20;
/**
* 最大批处理条数
*/
private Integer maxBatchSize = 50;
/**
* 连接超时秒数
*/
private Integer connectTimeoutSeconds = 10;
/**
* 读取超时秒数
*/
private Integer readTimeoutSeconds = 60;
/**
* 单文件最大大小
*/
private Long maxFileSizeBytes = 524288000L;
/**
* HLS 最大分片数
*/
private Integer maxHlsSegments = 500;
/**
* HLS 最大总下载大小
*/
private Long maxHlsTotalSizeBytes = 1073741824L;
/**
* 采样错误条数上限
*/
private Integer maxErrorSamples = 20;
/**
* 播放列表最大大小
*/
private Long maxPlaylistSizeBytes = 2097152L;
/**
* 允许的域名白名单,留空则不限制
*/
private List<String> allowedHosts = new ArrayList<>();
public Duration taskExpireDuration() {
return Duration.ofHours(taskExpireHours == null ? 24L : taskExpireHours.longValue());
}
public boolean isHostAllowed(String host) {
if (host == null || host.isBlank()) {
return false;
}
if (allowedHosts == null || allowedHosts.isEmpty()) {
return true;
}
String normalizedHost = host.toLowerCase(Locale.ROOT);
for (String allowedHost : allowedHosts) {
if (allowedHost == null || allowedHost.isBlank()) {
continue;
}
String normalizedAllowed = allowedHost.toLowerCase(Locale.ROOT).trim();
if (normalizedHost.equals(normalizedAllowed) || normalizedHost.endsWith("." + normalizedAllowed)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,32 @@
package com.hotwj.platform.system.maintenance.controller;
import com.hotwj.platform.system.maintenance.domain.bo.MediaResourceMetadataRepairRequest;
import com.hotwj.platform.system.maintenance.domain.vo.MediaResourceMetadataRepairSubmitVo;
import com.hotwj.platform.system.maintenance.domain.vo.MediaResourceMetadataRepairTaskStatusVo;
import com.hotwj.platform.system.maintenance.service.IMediaResourceMetadataRepairService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.springframework.web.bind.annotation.*;
/**
* 系统维护-媒体资源元数据修复
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/maintenance/media-resource")
public class MediaResourceMaintenanceController extends BaseController {
private final IMediaResourceMetadataRepairService mediaResourceMetadataRepairService;
@PostMapping("/repair-metadata")
public R<MediaResourceMetadataRepairSubmitVo> repairMetadata(@Valid @RequestBody MediaResourceMetadataRepairRequest request) {
return R.ok(mediaResourceMetadataRepairService.submitRepairTask(request));
}
@GetMapping("/repair-metadata/{taskId}")
public R<MediaResourceMetadataRepairTaskStatusVo> getTaskStatus(@PathVariable String taskId) {
return R.ok(mediaResourceMetadataRepairService.getTaskStatus(taskId));
}
}

View File

@@ -0,0 +1,53 @@
package com.hotwj.platform.system.maintenance.controller;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairExecuteRequest;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairQueryBo;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairTaskRequest;
import com.hotwj.platform.system.maintenance.domain.vo.*;
import com.hotwj.platform.system.maintenance.service.IPhotoEvidenceRepairService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/maintenance/photo-evidence")
public class PhotoEvidenceMaintenanceController extends BaseController {
private final IPhotoEvidenceRepairService photoEvidenceRepairService;
@GetMapping("/list")
@Operation(summary = "分页查询异常图片水印记录")
public TableDataInfo<PhotoEvidenceRepairCandidateVo> list(PhotoEvidenceRepairQueryBo queryBo, PageQuery pageQuery) {
return photoEvidenceRepairService.queryPageList(queryBo, pageQuery);
}
@GetMapping("/preview/{evidenceId}")
@Operation(summary = "生成图片水印修复预览")
public R<PhotoEvidenceRepairPreviewVo> preview(@PathVariable Long evidenceId) {
return R.ok(photoEvidenceRepairService.preview(evidenceId));
}
@PostMapping("/repair")
@Operation(summary = "执行单条图片水印修复")
public R<PhotoEvidenceRepairExecuteVo> repair(@Valid @RequestBody PhotoEvidenceRepairExecuteRequest request) {
return R.ok(photoEvidenceRepairService.repair(request));
}
@PostMapping("/repair-task")
@Operation(summary = "创建批量图片水印修复任务")
public R<PhotoEvidenceRepairSubmitVo> submitRepairTask(@Valid @RequestBody PhotoEvidenceRepairTaskRequest request) {
return R.ok(photoEvidenceRepairService.submitRepairTask(request));
}
@GetMapping("/repair-task/{taskId}")
@Operation(summary = "查询图片水印修复任务状态")
public R<PhotoEvidenceRepairTaskStatusVo> getTaskStatus(@PathVariable String taskId) {
return R.ok(photoEvidenceRepairService.getTaskStatus(taskId));
}
}

View File

@@ -0,0 +1,72 @@
package com.hotwj.platform.system.maintenance.domain.bo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 媒体资源元数据修复请求
*/
@Data
public class MediaResourceMetadataRepairRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 资源类型1=视频 2=音频
*/
private Long mediaType;
/**
* 分类编码
*/
private String categoryCode;
/**
* 公司ID
*/
private Long companyId;
/**
* 指定资源ID列表
*/
private List<Long> resourceIds;
/**
* 是否修复大小
*/
private Boolean repairSize;
/**
* 是否修复时长
*/
private Boolean repairDuration;
/**
* 是否覆盖已有有效值
*/
private Boolean overwriteExisting;
/**
* 是否只处理无效数据
*/
private Boolean onlyInvalid;
/**
* 单次处理上限
*/
private Integer limit;
/**
* 每批处理条数
*/
private Integer batchSize;
/**
* 是否跳过无法解析URL的记录
*/
private Boolean skipUnresolvedUrl;
}

View File

@@ -0,0 +1,17 @@
package com.hotwj.platform.system.maintenance.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class PhotoEvidenceRepairExecuteRequest {
@NotNull(message = "evidenceId不能为空")
private Long evidenceId;
/**
* 前端必须在预览确认后才能发起正式修复。
*/
@NotNull(message = "previewConfirmed不能为空")
private Boolean previewConfirmed;
}

View File

@@ -0,0 +1,34 @@
package com.hotwj.platform.system.maintenance.domain.bo;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class PhotoEvidenceRepairQueryBo {
private Long evidenceId;
private Long watermarkedOssId;
private Long originalOssId;
private Long companyId;
private String bizType;
private String bizId;
private Boolean onlyAbnormal;
private Boolean fileSizeZero;
private Boolean fileSuffixAbnormal;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date beginCreateTime;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endCreateTime;
}

View File

@@ -0,0 +1,20 @@
package com.hotwj.platform.system.maintenance.domain.bo;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class PhotoEvidenceRepairTaskRequest {
@NotEmpty(message = "evidenceIds不能为空")
private List<Long> evidenceIds;
/**
* 前端必须确保本次提交的记录都已经过人工预览确认。
*/
@NotNull(message = "previewConfirmed不能为空")
private Boolean previewConfirmed;
}

View File

@@ -0,0 +1,26 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 提交媒体资源元数据修复任务响应
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MediaResourceMetadataRepairSubmitVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String taskId;
private Boolean accepted;
private String message;
}

View File

@@ -0,0 +1,63 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 媒体资源元数据修复任务状态
*/
@Data
public class MediaResourceMetadataRepairTaskStatusVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String taskId;
private String status;
private Integer total = 0;
private Integer processedCount = 0;
private Integer successCount = 0;
private Integer skipCount = 0;
private Integer failCount = 0;
private Long startTime;
private Long endTime;
private Long durationMillis;
private String operatorName;
private String requestSummary;
private String message;
private List<Long> updatedIds = new ArrayList<>();
private List<ErrorSample> errorSamples = new ArrayList<>();
@Data
public static class ErrorSample implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long resourceId;
private String fileUrl;
private String resolvedUrl;
private String reason;
}
}

View File

@@ -0,0 +1,51 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Data
public class PhotoEvidenceRepairCandidateVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long evidenceId;
private Long watermarkedOssId;
private Long originalOssId;
private Long companyId;
private String bizType;
private String bizId;
private Date captureTimeClient;
private String addressSource;
private String addressText;
private Date createTime;
private String watermarkedFileSuffix;
private String watermarkedOriginalName;
private String watermarkedExt1;
private String originalImageUrl;
private String currentWatermarkedUrl;
private Long currentFileSize;
private String currentContentType;
private List<String> abnormalReasons;
}

View File

@@ -0,0 +1,29 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PhotoEvidenceRepairExecuteVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long evidenceId;
private Long watermarkedOssId;
private String oldFileName;
private String newFileName;
private String newUrl;
private String message;
}

View File

@@ -0,0 +1,46 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@Data
public class PhotoEvidenceRepairPreviewVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long evidenceId;
private Long watermarkedOssId;
private Long originalOssId;
private String originalImageUrl;
private String currentWatermarkedUrl;
private String previewDataUrl;
private MetadataSnapshot currentMetadata;
private MetadataSnapshot targetMetadata;
private List<String> abnormalReasons;
@Data
public static class MetadataSnapshot implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String fileSuffix;
private String originalName;
private Long fileSize;
private String contentType;
}
}

View File

@@ -0,0 +1,23 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PhotoEvidenceRepairSubmitVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String taskId;
private Boolean accepted;
private String message;
}

View File

@@ -0,0 +1,55 @@
package com.hotwj.platform.system.maintenance.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public class PhotoEvidenceRepairTaskStatusVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String taskId;
private String status;
private Integer total = 0;
private Integer processedCount = 0;
private Integer successCount = 0;
private Integer failCount = 0;
private Long startTime;
private Long endTime;
private Long durationMillis;
private String operatorName;
private String requestSummary;
private String message;
private List<Long> successEvidenceIds = new ArrayList<>();
private List<ErrorSample> errorSamples = new ArrayList<>();
@Data
public static class ErrorSample implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long evidenceId;
private Long watermarkedOssId;
private String reason;
}
}

View File

@@ -0,0 +1,21 @@
package com.hotwj.platform.system.maintenance.service;
import com.hotwj.platform.system.maintenance.domain.bo.MediaResourceMetadataRepairRequest;
import com.hotwj.platform.system.maintenance.domain.vo.MediaResourceMetadataRepairSubmitVo;
import com.hotwj.platform.system.maintenance.domain.vo.MediaResourceMetadataRepairTaskStatusVo;
/**
* 媒体资源元数据修复服务
*/
public interface IMediaResourceMetadataRepairService {
/**
* 提交修复任务
*/
MediaResourceMetadataRepairSubmitVo submitRepairTask(MediaResourceMetadataRepairRequest request);
/**
* 查询任务状态
*/
MediaResourceMetadataRepairTaskStatusVo getTaskStatus(String taskId);
}

View File

@@ -0,0 +1,21 @@
package com.hotwj.platform.system.maintenance.service;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairExecuteRequest;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairQueryBo;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairTaskRequest;
import com.hotwj.platform.system.maintenance.domain.vo.*;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
public interface IPhotoEvidenceRepairService {
TableDataInfo<PhotoEvidenceRepairCandidateVo> queryPageList(PhotoEvidenceRepairQueryBo queryBo, PageQuery pageQuery);
PhotoEvidenceRepairPreviewVo preview(Long evidenceId);
PhotoEvidenceRepairExecuteVo repair(PhotoEvidenceRepairExecuteRequest request);
PhotoEvidenceRepairSubmitVo submitRepairTask(PhotoEvidenceRepairTaskRequest request);
PhotoEvidenceRepairTaskStatusVo getTaskStatus(String taskId);
}

View File

@@ -0,0 +1,785 @@
package com.hotwj.platform.system.maintenance.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hotwj.platform.config.mediaResource.domain.HotMediaResource;
import com.hotwj.platform.config.mediaResource.mapper.HotMediaResourceMapper;
import com.hotwj.platform.system.maintenance.config.MediaResourceMetadataRepairProperties;
import com.hotwj.platform.system.maintenance.domain.bo.MediaResourceMetadataRepairRequest;
import com.hotwj.platform.system.maintenance.domain.vo.MediaResourceMetadataRepairSubmitVo;
import com.hotwj.platform.system.maintenance.domain.vo.MediaResourceMetadataRepairTaskStatusVo;
import com.hotwj.platform.system.maintenance.service.IMediaResourceMetadataRepairService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.service.OssService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serial;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 媒体资源元数据修复服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MediaResourceMetadataRepairServiceImpl implements IMediaResourceMetadataRepairService {
private static final String STATUS_PENDING = "PENDING";
private static final String STATUS_RUNNING = "RUNNING";
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String STATUS_PARTIAL_SUCCESS = "PARTIAL_SUCCESS";
private static final String STATUS_FAILED = "FAILED";
private static final Pattern PURE_NUMBER = Pattern.compile("^\\d+$");
private static final Pattern KEY_URI_PATTERN = Pattern.compile("URI=\"([^\"]+)\"");
private static final Pattern FFMPEG_DURATION_PATTERN = Pattern.compile("Duration: (\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d+)?)");
private final HotMediaResourceMapper hotMediaResourceMapper;
private final OssService ossService;
private final ScheduledExecutorService scheduledExecutorService;
private final MediaResourceMetadataRepairProperties properties;
@Override
public MediaResourceMetadataRepairSubmitVo submitRepairTask(MediaResourceMetadataRepairRequest request) {
MediaResourceMetadataRepairRequest normalized = normalizeRequest(request);
String taskId = UUID.randomUUID().toString().replace("-", "");
MediaResourceMetadataRepairTaskStatusVo taskStatus = new MediaResourceMetadataRepairTaskStatusVo();
taskStatus.setTaskId(taskId);
taskStatus.setStatus(STATUS_PENDING);
taskStatus.setMessage("任务已提交,等待执行");
taskStatus.setOperatorName(resolveOperatorName());
taskStatus.setRequestSummary(buildRequestSummary(normalized));
saveTask(taskStatus);
scheduledExecutorService.execute(() -> runTask(taskId, normalized));
return new MediaResourceMetadataRepairSubmitVo(taskId, Boolean.TRUE, "媒体资源元数据修复任务已提交");
}
@Override
public MediaResourceMetadataRepairTaskStatusVo getTaskStatus(String taskId) {
MediaResourceMetadataRepairTaskStatusVo taskStatus = loadTask(taskId);
if (taskStatus == null) {
throw new ServiceException("任务不存在或已过期");
}
return taskStatus;
}
private void runTask(String taskId, MediaResourceMetadataRepairRequest request) {
MediaResourceMetadataRepairTaskStatusVo taskStatus = requireTask(taskId);
long taskStart = System.currentTimeMillis();
Path taskWorkDir = getTaskWorkDir(taskId);
try {
taskStatus.setStatus(STATUS_RUNNING);
taskStatus.setStartTime(taskStart);
taskStatus.setMessage("任务执行中");
long total = countMatchingResources(request);
taskStatus.setTotal((int) Math.min(total, request.getLimit()));
saveTask(taskStatus);
if (taskStatus.getTotal() <= 0) {
taskStatus.setStatus(STATUS_SUCCESS);
taskStatus.setEndTime(System.currentTimeMillis());
taskStatus.setDurationMillis(taskStatus.getEndTime() - taskStart);
taskStatus.setMessage("未找到符合条件的媒体资源");
saveTask(taskStatus);
return;
}
long lastId = 0L;
while (taskStatus.getProcessedCount() < taskStatus.getTotal()) {
int fetchSize = Math.min(request.getBatchSize(), taskStatus.getTotal() - taskStatus.getProcessedCount());
List<HotMediaResource> resources = fetchResources(request, lastId, fetchSize);
if (resources.isEmpty()) {
break;
}
for (HotMediaResource resource : resources) {
lastId = resource.getId();
try {
ProcessResult result = processResource(taskId, request, resource);
if (result.type == ProcessResultType.SUCCESS) {
taskStatus.setSuccessCount(taskStatus.getSuccessCount() + 1);
taskStatus.getUpdatedIds().add(resource.getId());
} else if (result.type == ProcessResultType.SKIP) {
taskStatus.setSkipCount(taskStatus.getSkipCount() + 1);
} else {
recordFailure(taskStatus, resource, result.resolvedUrl, result.message);
}
} catch (Exception ex) {
log.error("媒体资源元数据修复失败, resourceId={}", resource.getId(), ex);
recordFailure(taskStatus, resource, null, abbreviate(ex.getMessage(), 300));
} finally {
taskStatus.setProcessedCount(taskStatus.getProcessedCount() + 1);
saveTask(taskStatus);
}
if (taskStatus.getProcessedCount() >= taskStatus.getTotal()) {
break;
}
}
}
taskStatus.setEndTime(System.currentTimeMillis());
taskStatus.setDurationMillis(taskStatus.getEndTime() - taskStart);
taskStatus.setStatus(determineFinalStatus(taskStatus));
taskStatus.setMessage(buildFinishMessage(taskStatus));
saveTask(taskStatus);
} catch (Exception ex) {
log.error("媒体资源元数据修复任务执行失败, taskId={}", taskId, ex);
taskStatus.setStatus(STATUS_FAILED);
taskStatus.setEndTime(System.currentTimeMillis());
taskStatus.setDurationMillis(taskStatus.getEndTime() - taskStart);
taskStatus.setMessage("任务执行失败: " + abbreviate(ex.getMessage(), 300));
saveTask(taskStatus);
} finally {
deleteQuietly(taskWorkDir);
}
}
private ProcessResult processResource(String taskId, MediaResourceMetadataRepairRequest request, HotMediaResource resource) throws IOException, InterruptedException {
ResolvedUrl resolvedUrl = resolveDownloadUrl(resource.getFileUrl());
if (resolvedUrl == null || StringUtils.isBlank(resolvedUrl.url)) {
String reason = "未解析到可用下载地址";
if (Boolean.TRUE.equals(request.getSkipUnresolvedUrl())) {
return ProcessResult.skip(reason, null);
}
return ProcessResult.fail(reason, null);
}
Path resourceDir = getResourceWorkDir(taskId, resource.getId());
try {
Files.createDirectories(resourceDir);
DownloadedResource downloadedResource = resolvedUrl.hls
? downloadHlsResource(resolvedUrl.url, resourceDir)
: downloadFileResource(resolvedUrl.url, resourceDir);
Long sizeBytes = downloadedResource.totalSizeBytes;
Long durationSeconds = probeDuration(downloadedResource.probePath, downloadedResource.extinfDuration);
HotMediaResource update = new HotMediaResource();
update.setId(resource.getId());
boolean hasUpdate = false;
if (Boolean.TRUE.equals(request.getRepairSize())) {
if (sizeBytes == null || sizeBytes <= 0) {
return ProcessResult.fail("未获取到有效资源大小", resolvedUrl.url);
}
if (Boolean.TRUE.equals(request.getOverwriteExisting()) || isInvalid(resource.getSizeBytes())) {
update.setSizeBytes(sizeBytes);
hasUpdate = true;
}
}
if (Boolean.TRUE.equals(request.getRepairDuration())) {
if (durationSeconds == null || durationSeconds <= 0) {
return ProcessResult.fail("未获取到有效媒体时长", resolvedUrl.url);
}
if (Boolean.TRUE.equals(request.getOverwriteExisting()) || isInvalid(resource.getDurationSeconds())) {
update.setDurationSeconds(durationSeconds);
hasUpdate = true;
}
}
if (!hasUpdate) {
return ProcessResult.skip("未命中可覆盖字段", resolvedUrl.url);
}
if (hotMediaResourceMapper.updateById(update) <= 0) {
return ProcessResult.fail("数据库更新失败", resolvedUrl.url);
}
return ProcessResult.success("修复成功", resolvedUrl.url);
} finally {
deleteQuietly(resourceDir);
}
}
private ResolvedUrl resolveDownloadUrl(String fileUrl) {
String candidate = firstToken(fileUrl);
if (StringUtils.isBlank(candidate)) {
return null;
}
String resolvedUrl = candidate;
if (PURE_NUMBER.matcher(candidate).matches()) {
resolvedUrl = firstToken(ossService.selectUrlByIds(candidate));
}
if (StringUtils.isBlank(resolvedUrl)) {
return null;
}
URI uri = toHttpUri(resolvedUrl);
if (!properties.isHostAllowed(uri.getHost())) {
throw new ServiceException("下载地址不在允许的域名白名单中: " + uri.getHost());
}
String path = StringUtils.defaultString(uri.getPath()).toLowerCase(Locale.ROOT);
return new ResolvedUrl(uri.toString(), path.endsWith(".m3u8"));
}
private DownloadedResource downloadFileResource(String url, Path resourceDir) throws IOException, InterruptedException {
URI uri = toHttpUri(url);
String extension = extensionFromPath(uri.getPath(), ".bin");
Path targetFile = resourceDir.resolve("resource" + extension);
long size = downloadToFile(uri, targetFile, properties.getMaxFileSizeBytes());
if (size <= 0) {
throw new ServiceException("下载文件大小为0");
}
return new DownloadedResource(targetFile, size, null);
}
private DownloadedResource downloadHlsResource(String url, Path resourceDir) throws IOException, InterruptedException {
HlsContext context = new HlsContext();
HlsPlaylistResult playlistResult = localizePlaylist(toHttpUri(url), resourceDir.resolve("root.m3u8"), context);
if (context.totalSize <= 0) {
throw new ServiceException("HLS 下载结果为空");
}
return new DownloadedResource(playlistResult.probePath, context.totalSize, playlistResult.extinfDuration);
}
private HlsPlaylistResult localizePlaylist(URI playlistUri, Path localPlaylistPath, HlsContext context) throws IOException, InterruptedException {
byte[] contentBytes = downloadToBytes(playlistUri, properties.getMaxPlaylistSizeBytes());
context.addBytes(contentBytes.length);
Files.createDirectories(localPlaylistPath.getParent());
String content = new String(contentBytes, StandardCharsets.UTF_8);
if (content.contains("#EXT-X-STREAM-INF")) {
Files.write(localPlaylistPath, contentBytes);
URI variantUri = findFirstVariantUri(playlistUri, content);
if (variantUri == null) {
throw new ServiceException("未找到可用的 HLS 变体播放列表");
}
return localizePlaylist(variantUri, localPlaylistPath.getParent().resolve("variant_0.m3u8"), context);
}
List<String> outputLines = new ArrayList<>();
long extinfDuration = 0L;
String[] lines = content.split("\\r?\\n", -1);
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.startsWith("#EXTINF:")) {
extinfDuration += parseExtinfDuration(trimmed);
outputLines.add(line);
continue;
}
if (trimmed.startsWith("#EXT-X-KEY") || trimmed.startsWith("#EXT-X-MAP")) {
outputLines.add(rewriteAttributeUriLine(playlistUri, line, localPlaylistPath.getParent(), context));
continue;
}
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
outputLines.add(line);
continue;
}
URI resourceUri = playlistUri.resolve(trimmed);
String lowerPath = StringUtils.defaultString(resourceUri.getPath()).toLowerCase(Locale.ROOT);
if (lowerPath.endsWith(".m3u8")) {
String childName = "nested_" + context.nextFileIndex() + ".m3u8";
localizePlaylist(resourceUri, localPlaylistPath.getParent().resolve(childName), context);
outputLines.add(childName);
} else {
String fileName = buildHlsAssetName(resourceUri, "segment_" + context.nextFileIndex());
Path target = localPlaylistPath.getParent().resolve(fileName);
downloadToFile(resourceUri, target, properties.getMaxHlsTotalSizeBytes());
context.addSegment();
context.addBytes(Files.size(target));
outputLines.add(fileName);
}
}
Files.writeString(localPlaylistPath, String.join(System.lineSeparator(), outputLines), StandardCharsets.UTF_8);
return new HlsPlaylistResult(localPlaylistPath, extinfDuration > 0 ? extinfDuration : null);
}
private String rewriteAttributeUriLine(URI baseUri, String line, Path localDir, HlsContext context) throws IOException, InterruptedException {
Matcher matcher = KEY_URI_PATTERN.matcher(line);
if (!matcher.find()) {
return line;
}
URI assetUri = baseUri.resolve(matcher.group(1));
String fileName = buildHlsAssetName(assetUri, "asset_" + context.nextFileIndex());
Path target = localDir.resolve(fileName);
long fileSize = downloadToFile(assetUri, target, properties.getMaxHlsTotalSizeBytes());
context.addBytes(fileSize);
return matcher.replaceFirst("URI=\"" + fileName.replace("\\", "/") + "\"");
}
private URI findFirstVariantUri(URI playlistUri, String content) {
String[] lines = content.split("\\r?\\n");
for (int i = 0; i < lines.length; i++) {
if (!lines[i].trim().startsWith("#EXT-X-STREAM-INF")) {
continue;
}
for (int j = i + 1; j < lines.length; j++) {
String candidate = lines[j].trim();
if (candidate.isEmpty()) {
continue;
}
if (candidate.startsWith("#")) {
break;
}
return playlistUri.resolve(candidate);
}
}
return null;
}
private Long probeDuration(Path probePath, Long extinfDuration) throws IOException, InterruptedException {
Long duration = probeByFfprobe(probePath);
if (duration != null && duration > 0) {
return duration;
}
duration = probeByFfmpeg(probePath);
if (duration != null && duration > 0) {
return duration;
}
return extinfDuration;
}
private Long probeByFfprobe(Path probePath) throws IOException, InterruptedException {
List<String> command = List.of(
properties.getFfprobeCommand(),
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
probePath.toAbsolutePath().toString()
);
CommandResult commandResult = runCommand(command, Duration.ofSeconds(properties.getReadTimeoutSeconds()));
if (commandResult.exitCode != 0) {
return null;
}
String output = StringUtils.trim(commandResult.output);
if (StringUtils.isBlank(output)) {
return null;
}
try {
double seconds = Double.parseDouble(output);
long rounded = Math.round(seconds);
return rounded > 0 ? rounded : null;
} catch (NumberFormatException ignored) {
return null;
}
}
private Long probeByFfmpeg(Path probePath) throws IOException, InterruptedException {
List<String> command = List.of(
properties.getFfmpegCommand(),
"-i",
probePath.toAbsolutePath().toString()
);
CommandResult commandResult = runCommand(command, Duration.ofSeconds(properties.getReadTimeoutSeconds()));
Matcher matcher = FFMPEG_DURATION_PATTERN.matcher(commandResult.output);
if (!matcher.find()) {
return null;
}
int hours = Integer.parseInt(matcher.group(1));
int minutes = Integer.parseInt(matcher.group(2));
double seconds = Double.parseDouble(matcher.group(3));
long duration = Math.round(hours * 3600L + minutes * 60L + seconds);
return duration > 0 ? duration : null;
}
private CommandResult runCommand(List<String> command, Duration timeout) throws IOException, InterruptedException {
Process process = new ProcessBuilder(command)
.redirectErrorStream(true)
.start();
boolean finished = process.waitFor(timeout.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS);
if (!finished) {
process.destroyForcibly();
throw new ServiceException("媒体探测命令执行超时: " + command.get(0));
}
String output;
try (InputStream inputStream = process.getInputStream()) {
output = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
return new CommandResult(process.exitValue(), output);
}
private long countMatchingResources(MediaResourceMetadataRepairRequest request) {
return hotMediaResourceMapper.selectCount(buildBaseQuery(request));
}
private List<HotMediaResource> fetchResources(MediaResourceMetadataRepairRequest request, long lastId, int fetchSize) {
LambdaQueryWrapper<HotMediaResource> queryWrapper = buildBaseQuery(request);
queryWrapper.gt(lastId > 0, HotMediaResource::getId, lastId);
queryWrapper.orderByAsc(HotMediaResource::getId);
queryWrapper.last("limit " + fetchSize);
return hotMediaResourceMapper.selectList(queryWrapper);
}
private LambdaQueryWrapper<HotMediaResource> buildBaseQuery(MediaResourceMetadataRepairRequest request) {
LambdaQueryWrapper<HotMediaResource> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(request.getCompanyId() != null, HotMediaResource::getCompanyId, request.getCompanyId());
queryWrapper.eq(request.getMediaType() != null, HotMediaResource::getMediaType, request.getMediaType());
queryWrapper.eq(StringUtils.isNotBlank(request.getCategoryCode()), HotMediaResource::getCategoryCode, request.getCategoryCode());
queryWrapper.in(request.getResourceIds() != null && !request.getResourceIds().isEmpty(), HotMediaResource::getId, request.getResourceIds());
if (Boolean.TRUE.equals(request.getOnlyInvalid())) {
if (Boolean.TRUE.equals(request.getRepairSize()) && Boolean.TRUE.equals(request.getRepairDuration())) {
queryWrapper.and(wrapper -> wrapper
.nested(size -> size.isNull(HotMediaResource::getSizeBytes).or().le(HotMediaResource::getSizeBytes, 0))
.or()
.nested(duration -> duration.isNull(HotMediaResource::getDurationSeconds).or().le(HotMediaResource::getDurationSeconds, 0))
);
} else if (Boolean.TRUE.equals(request.getRepairSize())) {
queryWrapper.and(wrapper -> wrapper.isNull(HotMediaResource::getSizeBytes).or().le(HotMediaResource::getSizeBytes, 0));
} else if (Boolean.TRUE.equals(request.getRepairDuration())) {
queryWrapper.and(wrapper -> wrapper.isNull(HotMediaResource::getDurationSeconds).or().le(HotMediaResource::getDurationSeconds, 0));
}
}
return queryWrapper;
}
private MediaResourceMetadataRepairRequest normalizeRequest(MediaResourceMetadataRepairRequest request) {
if (request == null) {
throw new ServiceException("请求参数不能为空");
}
request.setRepairSize(request.getRepairSize() == null ? Boolean.TRUE : request.getRepairSize());
request.setRepairDuration(request.getRepairDuration() == null ? Boolean.TRUE : request.getRepairDuration());
if (!Boolean.TRUE.equals(request.getRepairSize()) && !Boolean.TRUE.equals(request.getRepairDuration())) {
throw new ServiceException("至少需要开启一个修复项");
}
request.setOverwriteExisting(request.getOverwriteExisting() == null ? Boolean.FALSE : request.getOverwriteExisting());
request.setOnlyInvalid(request.getOnlyInvalid() == null ? Boolean.TRUE : request.getOnlyInvalid());
request.setSkipUnresolvedUrl(request.getSkipUnresolvedUrl() == null ? Boolean.TRUE : request.getSkipUnresolvedUrl());
int limit = request.getLimit() == null || request.getLimit() <= 0 ? properties.getDefaultLimit() : request.getLimit();
request.setLimit(Math.min(limit, properties.getMaxLimit()));
int batchSize = request.getBatchSize() == null || request.getBatchSize() <= 0 ? properties.getDefaultBatchSize() : request.getBatchSize();
request.setBatchSize(Math.min(batchSize, properties.getMaxBatchSize()));
return request;
}
private void saveTask(MediaResourceMetadataRepairTaskStatusVo taskStatus) {
RedisUtils.setCacheObject(properties.getTaskKeyPrefix() + taskStatus.getTaskId(), taskStatus, properties.taskExpireDuration());
}
private MediaResourceMetadataRepairTaskStatusVo loadTask(String taskId) {
if (StringUtils.isBlank(taskId)) {
return null;
}
return RedisUtils.getCacheObject(properties.getTaskKeyPrefix() + taskId);
}
private MediaResourceMetadataRepairTaskStatusVo requireTask(String taskId) {
MediaResourceMetadataRepairTaskStatusVo taskStatus = loadTask(taskId);
if (taskStatus == null) {
throw new ServiceException("任务不存在或已过期");
}
return taskStatus;
}
private void recordFailure(MediaResourceMetadataRepairTaskStatusVo taskStatus, HotMediaResource resource, String resolvedUrl, String reason) {
taskStatus.setFailCount(taskStatus.getFailCount() + 1);
if (taskStatus.getErrorSamples().size() >= properties.getMaxErrorSamples()) {
return;
}
MediaResourceMetadataRepairTaskStatusVo.ErrorSample errorSample = new MediaResourceMetadataRepairTaskStatusVo.ErrorSample();
errorSample.setResourceId(resource.getId());
errorSample.setFileUrl(resource.getFileUrl());
errorSample.setResolvedUrl(resolvedUrl);
errorSample.setReason(reason);
taskStatus.getErrorSamples().add(errorSample);
}
private String determineFinalStatus(MediaResourceMetadataRepairTaskStatusVo taskStatus) {
if (taskStatus.getFailCount() > 0 && taskStatus.getSuccessCount() > 0) {
return STATUS_PARTIAL_SUCCESS;
}
if (taskStatus.getFailCount() > 0 && taskStatus.getSuccessCount() <= 0) {
return STATUS_FAILED;
}
return STATUS_SUCCESS;
}
private String buildFinishMessage(MediaResourceMetadataRepairTaskStatusVo taskStatus) {
return String.format(
Locale.ROOT,
"任务完成:成功 %d跳过 %d失败 %d",
taskStatus.getSuccessCount(),
taskStatus.getSkipCount(),
taskStatus.getFailCount()
);
}
private String buildRequestSummary(MediaResourceMetadataRepairRequest request) {
List<String> parts = new ArrayList<>();
parts.add("mediaType=" + (request.getMediaType() == null ? "ALL" : request.getMediaType()));
parts.add("categoryCode=" + StringUtils.defaultIfBlank(request.getCategoryCode(), "ALL"));
parts.add("companyId=" + (request.getCompanyId() == null ? "ALL" : request.getCompanyId()));
parts.add("repairSize=" + request.getRepairSize());
parts.add("repairDuration=" + request.getRepairDuration());
parts.add("overwriteExisting=" + request.getOverwriteExisting());
parts.add("onlyInvalid=" + request.getOnlyInvalid());
parts.add("limit=" + request.getLimit());
parts.add("batchSize=" + request.getBatchSize());
if (request.getResourceIds() != null && !request.getResourceIds().isEmpty()) {
parts.add("resourceIds=" + request.getResourceIds().size());
}
return String.join(", ", parts);
}
private String resolveOperatorName() {
try {
return LoginHelper.getUsername();
} catch (Exception ignored) {
return "unknown";
}
}
private Path getTaskWorkDir(String taskId) {
return Path.of(properties.getWorkDir(), taskId);
}
private Path getResourceWorkDir(String taskId, Long resourceId) {
return getTaskWorkDir(taskId).resolve(resourceId + "_" + System.currentTimeMillis());
}
private long downloadToFile(URI uri, Path targetFile, long maxSizeBytes) throws IOException, InterruptedException {
HttpResponse<InputStream> response = httpClient().send(buildRequest(uri), HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new ServiceException("下载失败, HTTP状态码: " + response.statusCode());
}
Files.createDirectories(targetFile.getParent());
long total = 0L;
try (InputStream inputStream = response.body()) {
byte[] buffer = new byte[8192];
int read;
try (var outputStream = Files.newOutputStream(targetFile)) {
while ((read = inputStream.read(buffer)) != -1) {
total += read;
if (maxSizeBytes > 0 && total > maxSizeBytes) {
throw new ServiceException("下载文件超过大小限制");
}
outputStream.write(buffer, 0, read);
}
}
}
return total;
}
private byte[] downloadToBytes(URI uri, long maxSizeBytes) throws IOException, InterruptedException {
HttpResponse<InputStream> response = httpClient().send(buildRequest(uri), HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new ServiceException("下载失败, HTTP状态码: " + response.statusCode());
}
try (InputStream inputStream = response.body()) {
byte[] buffer = new byte[8192];
int read;
int total = 0;
var outputStream = new java.io.ByteArrayOutputStream();
while ((read = inputStream.read(buffer)) != -1) {
total += read;
if (maxSizeBytes > 0 && total > maxSizeBytes) {
throw new ServiceException("下载内容超过大小限制");
}
outputStream.write(buffer, 0, read);
}
return outputStream.toByteArray();
}
}
private HttpRequest buildRequest(URI uri) {
return HttpRequest.newBuilder(uri)
.GET()
.timeout(Duration.ofSeconds(properties.getReadTimeoutSeconds()))
.header("User-Agent", "hotwj-media-resource-repair")
.build();
}
private HttpClient httpClient() {
return HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(properties.getConnectTimeoutSeconds()))
.build();
}
private URI toHttpUri(String url) {
try {
URI uri = new URI(StringUtils.trim(url));
String scheme = StringUtils.defaultString(uri.getScheme()).toLowerCase(Locale.ROOT);
if (!"http".equals(scheme) && !"https".equals(scheme)) {
throw new ServiceException("仅支持 http/https 下载地址");
}
return uri;
} catch (URISyntaxException ex) {
throw new ServiceException("URL 不合法: " + abbreviate(url, 200));
}
}
private String firstToken(String raw) {
if (StringUtils.isBlank(raw)) {
return null;
}
String[] values = raw.split("[,\\r\\n]");
for (String value : values) {
if (StringUtils.isNotBlank(value)) {
return value.trim();
}
}
return null;
}
private boolean isInvalid(Long value) {
return value == null || value <= 0;
}
private String buildHlsAssetName(URI uri, String defaultName) {
return defaultName + extensionFromPath(uri.getPath(), ".bin");
}
private String extensionFromPath(String path, String defaultExtension) {
if (StringUtils.isBlank(path)) {
return defaultExtension;
}
int index = path.lastIndexOf('.');
if (index < 0 || index == path.length() - 1) {
return defaultExtension;
}
String extension = path.substring(index);
return extension.length() > 12 ? defaultExtension : extension;
}
private long parseExtinfDuration(String line) {
String value = line.substring("#EXTINF:".length());
int commaIndex = value.indexOf(',');
if (commaIndex >= 0) {
value = value.substring(0, commaIndex);
}
try {
return Math.round(Double.parseDouble(value.trim()));
} catch (NumberFormatException ignored) {
return 0L;
}
}
private String abbreviate(String value, int maxLength) {
if (value == null) {
return "";
}
String normalized = value.replaceAll("\\s+", " ").trim();
if (normalized.length() <= maxLength) {
return normalized;
}
return normalized.substring(0, Math.max(maxLength - 3, 0)) + "...";
}
private void deleteQuietly(Path path) {
if (path == null || !Files.exists(path)) {
return;
}
try {
if (Files.isDirectory(path)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
for (Path child : stream) {
deleteQuietly(child);
}
}
}
Files.deleteIfExists(path);
} catch (Exception ex) {
log.warn("删除临时文件失败: {}", path, ex);
}
}
private enum ProcessResultType {
SUCCESS,
SKIP,
FAIL
}
private static class ProcessResult {
private final ProcessResultType type;
private final String message;
private final String resolvedUrl;
private ProcessResult(ProcessResultType type, String message, String resolvedUrl) {
this.type = type;
this.message = message;
this.resolvedUrl = resolvedUrl;
}
private static ProcessResult success(String message, String resolvedUrl) {
return new ProcessResult(ProcessResultType.SUCCESS, message, resolvedUrl);
}
private static ProcessResult skip(String message, String resolvedUrl) {
return new ProcessResult(ProcessResultType.SKIP, message, resolvedUrl);
}
private static ProcessResult fail(String message, String resolvedUrl) {
return new ProcessResult(ProcessResultType.FAIL, message, resolvedUrl);
}
}
private static class ResolvedUrl {
private final String url;
private final boolean hls;
private ResolvedUrl(String url, boolean hls) {
this.url = url;
this.hls = hls;
}
}
private static class DownloadedResource {
private final Path probePath;
private final Long totalSizeBytes;
private final Long extinfDuration;
private DownloadedResource(Path probePath, Long totalSizeBytes, Long extinfDuration) {
this.probePath = probePath;
this.totalSizeBytes = totalSizeBytes;
this.extinfDuration = extinfDuration;
}
}
private static class HlsPlaylistResult {
private final Path probePath;
private final Long extinfDuration;
private HlsPlaylistResult(Path probePath, Long extinfDuration) {
this.probePath = probePath;
this.extinfDuration = extinfDuration;
}
}
private static class CommandResult {
private final int exitCode;
private final String output;
private CommandResult(int exitCode, String output) {
this.exitCode = exitCode;
this.output = output;
}
}
private class HlsContext implements java.io.Serializable {
@Serial
private static final long serialVersionUID = 1L;
private long totalSize = 0L;
private int segmentCount = 0;
private int fileIndex = 0;
private void addBytes(long size) {
totalSize += size;
if (properties.getMaxHlsTotalSizeBytes() != null && properties.getMaxHlsTotalSizeBytes() > 0 && totalSize > properties.getMaxHlsTotalSizeBytes()) {
throw new ServiceException("HLS 总下载大小超过限制");
}
}
private void addSegment() {
segmentCount++;
if (properties.getMaxHlsSegments() != null && properties.getMaxHlsSegments() > 0 && segmentCount > properties.getMaxHlsSegments()) {
throw new ServiceException("HLS 分片数超过限制");
}
}
private int nextFileIndex() {
fileIndex++;
return fileIndex;
}
}
}

View File

@@ -0,0 +1,502 @@
package com.hotwj.platform.system.maintenance.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hotwj.platform.common.photoEvidence.domain.HotPhotoEvidence;
import com.hotwj.platform.common.photoEvidence.mapper.HotPhotoEvidenceMapper;
import com.hotwj.platform.common.photoEvidence.support.PhotoEvidenceWatermarkSupport;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairExecuteRequest;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairQueryBo;
import com.hotwj.platform.system.maintenance.domain.bo.PhotoEvidenceRepairTaskRequest;
import com.hotwj.platform.system.maintenance.domain.vo.*;
import com.hotwj.platform.system.maintenance.service.IPhotoEvidenceRepairService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.redis.utils.CacheUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.SysOss;
import org.dromara.system.domain.SysOssExt;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.mapper.SysOssMapper;
import org.dromara.system.service.ISysOssService;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
@Slf4j
@Service
@RequiredArgsConstructor
public class PhotoEvidenceRepairServiceImpl implements IPhotoEvidenceRepairService {
private static final String STATUS_PENDING = "PENDING";
private static final String STATUS_RUNNING = "RUNNING";
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String STATUS_PARTIAL_SUCCESS = "PARTIAL_SUCCESS";
private static final String STATUS_FAILED = "FAILED";
private static final String TASK_KEY_PREFIX = "photo_evidence_repair_task:";
private static final int MAX_ERROR_SAMPLES = 20;
private final HotPhotoEvidenceMapper hotPhotoEvidenceMapper;
private final SysOssMapper sysOssMapper;
private final ISysOssService ossService;
private final ScheduledExecutorService scheduledExecutorService;
private final PhotoEvidenceWatermarkSupport watermarkSupport;
@Override
public TableDataInfo<PhotoEvidenceRepairCandidateVo> queryPageList(PhotoEvidenceRepairQueryBo queryBo, PageQuery pageQuery) {
PhotoEvidenceRepairQueryBo normalizedQuery = normalizeQuery(queryBo);
PageQuery normalizedPage = normalizePage(pageQuery);
long total = hotPhotoEvidenceMapper.countRepairCandidates(normalizedQuery);
if (total <= 0) {
return new TableDataInfo<>(Collections.emptyList(), 0);
}
List<PhotoEvidenceRepairCandidateVo> rows = hotPhotoEvidenceMapper.selectRepairCandidates(
normalizedQuery,
normalizedPage.getFirstNum(),
normalizedPage.getPageSize()
);
fillCandidateRuntimeFields(rows);
return new TableDataInfo<>(rows, total);
}
@Override
public PhotoEvidenceRepairPreviewVo preview(Long evidenceId) {
HotPhotoEvidence evidence = requireEvidence(evidenceId);
SysOssVo originalOss = requireOss(evidence.getOriginalOssId(), "原始图片不存在");
SysOssVo watermarkedOss = requireOss(evidence.getWatermarkedOssId(), "水印图片不存在");
byte[] previewBytes = watermarkSupport.renderToBytes(
downloadOssFile(originalOss),
resolveCaptureTime(evidence),
resolveWatermarkAddress(evidence)
);
PhotoEvidenceRepairPreviewVo previewVo = new PhotoEvidenceRepairPreviewVo();
previewVo.setEvidenceId(evidence.getId());
previewVo.setOriginalOssId(evidence.getOriginalOssId());
previewVo.setWatermarkedOssId(evidence.getWatermarkedOssId());
previewVo.setOriginalImageUrl(originalOss.getUrl());
previewVo.setCurrentWatermarkedUrl(watermarkedOss.getUrl());
previewVo.setPreviewDataUrl("data:" + PhotoEvidenceWatermarkSupport.OUTPUT_CONTENT_TYPE + ";base64," +
Base64.getEncoder().encodeToString(previewBytes));
previewVo.setCurrentMetadata(buildMetadataSnapshot(watermarkedOss, parseExt(watermarkedOss.getExt1())));
previewVo.setTargetMetadata(buildTargetMetadata(watermarkedOss.getOssId(), previewBytes.length));
previewVo.setAbnormalReasons(resolveAbnormalReasons(watermarkedOss.getFileSuffix(), watermarkedOss.getExt1()));
return previewVo;
}
@Override
public PhotoEvidenceRepairExecuteVo repair(PhotoEvidenceRepairExecuteRequest request) {
requirePreviewConfirmed(request.getPreviewConfirmed());
return doRepair(request.getEvidenceId());
}
@Override
public PhotoEvidenceRepairSubmitVo submitRepairTask(PhotoEvidenceRepairTaskRequest request) {
requirePreviewConfirmed(request.getPreviewConfirmed());
List<Long> evidenceIds = normalizeEvidenceIds(request.getEvidenceIds());
if (evidenceIds.isEmpty()) {
throw new ServiceException("请选择需要修复的记录");
}
String taskId = UUID.randomUUID().toString().replace("-", "");
PhotoEvidenceRepairTaskStatusVo taskStatus = new PhotoEvidenceRepairTaskStatusVo();
taskStatus.setTaskId(taskId);
taskStatus.setStatus(STATUS_PENDING);
taskStatus.setTotal(evidenceIds.size());
taskStatus.setOperatorName(resolveOperatorName());
taskStatus.setRequestSummary("evidenceIds=" + evidenceIds.size());
taskStatus.setMessage("任务已提交,等待执行");
saveTask(taskStatus);
scheduledExecutorService.execute(() -> runTask(taskId, evidenceIds));
return new PhotoEvidenceRepairSubmitVo(taskId, Boolean.TRUE, "图片水印修复任务已提交");
}
@Override
public PhotoEvidenceRepairTaskStatusVo getTaskStatus(String taskId) {
PhotoEvidenceRepairTaskStatusVo taskStatus = loadTask(taskId);
if (taskStatus == null) {
throw new ServiceException("任务不存在或已过期");
}
return taskStatus;
}
private void runTask(String taskId, List<Long> evidenceIds) {
PhotoEvidenceRepairTaskStatusVo taskStatus = requireTask(taskId);
long startTime = System.currentTimeMillis();
try {
taskStatus.setStatus(STATUS_RUNNING);
taskStatus.setStartTime(startTime);
taskStatus.setMessage("任务执行中");
saveTask(taskStatus);
for (Long evidenceId : evidenceIds) {
try {
PhotoEvidenceRepairExecuteVo result = doRepair(evidenceId);
taskStatus.setSuccessCount(taskStatus.getSuccessCount() + 1);
taskStatus.getSuccessEvidenceIds().add(result.getEvidenceId());
} catch (Exception ex) {
log.error("图片水印修复失败, evidenceId={}", evidenceId, ex);
recordFailure(taskStatus, evidenceId, abbreviate(ex.getMessage(), 300));
} finally {
taskStatus.setProcessedCount(taskStatus.getProcessedCount() + 1);
saveTask(taskStatus);
}
}
taskStatus.setEndTime(System.currentTimeMillis());
taskStatus.setDurationMillis(taskStatus.getEndTime() - startTime);
taskStatus.setStatus(determineFinalStatus(taskStatus));
taskStatus.setMessage(buildFinishMessage(taskStatus));
saveTask(taskStatus);
} catch (Exception ex) {
log.error("图片水印修复任务执行失败, taskId={}", taskId, ex);
taskStatus.setStatus(STATUS_FAILED);
taskStatus.setEndTime(System.currentTimeMillis());
taskStatus.setDurationMillis(taskStatus.getEndTime() - startTime);
taskStatus.setMessage("任务执行失败: " + abbreviate(ex.getMessage(), 300));
saveTask(taskStatus);
}
}
private PhotoEvidenceRepairExecuteVo doRepair(Long evidenceId) {
HotPhotoEvidence evidence = requireEvidence(evidenceId);
SysOssVo originalOss = requireOss(evidence.getOriginalOssId(), "原始图片不存在");
SysOssVo watermarkedOss = requireOss(evidence.getWatermarkedOssId(), "水印图片不存在");
PhotoEvidenceWatermarkSupport.RenderedWatermark renderedWatermark = null;
try {
renderedWatermark = watermarkSupport.renderToTempFile(
downloadOssFile(originalOss),
resolveCaptureTime(evidence),
resolveWatermarkAddress(evidence)
);
String oldFileName = watermarkedOss.getFileName();
OssClient storage = resolveStorage(watermarkedOss.getService());
String objectKey = storage.getPath("photo-evidence", PhotoEvidenceWatermarkSupport.OUTPUT_SUFFIX);
UploadResult uploadResult = storage.upload(
renderedWatermark.getFile().toPath(),
objectKey,
null,
PhotoEvidenceWatermarkSupport.OUTPUT_CONTENT_TYPE
);
SysOssExt ext = parseExt(watermarkedOss.getExt1());
if (ext == null) {
ext = new SysOssExt();
}
ext.setFileSize(renderedWatermark.getFileSize());
ext.setContentType(PhotoEvidenceWatermarkSupport.OUTPUT_CONTENT_TYPE);
SysOss update = new SysOss();
update.setOssId(watermarkedOss.getOssId());
update.setFileName(uploadResult.getFilename());
update.setUrl(uploadResult.getUrl());
update.setFileSuffix(PhotoEvidenceWatermarkSupport.OUTPUT_SUFFIX);
update.setOriginalName(watermarkSupport.buildStandardOriginalName(watermarkedOss.getOssId()));
update.setService(resolveServiceKey(watermarkedOss.getService()));
update.setExt1(JsonUtils.toJsonString(ext));
if (sysOssMapper.updateById(update) <= 0) {
throw new ServiceException("覆盖原水印 sys_oss 记录失败");
}
CacheUtils.evict(CacheNames.SYS_OSS, watermarkedOss.getOssId());
SysOssVo refreshed = requireOss(watermarkedOss.getOssId(), "修复后的水印图片不存在");
log.info("图片水印修复成功, evidenceId={}, watermarkedOssId={}, oldFileName={}, newFileName={}",
evidenceId, watermarkedOss.getOssId(), oldFileName, refreshed.getFileName());
return new PhotoEvidenceRepairExecuteVo(
evidenceId,
watermarkedOss.getOssId(),
oldFileName,
refreshed.getFileName(),
refreshed.getUrl(),
"修复成功"
);
} finally {
if (renderedWatermark != null) {
watermarkSupport.deleteQuietly(renderedWatermark.getFile());
}
}
}
private PhotoEvidenceRepairPreviewVo.MetadataSnapshot buildMetadataSnapshot(SysOssVo oss, SysOssExt ext) {
PhotoEvidenceRepairPreviewVo.MetadataSnapshot snapshot = new PhotoEvidenceRepairPreviewVo.MetadataSnapshot();
if (oss != null) {
snapshot.setFileSuffix(oss.getFileSuffix());
snapshot.setOriginalName(oss.getOriginalName());
}
if (ext != null) {
snapshot.setFileSize(ext.getFileSize());
snapshot.setContentType(ext.getContentType());
}
return snapshot;
}
private PhotoEvidenceRepairPreviewVo.MetadataSnapshot buildTargetMetadata(Long watermarkedOssId, long fileSize) {
PhotoEvidenceRepairPreviewVo.MetadataSnapshot snapshot = new PhotoEvidenceRepairPreviewVo.MetadataSnapshot();
snapshot.setFileSuffix(PhotoEvidenceWatermarkSupport.OUTPUT_SUFFIX);
snapshot.setOriginalName(watermarkSupport.buildStandardOriginalName(watermarkedOssId));
snapshot.setFileSize(fileSize);
snapshot.setContentType(PhotoEvidenceWatermarkSupport.OUTPUT_CONTENT_TYPE);
return snapshot;
}
private void fillCandidateRuntimeFields(List<PhotoEvidenceRepairCandidateVo> rows) {
if (rows == null || rows.isEmpty()) {
return;
}
Set<Long> ossIds = new LinkedHashSet<>();
for (PhotoEvidenceRepairCandidateVo row : rows) {
if (row.getOriginalOssId() != null) {
ossIds.add(row.getOriginalOssId());
}
if (row.getWatermarkedOssId() != null) {
ossIds.add(row.getWatermarkedOssId());
}
}
Map<Long, SysOssVo> ossMap = listOssAsMap(ossIds);
for (PhotoEvidenceRepairCandidateVo row : rows) {
SysOssVo originalOss = ossMap.get(row.getOriginalOssId());
SysOssVo watermarkedOss = ossMap.get(row.getWatermarkedOssId());
if (originalOss != null) {
row.setOriginalImageUrl(originalOss.getUrl());
}
if (watermarkedOss != null) {
row.setCurrentWatermarkedUrl(watermarkedOss.getUrl());
row.setWatermarkedFileSuffix(watermarkedOss.getFileSuffix());
row.setWatermarkedOriginalName(watermarkedOss.getOriginalName());
row.setWatermarkedExt1(watermarkedOss.getExt1());
}
SysOssExt ext = parseExt(row.getWatermarkedExt1());
if (ext != null) {
row.setCurrentFileSize(ext.getFileSize());
row.setCurrentContentType(ext.getContentType());
}
row.setAbnormalReasons(resolveAbnormalReasons(row.getWatermarkedFileSuffix(), row.getWatermarkedExt1()));
}
}
private Map<Long, SysOssVo> listOssAsMap(Collection<Long> ossIds) {
if (ossIds == null || ossIds.isEmpty()) {
return Collections.emptyMap();
}
List<SysOssVo> ossList = ossService.listByIds(ossIds);
Map<Long, SysOssVo> result = new LinkedHashMap<>();
for (SysOssVo item : ossList) {
if (item != null && item.getOssId() != null) {
result.put(item.getOssId(), item);
}
}
return result;
}
private List<String> resolveAbnormalReasons(String fileSuffix, String ext1Json) {
List<String> reasons = new ArrayList<>();
if (isAbnormalSuffix(fileSuffix)) {
reasons.add("文件后缀异常");
}
SysOssExt ext = parseExt(ext1Json);
if (ext != null && ext.getFileSize() != null && ext.getFileSize() <= 0) {
reasons.add("文件大小为0");
}
return reasons;
}
private boolean isAbnormalSuffix(String fileSuffix) {
if (StringUtils.isBlank(fileSuffix)) {
return false;
}
return fileSuffix.matches("^\\.?\\d+$");
}
private SysOssExt parseExt(String ext1Json) {
if (StringUtils.isBlank(ext1Json)) {
return null;
}
try {
return JsonUtils.parseObject(ext1Json, SysOssExt.class);
} catch (Exception ex) {
log.warn("解析 sys_oss.ext1 失败: {}", abbreviate(ext1Json, 200), ex);
return null;
}
}
private PhotoEvidenceRepairQueryBo normalizeQuery(PhotoEvidenceRepairQueryBo queryBo) {
PhotoEvidenceRepairQueryBo query = queryBo == null ? new PhotoEvidenceRepairQueryBo() : queryBo;
if (query.getOnlyAbnormal() == null) {
query.setOnlyAbnormal(Boolean.TRUE);
}
return query;
}
private PageQuery normalizePage(PageQuery pageQuery) {
PageQuery normalized = pageQuery == null ? new PageQuery(20, 1) : pageQuery;
if (normalized.getPageNum() == null || normalized.getPageNum() <= 0) {
normalized.setPageNum(1);
}
if (normalized.getPageSize() == null || normalized.getPageSize() <= 0) {
normalized.setPageSize(20);
}
return normalized;
}
private HotPhotoEvidence requireEvidence(Long evidenceId) {
if (evidenceId == null) {
throw new ServiceException("evidenceId不能为空");
}
HotPhotoEvidence evidence = hotPhotoEvidenceMapper.selectOne(
Wrappers.<HotPhotoEvidence>lambdaQuery()
.eq(HotPhotoEvidence::getId, evidenceId)
.eq(HotPhotoEvidence::getIsDeleted, 0L)
);
if (evidence == null) {
throw new ServiceException("取证记录不存在");
}
return evidence;
}
private SysOssVo requireOss(Long ossId, String message) {
if (ossId == null) {
throw new ServiceException(message);
}
SysOssVo oss = ossService.getById(ossId);
if (oss == null) {
throw new ServiceException(message);
}
return oss;
}
private byte[] downloadOssFile(SysOssVo oss) {
OssClient storage = resolveStorage(oss.getService());
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
storage.download(oss.getFileName(), baos, null);
byte[] bytes = baos.toByteArray();
if (bytes.length <= 0) {
throw new ServiceException("原始图片为空文件");
}
return bytes;
} catch (Exception ex) {
throw new ServiceException("原始图片下载失败: " + ex.getMessage());
}
}
private Date resolveCaptureTime(HotPhotoEvidence evidence) {
return evidence.getCaptureTimeClient() != null ? evidence.getCaptureTimeClient() : evidence.getCaptureTimeServer();
}
private String resolveWatermarkAddress(HotPhotoEvidence evidence) {
if (StringUtils.equalsIgnoreCase("SERVER", evidence.getAddressSource()) && StringUtils.isNotBlank(evidence.getAddressTextServer())) {
return evidence.getAddressTextServer();
}
if (StringUtils.isNotBlank(evidence.getAddressTextClient())) {
return evidence.getAddressTextClient();
}
return evidence.getAddressTextServer();
}
private OssClient resolveStorage(String serviceKey) {
String resolvedServiceKey = resolveServiceKey(serviceKey);
return StringUtils.isBlank(resolvedServiceKey) ? OssFactory.instance() : OssFactory.instance(resolvedServiceKey);
}
private String resolveServiceKey(String serviceKey) {
return StringUtils.isBlank(serviceKey) ? null : serviceKey;
}
private void requirePreviewConfirmed(Boolean previewConfirmed) {
if (!Boolean.TRUE.equals(previewConfirmed)) {
throw new ServiceException("请先完成预览确认后再执行修复");
}
}
private List<Long> normalizeEvidenceIds(List<Long> evidenceIds) {
if (evidenceIds == null || evidenceIds.isEmpty()) {
return Collections.emptyList();
}
return evidenceIds.stream()
.filter(Objects::nonNull)
.distinct()
.toList();
}
private void saveTask(PhotoEvidenceRepairTaskStatusVo taskStatus) {
RedisUtils.setCacheObject(TASK_KEY_PREFIX + taskStatus.getTaskId(), taskStatus);
}
private PhotoEvidenceRepairTaskStatusVo loadTask(String taskId) {
if (StringUtils.isBlank(taskId)) {
return null;
}
return RedisUtils.getCacheObject(TASK_KEY_PREFIX + taskId);
}
private PhotoEvidenceRepairTaskStatusVo requireTask(String taskId) {
PhotoEvidenceRepairTaskStatusVo taskStatus = loadTask(taskId);
if (taskStatus == null) {
throw new ServiceException("任务不存在或已过期");
}
return taskStatus;
}
private void recordFailure(PhotoEvidenceRepairTaskStatusVo taskStatus, Long evidenceId, String reason) {
taskStatus.setFailCount(taskStatus.getFailCount() + 1);
if (taskStatus.getErrorSamples().size() >= MAX_ERROR_SAMPLES) {
return;
}
PhotoEvidenceRepairTaskStatusVo.ErrorSample errorSample = new PhotoEvidenceRepairTaskStatusVo.ErrorSample();
errorSample.setEvidenceId(evidenceId);
PhotoEvidenceRepairCandidateVo candidate = hotPhotoEvidenceMapper.selectRepairCandidateByEvidenceId(evidenceId);
if (candidate != null) {
errorSample.setWatermarkedOssId(candidate.getWatermarkedOssId());
}
errorSample.setReason(reason);
taskStatus.getErrorSamples().add(errorSample);
}
private String determineFinalStatus(PhotoEvidenceRepairTaskStatusVo taskStatus) {
if (taskStatus.getFailCount() > 0 && taskStatus.getSuccessCount() > 0) {
return STATUS_PARTIAL_SUCCESS;
}
if (taskStatus.getFailCount() > 0) {
return STATUS_FAILED;
}
return STATUS_SUCCESS;
}
private String buildFinishMessage(PhotoEvidenceRepairTaskStatusVo taskStatus) {
return String.format(
Locale.ROOT,
"任务完成:成功 %d失败 %d",
taskStatus.getSuccessCount(),
taskStatus.getFailCount()
);
}
private String resolveOperatorName() {
try {
return LoginHelper.getUsername();
} catch (Exception ignored) {
return "unknown";
}
}
private String abbreviate(String value, int maxLength) {
if (value == null) {
return "";
}
String normalized = value.replaceAll("\\s+", " ").trim();
if (normalized.length() <= maxLength) {
return normalized;
}
return normalized.substring(0, Math.max(0, maxLength - 3)) + "...";
}
}

View File

@@ -1084,13 +1084,13 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
// } else {
boolean isLongTerm = d.getDriverLicenseLongTerm() != null && d.getDriverLicenseLongTerm() == 1L;
if (!isLongTerm && isDriver) {
checkDateRisk(dlRisks, convertDateToLocalDate(d.getDriverLicenseExpireDate()), "驾驶证: (" + name + ")", "驾驶证", "driver_license", 90, 1, driverId, null, linkBase + "1");
checkDateRisk(dlRisks, convertDateToLocalDate(d.getDriverLicenseExpireDate()), "驾驶证: (" + name + ")", "驾驶证", "driver_license", 30, 1, driverId, null, linkBase + "1");
}
// }
// 4. 从业资格证
if (d.getQualificationValidEndDate() != null) {
checkDateRisk(qualRisks, convertDateToLocalDate(d.getQualificationValidEndDate()), "从业资格证: (" + name + ")", "从业资格证", "qualification_certificate", 90, 1, driverId, null, linkBase + "1");
checkDateRisk(qualRisks, convertDateToLocalDate(d.getQualificationValidEndDate()), "从业资格证: (" + name + ")", "从业资格证", "qualification_certificate", 30, 1, driverId, null, linkBase + "1");
} else {
// 如果不是驾驶员(如押运员)且缺失,则预警
if (!isDriver) {

View File

@@ -74,7 +74,8 @@ public class GenTableServiceImpl implements IGenTableService {
public List<GenTableColumn> selectGenTableColumnListByTableId(Long tableId) {
return genTableColumnMapper.selectList(new LambdaQueryWrapper<GenTableColumn>()
.eq(GenTableColumn::getTableId, tableId)
.orderByAsc(GenTableColumn::getSort));
.orderByAsc(GenTableColumn::getSort)
.orderByAsc(GenTableColumn::getColumnId));
}
/**
@@ -105,7 +106,8 @@ public class GenTableServiceImpl implements IGenTableService {
.like(StringUtils.isNotBlank(genTable.getTableComment()), "lower(table_comment)", StringUtils.lowerCase(genTable.getTableComment()))
.between(params.get("beginTime") != null && params.get("endTime") != null,
"create_time", params.get("beginTime"), params.get("endTime"))
.orderByDesc("update_time");
.orderByDesc("update_time")
.orderByDesc("table_id");
return wrapper;
}

View File

@@ -135,7 +135,8 @@ public interface SysDeptMapper extends BaseMapperPlus<SysDept, SysDeptVo> {
wrapper.select(SysDept::getDeptId)
.inSql(SysDept::getDeptId, this.buildDeptByRoleSql(roleId))
.orderByAsc(SysDept::getParentId)
.orderByAsc(SysDept::getOrderNum);
.orderByAsc(SysDept::getOrderNum)
.orderByAsc(SysDept::getDeptId);
if (deptCheckStrictly) {
wrapper.notInSql(SysDept::getDeptId, this.buildParentDeptByRoleSql(roleId));
}

Some files were not shown because too many files have changed in this diff Show More