| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
aa593aecbb | feat: eqpbookChannel表新增useMode字段的汇聚 | 6 days ago |
|
|
adf227e030 | feat: eqpbookChannel表新增useMode字段用于区分摄像头设备类型 | 1 week ago |
|
|
cbb216aa38 | feat: 当登录ip值为空或*时,允许任意ip进行登录 | 1 week ago |
|
|
666f11175b | feat: 部门树和菜单树新增status字段,表示启用/禁用 | 2 weeks ago |
|
|
6ad6dd9fb5 | feat: 汇聚agg_station新增order_num自定义排序 | 2 weeks ago |
|
|
932d5b0824 | feat: 新增中恒token登录后的“动环”相关角色同步 | 2 weeks ago |
|
|
419d4e2b36 | Merge branch 'feature/agg' of http://git.ht-atia.cn/qinyanlei/inspect into feature/agg | 2 weeks ago |
|
|
9f42c6fc59 |
perf: 汇聚层访问站端改用 HTTP 连接池
- RestTemplate 改用 PoolingHttpClientConnectionManager(总连接 100、单站端 40、 取连接等待 3s、空闲 30s 清理),避免打开多画面时每次请求都新建 TCP 连接; 实测九画面并发起流最慢从 5.6s 降到 3.7s,起流不再触碰页面 8 秒超时 |
2 weeks ago |
|
|
ee5c111a47 | fix:修复三方登录异常;同步三方aollowIp设为*;对null值做处理 | 2 weeks ago |
|
|
22ad366412 | feat: 定时同步中恒用户相关信息,并实现中恒token登录 | 2 weeks ago |
|
|
aa604167e0 | fix: 修复汇聚缺陷记录按时间查询;暂时不统计轻微缺陷数量 | 2 weeks ago |
|
|
2811ed7568 | feat: 新增channel树的deviceSource筛选;新增对channel表capabilities字段的汇聚;新增汇聚area/eqpbook/channel表的order_num字段自定义排序 | 3 weeks ago |
|
|
8f69b1e95b | feat: 汇聚端 nginx 按 stationCode 将前端对图片、文件、视频流的请求反代到各站端;实时流的启停由汇聚直接调用站端接口 | 3 weeks ago |
|
|
3d4ca6ad4a | feat: 更新gitignore | 1 month ago |
|
|
d518479bb4 | 汇聚代码第一次提交 | 1 month ago |
|
|
3db44ddabc | feat: 表计meter算法支持对读数“正常”的解析 | 2 months ago |
|
|
c25a8d0354 | fix: 修复报告上传消息无限重投递,超过 3 次直接丢弃 | 2 months ago |
|
|
d5b7c89d98 | Merge branch 'yanyuan' of http://git.ht-atia.cn/qinyanlei/inspect into yanyuan | 2 months ago |
|
|
8b7b8c340f | feat: 缺陷记录添加算法筛选,按算法名精确匹配 | 2 months ago |
|
|
5667eaea71 | feat: 新增分析流程完结字段completed用于数据上送 | 2 months ago |
|
|
ff5dbf20b0 | feat: 缺陷记录添加下拉筛选,按缺陷描述精确匹配 | 2 months ago |
|
|
2fde923bf6 | temp: 临时屏蔽 RequiresPermissions 注解 | 2 months ago |
|
|
86055b2281 | feat: 新增通道查询返回台账名称 | 2 months ago |
|
|
c7e0677b81 | feat: 新增接口用于明文用户密码获取token | 2 months ago |
|
|
f7d4b63c2e | Merge branch 'yanyuan' of http://git.ht-atia.cn/qinyanlei/inspect into yanyuan | 2 months ago |
|
|
40854cc240 | fix: 修复修改用户信息时会将用户密码同步修改 | 2 months ago |
|
|
166ccb8b2b | feat: 缺陷记录添加筛选条件 | 2 months ago |
|
|
b5ec64abe0 | feat: 点位数据分析完成时通过WS上送 | 2 months ago |
|
|
6313f95ded |
fix(redis): 按分钟执行的间隔任务不再生成窗口外的策略 key
上一提交(移除 KEYS 全库扫描)把"清理窗口外策略 key"的动作从"每个执行时刻一次"提到 "每个任务一次"。但按分钟执行的间隔任务,其 exeTimes 由 getExecTimesByMinute 生成到 次日 00:00,会超出任务配置的 intervalEndTime:原先每次存 key 前的清理会把窗口外的 key 删掉(除最后一次外),提取到循环外之后这些窗口外的 key 会全部留存,导致调度器在配置 窗口之外仍然执行任务。 本提交在按分钟执行的循环内补上守卫:晚于 intervalEndTime 的时刻直接跳过,既不清理也不 生成策略 key,与"只保留 finalDate 之内的策略 key"的原意保持一致(按小时执行的 exeTimes 本来就以 intervalEndTime 为界,不受影响)。 |
2 months ago |
|
|
de77ada07a |
fix(redis): 生产路径禁止使用 KEYS,改用 SCAN,修复 Redis 阻塞导致巡检服务不可用
现象:巡检服务(dliip-patrol)容器连续约 9 小时 unhealthy;Tomcat 200 个请求线程 全部阻塞在 Lettuce(AsyncCommand.await 无超时,永不失败),容器 CPU 在 2%~500% 间跳变, 健康检查(curl 127.0.0.1:9901,10s 超时)连续 284 次超时。 根因:inspect-job 定时调用 /exec/makeCurrentDayTask,该接口遍历所有启用任务,对每个 "间隔执行"任务的每个执行时刻调用一次 parseTaskToRedis,而 parseTaskToRedis 内部用 redisService.keys(TASK_CODE@taskCode@*) 做全库扫描来清理策略 key。于是一次调用会对 Redis 发起成百上千次 KEYS:Redis 单线程,单次 KEYS 约 13ms 且返回体巨大(实测输出 8MB/s,单个客户端连接积压 321 条回复 / 5.8MB 输出缓冲),KEYS 执行期间其它所有客户端 的命令全部排队,实测 Redis 响应延迟 avg 287ms / max 1365ms,进而导致上游业务请求堆积、 Tomcat 线程池被占满、健康检查排不上队而超时。 修复: 1. RedisService 新增 scan(pattern[, count]),基于 SCAN 游标分批读取,不阻塞其它客户端; 原 keys() 标记 @Deprecated 并注明生产环境禁用。 2. 全仓库 11 处 redisService.keys() 调用点改为 scan()。 3. PatrolTaskExecController:抽出 cleanTaskKeysAfter(),策略 key 清理由"每个执行时刻一次" 改为"每个任务一次",一次 makeCurrentDayTask 的 KEYS 次数从成百上千次降到与任务数同量级。 4. PatrolTaskController:批量删除任务时原为 N 个 taskId 扫描 N 次全库,改为只扫描一次。 影响面:仅改变"按 pattern 查找 key"的实现方式(KEYS→SCAN,语义等价;SCAN 可能重复返回 同一个 key,对删除/遍历场景无影响),未改动任何业务判定逻辑。 |
2 months ago |
|
|
0aef3b2e15 | feat: 取消数据查询权限限制 | 2 months ago |
|
|
f37f7bbecc | fix: 动环巡检报告目录调整 | 2 months ago |
|
|
fef4796256 | fix: 动环巡检报告排版对齐机器人巡检报告模板 | 2 months ago |
|
|
8a19df5709 | refactor: 优化盐源ws推送,支持任意消息格式 | 2 months ago |
|
|
aa54054e76 | feat: 临时屏蔽接口所需权限 | 2 months ago |
|
|
c81de07f7a | fix:巡视报告格式,字体修改 | 2 months ago |
|
|
443d30bf02 | fix:动环报告合并及归档推送逻辑修改,新增了inspection_report_task表 | 2 months ago |
|
|
a2649203c3 | fix:报告推送任务id修改,新增自动归档开关与推送开关 | 2 months ago |
|
|
b88c984f1c | feat:雨刮和框选3D定位 | 2 months ago |
|
|
c10550aaef | Merge branch 'yanyuan' of https://git.ht-atia.cn/qinyanlei/inspect into yanyuan | 2 months ago |
|
|
5ac0de509c | fix: 修复因计划任务过多判断,导致任务错过下发时间未执行 | 2 months ago |
|
|
e8a501d488 | feat:巡视报告标题字体黑体 4号,标题段落首行缩进1字符 | 2 months ago |
|
|
d07dda28b5 | feat:缺陷汇总接口,相机通道接口 | 2 months ago |
|
|
c87a45671c | feat:盐源新版巡视报告字体嵌入,及http推送外部接口 | 2 months ago |
|
|
57e78d5c3f | feat:盐源新版巡视报告,及自动归档,websocket推送 | 3 months ago |
|
|
7bc91a250c | feat: 新增getToken接口,用于第三方调用接口鉴权 | 3 months ago |
|
|
6aaae83f4f | feat: 新增盐源ws接口(通过AlarmPublisher发送ws) | 3 months ago |
|
|
91706e3913 | fix:巡视报告异常捕获 | 3 months ago |
|
|
57c6724f4d | feat:视频预览,预置位操作,云台控制,抓拍,录像下载 | 3 months ago |
|
|
c9a56d778a | feat:1.历史回放接口,2.台账接口修改 | 3 months ago |
|
|
7403bcd61f | 第一次提交:1.新增inspect-main-api模块,用于开发盐源新特性接口;2.启动全局异常捕获器,对服务异常统一封装 | 3 months ago |
| @ -0,0 +1,24 @@ | |||
| package com.inspect.admin.feign; | |||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||
| import com.inspect.admin.feign.factory.RemoteExtAuthFallbackFactory; | |||
| import com.inspect.base.core.constant.SecurityConstants; | |||
| import com.inspect.base.core.domain.Response; | |||
| import org.springframework.cloud.openfeign.FeignClient; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.RequestHeader; | |||
| /** | |||
| * 调用 inspect-main 内部接口,由 inspect-main 向外部盐源平台换取登录用户。 | |||
| */ | |||
| @FeignClient( | |||
| contextId = "remoteExtAuthService", | |||
| value = "inspect-main", | |||
| fallbackFactory = RemoteExtAuthFallbackFactory.class | |||
| ) | |||
| public interface RemoteExtAuthService { | |||
| @GetMapping("/inner/ext_auth/loginUser") | |||
| Response<JoinbrightUser> getLoginUser(@RequestHeader("joinbright-token") String joinbrightToken, | |||
| @RequestHeader(SecurityConstants.FROM_SOURCE) String source); | |||
| } | |||
| @ -0,0 +1,30 @@ | |||
| package com.inspect.admin.feign; | |||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||
| import com.inspect.admin.feign.factory.RemoteExtUserFallbackFactory; | |||
| import com.inspect.base.core.constant.SecurityConstants; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import org.springframework.cloud.openfeign.FeignClient; | |||
| import org.springframework.web.bind.annotation.PostMapping; | |||
| import org.springframework.web.bind.annotation.RequestBody; | |||
| import org.springframework.web.bind.annotation.RequestHeader; | |||
| import org.springframework.web.bind.annotation.RequestParam; | |||
| /** | |||
| * 调用 dliip-system 的内部接口,为外部登录用户自动建号 / 绑定本地账号。 | |||
| */ | |||
| @FeignClient( | |||
| contextId = "remoteExtUserService", | |||
| value = "inspect-management", | |||
| fallbackFactory = RemoteExtUserFallbackFactory.class | |||
| ) | |||
| public interface RemoteExtUserService { | |||
| @PostMapping("/inner/ext_user/ensure") | |||
| AjaxResult ensureUser(@RequestBody JoinbrightUser user, | |||
| @RequestHeader(SecurityConstants.FROM_SOURCE) String source); | |||
| @PostMapping("/inner/ext_user/syncRoles") | |||
| AjaxResult syncRoles(@RequestParam("username") String username, | |||
| @RequestHeader(SecurityConstants.FROM_SOURCE) String source); | |||
| } | |||
| @ -0,0 +1,37 @@ | |||
| package com.inspect.admin.feign.domain; | |||
| import lombok.Data; | |||
| import java.io.Serializable; | |||
| /** | |||
| * 内部接口 /inner/ext_auth/loginUser 返回的外部登录用户信息。 | |||
| * 与 inspect-main-api 的 ExtLoginUser 按 JSON 字段约定保持一致。 | |||
| */ | |||
| @Data | |||
| public class JoinbrightUser implements Serializable { | |||
| private static final long serialVersionUID = 1L; | |||
| /** 用户id(对应本地 sys_user.ext_id) */ | |||
| private String id; | |||
| /** 账号 */ | |||
| private String username; | |||
| /** 用户名 */ | |||
| private String realName; | |||
| /** 账号状态编码,0201401 为激活 */ | |||
| private String accountStatus; | |||
| /** 账号状态文本,如“激活” */ | |||
| private String accountStatusText; | |||
| /** 所属部门id(对应本地 sys_dept.ext_id) */ | |||
| private String deptId; | |||
| /** 所属部门名称 */ | |||
| private String deptName; | |||
| /** 手机号 */ | |||
| private String mobilePhone; | |||
| /** 邮箱 */ | |||
| private String email; | |||
| /** 所属场站id */ | |||
| private String boosterStationId; | |||
| /** 所属场站名称 */ | |||
| private String boosterStationName; | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.admin.feign.factory; | |||
| import com.inspect.admin.feign.RemoteExtAuthService; | |||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||
| import com.inspect.base.core.domain.Response; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.cloud.openfeign.FallbackFactory; | |||
| import org.springframework.stereotype.Component; | |||
| @Component | |||
| public class RemoteExtAuthFallbackFactory implements FallbackFactory<RemoteExtAuthService> { | |||
| private static final Logger log = LoggerFactory.getLogger(RemoteExtAuthFallbackFactory.class); | |||
| @Override | |||
| public RemoteExtAuthService create(final Throwable throwable) { | |||
| log.error("[JOINBRIGHT] inspect-main 内部服务调用失败:{}", throwable.getMessage()); | |||
| return new RemoteExtAuthService() { | |||
| @Override | |||
| public Response<JoinbrightUser> getLoginUser(String joinbrightToken, String source) { | |||
| return Response.fail(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部认证服务暂不可用,请稍后重试"); | |||
| } | |||
| }; | |||
| } | |||
| } | |||
| @ -0,0 +1,31 @@ | |||
| package com.inspect.admin.feign.factory; | |||
| import com.inspect.admin.feign.RemoteExtUserService; | |||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.cloud.openfeign.FallbackFactory; | |||
| import org.springframework.stereotype.Component; | |||
| @Component | |||
| public class RemoteExtUserFallbackFactory implements FallbackFactory<RemoteExtUserService> { | |||
| private static final Logger log = LoggerFactory.getLogger(RemoteExtUserFallbackFactory.class); | |||
| @Override | |||
| public RemoteExtUserService create(final Throwable throwable) { | |||
| log.error("[JOINBRIGHT] inspect-management 用户建号服务调用失败:{}", throwable.getMessage()); | |||
| return new RemoteExtUserService() { | |||
| @Override | |||
| public AjaxResult ensureUser(JoinbrightUser user, String source) { | |||
| return AjaxResult.error(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部账号同步服务暂不可用,请稍后重试"); | |||
| } | |||
| @Override | |||
| public AjaxResult syncRoles(String username, String source) { | |||
| return AjaxResult.error(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部角色同步服务暂不可用"); | |||
| } | |||
| }; | |||
| } | |||
| } | |||
| @ -0,0 +1,152 @@ | |||
| package com.inspect.admin.service; | |||
| import com.inspect.admin.feign.RemoteExtAuthService; | |||
| import com.inspect.admin.feign.RemoteExtUserService; | |||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||
| import com.inspect.base.core.constant.SecurityConstants; | |||
| import com.inspect.base.core.domain.Response; | |||
| import com.inspect.base.core.exception.ServiceException; | |||
| import com.inspect.base.core.utils.StringUtils; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import com.inspect.common.security.service.TokenService; | |||
| import com.inspect.system.base.model.LoginUser; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.stereotype.Service; | |||
| import javax.annotation.Resource; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import java.util.Map; | |||
| /** | |||
| * joinbright 页面集成单点登录: | |||
| * 1. 前端在 header(优先)或 query 传入 joinbright-token; | |||
| * 2. 经 inspect-main 内部接口向外部平台 /ext_auth/getLoginUser 换取用户信息; | |||
| * 3. 外部认证失败或未返回用户名:返回 4001,前端回登录页,不再降级 guest/htjc; | |||
| * 4. 平台已有该账号:直接签发平台 token;平台没有:按外部数据自动建号(密码 abcd1234*、角色 common); | |||
| * 5. 同名本地账号按外部数据绑定并覆盖,内置账号 admin/guest/htjc 受保护,返回 4009。 | |||
| */ | |||
| @Service | |||
| public class JoinbrightLoginService { | |||
| private static final Logger log = LoggerFactory.getLogger(JoinbrightLoginService.class); | |||
| private static final String JOINBRIGHT_TOKEN = "joinbright-token"; | |||
| private static final String MSG_TOKEN_INVALID = "登录凭证无效,请重新登录"; | |||
| private static final String MSG_SERVICE_UNAVAILABLE = "外部认证服务暂不可用,请稍后重试"; | |||
| @Resource | |||
| private RemoteExtAuthService remoteExtAuthService; | |||
| @Resource | |||
| private RemoteExtUserService remoteExtUserService; | |||
| @Resource | |||
| private SysLoginService sysLoginService; | |||
| @Resource | |||
| private TokenService tokenService; | |||
| public Map<String, Object> login(HttpServletRequest request) { | |||
| String joinbrightToken = request.getParameter(JOINBRIGHT_TOKEN); | |||
| if (StringUtils.isEmpty(joinbrightToken)) { | |||
| throw new ServiceException(MSG_TOKEN_INVALID, ExtAuthConstants.TOKEN_INVALID); | |||
| } | |||
| JoinbrightUser externalUser = getExternalUser(joinbrightToken); | |||
| ensurePlatformUser(externalUser); | |||
| syncExternalRoles(externalUser.getUsername()); | |||
| LoginUser loginUser = loadPlatformUser(externalUser.getUsername()); | |||
| return tokenService.createToken(loginUser); | |||
| } | |||
| /** | |||
| * 调内部接口换取外部用户信息;任何失败都直接抛业务异常,由前端按错误码回登录页。 | |||
| */ | |||
| private JoinbrightUser getExternalUser(String joinbrightToken) { | |||
| Response<JoinbrightUser> response; | |||
| try { | |||
| response = remoteExtAuthService.getLoginUser(joinbrightToken, SecurityConstants.INNER); | |||
| } catch (Exception e) { | |||
| log.error("[JOINBRIGHT] 调用外部认证服务异常", e); | |||
| throw new ServiceException(MSG_SERVICE_UNAVAILABLE, ExtAuthConstants.SERVICE_UNAVAILABLE); | |||
| } | |||
| if (response == null) { | |||
| log.warn("[JOINBRIGHT] 外部认证服务无响应"); | |||
| throw new ServiceException(MSG_SERVICE_UNAVAILABLE, ExtAuthConstants.SERVICE_UNAVAILABLE); | |||
| } | |||
| if (response.getCode() == ExtAuthConstants.SERVICE_UNAVAILABLE) { | |||
| throw new ServiceException(defaultMessage(response.getMsg(), MSG_SERVICE_UNAVAILABLE), | |||
| ExtAuthConstants.SERVICE_UNAVAILABLE); | |||
| } | |||
| if (response.getCode() != Response.SUCCESS) { | |||
| log.warn("[JOINBRIGHT] 外部认证失败: code={}, msg={}", response.getCode(), response.getMsg()); | |||
| throw new ServiceException(defaultMessage(response.getMsg(), MSG_TOKEN_INVALID), | |||
| ExtAuthConstants.TOKEN_INVALID); | |||
| } | |||
| JoinbrightUser user = response.getData(); | |||
| if (user == null || StringUtils.isEmpty(user.getUsername())) { | |||
| log.warn("[JOINBRIGHT] 外部认证未返回用户名"); | |||
| throw new ServiceException("未获取到外部用户信息,请重新登录", ExtAuthConstants.TOKEN_INVALID); | |||
| } | |||
| return user; | |||
| } | |||
| /** | |||
| * 平台用户不存在时自动建号,存在时由 dliip-system 按 ext_id 更新/绑定。 | |||
| */ | |||
| private void ensurePlatformUser(JoinbrightUser externalUser) { | |||
| AjaxResult result; | |||
| try { | |||
| result = remoteExtUserService.ensureUser(externalUser, SecurityConstants.INNER); | |||
| } catch (Exception e) { | |||
| log.error("[JOINBRIGHT] 调用用户建号服务异常, username={}", externalUser.getUsername(), e); | |||
| throw new ServiceException("外部账号同步失败,请稍后重试", ExtAuthConstants.SERVICE_UNAVAILABLE); | |||
| } | |||
| if (result == null) { | |||
| throw new ServiceException("外部账号同步失败,请稍后重试", ExtAuthConstants.SERVICE_UNAVAILABLE); | |||
| } | |||
| int code = readCode(result); | |||
| if (code != Response.SUCCESS) { | |||
| String message = defaultMessage((String) result.get(AjaxResult.MSG_TAG), "外部账号同步失败,请稍后重试"); | |||
| log.warn("[JOINBRIGHT] 用户建号失败: code={}, msg={}", code, message); | |||
| throw new ServiceException(message, code); | |||
| } | |||
| } | |||
| /** | |||
| * 登录时同步外部角色:失败只记录日志,不阻断登录(fail-open)。 | |||
| */ | |||
| private void syncExternalRoles(String username) { | |||
| try { | |||
| AjaxResult result = remoteExtUserService.syncRoles(username, SecurityConstants.INNER); | |||
| if (result == null || readCode(result) != Response.SUCCESS) { | |||
| log.warn("[JOINBRIGHT] 外部角色同步未成功,按现有角色放行, username={}, result={}", | |||
| username, result); | |||
| return; | |||
| } | |||
| log.info("[JOINBRIGHT] 外部角色同步完成, username={}, msg={}", | |||
| username, result.get(AjaxResult.MSG_TAG)); | |||
| } catch (Exception e) { | |||
| log.warn("[JOINBRIGHT] 外部角色同步异常,按现有角色放行, username={}", username, e); | |||
| } | |||
| } | |||
| private LoginUser loadPlatformUser(String username) { | |||
| try { | |||
| return sysLoginService.getLoginUserByUsername(username); | |||
| } catch (ServiceException e) { | |||
| String message = e.getMessage(); | |||
| boolean accountState = message != null | |||
| && (message.contains("停用") || message.contains("删除")); | |||
| throw new ServiceException(message, | |||
| accountState ? ExtAuthConstants.USER_DISABLED : ExtAuthConstants.SERVICE_UNAVAILABLE); | |||
| } | |||
| } | |||
| private int readCode(AjaxResult result) { | |||
| Object value = result.get(AjaxResult.CODE_TAG); | |||
| return value instanceof Number ? ((Number) value).intValue() : Response.FAIL; | |||
| } | |||
| private String defaultMessage(String message, String defaultMessage) { | |||
| return StringUtils.isNotEmpty(message) ? message : defaultMessage; | |||
| } | |||
| } | |||
| @ -0,0 +1,16 @@ | |||
| package com.inspect.base.core.constant; | |||
| /** | |||
| * 外部平台(joinbright)集成错误码。 | |||
| * 由 dliip-auth 的 joinbrightLogin 统一返回给前端,便于前端区分“回登录页”与“服务异常”。 | |||
| */ | |||
| public class ExtAuthConstants { | |||
| /** joinbright-token 缺失/无效,或外部平台未返回用户名 */ | |||
| public static final int TOKEN_INVALID = 4001; | |||
| /** 平台账号已停用或已删除 */ | |||
| public static final int USER_DISABLED = 4003; | |||
| /** 同名账号冲突(受保护账号不允许被外部账号绑定) */ | |||
| public static final int USER_CONFLICT = 4009; | |||
| /** 外部认证 / 用户同步服务不可用 */ | |||
| public static final int SERVICE_UNAVAILABLE = 5003; | |||
| } | |||
| @ -0,0 +1,21 @@ | |||
| package com.inspect.base.core.enums; | |||
| import lombok.Getter; | |||
| @Getter | |||
| public enum DefectLevel { | |||
| // 0=无, 1=紧急, 2=严重, 3=一般, 4=轻微 | |||
| NONE(0, "无"), | |||
| URGENT(1, "重大"), | |||
| SERIOUS(2, "严重"), | |||
| NORMAL(3, "一般"), | |||
| MINOR(4, "轻微"); | |||
| private final Integer code; | |||
| private final String info; | |||
| DefectLevel(Integer code, String info) { | |||
| this.code = code; | |||
| this.info = info; | |||
| } | |||
| } | |||
| @ -0,0 +1,22 @@ | |||
| package com.inspect.base.core.enums; | |||
| public enum UserType { | |||
| SYSTEM("0", "系统用户"), | |||
| EXTERNAL("1", "外部用户"); | |||
| private final String code; | |||
| private final String info; | |||
| private UserType(String code, String info) { | |||
| this.code = code; | |||
| this.info = info; | |||
| } | |||
| public String getCode() { | |||
| return this.code; | |||
| } | |||
| public String getInfo() { | |||
| return this.info; | |||
| } | |||
| } | |||
| @ -0,0 +1,102 @@ | |||
| package com.inspect.ivs.service; | |||
| import com.alibaba.fastjson.JSONObject; | |||
| import com.inspect.ivs.util.WebClientUtils; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.beans.factory.annotation.Value; | |||
| import org.springframework.http.HttpStatus; | |||
| import org.springframework.http.MediaType; | |||
| import org.springframework.http.ResponseEntity; | |||
| import org.springframework.stereotype.Component; | |||
| import org.springframework.web.reactive.function.client.WebClient; | |||
| import org.springframework.web.reactive.function.client.WebClientException; | |||
| @Component | |||
| public class HlsPlaybackComponent { | |||
| private static final Logger log = LoggerFactory.getLogger(HlsPlaybackComponent.class); | |||
| @Value("${ivs.nvr.hls.url:http://192.168.1.86:28081}") | |||
| private String url; | |||
| private final WebClient webClient = WebClientUtils.create(); | |||
| public ResponseEntity<JSONObject> get(String uri) { | |||
| log.info("[NVR HLS] GET url: {}", url.concat(uri)); | |||
| return exchange(webClient.get().uri(url.concat(uri))); | |||
| } | |||
| public ResponseEntity<JSONObject> post(String uri, Object body) { | |||
| log.info("[NVR HLS] POST url: {}, body: {}", url.concat(uri), JSONObject.toJSONString(body)); | |||
| return exchange(webClient.post().uri(url.concat(uri)) | |||
| .contentType(MediaType.APPLICATION_JSON) | |||
| .bodyValue(body)); | |||
| } | |||
| public ResponseEntity<JSONObject> delete(String uri) { | |||
| log.info("[NVR HLS] DELETE url: {}", url.concat(uri)); | |||
| return exchange(webClient.delete().uri(url.concat(uri))); | |||
| } | |||
| public ResponseEntity<byte[]> getBinary(String uri) { | |||
| log.info("[NVR HLS] GET binary url: {}", url.concat(uri)); | |||
| return exchangeBinary(webClient.get().uri(url.concat(uri))); | |||
| } | |||
| public ResponseEntity<byte[]> postBinary(String uri, Object body) { | |||
| log.info("[NVR HLS] POST binary url: {}, body: {}", url.concat(uri), JSONObject.toJSONString(body)); | |||
| return exchangeBinary(webClient.post().uri(url.concat(uri)) | |||
| .contentType(MediaType.APPLICATION_JSON) | |||
| .bodyValue(body)); | |||
| } | |||
| private ResponseEntity<JSONObject> exchange(WebClient.RequestHeadersSpec<?> spec) { | |||
| assert webClient != null; | |||
| try { | |||
| return spec.exchangeToMono(response -> | |||
| response.bodyToMono(String.class) | |||
| .defaultIfEmpty("") | |||
| .map(body -> { | |||
| int status = response.statusCode().value(); | |||
| ResponseEntity<JSONObject> entity; | |||
| if (body == null || body.trim().isEmpty()) { | |||
| entity = new ResponseEntity<>((JSONObject) null, HttpStatus.valueOf(status)); | |||
| } else { | |||
| entity = ResponseEntity.status(status) | |||
| .contentType(MediaType.APPLICATION_JSON) | |||
| .body(JSONObject.parseObject(body)); | |||
| } | |||
| return entity; | |||
| })) | |||
| .block(); | |||
| } catch (WebClientException e) { | |||
| log.error("[NVR HLS] request failed, url: {}", url, e); | |||
| JSONObject error = new JSONObject(); | |||
| error.put("code", HttpStatus.BAD_GATEWAY.value()); | |||
| error.put("message", "NVR HLS服务不可用"); | |||
| return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(error); | |||
| } | |||
| } | |||
| private ResponseEntity<byte[]> exchangeBinary(WebClient.RequestHeadersSpec<?> spec) { | |||
| assert webClient != null; | |||
| try { | |||
| return spec.exchangeToMono(response -> | |||
| response.bodyToMono(byte[].class) | |||
| .defaultIfEmpty(new byte[0]) | |||
| .map(bytes -> { | |||
| MediaType mediaType = response.headers().contentType().orElse(MediaType.APPLICATION_OCTET_STREAM); | |||
| return ResponseEntity.status(response.statusCode().value()) | |||
| .contentType(mediaType) | |||
| .body(bytes); | |||
| })) | |||
| .block(); | |||
| } catch (WebClientException e) { | |||
| log.error("[NVR HLS] request failed, url: {}", url, e); | |||
| return ResponseEntity.status(HttpStatus.BAD_GATEWAY) | |||
| .contentType(MediaType.APPLICATION_JSON) | |||
| .body(new byte[0]); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,23 @@ | |||
| package com.inspect.job.client; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import org.springframework.cloud.openfeign.FeignClient; | |||
| import org.springframework.web.bind.annotation.PostMapping; | |||
| import org.springframework.web.bind.annotation.RequestHeader; | |||
| /** | |||
| * 外部平台组织同步客户端(调用 inspect-management 的内部接口)。 | |||
| * 定时任务线程没有请求上下文,必须显式传 from-source=inner。 | |||
| */ | |||
| @FeignClient(name = "inspect-management", contextId = "ext-org-sync") | |||
| public interface ExtOrgSyncClient { | |||
| @PostMapping("/inner/ext_sync/all") | |||
| AjaxResult syncAll(@RequestHeader("from-source") String source); | |||
| @PostMapping("/inner/ext_sync/dept") | |||
| AjaxResult syncDept(@RequestHeader("from-source") String source); | |||
| @PostMapping("/inner/ext_sync/user") | |||
| AjaxResult syncUser(@RequestHeader("from-source") String source); | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.job.client; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import org.springframework.cloud.openfeign.FeignClient; | |||
| import org.springframework.web.bind.annotation.PostMapping; | |||
| import org.springframework.web.bind.annotation.RequestParam; | |||
| /** | |||
| * 巡检报告定时归档客户端(调用 inspect-main 的动环归档按时间窗口接口)。 | |||
| * | |||
| * @author inspect | |||
| */ | |||
| @FeignClient(name = "inspect-main", contextId = "inspect-report-archive") | |||
| public interface ReportArchiveClient { | |||
| /** | |||
| * 归档 [startTime, endTime) 窗口内“已完成且未归档”的巡检执行: | |||
| * 合并生成一份动环报告,报告生成上传后自动推送外部平台;无待归档任务时不生成不推送。 | |||
| * | |||
| * @param startTime 窗口开始时间 yyyy-MM-dd HH:mm:ss(含) | |||
| * @param endTime 窗口结束时间 yyyy-MM-dd HH:mm:ss(不含) | |||
| */ | |||
| @PostMapping({"/resultmain/archiveByTime"}) | |||
| AjaxResult archiveByTime(@RequestParam("startTime") String startTime, | |||
| @RequestParam("endTime") String endTime); | |||
| } | |||
| @ -0,0 +1,63 @@ | |||
| package com.inspect.job.task; | |||
| import com.inspect.base.core.constant.SecurityConstants; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import com.inspect.job.client.ExtOrgSyncClient; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.springframework.stereotype.Component; | |||
| import java.util.function.Function; | |||
| /** | |||
| * 外部平台(盐源智慧感知平台)部门 / 用户同步定时任务。 | |||
| * <p> | |||
| * 在“定时任务”中配置调用: | |||
| * <ul> | |||
| * <li>{@code extOrgSyncTask.syncAll()}:部门 + 用户全量同步(建议每日凌晨);</li> | |||
| * <li>{@code extOrgSyncTask.syncUser()}:部门 + 用户同步;</li> | |||
| * <li>{@code extOrgSyncTask.syncDept()}:仅部门同步。</li> | |||
| * </ul> | |||
| * 角色同步暂缓。调用失败直接抛出异常,由 sys_job_log 记录失败原因。 | |||
| */ | |||
| @Slf4j | |||
| @Component("extOrgSyncTask") | |||
| public class ExtOrgSyncTask { | |||
| private final ExtOrgSyncClient extOrgSyncClient; | |||
| public ExtOrgSyncTask(ExtOrgSyncClient extOrgSyncClient) { | |||
| this.extOrgSyncClient = extOrgSyncClient; | |||
| } | |||
| public void syncAll() { | |||
| invoke("外部组织全量同步", extOrgSyncClient::syncAll); | |||
| } | |||
| public void syncDept() { | |||
| invoke("外部部门同步", extOrgSyncClient::syncDept); | |||
| } | |||
| public void syncUser() { | |||
| invoke("外部用户同步", extOrgSyncClient::syncUser); | |||
| } | |||
| private void invoke(String action, Function<String, AjaxResult> call) { | |||
| long start = System.currentTimeMillis(); | |||
| log.info("[EXT-SYNC] {} 开始", action); | |||
| AjaxResult result; | |||
| try { | |||
| result = call.apply(SecurityConstants.INNER); | |||
| } catch (Exception e) { | |||
| log.error("[EXT-SYNC] {} 调用异常", action, e); | |||
| throw new RuntimeException(action + "调用异常: " + e.getMessage(), e); | |||
| } | |||
| Object code = result == null ? null : result.get(AjaxResult.CODE_TAG); | |||
| Object msg = result == null ? null : result.get(AjaxResult.MSG_TAG); | |||
| long elapsed = System.currentTimeMillis() - start; | |||
| if (result == null || !(code instanceof Number) || ((Number) code).intValue() != 200) { | |||
| log.error("[EXT-SYNC] {} 失败, code={}, msg={}, 耗时={}ms", action, code, msg, elapsed); | |||
| throw new RuntimeException(action + "失败: " + (msg == null ? "无响应" : msg)); | |||
| } | |||
| log.info("[EXT-SYNC] {} 完成, msg={}, 耗时={}ms", action, msg, elapsed); | |||
| } | |||
| } | |||
| @ -0,0 +1,81 @@ | |||
| package com.inspect.job.task; | |||
| import com.inspect.base.core.web.domain.AjaxResult; | |||
| import com.inspect.job.client.ReportArchiveClient; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.springframework.stereotype.Component; | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import java.time.LocalTime; | |||
| import java.time.format.DateTimeFormatter; | |||
| /** | |||
| * 巡检报告定时归档任务(sys_job 在数据库灵活控制)。 | |||
| * <p> | |||
| * 由 sql/sys_job_report_archive.sql 预置两条 sys_job 调用: | |||
| * <ul> | |||
| * <li>archiveMorning:每日 07:00,归档 前一日16:00 ~ 当日07:00 数据并推送平台;</li> | |||
| * <li>archiveEvening:每日 16:00,归档 当日07:00 ~ 当日16:00 数据并推送平台。</li> | |||
| * </ul> | |||
| * 窗口截止点(07:00 / 16:00)即为报告批次语义,若后续确认新的生成时间点, | |||
| * 需同时调整 sys_job 的 cron_expression 与下方 {@link #MORNING_END}/{@link #EVENING_END} 截止点。 | |||
| * | |||
| * @author inspect | |||
| */ | |||
| @Slf4j | |||
| @Component("reportArchiveScheduleTask") | |||
| public class ReportArchiveScheduleTask { | |||
| /** 数据窗口截止时间:早晨批次 = 当日 07:00 */ | |||
| private static final LocalTime MORNING_END = LocalTime.of(7, 0, 0); | |||
| /** 数据窗口截止时间:下午批次 = 当日 16:00 */ | |||
| private static final LocalTime EVENING_END = LocalTime.of(16, 0, 0); | |||
| private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |||
| private final ReportArchiveClient reportArchiveClient; | |||
| public ReportArchiveScheduleTask(ReportArchiveClient reportArchiveClient) { | |||
| this.reportArchiveClient = reportArchiveClient; | |||
| } | |||
| /** | |||
| * sys_job:每日 07:00 归档 前一日16:00 ~ 当日07:00 的巡检数据。 | |||
| */ | |||
| public void archiveMorning() { | |||
| LocalDate today = LocalDate.now(); | |||
| // 前一日 16:00 | |||
| LocalDateTime start = LocalDateTime.of(today.minusDays(1), EVENING_END); | |||
| // 当日 07:00 | |||
| LocalDateTime end = LocalDateTime.of(today, MORNING_END); | |||
| archiveByWindow("早晨批次(前一日16:00~当日07:00)", start, end); | |||
| } | |||
| /** | |||
| * sys_job:每日 16:00 归档 当日07:00 ~ 当日16:00 的巡检数据。 | |||
| */ | |||
| public void archiveEvening() { | |||
| LocalDate today = LocalDate.now(); | |||
| LocalDateTime start = LocalDateTime.of(today, MORNING_END); | |||
| LocalDateTime end = LocalDateTime.of(today, EVENING_END); | |||
| archiveByWindow("下午批次(当日07:00~当日16:00)", start, end); | |||
| } | |||
| /** | |||
| * 按窗口触发 inspect-main 定时归档;无待归档任务时主服务会跳过(不生成不推送)。 | |||
| * 调用失败直接抛出异常,由 sys_job_log 记录失败。 | |||
| */ | |||
| private void archiveByWindow(String batchName, LocalDateTime start, LocalDateTime end) { | |||
| String startTime = start.format(DATE_TIME); | |||
| String endTime = end.format(DATE_TIME); | |||
| log.info("[定时归档] {} 开始, 数据窗口: {} ~ {}", batchName, startTime, endTime); | |||
| AjaxResult result = reportArchiveClient.archiveByTime(startTime, endTime); | |||
| Object code = result == null ? null : result.get(AjaxResult.CODE_TAG); | |||
| Object msg = result == null ? null : result.get(AjaxResult.MSG_TAG); | |||
| log.info("[定时归档] {} 完成, code={}, msg={}", batchName, code, msg); | |||
| if (result == null || !(code instanceof Number) || ((Number) code).intValue() != 200) { | |||
| throw new RuntimeException("定时归档调用失败: " + (msg == null ? "无响应" : msg)); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,62 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>com.inspect</groupId> | |||
| <artifactId>inspect-main</artifactId> | |||
| <version>3.4.0</version> | |||
| </parent> | |||
| <artifactId>inspect-main-agg</artifactId> | |||
| <version>3.4.0</version> | |||
| <name>inspect-main-agg</name> | |||
| <description>inspect-main-agg</description> | |||
| <properties> | |||
| <java.version>1.8</java.version> | |||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
| <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | |||
| <spring-boot.version>2.6.13</spring-boot.version> | |||
| </properties> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>org.springframework.boot</groupId> | |||
| <artifactId>spring-boot-starter</artifactId> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.springframework.boot</groupId> | |||
| <artifactId>spring-boot-starter-test</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.springframework.boot</groupId> | |||
| <artifactId>spring-boot-starter-web</artifactId> | |||
| </dependency> | |||
| <!-- 用连接池复用 TCP 连接 --> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>httpclient</artifactId> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.inspect</groupId> | |||
| <artifactId>inspect-base-core</artifactId> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mybatis.spring.boot</groupId> | |||
| <artifactId>mybatis-spring-boot-starter</artifactId> | |||
| </dependency> <dependency> | |||
| <groupId>org.springframework.boot</groupId> | |||
| <artifactId>spring-boot-starter-data-redis</artifactId> | |||
| </dependency> | |||
| </dependencies> | |||
| <dependencyManagement> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>org.springframework.boot</groupId> | |||
| <artifactId>spring-boot-dependencies</artifactId> | |||
| <version>2.6.6</version> | |||
| <type>pom</type> | |||
| <scope>import</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| </dependencyManagement> | |||
| </project> | |||
| @ -0,0 +1,20 @@ | |||
| package com.inspect.agg; | |||
| import org.mybatis.spring.annotation.MapperScan; | |||
| import org.springframework.boot.SpringApplication; | |||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |||
| import org.springframework.scheduling.annotation.EnableScheduling; | |||
| /** | |||
| * 智能巡检数据汇聚系统 - 中心采集服务(区域试点)。 | |||
| */ | |||
| @SpringBootApplication | |||
| @EnableScheduling | |||
| @MapperScan("com.inspect.agg.mapper") | |||
| public class AggregationApplication { | |||
| public static void main(String[] args) { | |||
| SpringApplication.run(AggregationApplication.class, args); | |||
| } | |||
| } | |||
| @ -0,0 +1,28 @@ | |||
| package com.inspect.agg.calendar; | |||
| import java.io.Serializable; | |||
| import java.time.LocalDateTime; | |||
| import lombok.AllArgsConstructor; | |||
| import lombok.Builder; | |||
| import lombok.Data; | |||
| import lombok.NoArgsConstructor; | |||
| @Data | |||
| @Builder | |||
| @NoArgsConstructor | |||
| @AllArgsConstructor | |||
| public class AggCalendarEvent implements Serializable { | |||
| private static final long serialVersionUID = 1L; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| private String id; | |||
| private String name; | |||
| private String status; | |||
| private String taskCode; | |||
| private String source; | |||
| private LocalDateTime startTime; | |||
| private LocalDateTime endTime; | |||
| } | |||
| @ -0,0 +1,180 @@ | |||
| package com.inspect.agg.calendar; | |||
| import java.time.DayOfWeek; | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import java.time.LocalTime; | |||
| import java.time.YearMonth; | |||
| import java.time.temporal.TemporalAdjusters; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.inspect.agg.entity.AggPatrolTask; | |||
| import com.inspect.agg.mapper.AggPatrolTaskMapper; | |||
| import com.inspect.base.core.enums.ExecStatus; | |||
| import com.inspect.base.core.enums.IntervalType; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 根据汇聚方案规则生成指定月份的待执行计划。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class AggTaskCalendarGenerator { | |||
| private static final String STATE_PENDING = "5"; | |||
| private final AggPatrolTaskMapper taskMapper; | |||
| public List<AggCalendarEvent> generateMonth(String stationCode, int year, int month) { | |||
| YearMonth yearMonth = YearMonth.of(year, month); | |||
| LocalDate firstDay = yearMonth.atDay(1); | |||
| LocalDate lastDay = yearMonth.atEndOfMonth(); | |||
| LocalDateTime monthStart = firstDay.atStartOfDay(); | |||
| LocalDateTime monthEnd = lastDay.atTime(23, 59, 59); | |||
| LocalDateTime now = LocalDateTime.now(); | |||
| if (monthEnd.isBefore(now)) { | |||
| return Collections.emptyList(); | |||
| } | |||
| List<AggCalendarEvent> events = new ArrayList<>(); | |||
| List<AggPatrolTask> tasks = taskMapper.selectEnabledTasks(stationCode); | |||
| for (AggPatrolTask task : tasks) { | |||
| if (!StringUtils.hasText(task.getDevType())) { | |||
| continue; | |||
| } | |||
| try { | |||
| if (ExecStatus.REGULAR.getCode().equals(task.getExecutionStatus())) { | |||
| generateRegular(task, monthStart, monthEnd, now, events); | |||
| } else if (ExecStatus.PERIODIC.getCode().equals(task.getExecutionStatus())) { | |||
| generatePeriodic(task, monthStart, monthEnd, now, events); | |||
| } else if (ExecStatus.INTERVAL.getCode().equals(task.getExecutionStatus())) { | |||
| generateInterval(task, monthStart, monthEnd, now, events); | |||
| } | |||
| } catch (Exception e) { | |||
| log.warn("generate calendar failed: station={}, taskId={}, error={}", | |||
| task.getStationCode(), task.getTaskId(), e.getMessage()); | |||
| } | |||
| } | |||
| events.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime())); | |||
| return events; | |||
| } | |||
| private void generateRegular(AggPatrolTask task, LocalDateTime monthStart, LocalDateTime monthEnd, | |||
| LocalDateTime now, List<AggCalendarEvent> events) { | |||
| LocalDateTime startTime = task.getFixedStartTime(); | |||
| if (startTime != null && startTime.isAfter(now) | |||
| && !startTime.isBefore(monthStart) && !startTime.isAfter(monthEnd)) { | |||
| events.add(event(task, startTime, null)); | |||
| } | |||
| } | |||
| private void generatePeriodic(AggPatrolTask task, LocalDateTime monthStart, LocalDateTime monthEnd, | |||
| LocalDateTime now, List<AggCalendarEvent> events) { | |||
| if (task.getCycleStartTime() == null || task.getCycleEndTime() == null | |||
| || !StringUtils.hasText(task.getCycleExecuteTime())) { | |||
| return; | |||
| } | |||
| LocalDateTime start = max(monthStart, task.getCycleStartTime()); | |||
| LocalDateTime end = min(monthEnd, task.getCycleEndTime()); | |||
| if (end.isBefore(start)) { | |||
| return; | |||
| } | |||
| String[] executeTimes = task.getCycleExecuteTime().split(","); | |||
| if (StringUtils.hasText(task.getCycleWeek())) { | |||
| String[] weeks = task.getCycleWeek().split(","); | |||
| for (LocalDate date = start.toLocalDate(); !date.isAfter(end.toLocalDate()); date = date.plusDays(1)) { | |||
| for (int i = 0; i < weeks.length && i < executeTimes.length; i++) { | |||
| if (date.getDayOfWeek() == parseDayOfWeek(weeks[i])) { | |||
| addIfVisible(task, date, executeTimes[i], now, end, events); | |||
| } | |||
| } | |||
| } | |||
| } else if (StringUtils.hasText(task.getCycleMonth())) { | |||
| String[] monthDays = task.getCycleMonth().split(","); | |||
| for (LocalDate date = start.toLocalDate(); !date.isAfter(end.toLocalDate()); date = date.plusDays(1)) { | |||
| for (int i = 0; i < monthDays.length && i < executeTimes.length; i++) { | |||
| if (date.getDayOfMonth() == Integer.parseInt(monthDays[i].trim())) { | |||
| addIfVisible(task, date, executeTimes[i], now, end, events); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| private void generateInterval(AggPatrolTask task, LocalDateTime monthStart, LocalDateTime monthEnd, | |||
| LocalDateTime now, List<AggCalendarEvent> events) { | |||
| if (!IntervalType.BY_HOUR.getCode().equals(task.getIntervalType()) | |||
| || task.getIntervalNumber() == null || task.getIntervalNumber() <= 0 | |||
| || task.getIntervalStartTime() == null || task.getIntervalEndTime() == null | |||
| || !StringUtils.hasText(task.getIntervalExecuteTime())) { | |||
| return; | |||
| } | |||
| LocalDateTime start = task.getIntervalStartTime(); | |||
| LocalDateTime end = min(monthEnd, task.getIntervalEndTime()); | |||
| LocalTime executeTime = LocalTime.parse(task.getIntervalExecuteTime().trim()); | |||
| LocalDateTime cursor = LocalDateTime.of(start.toLocalDate(), executeTime); | |||
| while (cursor.isBefore(start)) { | |||
| cursor = cursor.plusHours(task.getIntervalNumber()); | |||
| } | |||
| while (!cursor.isAfter(end)) { | |||
| if (cursor.isAfter(now) && !cursor.isBefore(monthStart)) { | |||
| events.add(event(task, cursor, null)); | |||
| } | |||
| cursor = cursor.plusHours(task.getIntervalNumber()); | |||
| } | |||
| } | |||
| private void addIfVisible(AggPatrolTask task, LocalDate date, String time, | |||
| LocalDateTime now, LocalDateTime end, | |||
| List<AggCalendarEvent> events) { | |||
| if (!StringUtils.hasText(time)) { | |||
| return; | |||
| } | |||
| LocalDateTime startTime = LocalDateTime.of(date, LocalTime.parse(time.trim())); | |||
| if (startTime.isAfter(now) && !startTime.isAfter(end)) { | |||
| events.add(event(task, startTime, null)); | |||
| } | |||
| } | |||
| private AggCalendarEvent event(AggPatrolTask task, LocalDateTime startTime, LocalDateTime endTime) { | |||
| return AggCalendarEvent.builder() | |||
| .stationCode(task.getStationCode()) | |||
| .stationName(task.getStationName()) | |||
| .id(task.getTaskCode()) | |||
| .taskCode(task.getTaskCode()) | |||
| .name(task.getTaskName()) | |||
| .status("pending") | |||
| .source("plan") | |||
| .startTime(startTime) | |||
| .endTime(endTime) | |||
| .build(); | |||
| } | |||
| private static LocalDateTime max(LocalDateTime a, LocalDateTime b) { | |||
| return a.isAfter(b) ? a : b; | |||
| } | |||
| private static LocalDateTime min(LocalDateTime a, LocalDateTime b) { | |||
| return a.isBefore(b) ? a : b; | |||
| } | |||
| private static DayOfWeek parseDayOfWeek(String value) { | |||
| String week = value == null ? "" : value.trim(); | |||
| if ("星期一".equals(week) || "1".equals(week)) return DayOfWeek.MONDAY; | |||
| if ("星期二".equals(week) || "2".equals(week)) return DayOfWeek.TUESDAY; | |||
| if ("星期三".equals(week) || "3".equals(week)) return DayOfWeek.WEDNESDAY; | |||
| if ("星期四".equals(week) || "4".equals(week)) return DayOfWeek.THURSDAY; | |||
| if ("星期五".equals(week) || "5".equals(week)) return DayOfWeek.FRIDAY; | |||
| if ("星期六".equals(week) || "6".equals(week)) return DayOfWeek.SATURDAY; | |||
| if ("星期日".equals(week) || "7".equals(week)) return DayOfWeek.SUNDAY; | |||
| throw new IllegalArgumentException("不支持的星期: " + week); | |||
| } | |||
| } | |||
| @ -0,0 +1,340 @@ | |||
| package com.inspect.agg.calendar; | |||
| import java.time.Duration; | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import java.time.YearMonth; | |||
| import java.time.format.DateTimeFormatter; | |||
| import java.util.ArrayList; | |||
| import java.util.HashMap; | |||
| import java.util.LinkedHashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import java.util.Set; | |||
| import java.util.TreeMap; | |||
| import org.springframework.data.redis.core.StringRedisTemplate; | |||
| import org.springframework.scheduling.annotation.Scheduled; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.fasterxml.jackson.core.type.TypeReference; | |||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||
| import com.inspect.agg.entity.AggPatrolTaskStatus; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggPatrolTaskStatusMapper; | |||
| import com.inspect.agg.service.StationRegistry; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 汇聚计划服务:规则生成未来计划,合并实际任务状态,并使用 Redis 缓存。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class AggTaskCalendarService { | |||
| private static final String CACHE_PREFIX = "agg:calendar:v2:"; | |||
| private static final Duration CACHE_TTL = Duration.ofHours(12); | |||
| private static final DateTimeFormatter DATE_TIME_FORMATTER = | |||
| DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |||
| private static final DateTimeFormatter DATE_FORMATTER = | |||
| DateTimeFormatter.ofPattern("yyyy-MM-dd"); | |||
| private static final TypeReference<List<AggCalendarEvent>> EVENT_LIST_TYPE = | |||
| new TypeReference<List<AggCalendarEvent>>() { }; | |||
| private static final TypeReference<List<Map<String, Object>>> STATS_LIST_TYPE = | |||
| new TypeReference<List<Map<String, Object>>>() { }; | |||
| private final AggTaskCalendarGenerator calendarGenerator; | |||
| private final AggPatrolTaskStatusMapper taskStatusMapper; | |||
| private final StationRegistry stationRegistry; | |||
| private final StringRedisTemplate redisTemplate; | |||
| private final ObjectMapper objectMapper; | |||
| public List<Map<String, Object>> year(String stationCode, int year) { | |||
| String scope = scope(stationCode); | |||
| String cacheKey = CACHE_PREFIX + "summary:" + scope + ":" + year; | |||
| List<Map<String, Object>> cached = readCache(cacheKey, STATS_LIST_TYPE); | |||
| if (cached != null) { | |||
| return cached; | |||
| } | |||
| List<Map<String, Object>> result = new ArrayList<>(); | |||
| for (int month = 1; month <= 12; month++) { | |||
| Map<String, Object> item = new LinkedHashMap<>(); | |||
| item.put("month", month); | |||
| item.put("executed", 0L); | |||
| item.put("executing", 0L); | |||
| item.put("pending", 0L); | |||
| item.put("total", 0L); | |||
| result.add(item); | |||
| } | |||
| for (int month = 1; month <= 12; month++) { | |||
| List<AggCalendarEvent> events = visibleEvents(loadMonthEvents(stationCode, year, month)); | |||
| Map<String, Object> item = result.get(month - 1); | |||
| long executed = count(events, "executed"); | |||
| long executing = count(events, "executing"); | |||
| long pending = count(events, "pending"); | |||
| item.put("executed", executed); | |||
| item.put("executing", executing); | |||
| item.put("pending", pending); | |||
| item.put("total", executed + executing + pending); | |||
| } | |||
| writeCache(cacheKey, result, summaryTtl(year)); | |||
| return result; | |||
| } | |||
| public List<Map<String, Object>> month(String stationCode, int year, int month) { | |||
| List<AggCalendarEvent> events = visibleEvents(loadMonthEvents(stationCode, year, month)); | |||
| applyStationNames(events); | |||
| return toMaps(events); | |||
| } | |||
| public List<Map<String, Object>> day(String stationCode, int year, int month, int day) { | |||
| LocalDate target = LocalDate.of(year, month, day); | |||
| List<AggCalendarEvent> result = new ArrayList<>(); | |||
| for (AggCalendarEvent event : visibleEvents(loadMonthEvents(stationCode, year, month))) { | |||
| if (event.getStartTime() != null && target.equals(event.getStartTime().toLocalDate())) { | |||
| result.add(event); | |||
| } | |||
| } | |||
| applyStationNames(result); | |||
| return toMaps(result); | |||
| } | |||
| public void evictStation(String stationCode) { | |||
| if (!StringUtils.hasText(stationCode)) { | |||
| return; | |||
| } | |||
| YearMonth now = YearMonth.now(); | |||
| for (int i = -12; i <= 12; i++) { | |||
| YearMonth month = now.plusMonths(i); | |||
| deleteQuietly(monthKey(stationCode, month.getYear(), month.getMonthValue())); | |||
| } | |||
| deleteQuietly(CACHE_PREFIX + "summary:" + stationCode + ":" + now.getYear()); | |||
| deleteQuietly(CACHE_PREFIX + "summary:" + stationCode + ":" + (now.getYear() + 1)); | |||
| evictAllScopes(); | |||
| } | |||
| @Scheduled(cron = "0 50 0 * * ?") | |||
| public void refreshFutureCalendar() { | |||
| YearMonth current = YearMonth.now(); | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| for (int i = 0; i < 6; i++) { | |||
| YearMonth month = current.plusMonths(i); | |||
| deleteQuietly(monthKey(station.getStationCode(), month.getYear(), month.getMonthValue())); | |||
| loadStationMonthCached(station.getStationCode(), month.getYear(), month.getMonthValue()); | |||
| } | |||
| } | |||
| evictAllScopes(); | |||
| } | |||
| private List<AggCalendarEvent> loadMonthEvents(String stationCode, int year, int month) { | |||
| String scope = scope(stationCode); | |||
| String key = monthKey(stationCode, year, month); | |||
| List<AggCalendarEvent> cached = readCache(key, EVENT_LIST_TYPE); | |||
| if (cached != null) { | |||
| return cached; | |||
| } | |||
| List<AggCalendarEvent> result = new ArrayList<>(); | |||
| if (StringUtils.hasText(stationCode)) { | |||
| result.addAll(loadStationMonth(stationCode, year, month)); | |||
| } else { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| result.addAll(loadStationMonthCached(station.getStationCode(), year, month)); | |||
| } | |||
| result = merge(result); | |||
| } | |||
| writeCache(key, result, monthTtl(year, month)); | |||
| return result; | |||
| } | |||
| private List<AggCalendarEvent> loadStationMonthCached(String stationCode, int year, int month) { | |||
| String key = monthKey(stationCode, year, month); | |||
| List<AggCalendarEvent> cached = readCache(key, EVENT_LIST_TYPE); | |||
| if (cached != null) { | |||
| return cached; | |||
| } | |||
| List<AggCalendarEvent> result = loadStationMonth(stationCode, year, month); | |||
| writeCache(key, result, monthTtl(year, month)); | |||
| return result; | |||
| } | |||
| private List<AggCalendarEvent> loadStationMonth(String stationCode, int year, int month) { | |||
| List<AggCalendarEvent> events = new ArrayList<>(); | |||
| events.addAll(calendarGenerator.generateMonth(stationCode, year, month)); | |||
| List<AggPatrolTaskStatus> actualTasks = | |||
| taskStatusMapper.selectDailyStatsByYearAndMonth(stationCode, year, month); | |||
| for (AggPatrolTaskStatus task : actualTasks) { | |||
| if (task.getStartTime() == null) { | |||
| continue; | |||
| } | |||
| events.add(AggCalendarEvent.builder() | |||
| .stationCode(task.getStationCode()) | |||
| .stationName(task.getStationName()) | |||
| .id(task.getTaskPatrolledId()) | |||
| .taskCode(task.getTaskCode()) | |||
| .name(task.getTaskName()) | |||
| .status(stateName(task.getTaskState())) | |||
| .source("actual") | |||
| .startTime(task.getStartTime()) | |||
| .endTime(task.getEndTime()) | |||
| .build()); | |||
| } | |||
| return merge(events); | |||
| } | |||
| private static List<AggCalendarEvent> merge(List<AggCalendarEvent> events) { | |||
| Map<String, AggCalendarEvent> unique = new HashMap<>(); | |||
| for (AggCalendarEvent event : events) { | |||
| if (event.getStartTime() == null) { | |||
| continue; | |||
| } | |||
| String key = event.getStationCode() + "|" + event.getTaskCode() + "|" + event.getStartTime(); | |||
| AggCalendarEvent existing = unique.get(key); | |||
| if (existing == null || "actual".equals(event.getSource())) { | |||
| unique.put(key, event); | |||
| } | |||
| } | |||
| List<AggCalendarEvent> result = new ArrayList<>(unique.values()); | |||
| result.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime())); | |||
| return result; | |||
| } | |||
| private List<AggCalendarEvent> visibleEvents(List<AggCalendarEvent> events) { | |||
| LocalDateTime now = LocalDateTime.now(); | |||
| List<AggCalendarEvent> result = new ArrayList<>(); | |||
| for (AggCalendarEvent event : events) { | |||
| if ("plan".equals(event.getSource()) | |||
| && event.getStartTime() != null && !event.getStartTime().isAfter(now)) { | |||
| continue; | |||
| } | |||
| result.add(event); | |||
| } | |||
| return result; | |||
| } | |||
| private List<Map<String, Object>> toMaps(List<AggCalendarEvent> events) { | |||
| List<Map<String, Object>> result = new ArrayList<>(); | |||
| for (AggCalendarEvent event : events) { | |||
| Map<String, Object> item = new LinkedHashMap<>(); | |||
| item.put("id", event.getId()); | |||
| item.put("name", event.getName()); | |||
| item.put("status", event.getStatus()); | |||
| item.put("start", event.getStartTime() == null ? null : event.getStartTime().format(DATE_FORMATTER)); | |||
| item.put("end", event.getEndTime() == null ? null : event.getEndTime().format(DATE_FORMATTER)); | |||
| item.put("startTime", event.getStartTime() == null ? null : event.getStartTime().format(DATE_TIME_FORMATTER)); | |||
| item.put("endTime", event.getEndTime() == null ? null : event.getEndTime().format(DATE_TIME_FORMATTER)); | |||
| item.put("stationCode", event.getStationCode()); | |||
| item.put("stationName", event.getStationName() != null ? event.getStationName() : event.getStationCode()); | |||
| result.add(item); | |||
| } | |||
| return result; | |||
| } | |||
| private void applyStationNames(List<AggCalendarEvent> events) { | |||
| if (events == null || events.isEmpty()) { | |||
| return; | |||
| } | |||
| boolean needLookup = false; | |||
| for (AggCalendarEvent event : events) { | |||
| if (StringUtils.hasText(event.getStationCode()) && !StringUtils.hasText(event.getStationName())) { | |||
| needLookup = true; | |||
| break; | |||
| } | |||
| } | |||
| if (!needLookup) { | |||
| return; | |||
| } | |||
| Map<String, String> stationNames = new HashMap<>(); | |||
| for (AggStation station : stationRegistry.listAll()) { | |||
| stationNames.put(station.getStationCode(), station.getStationName()); | |||
| } | |||
| for (AggCalendarEvent event : events) { | |||
| if (StringUtils.hasText(event.getStationCode()) && !StringUtils.hasText(event.getStationName())) { | |||
| event.setStationName(stationNames.getOrDefault(event.getStationCode(), event.getStationCode())); | |||
| } | |||
| } | |||
| } | |||
| private static long count(List<AggCalendarEvent> events, String status) { | |||
| long count = 0; | |||
| for (AggCalendarEvent event : events) { | |||
| if (status.equals(event.getStatus())) { | |||
| count++; | |||
| } | |||
| } | |||
| return count; | |||
| } | |||
| private static String stateName(String state) { | |||
| if ("1".equals(state)) return "executed"; | |||
| if ("2".equals(state)) return "executing"; | |||
| if ("3".equals(state)) return "paused"; | |||
| if ("4".equals(state)) return "terminate"; | |||
| if ("5".equals(state)) return "pending"; | |||
| if ("6".equals(state)) return "exceeded"; | |||
| return null; | |||
| } | |||
| private String monthKey(String stationCode, int year, int month) { | |||
| return CACHE_PREFIX + "events:" + scope(stationCode) + ":" | |||
| + year + "-" + String.format("%02d", month); | |||
| } | |||
| private static String scope(String stationCode) { | |||
| return StringUtils.hasText(stationCode) ? stationCode : "ALL"; | |||
| } | |||
| private <T> T readCache(String key, TypeReference<T> type) { | |||
| try { | |||
| String value = redisTemplate.opsForValue().get(key); | |||
| return StringUtils.hasText(value) ? objectMapper.readValue(value, type) : null; | |||
| } catch (Exception e) { | |||
| log.warn("read calendar cache failed: key={}, error={}", key, e.getMessage()); | |||
| return null; | |||
| } | |||
| } | |||
| private void writeCache(String key, Object value, Duration ttl) { | |||
| try { | |||
| redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl); | |||
| } catch (Exception e) { | |||
| log.warn("write calendar cache failed: key={}, error={}", key, e.getMessage()); | |||
| } | |||
| } | |||
| private static Duration monthTtl(int year, int month) { | |||
| YearMonth current = YearMonth.now(); | |||
| YearMonth target = YearMonth.of(year, month); | |||
| if (target.equals(current)) return Duration.ofMinutes(1); | |||
| if (target.equals(current.plusMonths(1))) return Duration.ofHours(1); | |||
| return CACHE_TTL; | |||
| } | |||
| private static Duration summaryTtl(int year) { | |||
| return year == YearMonth.now().getYear() ? Duration.ofMinutes(5) : CACHE_TTL; | |||
| } | |||
| private void deleteQuietly(String key) { | |||
| try { | |||
| redisTemplate.delete(key); | |||
| } catch (Exception e) { | |||
| log.warn("delete calendar cache failed: key={}, error={}", key, e.getMessage()); | |||
| } | |||
| } | |||
| private void evictAllScopes() { | |||
| YearMonth current = YearMonth.now(); | |||
| for (int i = 0; i < 6; i++) { | |||
| YearMonth month = current.plusMonths(i); | |||
| deleteQuietly(monthKey(null, month.getYear(), month.getMonthValue())); | |||
| } | |||
| deleteQuietly(CACHE_PREFIX + "summary:ALL:" + current.getYear()); | |||
| deleteQuietly(CACHE_PREFIX + "summary:ALL:" + (current.getYear() + 1)); | |||
| } | |||
| } | |||
| @ -0,0 +1,111 @@ | |||
| package com.inspect.agg.config; | |||
| import lombok.Data; | |||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||
| import org.springframework.stereotype.Component; | |||
| /** | |||
| * 汇聚采集服务配置(prefix = agg):默认值写在代码中;application.yml 已无 agg.* 配置项。 | |||
| * 站点配置已迁移到 DB 表 agg_station(见 StationRegistry),不再放 yaml。 | |||
| */ | |||
| @Data | |||
| @Component | |||
| @ConfigurationProperties(prefix = "agg") | |||
| public class AggProperties { | |||
| private Sync sync = new Sync(); | |||
| private Proxy proxy = new Proxy(); | |||
| private Video video = new Video(); | |||
| private File file = new File(); | |||
| private Http http = new Http(); | |||
| private Statistic statistic = new Statistic(); | |||
| @Data | |||
| public static class Sync { | |||
| /** 基础数据同步公共配置(轮询周期见 SyncDefaults,已写死在代码) */ | |||
| private Base base = new Base(); | |||
| private Dynamic dynamic = new Dynamic(); | |||
| private AnalysisSummary analysisSummary = new AnalysisSummary(); | |||
| @Data | |||
| public static class Base { | |||
| private int pageSize = 100; | |||
| /** 启动时先同步一次(联调用) */ | |||
| private boolean runOnStart = false; | |||
| } | |||
| @Data | |||
| public static class Dynamic { | |||
| /** 启动时先同步一次 */ | |||
| private boolean runOnStart = false; | |||
| } | |||
| @Data | |||
| public static class AnalysisSummary { | |||
| private boolean runOnStart = true; | |||
| } | |||
| } | |||
| @Data | |||
| public static class Proxy { | |||
| /** 允许代理的路径前缀(为空=不限制);建议按需收紧,如 patrol/、v1/ */ | |||
| private java.util.List<String> allowedPrefixes = new java.util.ArrayList<>(); | |||
| } | |||
| @Data | |||
| public static class Video { | |||
| /** 集控 ZLMediaKit API/播放地址(如 http://192.168.1.116:33380) */ | |||
| private String zlmBaseUrl = "http://192.168.1.116:33380"; | |||
| /** ZLM config.ini 的 api.secret */ | |||
| private String zlmSecret = "Xg@123456"; | |||
| /** 拉流目标 app */ | |||
| private String app = "live"; | |||
| /** 播放地址对办公网络暴露的地址(默认同 zlmBaseUrl) */ | |||
| private String publicBaseUrl = ""; | |||
| /** 站端 ZLM 直连流的集控 nginx 地址;留空时按当前请求 Host 自动拼接 */ | |||
| private String nginxBaseUrl = ""; | |||
| /** 是否启用 RTSP TCP 传输(0=TCP, 1=UDP) */ | |||
| private int rtspTransport = 0; | |||
| /** 拉流重试次数(-1=无限) */ | |||
| private int retryCount = -1; | |||
| /** ZLM hook 接收接口 token(内部鉴权) */ | |||
| private String hookToken = "inspect-agg-hook"; | |||
| /** 无 HLS 请求多少毫秒后视为空闲 */ | |||
| private long hookIdleTimeoutMs = 30_000L; | |||
| } | |||
| @Data | |||
| public static class File { | |||
| /** 站端图片/文件反代的集控 nginx 地址;留空时按当前请求 Host 自动拼接 */ | |||
| private String nginxBaseUrl = ""; | |||
| /** 是否在 /agg/proxy 通用代理的 JSON 响应中递归改写图片地址 */ | |||
| private boolean rewriteProxyResponse = true; | |||
| } | |||
| @Data | |||
| public static class Http { | |||
| private int connectTimeoutMs = 10_000; | |||
| private int readTimeoutMs = 30_000; | |||
| /** | |||
| * 连接池总连接上限。 | |||
| */ | |||
| private int poolMaxTotal = 100; | |||
| /** 同一站端的连接上限;一次九画面并发起流按 3 路一批,留足余量。 */ | |||
| private int poolMaxPerRoute = 40; | |||
| /** 从连接池取空闲连接的等待上限(毫秒),避免池满时无限等待。 */ | |||
| private int connectionRequestTimeoutMs = 3_000; | |||
| } | |||
| @Data | |||
| public static class Statistic { | |||
| /** 是否统计轻微缺陷;false 时 minorCount 恒为 0,且不计入 defectCount */ | |||
| private boolean minorDefectEnabled = false; | |||
| } | |||
| } | |||
| @ -0,0 +1,83 @@ | |||
| package com.inspect.agg.config; | |||
| import java.nio.charset.StandardCharsets; | |||
| import java.util.List; | |||
| import java.util.concurrent.TimeUnit; | |||
| import org.apache.http.client.config.RequestConfig; | |||
| import org.apache.http.impl.client.CloseableHttpClient; | |||
| import org.apache.http.impl.client.HttpClients; | |||
| import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; | |||
| import org.springframework.boot.web.client.RestTemplateBuilder; | |||
| import org.springframework.context.annotation.Bean; | |||
| import org.springframework.context.annotation.Configuration; | |||
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |||
| import org.springframework.http.converter.HttpMessageConverter; | |||
| import org.springframework.http.converter.StringHttpMessageConverter; | |||
| import org.springframework.web.client.RestTemplate; | |||
| /** | |||
| * HTTP 客户端配置:连接池 + 超时 + UTF-8(站点接口返回中文,必须 UTF-8 解码)。 | |||
| * | |||
| * <p>聚合层向站端发起的起流/停流请求在打开多画面时会同时出现十几个,串行新建 TCP | |||
| * 连接会明显放大延迟并让部分请求打满页面 8 秒超时;这里改为复用连接的连接池。</p> | |||
| */ | |||
| @Configuration | |||
| public class RestTemplateConfig { | |||
| private final AggProperties aggProperties; | |||
| /** 显式构造器注入(不用 Lombok,避免 IDE 静态检查误报找不到 Bean) */ | |||
| public RestTemplateConfig(AggProperties aggProperties) { | |||
| this.aggProperties = aggProperties; | |||
| } | |||
| @Bean | |||
| public RestTemplate restTemplate(RestTemplateBuilder builder) { | |||
| AggProperties.Http http = aggProperties.getHttp(); | |||
| // 连接池:总连接数与单站端连接数都要覆盖“同时在飞的站端请求”。 | |||
| PoolingHttpClientConnectionManager connectionManager = | |||
| new PoolingHttpClientConnectionManager(); | |||
| connectionManager.setMaxTotal(Math.max(1, http.getPoolMaxTotal())); | |||
| connectionManager.setDefaultMaxPerRoute(Math.max(1, http.getPoolMaxPerRoute())); | |||
| // 超时与池等待:取连接不能无限等,否则池满时请求会一直挂到页面超时。 | |||
| RequestConfig requestConfig = RequestConfig.custom() | |||
| .setConnectTimeout(http.getConnectTimeoutMs()) | |||
| .setSocketTimeout(http.getReadTimeoutMs()) | |||
| .setConnectionRequestTimeout(http.getConnectionRequestTimeoutMs()) | |||
| .build(); | |||
| CloseableHttpClient httpClient = HttpClients.custom() | |||
| .setConnectionManager(connectionManager) | |||
| .setDefaultRequestConfig(requestConfig) | |||
| // 定期清理失效空闲连接,避免站端重启后一直复用坏连接。 | |||
| .evictExpiredConnections() | |||
| .evictIdleConnections(30L, TimeUnit.SECONDS) | |||
| .build(); | |||
| // 只在这里指定请求工厂;连接与读取超时统一由上面的 RequestConfig 控制, | |||
| // 避免与 RestTemplateBuilder 的超时设置叠加产生歧义。 | |||
| RestTemplate restTemplate = builder | |||
| .requestFactory(() -> { | |||
| HttpComponentsClientHttpRequestFactory factory = | |||
| new HttpComponentsClientHttpRequestFactory(httpClient); | |||
| factory.setConnectTimeout(http.getConnectTimeoutMs()); | |||
| factory.setReadTimeout(http.getReadTimeoutMs()); | |||
| return factory; | |||
| }) | |||
| .build(); | |||
| // 默认 StringHttpMessageConverter 用 ISO-8859-1,会破坏站点返回的中文 | |||
| List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters(); | |||
| for (HttpMessageConverter<?> converter : converters) { | |||
| if (converter instanceof StringHttpMessageConverter) { | |||
| StringHttpMessageConverter stringConverter = (StringHttpMessageConverter) converter; | |||
| stringConverter.setDefaultCharset(StandardCharsets.UTF_8); | |||
| } | |||
| } | |||
| return restTemplate; | |||
| } | |||
| } | |||
| @ -0,0 +1,19 @@ | |||
| package com.inspect.agg.config; | |||
| /** | |||
| * 同步相关默认配置(写死在代码内;如需调整改这里并重新构建)。 | |||
| */ | |||
| public final class SyncDefaults { | |||
| /** 基础数据全量轮询周期(毫秒):5 分钟 */ | |||
| public static final String BASE_INTERVAL_MS = "300000"; | |||
| /** 任务执行活跃子集轮询周期(毫秒):60 秒 */ | |||
| public static final String DYNAMIC_INTERVAL_MS = "60000"; | |||
| /** 动态数据全量对账周期(毫秒):5 分钟 */ | |||
| public static final String DYNAMIC_FULL_INTERVAL_MS = "300000"; | |||
| private SyncDefaults() { | |||
| } | |||
| } | |||
| @ -0,0 +1,38 @@ | |||
| package com.inspect.agg.dto; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * Response item for /agg/patrolpoint/list/monitor. | |||
| * The patrolpoint* JSON names match the station-side API contract. | |||
| */ | |||
| @Data | |||
| public class AggMonitorPatrolPointVo { | |||
| private Long id; | |||
| private String stationCode; | |||
| private String stationName; | |||
| private Long patrolpointId; | |||
| private String patrolpointCode; | |||
| private String patrolpointName; | |||
| private Long mainDeviceId; | |||
| private String deviceName; | |||
| private Long deviceId; | |||
| private Long areaId; | |||
| private String areaName; | |||
| private String patrolContent; | |||
| private String purpose; | |||
| private String patrolTypeId; | |||
| private String pointImptLevel; | |||
| private String alarmThreshold; | |||
| private String algSubtypeIds; | |||
| private String algSubtypeName; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,15 @@ | |||
| package com.inspect.agg.dto; | |||
| import lombok.Data; | |||
| /** | |||
| * 汇聚视频接口请求体。 | |||
| */ | |||
| @Data | |||
| public class VideoRequest { | |||
| private String cameraCode; | |||
| private Integer streamType = 2; | |||
| private Integer direct = 0; | |||
| private String rtspUrl; | |||
| } | |||
| @ -0,0 +1,13 @@ | |||
| package com.inspect.agg.entity; | |||
| import lombok.Data; | |||
| /** | |||
| * Shared station algorithm subtype dictionary (read-only). | |||
| */ | |||
| @Data | |||
| public class AggAlgSubtype { | |||
| private Long algSubtypeId; | |||
| private String algSubtypeCode; | |||
| private String algSubtypeName; | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import lombok.Data; | |||
| /** | |||
| * 站端缺陷月度汇总表。 | |||
| */ | |||
| @Data | |||
| public class AggAnalysisDefectMonth { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| private LocalDate statMonth; | |||
| private String pointType; | |||
| private Integer minorCount; | |||
| private Integer generalCount; | |||
| private Integer seriousCount; | |||
| private Integer urgentCount; | |||
| private Integer normalCount; | |||
| private LocalDateTime syncedAt; | |||
| } | |||
| @ -0,0 +1,43 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 区域镜像表 agg_basedata_area(源表:站点 basedata_area)。 | |||
| */ | |||
| @Data | |||
| public class AggBasedataArea { | |||
| private Long id; | |||
| /** 站点编码 */ | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| /** 源区域 id */ | |||
| private Long areaId; | |||
| /** 父区域 id */ | |||
| private Long parentId; | |||
| /** 祖级列表,如 ,1,2002 */ | |||
| private String ancestors; | |||
| private String areaName; | |||
| private String areaCode; | |||
| private String areaAddr; | |||
| /** station / station_in / bayunit */ | |||
| private String areaType; | |||
| private String deviceTypeCode; | |||
| private Integer orderNum; | |||
| /** 0正常 1停用 */ | |||
| private String status; | |||
| private String remark; | |||
| /** 站点侧 update_time */ | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| /** 中心落库时间 */ | |||
| private LocalDateTime syncedAt; | |||
| /** 0存在 1站点侧已删除 */ | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,42 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 设备镜像表 agg_basedata_device(源表:站点 basedata_device,接口 /basedata/prodevmnt/device/list)。 | |||
| */ | |||
| @Data | |||
| public class AggBasedataDevice { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| /** 源设备 id */ | |||
| private Long deviceId; | |||
| /** 父设备 id */ | |||
| private Long parentId; | |||
| private String ancestors; | |||
| private Long areaId; | |||
| private String areaName; | |||
| private String deviceCode; | |||
| private String deviceName; | |||
| private String deviceTypeName; | |||
| private String deviceModel; | |||
| private String manufacturer; | |||
| /** 电力主设备标识 Yes/No */ | |||
| private String powerMainEquFlag; | |||
| private String stationNo; | |||
| private String monitorIndexNo; | |||
| /** 0正常 1停用 */ | |||
| private String status; | |||
| private Integer orderNum; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,37 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 设备类型镜像表 agg_basedata_devicetype(源表:站点 basedata_devicetype,接口 /basedata/devicetype/list)。 | |||
| */ | |||
| @Data | |||
| public class AggBasedataDevicetype { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| /** 源设备类型 id */ | |||
| private Long deviceTypeId; | |||
| private Long parentId; | |||
| private Integer orderNum; | |||
| private String ancestors; | |||
| private String ancestorsAddr; | |||
| private String deviceTypeName; | |||
| private String deviceTypeCode; | |||
| private String powerMainEquFlag; | |||
| private String phaseNum; | |||
| private String phase; | |||
| private String status; | |||
| private String professionType; | |||
| private Long level; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,42 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 设备台账镜像表 agg_basedata_eqpbook(源表:站点 basedata_eqpbook,接口 /basedata/eqpbook/list)。 | |||
| * 注意:源响应含 user/password 等敏感字段,本表不镜像。 | |||
| */ | |||
| @Data | |||
| public class AggBasedataEqpbook { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 源台账 id */ | |||
| private Long eqpbookId; | |||
| /** 汇聚自定义显示顺序 */ | |||
| private Integer orderNum; | |||
| private String patrolDeviceCode; | |||
| private String patrolDeviceName; | |||
| private String stationName; | |||
| private Long areaId; | |||
| private String areaName; | |||
| private String deviceSource; | |||
| private String deviceModel; | |||
| private String manufacturer; | |||
| /** 设备类型(type) */ | |||
| private String deviceType; | |||
| private String ipAddr; | |||
| private String port; | |||
| private String videoNvrCode; | |||
| private String mainSystemCode; | |||
| private String useMode; | |||
| private String videoMode; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,47 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 设备通道镜像表 agg_basedata_eqpbook_channel(源表:站点 basedata_eqpbook_channel,接口 /basedata/eqpbook/channel/list)。 | |||
| */ | |||
| @Data | |||
| public class AggBasedataEqpbookChannel { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| /** 源通道 id */ | |||
| private Long channelId; | |||
| /** 汇聚自定义显示顺序 */ | |||
| private Integer orderNum; | |||
| private String channelCode; | |||
| private String channelName; | |||
| /** vl/ir/ptz/ng */ | |||
| private String channelType; | |||
| /** 1在线 0离线 */ | |||
| private String onlineState; | |||
| private String channelContent; | |||
| private String host; | |||
| private String port; | |||
| private Long areaId; | |||
| private String areaName; | |||
| /** 所属台账 id */ | |||
| private Long eqpbookId; | |||
| private String patrolDeviceCode; | |||
| private String upSystemDeviceCode; | |||
| private String deviceSource; | |||
| /** 通道能力(JSON 字符串,树接口返回时解析为对象) */ | |||
| private String capabilities; | |||
| /** 使用类型(源表 basedata_eqpbook_channel.use_mode) */ | |||
| private String useMode; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,44 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 巡检点位镜像表 agg_basedata_patrolpoint(源表:站点 basedata_patrolpoint,接口 /basedata/patrolpointmnt/patrolpoint/list)。 | |||
| */ | |||
| @Data | |||
| public class AggBasedataPatrolpoint { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| /** 源点位 id */ | |||
| private Long patrolPointId; | |||
| private String patrolPointCode; | |||
| private String patrolPointName; | |||
| /** 主设备 id */ | |||
| private Long mainDeviceId; | |||
| private String deviceName; | |||
| /** 设备 id(真正配置点位的设备/部件) */ | |||
| private Long deviceId; | |||
| private Long areaId; | |||
| private String areaName; | |||
| private String patrolContent; | |||
| private String purpose; | |||
| /** 巡视类型,逗号分隔 */ | |||
| private String patrolTypeId; | |||
| /** 重要等级 Ⅰ/Ⅱ/Ⅲ */ | |||
| private String pointImptLevel; | |||
| /** 告警阈值 JSON */ | |||
| private String alarmThreshold; | |||
| /** Source algorithm subtype IDs, comma-separated. */ | |||
| private String algSubtypeIds; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,64 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||
| import lombok.Data; | |||
| /** | |||
| * Snapshot row returned by station-side /patrol/analysis/defect/list. | |||
| */ | |||
| @Data | |||
| public class AggDefectRecord { | |||
| @JsonIgnore | |||
| private Long id; | |||
| private String stationCode; | |||
| private String stationName; | |||
| private String deviceName; | |||
| private String channelName; | |||
| private String channelCode; | |||
| private String deviceSource; | |||
| private String pointName; | |||
| private String pointId; | |||
| private String warnStatus; | |||
| private String algName; | |||
| private String resValue; | |||
| private String img; | |||
| private String filterDesc; | |||
| private String brightDesc; | |||
| private String imgAnalyse; | |||
| private String brightImgAnalyse; | |||
| private String desc; | |||
| private String lineId; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime patrolTime; | |||
| private String pointStatus; | |||
| private String imgType; | |||
| private String dataType; | |||
| private String filter; | |||
| private String requestId; | |||
| private String resStatus; | |||
| private String brightResStatus; | |||
| private String areaName; | |||
| private String areaId; | |||
| private String algType; | |||
| private String objectId; | |||
| private String imageNormalUrlPath; | |||
| private String filePath; | |||
| private String suggestion; | |||
| private String originalValue; | |||
| private String taskPatrolId; | |||
| private String taskName; | |||
| private Integer defectLevel; | |||
| @JsonIgnore | |||
| private LocalDateTime syncedAt; | |||
| @JsonIgnore | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,50 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 巡视报告镜像表 agg_inspection_report。 | |||
| */ | |||
| @Data | |||
| public class AggInspectionReport { | |||
| private Long id; | |||
| private String stationCode; | |||
| private Long lineId; | |||
| private String stationName; | |||
| private String voltLevel; | |||
| private String stationType; | |||
| @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") | |||
| private LocalDateTime inspectionDate; | |||
| private String inspectionTaskName; | |||
| private String envInfo; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime inspectionStartTime; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime inspectionEndTime; | |||
| private String patrolStatistics; | |||
| private String checkPerson; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime checkTime; | |||
| private String description; | |||
| private String filePath; | |||
| private String taskId; | |||
| private String taskResultId; | |||
| private String taskPatrolledId; | |||
| private String location; | |||
| private String filter; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,65 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 任务方案镜像表 agg_patrol_task(源:patrol_task,接口 /patrol/task/list)。 | |||
| */ | |||
| @Data | |||
| public class AggPatrolTask { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| /** 源任务 id */ | |||
| private Long taskId; | |||
| private String taskCode; | |||
| private String taskName; | |||
| /** 巡视类型 */ | |||
| private String type; | |||
| private String priority; | |||
| private String deviceLevel; | |||
| private String deviceList; | |||
| private LocalDateTime fixedStartTime; | |||
| /** 0可用 1不可用 */ | |||
| private String isEnable; | |||
| /** 状态 */ | |||
| private String fileStatus; | |||
| /** 执行状态 */ | |||
| private String executionStatus; | |||
| private String taskCycle; | |||
| private String cycleMonth; | |||
| private String cycleWeek; | |||
| private String cycleExecuteTime; | |||
| private LocalDateTime cycleStartTime; | |||
| private LocalDateTime cycleEndTime; | |||
| private Integer intervalNumber; | |||
| private String intervalType; | |||
| private String intervalExecuteTime; | |||
| private LocalDateTime intervalStartTime; | |||
| private LocalDateTime intervalEndTime; | |||
| private LocalDateTime invalidStartTime; | |||
| private LocalDateTime invalidEndTime; | |||
| /** 1机器人 0无人机 2视频 */ | |||
| private String devType; | |||
| private String areaId; | |||
| private String areaName; | |||
| /** 字典 point_alg_category 的 dict_code,多个值逗号分隔 */ | |||
| private String pointType; | |||
| /** 方案点位总数,来源:站端 /patrol/task/list 统计返回 */ | |||
| private Integer totalPoints; | |||
| private String executionMode; | |||
| private String drafter; | |||
| private String specifiedAlg; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime createTime; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime updateTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,49 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * 任务执行状态镜像表 agg_patrol_task_status(源:patrol_task_status,接口 /patrol/taskstatus/list)。 | |||
| */ | |||
| @Data | |||
| public class AggPatrolTaskStatus { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| private Long lineId; | |||
| private String taskPatrolledId; | |||
| private String taskName; | |||
| private String taskCode; | |||
| /** 1已执行 2执行中 3暂停 4终止 5未执行 6超期 */ | |||
| private String taskState; | |||
| private LocalDateTime planStartTime; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime startTime; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime endTime; | |||
| private String taskProgress; | |||
| private String taskEstimatedTime; | |||
| private String patrolType; | |||
| private String posType; | |||
| private String code; | |||
| private String pos; | |||
| private String execType; | |||
| private String taskType; | |||
| private String mainId; | |||
| /** ??????????? /patrol/taskstatus/list ???? */ | |||
| private Integer qxNum; | |||
| /** ?????0??? 1????????? /patrol/taskstatus/list ???? */ | |||
| private String fileStatus; | |||
| private String summary; | |||
| private String description; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime createTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,45 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import lombok.Data; | |||
| /** | |||
| * result_analysis 汇聚镜像表。 | |||
| */ | |||
| @Data | |||
| public class AggResultAnalysis { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 仅用于展示:所属站点名称,不落业务表 */ | |||
| private String stationName; | |||
| private Long lineId; | |||
| private String resultType; | |||
| private String businessId; | |||
| private String resultContent; | |||
| private String resStatus; | |||
| private String pointStatus; | |||
| private String suggestion; | |||
| private String objectId; | |||
| private String description; | |||
| private String requestId; | |||
| private String taskPatrolId; | |||
| private String algType; | |||
| private String resValue; | |||
| private String resCode; | |||
| private String resConf; | |||
| private String resDesc; | |||
| private String result; | |||
| private String resImgUrl; | |||
| private String imageNormalUrlPath; | |||
| private String resPos; | |||
| private String filter; | |||
| private String originalValue; | |||
| private Integer defectLevel; | |||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") | |||
| private LocalDateTime createTime; | |||
| private LocalDateTime syncedAt; | |||
| private Integer isDeleted; | |||
| } | |||
| @ -0,0 +1,33 @@ | |||
| package com.inspect.agg.entity; | |||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||
| import lombok.Data; | |||
| /** | |||
| * 站点配置表 agg_station(DB 维护,替代 yaml 站点列表)。 | |||
| * 密码临时明文存储。 | |||
| */ | |||
| @Data | |||
| public class AggStation { | |||
| private Long id; | |||
| /** 站点编码(中心侧唯一) */ | |||
| private String stationCode; | |||
| private String stationName; | |||
| /** 站点 API 根地址,以 / 结尾 */ | |||
| @JsonIgnore | |||
| private String baseUrl; | |||
| /** 站端门户网址 */ | |||
| private String portalUrl; | |||
| @JsonIgnore | |||
| private String username; | |||
| /** 明文密码(临时;正式化前轮换并改加密/密钥) */ | |||
| @JsonIgnore | |||
| private String password; | |||
| /** 1启用 0停用 */ | |||
| private String enabled; | |||
| /** 汇聚自定义显示顺序(数据库手工维护) */ | |||
| private Integer orderNum; | |||
| } | |||
| @ -0,0 +1,27 @@ | |||
| package com.inspect.agg.entity; | |||
| import java.time.LocalDateTime; | |||
| import lombok.Data; | |||
| /** | |||
| * 同步运行日志(表 agg_sync_log)。 | |||
| */ | |||
| @Data | |||
| public class AggSyncLog { | |||
| private Long id; | |||
| private String stationCode; | |||
| /** 同步实体:area/device/point/eqpbook/channel/devicetype/task/taskplan */ | |||
| private String entity; | |||
| /** FULL / ACTIVE */ | |||
| private String syncType; | |||
| /** SUCCESS / FAILED */ | |||
| private String status; | |||
| private Integer rowCount; | |||
| private Integer deletedCount; | |||
| private String message; | |||
| private LocalDateTime startedAt; | |||
| private LocalDateTime finishedAt; | |||
| } | |||
| @ -0,0 +1,37 @@ | |||
| package com.inspect.agg.enums; | |||
| import org.springframework.util.StringUtils; | |||
| public enum AlgCategory { | |||
| INFRARED("882", "红外"), | |||
| METER("881", "表计"), | |||
| APPEARANCE("880", "外观"); | |||
| private final String code; | |||
| private final String label; | |||
| AlgCategory(String code, String label) { | |||
| this.code = code; | |||
| this.label = label; | |||
| } | |||
| public String getCode() { | |||
| return code; | |||
| } | |||
| public String getLabel() { | |||
| return label; | |||
| } | |||
| public static AlgCategory fromLabel(String label) { | |||
| if (!StringUtils.hasText(label)) { | |||
| return null; | |||
| } | |||
| for (AlgCategory category : values()) { | |||
| if (category.label.equals(label.trim())) { | |||
| return category; | |||
| } | |||
| } | |||
| throw new IllegalArgumentException("不支持的算法分类: " + label); | |||
| } | |||
| } | |||
| @ -0,0 +1,25 @@ | |||
| package com.inspect.agg.mapper; | |||
| import java.time.LocalDate; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggAnalysisDefectMonth; | |||
| @Mapper | |||
| public interface AggAnalysisDefectMonthMapper { | |||
| int upsert(AggAnalysisDefectMonth item); | |||
| int deleteRange(@Param("stationCode") String stationCode, | |||
| @Param("beginMonth") LocalDate beginMonth, | |||
| @Param("endMonth") LocalDate endMonth); | |||
| List<AggAnalysisDefectMonth> selectRange(@Param("stationCode") String stationCode, | |||
| @Param("beginMonth") LocalDate beginMonth, | |||
| @Param("endMonth") LocalDate endMonth); | |||
| long countAll(); | |||
| } | |||
| @ -0,0 +1,30 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggBasedataArea; | |||
| /** | |||
| * AggBasedataArea 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataAreaMapper.xml),无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggBasedataAreaMapper { | |||
| /** 幂等 upsert:按 (station_code, 源主键) */ | |||
| int upsert(AggBasedataArea e); | |||
| /** 全量对账:本次未出现的行标记 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| /** 列表(限制条数,limit<=0 表示不限制) */ | |||
| List<AggBasedataArea> listByStation(@Param("stationCode") String stationCode, @Param("limit") int limit); | |||
| /** 整站区域(用于组树) */ | |||
| List<AggBasedataArea> listForTree(@Param("stationCode") String stationCode); | |||
| long countByStation(@Param("stationCode") String stationCode); | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggBasedataDevice; | |||
| /** | |||
| * AggBasedataDevice 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataDeviceMapper.xml),无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggBasedataDeviceMapper { | |||
| /** 幂等 upsert:按 (station_code, 源主键) */ | |||
| int upsert(AggBasedataDevice e); | |||
| /** 全量对账:本次未出现的行标记 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggBasedataDevice> list(@Param("stationCode") String stationCode, @Param("areaId") Long areaId, @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode, @Param("areaId") Long areaId); | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggBasedataDevicetype; | |||
| /** | |||
| * AggBasedataDevicetype 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataDevicetypeMapper.xml),无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggBasedataDevicetypeMapper { | |||
| /** 幂等 upsert:按 (station_code, 源主键) */ | |||
| int upsert(AggBasedataDevicetype e); | |||
| /** 全量对账:本次未出现的行标记 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggBasedataDevicetype> list(@Param("stationCode") String stationCode, @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode); | |||
| } | |||
| @ -0,0 +1,28 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggBasedataEqpbookChannel; | |||
| /** | |||
| * AggBasedataEqpbookChannel 闀滃儚 Mapper锛歝ustom SQL 鍏ㄩ儴鍦?XML锛坮esources/mapper/AggBasedataEqpbookChannelMapper.xml锛夛紝鏃?QueryWrapper銆? */ | |||
| @Mapper | |||
| public interface AggBasedataEqpbookChannelMapper { | |||
| /** 骞傜瓑 upsert锛氭寜 (station_code, 婧愪富閿? */ | |||
| int upsert(AggBasedataEqpbookChannel e); | |||
| /** 鍏ㄩ噺瀵硅处锛氭湰娆℃湭鍑虹幇鐨勮鏍囪 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggBasedataEqpbookChannel> list(@Param("stationCode") String stationCode, @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode); | |||
| List<Map<String, Object>> countByDeviceSource(@Param("stationCode") String stationCode); | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggBasedataEqpbook; | |||
| /** | |||
| * AggBasedataEqpbook 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataEqpbookMapper.xml),无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggBasedataEqpbookMapper { | |||
| /** 幂等 upsert:按 (station_code, 源主键) */ | |||
| int upsert(AggBasedataEqpbook e); | |||
| /** 全量对账:本次未出现的行标记 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggBasedataEqpbook> list(@Param("stationCode") String stationCode, @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode); | |||
| } | |||
| @ -0,0 +1,31 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.dto.AggMonitorPatrolPointVo; | |||
| import com.inspect.agg.entity.AggBasedataPatrolpoint; | |||
| /** | |||
| * AggBasedataPatrolpoint 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataPatrolpointMapper.xml),无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggBasedataPatrolpointMapper { | |||
| /** 幂等 upsert:按 (station_code, 源主键) */ | |||
| int upsert(AggBasedataPatrolpoint e); | |||
| /** 全量对账:本次未出现的行标记 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggBasedataPatrolpoint> list(@Param("stationCode") String stationCode, @Param("deviceId") Long deviceId, | |||
| @Param("areaId") Long areaId, @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode, @Param("deviceId") Long deviceId, @Param("areaId") Long areaId); | |||
| List<AggMonitorPatrolPointVo> selectMonitorRelationList(@Param("stationCode") String stationCode, | |||
| @Param("deviceId") Long deviceId); | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.agg.mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggDefectRecord; | |||
| @Mapper | |||
| public interface AggDefectRecordMapper { | |||
| int upsert(AggDefectRecord record); | |||
| List<AggDefectRecord> selectActiveKeys(@Param("stationCode") String stationCode); | |||
| int markDeletedByIds(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggDefectRecord> selectList(@Param("stationCode") String stationCode, | |||
| @Param("pointName") String pointName, | |||
| @Param("beginTime") String beginTime, | |||
| @Param("endTime") String endTime, | |||
| @Param("algName") String algName, | |||
| @Param("defectLevel") Integer defectLevel); | |||
| List<String> selectDistinctAlgNames(@Param("stationCode") String stationCode); | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| package com.inspect.agg.mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggInspectionReport; | |||
| @Mapper | |||
| public interface AggInspectionReportMapper { | |||
| int upsert(AggInspectionReport report); | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("lineIds") List<Long> lineIds); | |||
| List<AggInspectionReport> selectList(@Param("stationCode") String stationCode, | |||
| @Param("cycleStartTime") String cycleStartTime, | |||
| @Param("cycleEndTime") String cycleEndTime, | |||
| @Param("inspectionTaskName") String inspectionTaskName); | |||
| long count(@Param("stationCode") String stationCode, | |||
| @Param("cycleStartTime") String cycleStartTime, | |||
| @Param("cycleEndTime") String cycleEndTime, | |||
| @Param("inspectionTaskName") String inspectionTaskName); | |||
| } | |||
| @ -0,0 +1,31 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggPatrolTask; | |||
| /** | |||
| * AggPatrolTask 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggPatrolTaskMapper.xml),无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggPatrolTaskMapper { | |||
| /** 幂等 upsert:按 (station_code, 源主键) */ | |||
| int upsert(AggPatrolTask e); | |||
| /** 全量对账:本次未出现的行标记 is_deleted=1 */ | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggPatrolTask> list(@Param("stationCode") String stationCode, @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode); | |||
| List<AggPatrolTask> selectTaskList(@Param("stationCode") String stationCode, | |||
| @Param("taskName") String taskName); | |||
| List<AggPatrolTask> selectEnabledTasks(@Param("stationCode") String stationCode); | |||
| } | |||
| @ -0,0 +1,46 @@ | |||
| package com.inspect.agg.mapper; | |||
| import java.time.LocalDateTime; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggPatrolTaskStatus; | |||
| @Mapper | |||
| public interface AggPatrolTaskStatusMapper { | |||
| int upsert(AggPatrolTaskStatus e); | |||
| int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids); | |||
| List<AggPatrolTaskStatus> list(@Param("stationCode") String stationCode, | |||
| @Param("taskState") String taskState, | |||
| @Param("limit") int limit); | |||
| long count(@Param("stationCode") String stationCode, @Param("taskState") String taskState); | |||
| List<AggPatrolTaskStatus> selectTaskStatusList(@Param("stationCode") String stationCode, | |||
| @Param("taskName") String taskName, | |||
| @Param("beginTime") String beginTime, | |||
| @Param("endTime") String endTime); | |||
| List<Map<String, Object>> countMonthlyStatsByYear(@Param("stationCode") String stationCode, | |||
| @Param("year") Integer year); | |||
| List<AggPatrolTaskStatus> selectDailyStatsByYearAndMonth(@Param("stationCode") String stationCode, | |||
| @Param("year") Integer year, | |||
| @Param("month") Integer month); | |||
| List<AggPatrolTaskStatus> selectDailyStatsByYearAndMonthAndDay(@Param("stationCode") String stationCode, | |||
| @Param("year") Integer year, | |||
| @Param("month") Integer month, | |||
| @Param("day") Integer day); | |||
| Map<String, Object> selectInspectionTaskStats(@Param("stationCode") String stationCode, | |||
| @Param("pointTypeCode") String pointTypeCode, | |||
| @Param("beginTime") LocalDateTime beginTime, | |||
| @Param("endTime") LocalDateTime endTime); | |||
| } | |||
| @ -0,0 +1,22 @@ | |||
| package com.inspect.agg.mapper; | |||
| import java.time.LocalDateTime; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggResultAnalysis; | |||
| @Mapper | |||
| public interface AggResultAnalysisMapper { | |||
| int upsert(AggResultAnalysis e); | |||
| List<Map<String, Object>> selectDefectStats(@Param("stationCode") String stationCode, | |||
| @Param("pointTypeCode") String pointTypeCode, | |||
| @Param("dateType") String dateType, | |||
| @Param("beginTime") LocalDateTime beginTime, | |||
| @Param("endTime") LocalDateTime endTime); | |||
| } | |||
| @ -0,0 +1,24 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Param; | |||
| import com.inspect.agg.entity.AggStation; | |||
| /** | |||
| * 站点配置 Mapper:custom SQL 在 XML,无 QueryWrapper。 | |||
| */ | |||
| @Mapper | |||
| public interface AggStationMapper { | |||
| List<AggStation> selectEnabled(); | |||
| List<AggStation> selectAllOrdered(); | |||
| AggStation selectByCode(@Param("stationCode") String stationCode); | |||
| List<AggStation> selectByEnabled(@Param("enabled") Integer enabled); | |||
| } | |||
| @ -0,0 +1,14 @@ | |||
| package com.inspect.agg.mapper; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import com.inspect.agg.entity.AggSyncLog; | |||
| /** | |||
| * 同步日志 Mapper:SQL 在 XML(AggSyncLogMapper.xml),纯 MyBatis。 | |||
| */ | |||
| @Mapper | |||
| public interface AggSyncLogMapper { | |||
| int insert(AggSyncLog log); | |||
| } | |||
| @ -0,0 +1,12 @@ | |||
| package com.inspect.agg.mapper; | |||
| import java.util.List; | |||
| import org.apache.ibatis.annotations.Mapper; | |||
| import com.inspect.agg.entity.AggAlgSubtype; | |||
| @Mapper | |||
| public interface BasedataAlgSubtypeMapper { | |||
| List<AggAlgSubtype> selectEnabledList(); | |||
| } | |||
| @ -0,0 +1,61 @@ | |||
| package com.inspect.agg.scheduler; | |||
| import org.springframework.boot.ApplicationArguments; | |||
| import org.springframework.boot.ApplicationRunner; | |||
| import org.springframework.scheduling.annotation.Scheduled; | |||
| import org.springframework.stereotype.Component; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.config.SyncDefaults; | |||
| import com.inspect.agg.service.AreaSyncService; | |||
| import com.inspect.agg.service.ChannelSyncService; | |||
| import com.inspect.agg.service.DevicetypeSyncService; | |||
| import com.inspect.agg.service.DeviceSyncService; | |||
| import com.inspect.agg.service.EqpbookSyncService; | |||
| import com.inspect.agg.service.InspectionReportSyncService; | |||
| import com.inspect.agg.service.PointSyncService; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 基础数据(区域/设备/点位/台账/通道/设备类型)轮询调度:定时周期执行 + 可选启动时先同步一次。 | |||
| */ | |||
| @Slf4j | |||
| @Component | |||
| @RequiredArgsConstructor | |||
| public class BaseSyncScheduler implements ApplicationRunner { | |||
| private final AreaSyncService areaSyncService; | |||
| private final DeviceSyncService deviceSyncService; | |||
| private final PointSyncService pointSyncService; | |||
| private final EqpbookSyncService eqpbookSyncService; | |||
| private final ChannelSyncService channelSyncService; | |||
| private final DevicetypeSyncService devicetypeSyncService; | |||
| private final InspectionReportSyncService inspectionReportSyncService; | |||
| private final AggProperties props; | |||
| @Scheduled(initialDelayString = SyncDefaults.BASE_INTERVAL_MS, | |||
| fixedDelayString = SyncDefaults.BASE_INTERVAL_MS) | |||
| public void scheduledBaseSync() { | |||
| syncAll(); | |||
| } | |||
| @Override | |||
| public void run(ApplicationArguments args) { | |||
| if (props.getSync().getBase().isRunOnStart()) { | |||
| log.info("run-on-start enabled, start base-data sync once"); | |||
| syncAll(); | |||
| } | |||
| } | |||
| private void syncAll() { | |||
| areaSyncService.syncAllStations(); | |||
| deviceSyncService.syncAllStations(); | |||
| pointSyncService.syncAllStations(); | |||
| eqpbookSyncService.syncAllStations(); | |||
| channelSyncService.syncAllStations(); | |||
| devicetypeSyncService.syncAllStations(); | |||
| inspectionReportSyncService.syncAllStations(); | |||
| } | |||
| } | |||
| @ -0,0 +1,28 @@ | |||
| package com.inspect.agg.scheduler; | |||
| import org.springframework.scheduling.annotation.Scheduled; | |||
| import org.springframework.stereotype.Component; | |||
| import com.inspect.agg.config.SyncDefaults; | |||
| import com.inspect.agg.service.DefectRecordSyncService; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| @Slf4j | |||
| @Component | |||
| @RequiredArgsConstructor | |||
| public class DefectRecordSyncScheduler { | |||
| private final DefectRecordSyncService defectRecordSyncService; | |||
| @Scheduled(initialDelay = 120_000L, fixedDelayString = SyncDefaults.BASE_INTERVAL_MS) | |||
| public void syncRecent() { | |||
| defectRecordSyncService.syncRecentAllStations(); | |||
| } | |||
| @Scheduled(cron = "0 10 3 * * ?") | |||
| public void syncFull() { | |||
| defectRecordSyncService.syncFullAllStations(); | |||
| } | |||
| } | |||
| @ -0,0 +1,53 @@ | |||
| package com.inspect.agg.scheduler; | |||
| import org.springframework.boot.ApplicationArguments; | |||
| import org.springframework.boot.ApplicationRunner; | |||
| import org.springframework.scheduling.annotation.Scheduled; | |||
| import org.springframework.stereotype.Component; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.config.SyncDefaults; | |||
| import com.inspect.agg.service.PatrolTaskSyncService; | |||
| import com.inspect.agg.service.TaskExecSyncService; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 动态数据调度: | |||
| * - 任务执行 ACTIVE:interval-ms(默认 60s)拉执行中子集 | |||
| * - FULL:full-interval-ms(默认 5min)任务执行 + 任务方案全量对账 | |||
| * 站点间并行(服务内 parallelStream)。 | |||
| */ | |||
| @Slf4j | |||
| @Component | |||
| @RequiredArgsConstructor | |||
| public class DynamicSyncScheduler implements ApplicationRunner { | |||
| private final TaskExecSyncService taskExecSyncService; | |||
| private final PatrolTaskSyncService patrolTaskSyncService; | |||
| private final AggProperties props; | |||
| @Scheduled(initialDelayString = SyncDefaults.DYNAMIC_INTERVAL_MS, | |||
| fixedDelayString = SyncDefaults.DYNAMIC_INTERVAL_MS) | |||
| public void scheduledActive() { | |||
| taskExecSyncService.syncActiveAllStations(); | |||
| } | |||
| @Scheduled(initialDelayString = SyncDefaults.DYNAMIC_FULL_INTERVAL_MS, | |||
| fixedDelayString = SyncDefaults.DYNAMIC_FULL_INTERVAL_MS) | |||
| public void scheduledFull() { | |||
| taskExecSyncService.syncFullAllStations(); | |||
| patrolTaskSyncService.syncFullAllStations(); | |||
| } | |||
| @Override | |||
| public void run(ApplicationArguments args) { | |||
| if (props.getSync().getDynamic().isRunOnStart()) { | |||
| log.info("dynamic run-on-start enabled, run ACTIVE+FULL once"); | |||
| taskExecSyncService.syncActiveAllStations(); | |||
| taskExecSyncService.syncFullAllStations(); | |||
| patrolTaskSyncService.syncFullAllStations(); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,33 @@ | |||
| package com.inspect.agg.scheduler; | |||
| import org.springframework.boot.ApplicationArguments; | |||
| import org.springframework.boot.ApplicationRunner; | |||
| import org.springframework.scheduling.annotation.Scheduled; | |||
| import org.springframework.stereotype.Component; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.service.AggAnalysisDefectMonthSyncService; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| @Slf4j | |||
| @Component | |||
| @RequiredArgsConstructor | |||
| public class ResultAnalysisSyncScheduler implements ApplicationRunner { | |||
| private final AggAnalysisDefectMonthSyncService syncService; | |||
| private final AggProperties props; | |||
| @Scheduled(cron = "0 30 2 * * ?") | |||
| public void syncLast12Months() { | |||
| syncService.syncLast12MonthsAllStations(); | |||
| } | |||
| @Override | |||
| public void run(ApplicationArguments args) { | |||
| if (props.getSync().getAnalysisSummary().isRunOnStart()) { | |||
| syncService.syncLast12MonthsAllStations(); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,100 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import java.time.YearMonth; | |||
| import java.time.format.DateTimeFormatter; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.entity.AggAnalysisDefectMonth; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggAnalysisDefectMonthMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class AggAnalysisDefectMonthSyncService { | |||
| private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM"); | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggAnalysisDefectMonthMapper defectMonthMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| public void syncLast12MonthsAllStations() { | |||
| stationRegistry.listEnabled().parallelStream().forEach(this::syncLast12Months); | |||
| } | |||
| public void syncLast12Months(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| String message = null; | |||
| YearMonth current = YearMonth.now(); | |||
| YearMonth first = current.minusMonths(11); | |||
| LocalDate begin = first.atDay(1); | |||
| LocalDate endExclusive = current.plusMonths(1).atDay(1); | |||
| try { | |||
| JsonNode root = stationApiClient.fetchAggDefectMonth( | |||
| station, first.format(MONTH_FORMAT), current.format(MONTH_FORMAT)); | |||
| if (root.path("code").asInt() != 200) { | |||
| throw new IllegalStateException(root.path("msg").asText(root.toString())); | |||
| } | |||
| JsonNode data = root.path("data"); | |||
| if (!data.isArray()) { | |||
| throw new IllegalStateException("station response data is not array: " + root); | |||
| } | |||
| defectMonthMapper.deleteRange(station.getStationCode(), begin, endExclusive); | |||
| for (JsonNode row : data) { | |||
| AggAnalysisDefectMonth item = map(row, station); | |||
| if (item == null) { | |||
| continue; | |||
| } | |||
| defectMonthMapper.upsert(item); | |||
| upserted++; | |||
| } | |||
| message = "rows=" + data.size() + ", upserted=" + upserted; | |||
| log.info("station={} analysis month summary ok: {}", station.getStationCode(), message); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} analysis month summary failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "analysis-month", "FULL", status, | |||
| upserted, 0, message, started, LocalDateTime.now()); | |||
| } | |||
| private AggAnalysisDefectMonth map(JsonNode row, AggStation station) { | |||
| String statMonth = Rows.text(row, "statMonth"); | |||
| if (!StringUtils.hasText(statMonth)) { | |||
| return null; | |||
| } | |||
| AggAnalysisDefectMonth item = new AggAnalysisDefectMonth(); | |||
| item.setStationCode(station.getStationCode()); | |||
| item.setStatMonth(YearMonth.parse(statMonth.trim(), MONTH_FORMAT).atDay(1)); | |||
| String pointType = Rows.text(row, "pointType"); | |||
| item.setPointType(StringUtils.hasText(pointType) ? pointType.trim() : ""); | |||
| item.setMinorCount(value(row, "minorCount")); | |||
| item.setGeneralCount(value(row, "generalCount")); | |||
| item.setSeriousCount(value(row, "seriousCount")); | |||
| item.setUrgentCount(value(row, "urgentCount")); | |||
| item.setNormalCount(value(row, "normalCount")); | |||
| item.setSyncedAt(LocalDateTime.now()); | |||
| return item; | |||
| } | |||
| private Integer value(JsonNode row, String key) { | |||
| Integer value = Rows.intVal(row, key); | |||
| return value == null ? 0 : value; | |||
| } | |||
| } | |||
| @ -0,0 +1,144 @@ | |||
| package com.inspect.agg.service; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.Comparator; | |||
| import java.util.HashMap; | |||
| import java.util.Iterator; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.fasterxml.jackson.databind.node.ArrayNode; | |||
| import com.fasterxml.jackson.databind.node.ObjectNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataArea; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataAreaMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import lombok.RequiredArgsConstructor; | |||
| /** | |||
| * 关系树数据尚未展开为汇聚表,按站点读取关系树并统一补充 stationCode。 | |||
| */ | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class AggDeviceTreeService { | |||
| private static final String TREE_PATH = | |||
| "basedata/prodevmnt/device/treeAreaDeviceSelectFilterByPreset/relation"; | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggProperties props; | |||
| private final AggBasedataAreaMapper areaMapper; | |||
| public List<JsonNode> loadRelationTree(String stationCode, Map<String, String> params) { | |||
| List<AggStation> stations; | |||
| if (StringUtils.hasText(stationCode)) { | |||
| stations = Collections.singletonList(stationRegistry.requireByCode(stationCode)); | |||
| } else { | |||
| stations = stationRegistry.listEnabled(); | |||
| } | |||
| boolean aggregate = !StringUtils.hasText(stationCode); | |||
| List<JsonNode> result = new ArrayList<>(); | |||
| int pageSize = props.getSync().getBase().getPageSize(); | |||
| for (AggStation station : stations) { | |||
| Map<Long, Integer> areaOrder = areaOrderMap(station.getStationCode()); | |||
| List<JsonNode> roots = stationApiClient.fetchAllRows( | |||
| station, TREE_PATH, pageSize, 20, params, "dataId"); | |||
| for (JsonNode root : roots) { | |||
| ObjectNode copy = (ObjectNode) root.deepCopy(); | |||
| normalizeNode(copy, station.getStationCode(), | |||
| station.getStationName() != null ? station.getStationName() : station.getStationCode(), | |||
| aggregate); | |||
| sortAreaChildren(copy, areaOrder); | |||
| result.add(copy); | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| private Map<Long, Integer> areaOrderMap(String stationCode) { | |||
| Map<Long, Integer> orderMap = new HashMap<>(); | |||
| for (AggBasedataArea area : areaMapper.listForTree(stationCode)) { | |||
| if (area.getAreaId() != null) { | |||
| orderMap.put(area.getAreaId(), area.getOrderNum() == null ? Integer.MAX_VALUE : area.getOrderNum()); | |||
| } | |||
| } | |||
| return orderMap; | |||
| } | |||
| private static void sortAreaChildren(JsonNode node, Map<Long, Integer> areaOrder) { | |||
| if (node == null || !node.isObject()) { | |||
| return; | |||
| } | |||
| JsonNode children = node.get("children"); | |||
| if (children == null || !children.isArray()) { | |||
| return; | |||
| } | |||
| List<JsonNode> sorted = new ArrayList<>(); | |||
| for (JsonNode child : children) { | |||
| sorted.add(child); | |||
| } | |||
| sorted.sort(Comparator | |||
| .comparingInt((JsonNode child) -> isAreaNode(child) ? 0 : 1) | |||
| .thenComparingInt(child -> isAreaNode(child) ? areaOrderValue(child, areaOrder) : 0)); | |||
| ArrayNode array = (ArrayNode) children; | |||
| array.removeAll(); | |||
| for (JsonNode child : sorted) { | |||
| array.add(child); | |||
| } | |||
| for (JsonNode child : array) { | |||
| sortAreaChildren(child, areaOrder); | |||
| } | |||
| } | |||
| private static boolean isAreaNode(JsonNode node) { | |||
| JsonNode flag = node.get("areaFlag"); | |||
| return flag != null && "Yes".equals(flag.asText()); | |||
| } | |||
| private static int areaOrderValue(JsonNode node, Map<Long, Integer> areaOrder) { | |||
| JsonNode areaId = node.get("areaId"); | |||
| if (areaId == null || areaId.isNull()) { | |||
| return Integer.MAX_VALUE; | |||
| } | |||
| Integer order = areaOrder.get(areaId.asLong()); | |||
| return order == null ? Integer.MAX_VALUE : order; | |||
| } | |||
| private static void normalizeNode(ObjectNode node, String stationCode, String stationName, | |||
| boolean prefixIds) { | |||
| if (node == null || !node.isObject()) { | |||
| return; | |||
| } | |||
| node.put("stationCode", stationCode); | |||
| node.put("stationName", stationName != null ? stationName : stationCode); | |||
| if (prefixIds) { | |||
| String dataId = text(node, "dataId"); | |||
| if (StringUtils.hasText(dataId)) { | |||
| node.put("dataId", stationCode + ":" + dataId); | |||
| } | |||
| String parentId = text(node, "parentId"); | |||
| if (StringUtils.hasText(parentId)) { | |||
| node.put("parentId", stationCode + ":" + parentId); | |||
| } | |||
| } | |||
| JsonNode children = node.get("children"); | |||
| if (children != null && children.isArray()) { | |||
| for (JsonNode child : children) { | |||
| normalizeNode((ObjectNode) child, stationCode, stationName, prefixIds); | |||
| } | |||
| } | |||
| } | |||
| private static String text(JsonNode node, String key) { | |||
| JsonNode value = node.get(key); | |||
| return value == null || value.isNull() ? null : value.asText(); | |||
| } | |||
| } | |||
| @ -0,0 +1,295 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDate; | |||
| import java.time.YearMonth; | |||
| import java.time.format.DateTimeFormatter; | |||
| import java.util.ArrayList; | |||
| import java.util.LinkedHashMap; | |||
| import java.util.HashSet; | |||
| import java.util.Set; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.inspect.agg.entity.AggAnalysisDefectMonth; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.enums.AlgCategory; | |||
| import com.inspect.agg.mapper.AggAnalysisDefectMonthMapper; | |||
| import com.inspect.agg.mapper.AggStationMapper; | |||
| import com.inspect.agg.mapper.AggPatrolTaskStatusMapper; | |||
| import lombok.RequiredArgsConstructor; | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class AggStatisticService { | |||
| private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM"); | |||
| private static final String MONTH = "month"; | |||
| private static final String QUARTER = "quarter"; | |||
| private static final String YEAR = "year"; | |||
| /** 统计窗口下限:最早只能查 2026-10(季度下限即 2026-Q4 的首月)。 */ | |||
| private static final YearMonth MIN_STAT_MONTH = YearMonth.of(2026, 10); | |||
| private static final YearMonth MIN_STAT_QUARTER_START = YearMonth.of(2026, 10); | |||
| private final AggAnalysisDefectMonthMapper defectMonthMapper; | |||
| private final AggPatrolTaskStatusMapper taskStatusMapper; | |||
| private final AggStationMapper stationMapper; | |||
| public List<Map<String, Object>> defectStatistic(String stationCode, String algCategory, | |||
| String dateType) { | |||
| List<AlgCategory> categories = parseCategories(algCategory); | |||
| String normalizedType = normalizeDateType(dateType); | |||
| YearMonth current = YearMonth.now(); | |||
| // 固定长度窗口:月 12 个、季 4 个、年 3 个;右端对齐当前周期, | |||
| // 若左端早于统计下限(2026-10),整体右移,窗口长度不变。 | |||
| YearMonth windowStart; | |||
| int windowMonths; | |||
| if (YEAR.equals(normalizedType)) { | |||
| int startYear = Math.max(current.getYear() - 2, MIN_STAT_MONTH.getYear()); | |||
| windowStart = YearMonth.of(startYear, 1); | |||
| windowMonths = 36; | |||
| } else if (QUARTER.equals(normalizedType)) { | |||
| YearMonth start = quarterStart(current).minusMonths(9); | |||
| windowStart = start.isBefore(MIN_STAT_QUARTER_START) ? MIN_STAT_QUARTER_START : start; | |||
| windowMonths = 12; | |||
| } else { | |||
| YearMonth start = current.minusMonths(11); | |||
| windowStart = start.isBefore(MIN_STAT_MONTH) ? MIN_STAT_MONTH : start; | |||
| windowMonths = 12; | |||
| } | |||
| LocalDate begin = windowStart.atDay(1); | |||
| LocalDate endExclusive = windowStart.plusMonths(windowMonths).atDay(1); | |||
| List<AggAnalysisDefectMonth> rows = defectMonthMapper.selectRange(stationCode, begin, endExclusive); | |||
| Map<YearMonth, Counts> monthlyMap = toMonthlyMap(rows, categories); | |||
| String stationName = stationDisplayName(stationCode); | |||
| List<Map<String, Object>> result = new ArrayList<>(); | |||
| if (YEAR.equals(normalizedType)) { | |||
| int startYear = windowStart.getYear(); | |||
| for (int i = 0; i < 3; i++) { | |||
| int year = startYear + i; | |||
| Counts counts = new Counts(); | |||
| for (int monthOffset = 0; monthOffset < 12; monthOffset++) { | |||
| counts.add(monthlyMap.get(YearMonth.of(year, 1).plusMonths(monthOffset))); | |||
| } | |||
| result.add(toPeriodResult(counts, String.valueOf(year), | |||
| normalizedType, algCategory, stationCode, stationName)); | |||
| } | |||
| return result; | |||
| } | |||
| if (QUARTER.equals(normalizedType)) { | |||
| for (int i = 0; i < 4; i++) { | |||
| YearMonth quarter = windowStart.plusMonths(i * 3L); | |||
| Counts counts = new Counts(); | |||
| for (int monthOffset = 0; monthOffset < 3; monthOffset++) { | |||
| counts.add(monthlyMap.get(quarter.plusMonths(monthOffset))); | |||
| } | |||
| result.add(toPeriodResult(counts, | |||
| quarter.getYear() + "-Q" + ((quarter.getMonthValue() - 1) / 3 + 1), | |||
| normalizedType, algCategory, stationCode, stationName)); | |||
| } | |||
| return result; | |||
| } | |||
| for (int i = 0; i < 12; i++) { | |||
| YearMonth month = windowStart.plusMonths(i); | |||
| Counts counts = monthlyMap.getOrDefault(month, new Counts()); | |||
| result.add(toPeriodResult(counts, month.format(MONTH_FORMAT), normalizedType, algCategory, | |||
| stationCode, stationName)); | |||
| } | |||
| return result; | |||
| } | |||
| public List<Map<String, Object>> inspectionStatistic(String stationCode, String algCategory) { | |||
| AlgCategory category = AlgCategory.fromLabel(algCategory); | |||
| String stationName = stationDisplayName(stationCode); | |||
| if (category != null) { | |||
| return java.util.Collections.singletonList(inspectionByCategory(stationCode, stationName, category)); | |||
| } | |||
| List<Map<String, Object>> result = new ArrayList<>(); | |||
| for (AlgCategory item : AlgCategory.values()) { | |||
| result.add(inspectionByCategory(stationCode, stationName, item)); | |||
| } | |||
| return result; | |||
| } | |||
| private Map<String, Object> inspectionByCategory(String stationCode, String stationName, | |||
| AlgCategory category) { | |||
| YearMonth current = YearMonth.now(); | |||
| LocalDate begin = current.atDay(1); | |||
| LocalDate endExclusive = current.plusMonths(1).atDay(1); | |||
| Map<String, Object> taskStats = taskStatusMapper.selectInspectionTaskStats( | |||
| stationCode, category.getCode(), begin.atStartOfDay(), endExclusive.atStartOfDay()); | |||
| List<AggAnalysisDefectMonth> rows = defectMonthMapper.selectRange(stationCode, begin, endExclusive); | |||
| Counts defectStats = new Counts(); | |||
| for (AggAnalysisDefectMonth row : rows) { | |||
| if (category.getCode().equals(emptyIfNull(row.getPointType()))) { | |||
| defectStats.add(row); | |||
| } | |||
| } | |||
| return toInspectionResult(category.getLabel(), taskStats, defectStats, stationCode, stationName); | |||
| } | |||
| private Map<YearMonth, Counts> toMonthlyMap(List<AggAnalysisDefectMonth> rows, | |||
| List<AlgCategory> categories) { | |||
| Set<String> codes = new HashSet<>(); | |||
| for (AlgCategory category : categories) { | |||
| codes.add(category.getCode()); | |||
| } | |||
| Map<YearMonth, Counts> result = new LinkedHashMap<>(); | |||
| for (AggAnalysisDefectMonth row : rows) { | |||
| String type = emptyIfNull(row.getPointType()); | |||
| boolean matched = categories.isEmpty() ? "".equals(type) : codes.contains(type); | |||
| if (!matched) { | |||
| continue; | |||
| } | |||
| YearMonth month = YearMonth.from(row.getStatMonth()); | |||
| result.computeIfAbsent(month, key -> new Counts()).add(row); | |||
| } | |||
| return result; | |||
| } | |||
| private Map<String, Object> toPeriodResult(Counts counts, String period, | |||
| String dateType, String algCategory, | |||
| String stationCode, String stationName) { | |||
| Map<String, Object> item = new LinkedHashMap<>(); | |||
| item.put("date", period); | |||
| item.put("dateType", dateType); | |||
| if (StringUtils.hasText(algCategory)) { | |||
| item.put("algCategory", algCategory); | |||
| } | |||
| if (StringUtils.hasText(stationCode)) { | |||
| item.put("stationCode", stationCode); | |||
| item.put("stationName", stationName != null ? stationName : stationCode); | |||
| } | |||
| item.put("generalCount", counts.general); | |||
| item.put("seriousCount", counts.serious); | |||
| item.put("urgentCount", counts.urgent); | |||
| item.put("normalCount", counts.normal); | |||
| item.put("defectCount", defectCount(counts)); | |||
| return item; | |||
| } | |||
| private Map<String, Object> toInspectionResult(String label, Map<String, Object> taskStats, | |||
| Counts defectStats, | |||
| String stationCode, String stationName) { | |||
| Map<String, Object> result = new LinkedHashMap<>(); | |||
| result.put("algCategory", label); | |||
| if (StringUtils.hasText(stationCode)) { | |||
| result.put("stationCode", stationCode); | |||
| result.put("stationName", stationName != null ? stationName : stationCode); | |||
| } | |||
| result.put("inspectedTaskCount", number(taskStats, "inspected_task_count")); | |||
| result.put("completedTaskCount", number(taskStats, "completed_task_count")); | |||
| result.put("defectCount", defectCount(defectStats)); | |||
| result.put("generalCount", defectStats.general); | |||
| result.put("seriousCount", defectStats.serious); | |||
| result.put("urgentCount", defectStats.urgent); | |||
| return result; | |||
| } | |||
| /** | |||
| * 缺陷总数只统计一般及以上缺陷,轻微缺陷不计入。 | |||
| */ | |||
| private long defectCount(Counts counts) { | |||
| return (long) counts.general + counts.serious + counts.urgent; | |||
| } | |||
| private String stationDisplayName(String stationCode) { | |||
| if (!StringUtils.hasText(stationCode)) { | |||
| return null; | |||
| } | |||
| AggStation station = stationMapper.selectByCode(stationCode); | |||
| if (station != null && StringUtils.hasText(station.getStationName())) { | |||
| return station.getStationName(); | |||
| } | |||
| return stationCode; | |||
| } | |||
| private static List<AlgCategory> parseCategories(String algCategory) { | |||
| List<AlgCategory> categories = new ArrayList<>(); | |||
| Set<String> codes = new HashSet<>(); | |||
| if (!StringUtils.hasText(algCategory)) { | |||
| return categories; | |||
| } | |||
| for (String part : algCategory.split(",")) { | |||
| String label = part.trim(); | |||
| if (label.isEmpty()) { | |||
| continue; | |||
| } | |||
| AlgCategory category = AlgCategory.fromLabel(label); | |||
| if (codes.add(category.getCode())) { | |||
| categories.add(category); | |||
| } | |||
| } | |||
| return categories; | |||
| } | |||
| private static String normalizeDateType(String dateType) { | |||
| String value = dateType == null ? "" : dateType.trim(); | |||
| if (QUARTER.equalsIgnoreCase(value)) { | |||
| return QUARTER; | |||
| } | |||
| if (YEAR.equalsIgnoreCase(value)) { | |||
| return YEAR; | |||
| } | |||
| return MONTH; | |||
| } | |||
| private static YearMonth quarterStart(YearMonth month) { | |||
| int quarterMonth = ((month.getMonthValue() - 1) / 3) * 3 + 1; | |||
| return YearMonth.of(month.getYear(), quarterMonth); | |||
| } | |||
| private static long number(Map<String, Object> row, String key) { | |||
| if (row == null) { | |||
| return 0L; | |||
| } | |||
| Object value = row.get(key); | |||
| if (value == null) { | |||
| value = row.get(key.toUpperCase()); | |||
| } | |||
| return value instanceof Number ? ((Number) value).longValue() : 0L; | |||
| } | |||
| private static String emptyIfNull(String value) { | |||
| return value == null ? "" : value; | |||
| } | |||
| private static class Counts { | |||
| private int general; | |||
| private int serious; | |||
| private int urgent; | |||
| private int normal; | |||
| private void add(AggAnalysisDefectMonth row) { | |||
| if (row == null) { | |||
| return; | |||
| } | |||
| general += nz(row.getGeneralCount()); | |||
| serious += nz(row.getSeriousCount()); | |||
| urgent += nz(row.getUrgentCount()); | |||
| normal += nz(row.getNormalCount()); | |||
| } | |||
| private void add(Counts other) { | |||
| if (other == null) { | |||
| return; | |||
| } | |||
| general += other.general; | |||
| serious += other.serious; | |||
| urgent += other.urgent; | |||
| normal += other.normal; | |||
| } | |||
| private static int nz(Integer value) { | |||
| return value == null ? 0 : value; | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,99 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataArea; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataAreaMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 区域同步服务(基础数据全量:源 basedata_area)。站点配置来自 DB(StationRegistry)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class AreaSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggBasedataAreaMapper aggAreaMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllAreas(station, | |||
| props.getSync().getBase().getPageSize(), 500); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggBasedataArea area = map(row, station); | |||
| if (area == null) { | |||
| continue; | |||
| } | |||
| aggAreaMapper.upsert(area); | |||
| if (area.getAreaId() != null) { | |||
| ids.add(area.getAreaId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggAreaMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} area sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} area sync failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "area", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggBasedataArea map(JsonNode r, AggStation station) { | |||
| Long areaId = Rows.longVal(r, "areaId", "area_id"); | |||
| if (areaId == null) { | |||
| return null; | |||
| } | |||
| AggBasedataArea a = new AggBasedataArea(); | |||
| a.setStationCode(station.getStationCode()); | |||
| a.setAreaId(areaId); | |||
| Long parentId = Rows.longVal(r, "parentId", "parent_id", "pid"); | |||
| a.setParentId(parentId == null ? 0L : parentId); | |||
| a.setAncestors(Rows.text(r, "ancestors")); | |||
| a.setAreaName(Rows.text(r, "areaName", "area_name")); | |||
| a.setAreaCode(Rows.text(r, "areaCode", "area_code")); | |||
| a.setAreaAddr(Rows.text(r, "areaAddr", "area_addr")); | |||
| a.setAreaType(Rows.text(r, "areaType", "area_type")); | |||
| a.setDeviceTypeCode(Rows.text(r, "deviceTypeCode", "device_type_code")); | |||
| Integer orderNum = Rows.intVal(r, "orderNum", "order_num"); | |||
| a.setOrderNum(orderNum == null ? 0 : orderNum); | |||
| a.setStatus(Rows.text(r, "status")); | |||
| a.setRemark(Rows.text(r, "remark")); | |||
| a.setUpdateTime(Rows.date(r, "updateTime", "update_time")); | |||
| a.setSyncedAt(LocalDateTime.now()); | |||
| a.setIsDeleted(0); | |||
| return a; | |||
| } | |||
| } | |||
| @ -0,0 +1,102 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataEqpbookChannel; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 设备通道同步服务(基础数据全量:源 basedata_eqpbook_channel)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class ChannelSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggBasedataEqpbookChannelMapper aggChannelMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllChannels(station, | |||
| props.getSync().getBase().getPageSize(), 100); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggBasedataEqpbookChannel c = map(row, station); | |||
| if (c == null) { | |||
| continue; | |||
| } | |||
| aggChannelMapper.upsert(c); | |||
| if (c.getChannelId() != null) { | |||
| ids.add(c.getChannelId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggChannelMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} channel sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} channel sync failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "channel", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggBasedataEqpbookChannel map(JsonNode r, AggStation station) { | |||
| Long id = Rows.longVal(r, "channelId"); | |||
| if (id == null) { | |||
| return null; | |||
| } | |||
| AggBasedataEqpbookChannel c = new AggBasedataEqpbookChannel(); | |||
| c.setStationCode(station.getStationCode()); | |||
| c.setChannelId(id); | |||
| c.setChannelCode(Rows.text(r, "channelCode")); | |||
| c.setChannelName(Rows.text(r, "channelName")); | |||
| c.setChannelType(Rows.text(r, "channelType")); | |||
| c.setOnlineState(Rows.text(r, "onlineState")); | |||
| c.setChannelContent(Rows.text(r, "channelContent")); | |||
| c.setHost(Rows.text(r, "host")); | |||
| c.setPort(Rows.text(r, "port")); | |||
| c.setAreaId(Rows.longVal(r, "areaId")); | |||
| c.setAreaName(Rows.text(r, "areaName")); | |||
| c.setEqpbookId(Rows.longVal(r, "eqpbookId")); | |||
| c.setPatrolDeviceCode(Rows.text(r, "patroldeviceCode", "patrolDeviceCode")); | |||
| c.setUpSystemDeviceCode(Rows.text(r, "upSystemDeviceCode")); | |||
| c.setDeviceSource(Rows.text(r, "deviceSource")); | |||
| c.setCapabilities(Rows.jsonText(r, "capabilities")); | |||
| c.setUseMode(Rows.text(r, "useMode", "use_mode")); | |||
| c.setUpdateTime(Rows.date(r, "updateTime")); | |||
| c.setSyncedAt(LocalDateTime.now()); | |||
| c.setIsDeleted(0); | |||
| return c; | |||
| } | |||
| } | |||
| @ -0,0 +1,185 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import java.util.HashMap; | |||
| import java.util.HashSet; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import java.util.Set; | |||
| import java.util.stream.Collectors; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggDefectRecord; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggDefectRecordMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * Syncs merged defect-record rows from station-side /patrol/analysis/defect/list. | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class DefectRecordSyncService { | |||
| private static final String PATH = "patrol/analysis/defect/list"; | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggDefectRecordMapper defectRecordMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncFullAllStations() { | |||
| stationRegistry.listEnabled().parallelStream().forEach(this::syncFull); | |||
| } | |||
| public void syncRecentAllStations() { | |||
| stationRegistry.listEnabled().parallelStream().forEach(this::syncRecent); | |||
| } | |||
| public void syncFull(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<AggDefectRecord> existing = defectRecordMapper.selectActiveKeys(station.getStationCode()); | |||
| Map<String, Long> existingIds = existing.stream() | |||
| .collect(Collectors.toMap(this::naturalKey, AggDefectRecord::getId, (oldValue, ignored) -> oldValue)); | |||
| List<JsonNode> rows = stationApiClient.fetchAllRows(station, PATH, | |||
| props.getSync().getBase().getPageSize(), 1000, "lineId"); | |||
| Set<String> presentKeys = new HashSet<>(); | |||
| for (JsonNode row : rows) { | |||
| AggDefectRecord record = map(row, station); | |||
| if (record == null) { | |||
| continue; | |||
| } | |||
| defectRecordMapper.upsert(record); | |||
| presentKeys.add(naturalKey(record)); | |||
| upserted++; | |||
| } | |||
| List<Long> missingIds = existingIds.entrySet().stream() | |||
| .filter(entry -> !presentKeys.contains(entry.getKey())) | |||
| .map(Map.Entry::getValue) | |||
| .collect(Collectors.toList()); | |||
| if (!missingIds.isEmpty()) { | |||
| deletedMarked = defectRecordMapper.markDeletedByIds(station.getStationCode(), missingIds); | |||
| } | |||
| message = "rows=" + rows.size() + ", deletedMarked=" + deletedMarked; | |||
| log.info("station={} defect record FULL ok: upserted={}, deletedMarked={}", | |||
| station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} defect record FULL failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "defect", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| public void syncRecent(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| String message = null; | |||
| try { | |||
| Map<String, String> query = new HashMap<>(); | |||
| query.put("beginTime", LocalDate.now().minusDays(7).toString()); | |||
| query.put("endTime", LocalDate.now().toString()); | |||
| List<JsonNode> rows = stationApiClient.fetchAllRows(station, PATH, | |||
| props.getSync().getBase().getPageSize(), 200, query, "lineId"); | |||
| for (JsonNode row : rows) { | |||
| AggDefectRecord record = map(row, station); | |||
| if (record != null) { | |||
| defectRecordMapper.upsert(record); | |||
| upserted++; | |||
| } | |||
| } | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} defect record RECENT ok: upserted={}", station.getStationCode(), upserted); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} defect record RECENT failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "defect", "RECENT", status, upserted, 0, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggDefectRecord map(JsonNode r, AggStation station) { | |||
| String taskPatrolId = text(r, "taskPatrolId"); | |||
| String objectId = text(r, "objectId"); | |||
| String algType = text(r, "algType"); | |||
| if (isBlank(taskPatrolId) || isBlank(objectId) || isBlank(algType)) { | |||
| return null; | |||
| } | |||
| AggDefectRecord e = new AggDefectRecord(); | |||
| e.setStationCode(station.getStationCode()); | |||
| e.setDeviceName(text(r, "deviceName")); | |||
| e.setChannelName(text(r, "channelName")); | |||
| e.setChannelCode(text(r, "channelCode")); | |||
| e.setDeviceSource(text(r, "deviceSource")); | |||
| e.setPointName(text(r, "pointName")); | |||
| e.setPointId(text(r, "pointId")); | |||
| e.setWarnStatus(text(r, "warnStatus")); | |||
| e.setAlgName(text(r, "algName")); | |||
| e.setResValue(text(r, "resValue")); | |||
| e.setImg(text(r, "img")); | |||
| e.setFilterDesc(text(r, "filterDesc")); | |||
| e.setBrightDesc(text(r, "brightDesc")); | |||
| e.setImgAnalyse(text(r, "imgAnalyse")); | |||
| e.setBrightImgAnalyse(text(r, "brightImgAnalyse")); | |||
| e.setDesc(text(r, "desc")); | |||
| e.setLineId(text(r, "lineId")); | |||
| e.setPatrolTime(Rows.date(r, "patrolTime")); | |||
| e.setPointStatus(text(r, "pointStatus")); | |||
| e.setImgType(text(r, "imgType")); | |||
| e.setDataType(text(r, "dataType")); | |||
| e.setFilter(text(r, "filter")); | |||
| e.setRequestId(text(r, "requestId")); | |||
| e.setResStatus(text(r, "resStatus")); | |||
| e.setBrightResStatus(text(r, "brightResStatus")); | |||
| e.setAreaName(text(r, "areaName")); | |||
| e.setAreaId(text(r, "areaId")); | |||
| e.setAlgType(algType); | |||
| e.setObjectId(objectId); | |||
| e.setImageNormalUrlPath(text(r, "imageNormalUrlPath")); | |||
| e.setFilePath(text(r, "filePath")); | |||
| e.setSuggestion(text(r, "suggestion")); | |||
| e.setOriginalValue(text(r, "originalValue")); | |||
| e.setTaskPatrolId(taskPatrolId); | |||
| e.setTaskName(text(r, "taskName")); | |||
| Integer defectLevel = Rows.intVal(r, "defectLevel"); | |||
| e.setDefectLevel(defectLevel == null ? 0 : defectLevel); | |||
| e.setSyncedAt(LocalDateTime.now()); | |||
| e.setIsDeleted(0); | |||
| return e; | |||
| } | |||
| private String naturalKey(AggDefectRecord r) { | |||
| return r.getStationCode() + "|" + r.getTaskPatrolId() + "|" + r.getObjectId() + "|" + r.getAlgType(); | |||
| } | |||
| private static String text(JsonNode node, String key) { | |||
| JsonNode value = node.get(key); | |||
| if (value == null || value.isNull()) { | |||
| return null; | |||
| } | |||
| return value.asText(); | |||
| } | |||
| private static boolean isBlank(String value) { | |||
| return value == null || value.trim().isEmpty(); | |||
| } | |||
| } | |||
| @ -0,0 +1,103 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataDevice; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataDeviceMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 设备同步服务(基础数据全量:源 basedata_device)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class DeviceSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggBasedataDeviceMapper aggDeviceMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllDevices(station, | |||
| props.getSync().getBase().getPageSize(), 500); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggBasedataDevice d = map(row, station); | |||
| if (d == null) { | |||
| continue; | |||
| } | |||
| aggDeviceMapper.upsert(d); | |||
| if (d.getDeviceId() != null) { | |||
| ids.add(d.getDeviceId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggDeviceMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} device sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} device sync failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "device", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggBasedataDevice map(JsonNode r, AggStation station) { | |||
| Long deviceId = Rows.longVal(r, "deviceId"); | |||
| if (deviceId == null) { | |||
| return null; | |||
| } | |||
| AggBasedataDevice d = new AggBasedataDevice(); | |||
| d.setStationCode(station.getStationCode()); | |||
| d.setDeviceId(deviceId); | |||
| Long parentId = Rows.longVal(r, "parentId"); | |||
| d.setParentId(parentId == null ? 0L : parentId); | |||
| d.setAncestors(Rows.text(r, "ancestors")); | |||
| d.setAreaId(Rows.longVal(r, "areaId")); | |||
| d.setAreaName(Rows.text(r, "areaName")); | |||
| d.setDeviceCode(Rows.text(r, "deviceCode")); | |||
| d.setDeviceName(Rows.text(r, "deviceName")); | |||
| d.setDeviceTypeName(Rows.text(r, "deviceTypeName")); | |||
| d.setDeviceModel(Rows.text(r, "deviceModel")); | |||
| d.setManufacturer(Rows.text(r, "manufacturer")); | |||
| d.setPowerMainEquFlag(Rows.text(r, "powerMainEquFlag")); | |||
| d.setStationNo(Rows.text(r, "stationNo")); | |||
| d.setMonitorIndexNo(Rows.text(r, "monitorIndexNo")); | |||
| d.setStatus(Rows.text(r, "status")); | |||
| Integer orderNum = Rows.intVal(r, "orderNum"); | |||
| d.setOrderNum(orderNum == null ? 0 : orderNum); | |||
| d.setUpdateTime(Rows.date(r, "updateTime")); | |||
| d.setSyncedAt(LocalDateTime.now()); | |||
| d.setIsDeleted(0); | |||
| return d; | |||
| } | |||
| } | |||
| @ -0,0 +1,102 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataDevicetype; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataDevicetypeMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 设备类型同步服务(基础数据全量:源 basedata_devicetype)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class DevicetypeSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggBasedataDevicetypeMapper aggDevicetypeMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllDeviceTypes(station, | |||
| props.getSync().getBase().getPageSize(), 100); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggBasedataDevicetype d = map(row, station); | |||
| if (d == null) { | |||
| continue; | |||
| } | |||
| aggDevicetypeMapper.upsert(d); | |||
| if (d.getDeviceTypeId() != null) { | |||
| ids.add(d.getDeviceTypeId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggDevicetypeMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} devicetype sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} devicetype sync failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "devicetype", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggBasedataDevicetype map(JsonNode r, AggStation station) { | |||
| Long id = Rows.longVal(r, "devicetypeId"); | |||
| if (id == null) { | |||
| return null; | |||
| } | |||
| AggBasedataDevicetype d = new AggBasedataDevicetype(); | |||
| d.setStationCode(station.getStationCode()); | |||
| d.setDeviceTypeId(id); | |||
| Long parentId = Rows.longVal(r, "parentId"); | |||
| d.setParentId(parentId == null ? 0L : parentId); | |||
| Integer orderNum = Rows.intVal(r, "orderNum"); | |||
| d.setOrderNum(orderNum == null ? 0 : orderNum); | |||
| d.setAncestors(Rows.text(r, "ancestors")); | |||
| d.setAncestorsAddr(Rows.text(r, "ancestorsAddr")); | |||
| d.setDeviceTypeName(Rows.text(r, "devicetypeName", "deviceTypeName")); | |||
| d.setDeviceTypeCode(Rows.text(r, "devicetypeCode", "deviceTypeCode")); | |||
| d.setPowerMainEquFlag(Rows.text(r, "powerMainEquFlag")); | |||
| d.setPhaseNum(Rows.text(r, "phaseNum")); | |||
| d.setPhase(Rows.text(r, "phase")); | |||
| d.setStatus(Rows.text(r, "status")); | |||
| d.setProfessionType(Rows.text(r, "professionType")); | |||
| Long level = Rows.longVal(r, "level"); | |||
| d.setLevel(level == null ? 0L : level); | |||
| d.setUpdateTime(Rows.date(r, "updateTime")); | |||
| d.setSyncedAt(LocalDateTime.now()); | |||
| d.setIsDeleted(0); | |||
| return d; | |||
| } | |||
| } | |||
| @ -0,0 +1,103 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataEqpbook; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataEqpbookMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 设备台账同步服务(基础数据全量:源 basedata_eqpbook)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class EqpbookSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggBasedataEqpbookMapper aggEqpbookMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllEqpbooks(station, | |||
| props.getSync().getBase().getPageSize(), 100); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggBasedataEqpbook e = map(row, station); | |||
| if (e == null) { | |||
| continue; | |||
| } | |||
| aggEqpbookMapper.upsert(e); | |||
| if (e.getEqpbookId() != null) { | |||
| ids.add(e.getEqpbookId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggEqpbookMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} eqpbook sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} eqpbook sync failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "eqpbook", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggBasedataEqpbook map(JsonNode r, AggStation station) { | |||
| Long id = Rows.longVal(r, "eqpbookId"); | |||
| if (id == null) { | |||
| return null; | |||
| } | |||
| AggBasedataEqpbook e = new AggBasedataEqpbook(); | |||
| e.setStationCode(station.getStationCode()); | |||
| e.setEqpbookId(id); | |||
| e.setPatrolDeviceCode(Rows.text(r, "patroldeviceCode", "patrolDeviceCode")); | |||
| e.setPatrolDeviceName(Rows.text(r, "patroldeviceName", "patrolDeviceName")); | |||
| e.setStationName(Rows.text(r, "stationName")); | |||
| e.setAreaId(Rows.longVal(r, "areaId")); | |||
| e.setAreaName(Rows.text(r, "areaName")); | |||
| e.setDeviceSource(Rows.text(r, "deviceSource")); | |||
| e.setDeviceModel(Rows.text(r, "deviceModel")); | |||
| e.setManufacturer(Rows.text(r, "manufacturer")); | |||
| e.setDeviceType(Rows.text(r, "type")); | |||
| e.setIpAddr(Rows.text(r, "ipAddr")); | |||
| e.setPort(Rows.text(r, "port")); | |||
| e.setVideoNvrCode(Rows.text(r, "videoNvrCode")); | |||
| e.setMainSystemCode(Rows.text(r, "mainSystemCode")); | |||
| e.setUseMode(Rows.text(r, "useMode")); | |||
| e.setVideoMode(Rows.text(r, "videoMode")); | |||
| e.setUpdateTime(Rows.date(r, "updateTime")); | |||
| e.setSyncedAt(LocalDateTime.now()); | |||
| e.setIsDeleted(0); | |||
| return e; | |||
| } | |||
| } | |||
| @ -0,0 +1,280 @@ | |||
| package com.inspect.agg.service; | |||
| import java.util.ArrayList; | |||
| import java.util.HashMap; | |||
| import java.util.HashSet; | |||
| import java.util.LinkedHashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import java.util.Set; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.fasterxml.jackson.core.type.TypeReference; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||
| import com.inspect.agg.entity.AggBasedataArea; | |||
| import com.inspect.agg.entity.AggBasedataEqpbook; | |||
| import com.inspect.agg.entity.AggBasedataEqpbookChannel; | |||
| import com.inspect.agg.mapper.AggBasedataAreaMapper; | |||
| import com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper; | |||
| import com.inspect.agg.mapper.AggBasedataEqpbookMapper; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 使用汇聚区域、台账和通道数据构建通道树。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class EqpbookTreeService { | |||
| private final AggBasedataAreaMapper areaMapper; | |||
| private final AggBasedataEqpbookMapper eqpbookMapper; | |||
| private final AggBasedataEqpbookChannelMapper channelMapper; | |||
| private final ObjectMapper objectMapper; | |||
| public List<Map<String, Object>> buildFilterTree(String stationCode, | |||
| String patrolDeviceName, | |||
| String onlineStatus, | |||
| String patrolDeviceTypeFlag, | |||
| String deviceSource, | |||
| String useMode) { | |||
| List<AggBasedataArea> areas = areaMapper.listForTree(stationCode); | |||
| List<AggBasedataEqpbook> eqpbooks = eqpbookMapper.list(stationCode, 0); | |||
| List<AggBasedataEqpbookChannel> channels = channelMapper.list(stationCode, 0); | |||
| Map<String, AggBasedataArea> areaIndex = new HashMap<>(); | |||
| for (AggBasedataArea area : areas) { | |||
| areaIndex.put(areaKey(area.getStationCode(), area.getAreaId()), area); | |||
| } | |||
| Map<String, List<AggBasedataEqpbookChannel>> channelIndex = new HashMap<>(); | |||
| for (AggBasedataEqpbookChannel channel : channels) { | |||
| if (!matchesDeviceSource(channel, deviceSource) || !matchesUseMode(channel, useMode)) { | |||
| continue; | |||
| } | |||
| channelIndex.computeIfAbsent(eqpbookKey(channel.getStationCode(), channel.getEqpbookId()), | |||
| key -> new ArrayList<>()).add(channel); | |||
| } | |||
| List<AggBasedataEqpbook> selectedEqpbooks = new ArrayList<>(); | |||
| Set<String> selectedAreaKeys = new HashSet<>(); | |||
| for (AggBasedataEqpbook eqpbook : eqpbooks) { | |||
| if (!matchesName(eqpbook, patrolDeviceName) || !matchesType(eqpbook, patrolDeviceTypeFlag)) { | |||
| continue; | |||
| } | |||
| List<AggBasedataEqpbookChannel> eqpbookChannels = | |||
| channelIndex.getOrDefault(eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId()), | |||
| new ArrayList<AggBasedataEqpbookChannel>()); | |||
| if (eqpbookChannels.isEmpty()) { | |||
| continue; | |||
| } | |||
| boolean online = false; | |||
| for (AggBasedataEqpbookChannel channel : eqpbookChannels) { | |||
| if ("1".equals(channel.getOnlineState())) { | |||
| online = true; | |||
| break; | |||
| } | |||
| } | |||
| if ("1".equals(onlineStatus) && !online) continue; | |||
| if ("0".equals(onlineStatus) && online) continue; | |||
| AggBasedataArea area = areaIndex.get(areaKey(eqpbook.getStationCode(), eqpbook.getAreaId())); | |||
| if (area == null) { | |||
| continue; | |||
| } | |||
| selectedEqpbooks.add(eqpbook); | |||
| collectAreaKeys(area, areaIndex, selectedAreaKeys); | |||
| } | |||
| Map<String, Map<String, Object>> areaNodes = new LinkedHashMap<>(); | |||
| for (AggBasedataArea area : areas) { | |||
| String key = areaKey(area.getStationCode(), area.getAreaId()); | |||
| if (!selectedAreaKeys.contains(key)) continue; | |||
| Map<String, Object> node = new LinkedHashMap<>(); | |||
| node.put("id", key); | |||
| String parentKey = area.getParentId() == null ? null : areaKey(area.getStationCode(), area.getParentId()); | |||
| node.put("parentId", selectedAreaKeys.contains(parentKey) ? parentKey : null); | |||
| node.put("label", area.getAreaName()); | |||
| node.put("areaId", area.getAreaId()); | |||
| node.put("areaName", area.getAreaName()); | |||
| node.put("areaFlag", "Yes"); | |||
| node.put("areaType", area.getAreaType()); | |||
| node.put("stationCode", area.getStationCode()); | |||
| node.put("stationName", area.getStationName() != null ? area.getStationName() : area.getStationCode()); | |||
| node.put("children", new ArrayList<Map<String, Object>>()); | |||
| areaNodes.put(key, node); | |||
| } | |||
| for (AggBasedataEqpbook eqpbook : selectedEqpbooks) { | |||
| String parentKey = areaKey(eqpbook.getStationCode(), eqpbook.getAreaId()); | |||
| Map<String, Object> areaNode = areaNodes.get(parentKey); | |||
| if (areaNode == null) continue; | |||
| List<AggBasedataEqpbookChannel> eqpbookChannels = | |||
| channelIndex.get(eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId())); | |||
| boolean online = false; | |||
| for (AggBasedataEqpbookChannel channel : eqpbookChannels) { | |||
| if ("1".equals(channel.getOnlineState())) { | |||
| online = true; | |||
| break; | |||
| } | |||
| } | |||
| String eqpbookNodeId = eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId()); | |||
| Map<String, Object> eqpbookNode = new LinkedHashMap<>(); | |||
| eqpbookNode.put("id", eqpbookNodeId); | |||
| eqpbookNode.put("parentId", parentKey); | |||
| eqpbookNode.put("label", eqpbook.getPatrolDeviceName()); | |||
| eqpbookNode.put("areaId", eqpbook.getAreaId()); | |||
| eqpbookNode.put("areaName", eqpbook.getAreaName()); | |||
| eqpbookNode.put("eqpbookId", eqpbook.getEqpbookId()); | |||
| eqpbookNode.put("patroldeviceFlag", "Yes"); | |||
| eqpbookNode.put("patroldeviceCode", eqpbook.getPatrolDeviceCode()); | |||
| eqpbookNode.put("patroldeviceName", eqpbook.getPatrolDeviceName()); | |||
| eqpbookNode.put("patroldeviceType", eqpbook.getDeviceType()); | |||
| eqpbookNode.put("patroldeviceOnlineStatus", online ? "1" : "0"); | |||
| eqpbookNode.put("type", eqpbook.getDeviceType()); | |||
| eqpbookNode.put("stationCode", eqpbook.getStationCode()); | |||
| eqpbookNode.put("stationName", eqpbook.getStationName() != null ? eqpbook.getStationName() : eqpbook.getStationCode()); | |||
| eqpbookNode.put("children", new ArrayList<Map<String, Object>>()); | |||
| children(areaNode).add(eqpbookNode); | |||
| for (AggBasedataEqpbookChannel channel : eqpbookChannels) { | |||
| Map<String, Object> channelNode = new LinkedHashMap<>(); | |||
| channelNode.put("id", eqpbookNodeId + ":channel:" + channel.getChannelId()); | |||
| channelNode.put("parentId", eqpbookNodeId); | |||
| channelNode.put("label", channel.getChannelName()); | |||
| channelNode.put("areaId", eqpbook.getAreaId()); | |||
| channelNode.put("areaName", eqpbook.getAreaName()); | |||
| channelNode.put("patroldeviceCode", eqpbook.getPatrolDeviceCode()); | |||
| channelNode.put("patroldeviceName", eqpbook.getPatrolDeviceName()); | |||
| channelNode.put("patroldeviceType", eqpbook.getDeviceType()); | |||
| channelNode.put("patroldeviceOnlineStatus", channel.getOnlineState()); | |||
| channelNode.put("channelId", channel.getChannelId()); | |||
| channelNode.put("channelCode", channel.getChannelCode()); | |||
| channelNode.put("channelName", channel.getChannelName()); | |||
| channelNode.put("channelFlag", "Yes"); | |||
| channelNode.put("channelType", channel.getChannelType()); | |||
| channelNode.put("deviceSource", channel.getDeviceSource()); | |||
| channelNode.put("capabilities", parseCapabilities(channel.getCapabilities())); | |||
| channelNode.put("useMode", channel.getUseMode()); | |||
| channelNode.put("host", channel.getHost()); | |||
| channelNode.put("port", channel.getPort()); | |||
| channelNode.put("stationCode", channel.getStationCode()); | |||
| channelNode.put("stationName", channel.getStationName() != null ? channel.getStationName() : channel.getStationCode()); | |||
| children(eqpbookNode).add(channelNode); | |||
| } | |||
| } | |||
| List<Map<String, Object>> roots = new ArrayList<>(); | |||
| for (Map<String, Object> node : areaNodes.values()) { | |||
| if (node.get("parentId") == null) { | |||
| roots.add(node); | |||
| } else { | |||
| Map<String, Object> parent = areaNodes.get(node.get("parentId")); | |||
| if (parent != null) { | |||
| children(parent).add(node); | |||
| } else { | |||
| roots.add(node); | |||
| } | |||
| } | |||
| } | |||
| return roots; | |||
| } | |||
| private Map<String, Object> parseCapabilities(String value) { | |||
| if (!StringUtils.hasText(value)) { | |||
| return null; | |||
| } | |||
| try { | |||
| JsonNode node = objectMapper.readTree(value); | |||
| if (node == null || !node.isObject()) { | |||
| log.warn("通道 capabilities 不是 JSON 对象,已忽略: {}", abbrev(value)); | |||
| return null; | |||
| } | |||
| return objectMapper.convertValue(node, new TypeReference<Map<String, Object>>() { | |||
| }); | |||
| } catch (Exception e) { | |||
| log.warn("通道 capabilities 解析失败,已忽略: {}", abbrev(value)); | |||
| return null; | |||
| } | |||
| } | |||
| private static String abbrev(String value) { | |||
| return value.length() <= 120 ? value : value.substring(0, 120); | |||
| } | |||
| @SuppressWarnings("unchecked") | |||
| private static List<Map<String, Object>> children(Map<String, Object> node) { | |||
| return (List<Map<String, Object>>) node.get("children"); | |||
| } | |||
| private static void collectAreaKeys(AggBasedataArea area, | |||
| Map<String, AggBasedataArea> areaIndex, | |||
| Set<String> keys) { | |||
| AggBasedataArea current = area; | |||
| while (current != null) { | |||
| String key = areaKey(current.getStationCode(), current.getAreaId()); | |||
| if (!keys.add(key)) break; | |||
| current = current.getParentId() == null | |||
| ? null | |||
| : areaIndex.get(areaKey(current.getStationCode(), current.getParentId())); | |||
| } | |||
| } | |||
| private static boolean matchesName(AggBasedataEqpbook eqpbook, String value) { | |||
| if (!StringUtils.hasText(value)) return true; | |||
| return value.equals(eqpbook.getPatrolDeviceCode()) | |||
| || (eqpbook.getPatrolDeviceName() != null | |||
| && eqpbook.getPatrolDeviceName().contains(value)); | |||
| } | |||
| private static boolean matchesDeviceSource(AggBasedataEqpbookChannel channel, String deviceSource) { | |||
| if (!StringUtils.hasText(deviceSource)) { | |||
| return true; | |||
| } | |||
| String expected = deviceSource.trim(); | |||
| String value = channel.getDeviceSource(); | |||
| String actual = value == null ? "" : value.trim(); | |||
| if ("其他".equals(expected)) { | |||
| return actual.isEmpty() || "其他".equals(actual); | |||
| } | |||
| return expected.equals(actual); | |||
| } | |||
| private static boolean matchesUseMode(AggBasedataEqpbookChannel channel, String useMode) { | |||
| if (!StringUtils.hasText(useMode)) { | |||
| return true; | |||
| } | |||
| String actual = channel.getUseMode() == null ? "" : channel.getUseMode().trim(); | |||
| for (String expected : useMode.split(",")) { | |||
| String value = expected.trim(); | |||
| if (!value.isEmpty() && value.equals(actual)) { | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| private static boolean matchesType(AggBasedataEqpbook eqpbook, String flag) { | |||
| if (!StringUtils.hasText(flag)) return true; | |||
| String type = eqpbook.getDeviceType(); | |||
| if ("nvr".equals(flag)) return "11".equals(type); | |||
| if ("ipc".equals(flag)) return type == null || (!"11".equals(type) && !"20".equals(type)); | |||
| if ("robotAndUav".equals(flag)) return "1".equals(type) || "2".equals(type) | |||
| || "3".equals(type) || "13".equals(type); | |||
| if ("uav".equals(flag)) return "13".equals(type); | |||
| return true; | |||
| } | |||
| private static String areaKey(String stationCode, Long areaId) { | |||
| return stationCode + ":area:" + areaId; | |||
| } | |||
| private static String eqpbookKey(String stationCode, Long eqpbookId) { | |||
| return stationCode + ":eqpbook:" + eqpbookId; | |||
| } | |||
| } | |||
| @ -0,0 +1,104 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggInspectionReport; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggInspectionReportMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 巡视报告镜像同步。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class InspectionReportSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggInspectionReportMapper reportMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllInspectionReports( | |||
| station, props.getSync().getBase().getPageSize(), 100); | |||
| List<Long> lineIds = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggInspectionReport report = map(row, station); | |||
| if (report == null) { | |||
| continue; | |||
| } | |||
| reportMapper.upsert(report); | |||
| lineIds.add(report.getLineId()); | |||
| upserted++; | |||
| } | |||
| deletedMarked = reportMapper.markDeletedExcept(station.getStationCode(), lineIds); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} inspection_report sync ok: upserted={}, deletedMarked={}", | |||
| station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} inspection_report sync failed: {}", | |||
| station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "report", "FULL", status, upserted, | |||
| deletedMarked, message, started, LocalDateTime.now()); | |||
| } | |||
| private AggInspectionReport map(JsonNode row, AggStation station) { | |||
| Long lineId = Rows.longVal(row, "lineId"); | |||
| if (lineId == null) { | |||
| return null; | |||
| } | |||
| AggInspectionReport report = new AggInspectionReport(); | |||
| report.setStationCode(station.getStationCode()); | |||
| report.setLineId(lineId); | |||
| report.setStationName(Rows.text(row, "stationName")); | |||
| report.setVoltLevel(Rows.text(row, "voltLevel")); | |||
| report.setStationType(Rows.text(row, "stationType")); | |||
| report.setInspectionDate(Rows.date(row, "inspectionDate")); | |||
| report.setInspectionTaskName(Rows.text(row, "inspectionTaskName")); | |||
| report.setEnvInfo(Rows.text(row, "envInfo")); | |||
| report.setInspectionStartTime(Rows.date(row, "inspectionStartTime")); | |||
| report.setInspectionEndTime(Rows.date(row, "inspectionEndTime")); | |||
| report.setPatrolStatistics(Rows.text(row, "patrolStatistics")); | |||
| report.setCheckPerson(Rows.text(row, "checkPerson")); | |||
| report.setCheckTime(Rows.date(row, "checkTime")); | |||
| report.setDescription(Rows.text(row, "description")); | |||
| report.setFilePath(Rows.text(row, "filePath")); | |||
| report.setTaskId(Rows.text(row, "taskId")); | |||
| report.setTaskResultId(Rows.text(row, "taskResultId")); | |||
| report.setTaskPatrolledId(Rows.text(row, "taskPatrolledId")); | |||
| report.setLocation(Rows.text(row, "location")); | |||
| report.setFilter(Rows.text(row, "filter")); | |||
| report.setSyncedAt(LocalDateTime.now()); | |||
| report.setIsDeleted(0); | |||
| return report; | |||
| } | |||
| } | |||
| @ -0,0 +1,120 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.calendar.AggTaskCalendarService; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggPatrolTask; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggPatrolTaskMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 任务方案同步服务(源 patrol_task,接口 /patrol/task/list,量小,周期全量对账)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class PatrolTaskSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggPatrolTaskMapper aggPatrolTaskMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| private final AggTaskCalendarService calendarService; | |||
| public void syncFullAllStations() { | |||
| stationRegistry.listEnabled().parallelStream().forEach(this::syncFull); | |||
| } | |||
| public void syncFull(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/task/list", | |||
| props.getSync().getBase().getPageSize(), 100, "taskId"); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggPatrolTask p = map(row, station); | |||
| if (p == null) { | |||
| continue; | |||
| } | |||
| aggPatrolTaskMapper.upsert(p); | |||
| if (p.getTaskId() != null) { | |||
| ids.add(p.getTaskId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggPatrolTaskMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| calendarService.evictStation(station.getStationCode()); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} patrol_task FULL ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} patrol_task FULL failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "taskplan", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggPatrolTask map(JsonNode r, AggStation station) { | |||
| Long taskId = Rows.longVal(r, "taskId"); | |||
| if (taskId == null) { | |||
| return null; | |||
| } | |||
| AggPatrolTask p = new AggPatrolTask(); | |||
| p.setStationCode(station.getStationCode()); | |||
| p.setTaskId(taskId); | |||
| p.setTaskCode(Rows.text(r, "taskCode")); | |||
| p.setTaskName(Rows.text(r, "taskName")); | |||
| p.setType(Rows.text(r, "type")); | |||
| p.setPriority(Rows.text(r, "priority")); | |||
| p.setDeviceLevel(Rows.text(r, "deviceLevel")); | |||
| p.setDeviceList(Rows.text(r, "deviceList")); | |||
| p.setFixedStartTime(Rows.date(r, "fixedStartTime")); | |||
| p.setIsEnable(Rows.text(r, "isEnable", "isenable")); | |||
| p.setFileStatus(Rows.text(r, "fileStatus")); | |||
| p.setExecutionStatus(Rows.text(r, "executionStatus")); | |||
| p.setTaskCycle(Rows.text(r, "taskCycle")); | |||
| p.setCycleMonth(Rows.text(r, "cycleMonth")); | |||
| p.setCycleWeek(Rows.text(r, "cycleWeek")); | |||
| p.setCycleExecuteTime(Rows.text(r, "cycleExecuteTime")); | |||
| p.setCycleStartTime(Rows.date(r, "cycleStartTime")); | |||
| p.setCycleEndTime(Rows.date(r, "cycleEndTime")); | |||
| p.setIntervalNumber(Rows.intVal(r, "intervalNumber")); | |||
| p.setIntervalType(Rows.text(r, "intervalType")); | |||
| p.setIntervalExecuteTime(Rows.text(r, "intervalExecuteTime")); | |||
| p.setIntervalStartTime(Rows.date(r, "intervalStartTime")); | |||
| p.setIntervalEndTime(Rows.date(r, "intervalEndTime")); | |||
| p.setInvalidStartTime(Rows.date(r, "invalidStartTime")); | |||
| p.setInvalidEndTime(Rows.date(r, "invalidEndTime")); | |||
| p.setDevType(Rows.text(r, "devType")); | |||
| p.setAreaId(Rows.text(r, "areaId")); | |||
| p.setAreaName(Rows.text(r, "areaName")); | |||
| p.setPointType(Rows.text(r, "pointType")); | |||
| Integer totalPoints = Rows.intVal(r, "totalPoints"); | |||
| p.setTotalPoints(totalPoints == null ? 0 : totalPoints); | |||
| p.setExecutionMode(Rows.text(r, "executionMode")); | |||
| p.setDrafter(Rows.text(r, "drafter")); | |||
| p.setSpecifiedAlg(Rows.text(r, "specifiedAlg")); | |||
| p.setCreateTime(Rows.date(r, "createTime")); | |||
| p.setSyncedAt(LocalDateTime.now()); | |||
| p.setIsDeleted(0); | |||
| return p; | |||
| } | |||
| } | |||
| @ -0,0 +1,99 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggBasedataPatrolpoint; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggBasedataPatrolpointMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 巡检点位同步服务(基础数据全量:源 basedata_patrolpoint)。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class PointSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggBasedataPatrolpointMapper aggPointMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncAllStations() { | |||
| for (AggStation station : stationRegistry.listEnabled()) { | |||
| syncStation(station); | |||
| } | |||
| } | |||
| public void syncStation(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllPatrolPoints(station, | |||
| props.getSync().getBase().getPageSize(), 500); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| AggBasedataPatrolpoint p = map(row, station); | |||
| if (p == null) { | |||
| continue; | |||
| } | |||
| aggPointMapper.upsert(p); | |||
| if (p.getPatrolPointId() != null) { | |||
| ids.add(p.getPatrolPointId()); | |||
| } | |||
| upserted++; | |||
| } | |||
| deletedMarked = aggPointMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} point sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} point sync failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "point", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggBasedataPatrolpoint map(JsonNode r, AggStation station) { | |||
| Long pointId = Rows.longVal(r, "patrolpointId"); | |||
| if (pointId == null) { | |||
| return null; | |||
| } | |||
| AggBasedataPatrolpoint p = new AggBasedataPatrolpoint(); | |||
| p.setStationCode(station.getStationCode()); | |||
| p.setPatrolPointId(pointId); | |||
| p.setPatrolPointCode(Rows.text(r, "patrolpointCode")); | |||
| p.setPatrolPointName(Rows.text(r, "patrolpointName")); | |||
| p.setMainDeviceId(Rows.longVal(r, "mainDeviceId")); | |||
| p.setDeviceId(Rows.longVal(r, "deviceId")); | |||
| p.setAreaId(Rows.longVal(r, "areaId")); | |||
| p.setAreaName(Rows.text(r, "areaName")); | |||
| p.setPatrolContent(Rows.text(r, "patrolContent")); | |||
| p.setPurpose(Rows.text(r, "purpose")); | |||
| p.setPatrolTypeId(Rows.text(r, "patrolTypeId")); | |||
| p.setPointImptLevel(Rows.text(r, "pointImptLevel")); | |||
| p.setAlarmThreshold(Rows.text(r, "alarmThreshold")); | |||
| p.setAlgSubtypeIds(Rows.text(r, "algSubtypeIds")); | |||
| p.setUpdateTime(Rows.date(r, "updateTime")); | |||
| p.setSyncedAt(LocalDateTime.now()); | |||
| p.setIsDeleted(0); | |||
| return p; | |||
| } | |||
| } | |||
| @ -0,0 +1,130 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.time.YearMonth; | |||
| import java.time.format.DateTimeFormatter; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggResultAnalysis; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggResultAnalysisMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import com.inspect.base.core.enums.DefectLevel; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * result_analysis 汇聚同步服务。 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class ResultAnalysisSyncService { | |||
| private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggResultAnalysisMapper resultAnalysisMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncLast12MonthsAllStations() { | |||
| stationRegistry.listEnabled().forEach(this::syncLast12Months); | |||
| } | |||
| public void syncRecentAllStations() { | |||
| LocalDateTime end = LocalDateTime.now(); | |||
| LocalDateTime begin = end.minusDays(7); | |||
| stationRegistry.listEnabled().forEach(station -> syncRange(station, begin, end, "RECENT")); | |||
| } | |||
| public void syncLast12Months(AggStation station) { | |||
| LocalDateTime begin = YearMonth.now().minusMonths(11).atDay(1).atStartOfDay(); | |||
| LocalDateTime end = YearMonth.now().plusMonths(1).atDay(1).atStartOfDay(); | |||
| syncRange(station, begin, end, "FULL_12M"); | |||
| } | |||
| private void syncRange(AggStation station, LocalDateTime begin, LocalDateTime end, String syncType) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| String message = null; | |||
| try { | |||
| Map<String, String> query = new HashMap<>(); | |||
| query.put("beginTime", begin.format(DATE_TIME)); | |||
| query.put("endTime", end.minusSeconds(1).format(DATE_TIME)); | |||
| List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/analysis/agg/list", | |||
| props.getSync().getBase().getPageSize(), 1000, query, "lineId"); | |||
| for (JsonNode row : rows) { | |||
| AggResultAnalysis item = map(row, station); | |||
| if (item == null) { | |||
| continue; | |||
| } | |||
| resultAnalysisMapper.upsert(item); | |||
| upserted++; | |||
| } | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} result_analysis {} ok: upserted={}", station.getStationCode(), syncType, upserted); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} result_analysis {} failed: {}", station.getStationCode(), syncType, e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "analysis", syncType, status, upserted, 0, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private AggResultAnalysis map(JsonNode row, AggStation station) { | |||
| Long lineId = Rows.longVal(row, "lineId"); | |||
| if (lineId == null) { | |||
| return null; | |||
| } | |||
| String resultType = Rows.text(row, "resultType"); | |||
| AggResultAnalysis item = new AggResultAnalysis(); | |||
| item.setStationCode(station.getStationCode()); | |||
| item.setLineId(lineId); | |||
| item.setResultType(resultType); | |||
| item.setBusinessId(Rows.text(row, "businessId")); | |||
| item.setResultContent(Rows.text(row, "resultContent")); | |||
| item.setResStatus(Rows.text(row, "resStatus")); | |||
| item.setPointStatus(Rows.text(row, "pointStatus")); | |||
| item.setSuggestion(Rows.text(row, "suggestion")); | |||
| item.setObjectId(Rows.text(row, "objectId")); | |||
| item.setDescription(Rows.text(row, "description")); | |||
| item.setRequestId(Rows.text(row, "requestId")); | |||
| item.setTaskPatrolId(Rows.text(row, "taskPatrolId")); | |||
| item.setAlgType(Rows.text(row, "algType")); | |||
| item.setResValue(Rows.text(row, "resValue")); | |||
| item.setResCode(Rows.text(row, "resCode")); | |||
| item.setResConf(Rows.text(row, "resConf")); | |||
| item.setResDesc(Rows.text(row, "resDesc")); | |||
| item.setResult(Rows.text(row, "result")); | |||
| item.setResImgUrl(Rows.text(row, "resImgUrl")); | |||
| item.setImageNormalUrlPath(Rows.text(row, "imageNormalUrlPath")); | |||
| item.setResPos(Rows.jsonText(row, "resPos")); | |||
| item.setFilter(Rows.text(row, "filter")); | |||
| item.setOriginalValue(Rows.text(row, "originalValue")); | |||
| Integer defectLevel = Rows.intVal(row, "defectLevel"); | |||
| item.setDefectLevel(defectLevel == null ? deriveDefectLevel(resultType) : defectLevel); | |||
| item.setCreateTime(Rows.date(row, "createTime")); | |||
| item.setSyncedAt(LocalDateTime.now()); | |||
| item.setIsDeleted(0); | |||
| return item; | |||
| } | |||
| private static int deriveDefectLevel(String resultType) { | |||
| if ("0".equals(resultType)) return DefectLevel.NORMAL.getCode(); | |||
| if ("2".equals(resultType)) return DefectLevel.MINOR.getCode(); | |||
| return DefectLevel.NONE.getCode(); | |||
| } | |||
| } | |||
| @ -0,0 +1,47 @@ | |||
| package com.inspect.agg.service; | |||
| import java.util.List; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.util.StringUtils; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggStationMapper; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 站点注册表(DB 维护):查询走 Mapper XML | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class StationRegistry { | |||
| private final AggStationMapper aggStationMapper; | |||
| /** 所有启用站点 */ | |||
| public List<AggStation> listEnabled() { | |||
| List<AggStation> list = aggStationMapper.selectEnabled(); | |||
| log.info("listEnabled: {}", list); | |||
| return list; | |||
| } | |||
| /** 全部站点(含停用) */ | |||
| public List<AggStation> listAll() { | |||
| return aggStationMapper.selectAllOrdered(); | |||
| } | |||
| /** 按编码查站点(不存在抛异常) */ | |||
| public AggStation requireByCode(String stationCode) { | |||
| if (!StringUtils.hasText(stationCode)) { | |||
| throw new IllegalArgumentException("stationCode 不能为空"); | |||
| } | |||
| AggStation station = aggStationMapper.selectByCode(stationCode); | |||
| if (station == null) { | |||
| throw new IllegalArgumentException("站点不存在: " + stationCode); | |||
| } | |||
| return station; | |||
| } | |||
| } | |||
| @ -0,0 +1,147 @@ | |||
| package com.inspect.agg.service; | |||
| import java.time.LocalDateTime; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.springframework.stereotype.Service; | |||
| import com.fasterxml.jackson.databind.JsonNode; | |||
| import com.inspect.agg.config.AggProperties; | |||
| import com.inspect.agg.entity.AggPatrolTaskStatus; | |||
| import com.inspect.agg.entity.AggStation; | |||
| import com.inspect.agg.mapper.AggPatrolTaskStatusMapper; | |||
| import com.inspect.agg.station.StationApiClient; | |||
| import com.inspect.agg.support.Rows; | |||
| import com.inspect.agg.support.SyncLogWriter; | |||
| import lombok.RequiredArgsConstructor; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| /** | |||
| * 任务执行同步服务(动态数据)。 | |||
| * 站点侧 patrol_task_status 无 update_time,无法时间水位: | |||
| * - ACTIVE:高频拉 taskState=2(执行中)子集,满足 1 分钟新鲜度 | |||
| * - FULL:周期全量 + 对账删除,覆盖终态/状态变化 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| @RequiredArgsConstructor | |||
| public class TaskExecSyncService { | |||
| private final StationRegistry stationRegistry; | |||
| private final StationApiClient stationApiClient; | |||
| private final AggPatrolTaskStatusMapper aggTaskExecMapper; | |||
| private final SyncLogWriter syncLogWriter; | |||
| private final AggProperties props; | |||
| public void syncActiveAllStations() { | |||
| stationRegistry.listEnabled().parallelStream().forEach(this::syncActive); | |||
| } | |||
| public void syncFullAllStations() { | |||
| stationRegistry.listEnabled().parallelStream().forEach(this::syncFull); | |||
| } | |||
| /** 高频:只拉执行中任务 */ | |||
| public void syncActive(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/taskstatus/list", | |||
| props.getSync().getBase().getPageSize(), 50, | |||
| Collections.singletonMap("taskState", "2"), "lineId"); | |||
| for (JsonNode row : rows) { | |||
| if (upsert(row, station)) { | |||
| upserted++; | |||
| } | |||
| } | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} task ACTIVE ok: upserted={}", station.getStationCode(), upserted); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} task ACTIVE failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "task", "ACTIVE", status, upserted, 0, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| /** 周期:全量拉取 + 对账删除 */ | |||
| public void syncFull(AggStation station) { | |||
| LocalDateTime started = LocalDateTime.now(); | |||
| String status = "SUCCESS"; | |||
| int upserted = 0; | |||
| int deletedMarked = 0; | |||
| String message = null; | |||
| try { | |||
| List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/taskstatus/list", | |||
| 500, 100, "lineId"); | |||
| List<Long> ids = new ArrayList<>(); | |||
| for (JsonNode row : rows) { | |||
| if (upsert(row, station) && row.hasNonNull("lineId")) { | |||
| ids.add(row.get("lineId").asLong()); | |||
| } | |||
| } | |||
| deletedMarked = aggTaskExecMapper.markDeletedExcept(station.getStationCode(), ids); | |||
| upserted = ids.size(); | |||
| message = "rows=" + rows.size(); | |||
| log.info("station={} task FULL ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked); | |||
| } catch (Exception e) { | |||
| status = "FAILED"; | |||
| message = Rows.abbrev(e.getMessage(), 480); | |||
| log.error("station={} task FULL failed: {}", station.getStationCode(), e.getMessage()); | |||
| } | |||
| syncLogWriter.write(station.getStationCode(), "task", "FULL", status, upserted, deletedMarked, | |||
| message, started, LocalDateTime.now()); | |||
| } | |||
| private boolean upsert(JsonNode r, AggStation station) { | |||
| AggPatrolTaskStatus e = map(r, station); | |||
| if (e == null) { | |||
| return false; | |||
| } | |||
| aggTaskExecMapper.upsert(e); | |||
| return true; | |||
| } | |||
| private AggPatrolTaskStatus map(JsonNode r, AggStation station) { | |||
| Long lineId = Rows.longVal(r, "lineId"); | |||
| if (lineId == null) { | |||
| return null; | |||
| } | |||
| AggPatrolTaskStatus e = new AggPatrolTaskStatus(); | |||
| e.setStationCode(station.getStationCode()); | |||
| e.setLineId(lineId); | |||
| e.setTaskPatrolledId(Rows.text(r, "taskPatrolledId")); | |||
| e.setTaskName(Rows.text(r, "taskName")); | |||
| e.setTaskCode(Rows.text(r, "taskCode")); | |||
| e.setTaskState(Rows.text(r, "taskState")); | |||
| e.setPlanStartTime(Rows.date(r, "planStartTime")); | |||
| e.setStartTime(Rows.date(r, "startTime")); | |||
| e.setEndTime(Rows.date(r, "endTime")); | |||
| e.setTaskProgress(Rows.text(r, "taskProgress")); | |||
| e.setTaskEstimatedTime(Rows.text(r, "taskEstimatedTime")); | |||
| e.setPatrolType(Rows.text(r, "patrolType")); | |||
| e.setPosType(Rows.text(r, "posType")); | |||
| e.setCode(Rows.text(r, "code")); | |||
| e.setPos(Rows.text(r, "pos")); | |||
| e.setExecType(Rows.text(r, "execType")); | |||
| e.setTaskType(Rows.text(r, "taskType")); | |||
| e.setMainId(Rows.text(r, "mainId")); | |||
| Integer qxNum = Rows.intVal(r, "qxNum"); | |||
| e.setQxNum(qxNum == null ? 0 : qxNum); | |||
| String fileStatus = Rows.text(r, "fileStatus"); | |||
| e.setFileStatus(fileStatus == null ? "0" : fileStatus); | |||
| e.setSummary(Rows.jsonText(r, "summary")); | |||
| e.setDescription(Rows.text(r, "description")); | |||
| e.setCreateTime(Rows.date(r, "createTime")); | |||
| e.setSyncedAt(LocalDateTime.now()); | |||
| e.setIsDeleted(0); | |||
| return e; | |||
| } | |||
| } | |||