71 Commits
Author SHA1 Message Date
wanghaoandClaude 570ed10c33 revert(watch): 还原listByWatch绑定过滤改动,恢复仅排序逻辑
Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 15:50:32 +08:00
wanghao d47737de5e Merge remote-tracking branch 'origin/xc' into xc 2026-09-07 15:39:06 +08:00
wanghaoandClaude 991dbabe8a fix(watch): listByWatch改为只展示未绑定手表的用户
Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 15:34:40 +08:00
lianlonggang 2f5712a678 feat(watch): listCustom 接口与导出增加 eventId、locateTime 字段
1. AbnormalEvent 新增 eventId、locateTime 字段
2. listCustom/largeScreenListCustom/listCustomForMap 三个接口返回新字段
3. 导出 Excel 通过 @Excel 注解自动支持新字段(顺序 12、13)
4. locateTime 时间格式化为 yyyy-MM-dd HH:mm:ss
5. 修正 createMonitorData 中遗漏的 eventId 与 locateTime 字段赋值
6. DDL 表名修正为 QH_WAT_MONITOR_DATA(项目实际表名)
2026-09-03 13:43:09 +08:00
lianlonggang 41bce5c0b2 feat(watch): notice 入参新增 eventId、locateTime;新增 updateMonitorGps 接口
1. WatchNotifyVo、WatchMonitorData 新增 eventId 与 locateTime 字段
2. 新增 POST /watchApi/updateMonitorGps 接口
3. watchNo + eventId 联合定位告警数据,任一缺失则忽略
4. 已存在 lon/lat 数据则忽略本次更新,避免覆盖
5. GPS84 → GCJ02 坐标转换 + 高德地址反查(参考 createMonitorData 逻辑)
6. 仅更新 GPS 相关字段,不推送大屏与短信
2026-09-03 10:52:44 +08:00
lianlonggang 5c8b56430d feat(db): watch_monitor_data 表新增 EVENT_ID 与 LOCATE_TIME 字段
为支持手表告警事件与定时GPS位置上报的关联,新增两个字段:
- EVENT_ID(事件ID,用于告警事件与GPS位置更新关联)
- LOCATE_TIME(定位时间,手表定时上报的GPS定位时间)

同时新增 IDX_EVENT_ID 单列索引与 IDX_WATCH_NO_EVENT_ID 联合索引,
优化 watchNo + eventId 联合定位查询性能。

数据库:118-DM 达梦数据库,SQL 全大写规范。
2026-09-03 10:52:38 +08:00
wanghaoandClaude 674891eba3 fix(watch): QH_WAT_USER_DATA并发写入唯一约束冲突时忽略重复插入
notice接口并发上报同一手表同一天数据时,先删后插存在竞态,
两个线程同时插入触发唯一约束WATCH_USER_DATA_WATCH_NO,
捕获DuplicateKeyException降级为warn日志,数据不丢失

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 11:05:26 +08:00
wanghao a8e8b53203 style(message): 精简权限过滤兜底分支注释 2026-08-10 16:40:20 +08:00
wanghao a835b874ad fix(message): notice_role仅用于菜单可见,列表只可查看本人创建
- applyAdminPermissionFilter 白名单移除 notice_role,使其落入兜底分支
- 兜底过滤字段由 loginUser.getId() 修正为 getUsername(),对齐 createBy 存储
2026-08-10 16:36:30 +08:00
wanghao b6058c5f80 feat(message): 消息通知列表权限白名单加入notice_role + 兜底本人创建可见 2026-08-10 16:03:30 +08:00
pengjie 245cffecb0 fix: 清除因PowerShell Set-Content引入的UTF-8 BOM字符 2026-08-10 10:21:23 +08:00
pengjie b7525d6c7c fix: 修复表重命名脚本语法,改用达梦兼容的 ALTER TABLE RENAME TO 2026-08-10 10:17:16 +08:00
pengjie 1ad322c16a refactor: 缩短急救宣教相关表名 QH_EME_FIRST_AID_* -> QH_EME_FIR_AID_*,解决表名超长问题 2026-08-10 10:14:40 +08:00
lianlonggang d08d20f1ee style(all): 去客户化处理 2026-08-06 17:47:04 +08:00
pengjieandClaude c260b1d79b style: AsyncExportService import合并为通配符
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 09:57:28 +08:00
pengjieandClaude 1028a4e4e1 fix: SysUserServiceImpl注入ISysBaseAPI产生循环依赖,加@Lazy解决
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 09:12:37 +08:00
pengjieandClaude 4a15ebe0d2 feat: 补充IM个人资料同步 + 群成员头像导入修复
1. 在8处头像/名称变更点补充imHelloApi.accountReg()调用,同步更新IM个人资料:
   - SysUserController.updateUserAvatar 用户换头像
   - SysUserController.appEdit 移动端编辑资料
   - SysUserController.edit 后台编辑用户
   - EmergencyProfessionUserServiceImpl.customEdit 应急人员编辑
   - EmergencyCenterAdminServiceImpl.updateAdmin 中心管理员编辑
   - HealthUserEmployeeExServiceImpl.updateUserInfo 员工编辑
   - ConDoctorApiController.update 专家端个人简介修改
   - AsyncExportService.importDoctor 批量导入医生
   所有syncImProfile方法内部try-catch兜底,失败不影响主流程。
2. AddGroupMemberDTO新增memberAvatar字段,TencentCloudImUtil.addGroupMember
   导入IM账号时传递FaceUrl,EmergencyApiServiceImpl.addGroupUser构建头像映射。
3. jeecg-system-biz/pom.xml新增health-im-api依赖。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 20:03:20 +08:00
pengjie 0ae963c4cf Revert "fix(im): 邀请入群时使用真实用户身份调用IM API,系统通知显示真实用户名而非administrator"
This reverts commit 07d6f21fed.
2026-08-04 19:33:13 +08:00
pengjieandClaude 07d6f21fed fix(im): 邀请入群时使用真实用户身份调用IM API,系统通知显示真实用户名而非administrator
- AddGroupMemberDTO 新增 fromAccount 内部字段
- TencentCloudImUtil 新增带 identifier 参数的 getHttpsUrl 重载
- addGroupMember 检测 fromAccount,有则用该用户 UserSig
- EmergencyApiServiceImpl.addGroupUser 传入当前登录用户ID

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 18:58:16 +08:00
pengjieandClaude 26ae76a398 fix(im): 创建应急群聊时群名称固定为"应急求助"
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 16:18:13 +08:00
pengjieandClaude 468314df64 refactor(emergency): searchExpert改为两表合集查询,同时搜索QH_EME_PROFESS_USER和qh_con_doctor
原来只查QH_EME_PROFESS_USER再补充qh_con_doctor字段,导致只在qh_con_doctor
中的专家搜不到。改为:
1. 分别查询两表(各自过滤name/排除已入群)
2. LinkedHashMap按userId去重合并
3. sys_user兜底填充realname/avatar/phone/deptName
4. 对profUsers单独补qh_con_doctor专家字段
5. 内存分页返回

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 11:42:47 +08:00
pengjie 3b7f260103 fix(emergency): myParticipatedOrders接口补充返回当前用户自己发起/被救助的应急工单 2026-08-04 11:41:47 +08:00
pengjie 693749fece Merge remote-tracking branch 'origin/xc' into xc 2026-08-04 11:08:52 +08:00
pengjieandClaude e9360d1ad9 feat(emergency): searchExpert接口返回ConDoctor专家数据 + 修复IM UserSig缓存key未区分secretKey的问题
1. searchExpert新增ConDoctor专家字段:
   - 新增ConDoctorMapper直接查询qh_con_doctor表(同库,避免跨模块依赖)
   - EmergencyProfessionUser新增17个@TableField(exist=false)字段承载专家数据
   - searchExpert方法中批量查询ConDoctor并逐字段填充
   - 用户非专家时ConDoctor字段为null
   - 更新API文档-移动端接口.md

2. IM签名缓存优化:
   - TencentCloudImUtil新增secretKey注入
   - Redis key从 silence:im_user_sig:{sdkAppId}:{id}
     改为 silence:im_user_sig:{hash(sdkAppId+secretKey)}:{id}
   - 解决secretKey变更时旧缓存签名仍被命中导致IM请求失败的问题

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 11:08:03 +08:00
lianlonggang ab0470c04c style(system): 短信查询相关员工电话号码功能修正;安眼健康监测大屏跳转时,获取token方式调整为统一认证直接获取 2026-07-29 11:49:16 +08:00
lianlonggang 05c37fd532 style(system): kafka集成 2026-07-24 14:40:15 +08:00
wanghao 670d82c868 Merge remote-tracking branch 'origin/xc' into xc 2026-07-23 17:12:57 +08:00
pengjie c3afd0adfa feat(emergency): isLiaisonUser改为查EMP_CARE_EMP表FIRST_AIDER_INFO判断急救人员身份
原逻辑硬编码返回true,现查询AYGC_YWK.EMP_CARE_EMP表中FIRST_AIDER_INFO
JSON字段是否包含当前登录用户工号,有则判定为急救人员。
2026-07-23 17:12:41 +08:00
pengjieandClaude 44e8ac7da5 chore(emergency): 简化群成员上限提示文案
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 11:24:46 +08:00
pengjieandClaude 1167ac792e fix(emergency): getActualGroupMemberList兜底补拉关爱/急救人员 + addGroupUser加9人上限
- 调用getActualGroupMemberList时先执行addCareUsersToGroup重新查询安眼系统,兜底补拉发起应急时未注册的关爱联络员/急救人员
- addCareUsersToGroup改为public,暴露给EmergencyOrderApiService调用
- addGroupUser增加9人群成员上限校验

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 11:13:40 +08:00
pengjieandClaude df41e0c8ce feat(emergency): getActualGroupMemberList自动补拉预设成员 + addGroupUser加9人上限
- getActualGroupMemberList: 查询群成员时自动检查预设成员(被救助人、操作人员、专业人员、紧急联系人、健康联络员),不在群中则自动补拉
- addGroupUser: 邀请前校验群成员总数不超过9人,超限返回提示

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 11:01:23 +08:00
pengjieandClaude f6f70ebf32 fix(emergency): insertOrderForFirstAider中将initiatorType从4改为1
急救员场景工单发起类型与应急就医保持一致,统一使用1

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 08:56:38 +08:00
pengjie 661079dd3a chore(emergency): isLiaisonUser临时硬编码返回true
联调阶段先放行所有用户,后续切换为急救员判定时再恢复查关爱人员关系表。
2026-07-22 08:53:14 +08:00
pengjie 9111cb40c6 refactor(emergency): 一人一案接口筛选条件由身份证号改为工号
身份证号在 EMP_BASIC_INFO 中非唯一标识,改用工号(EMPLOYEE_NUM)
匹配当前登录用户档案,避免同号多档时取错记录。
2026-07-21 14:27:02 +08:00
pengjieandClaude 3aa16600d8 feat(im): 新增importAccountsWithNames方法,addGroupMember时批量导入账号名称
- 新增 importAccountsWithNames 方法,使用腾讯IM新版 AccountList 格式导入账号及昵称
- addGroupMember 改为调用新方法,一次批量导入账号+名称
- multiAccountImport 旧方法保持不变,不影响其他调用方
- AddGroupMemberDTO.memberName 移除@JsonIgnore,确保Feign跨服务传输

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 17:35:25 +08:00
pengjieandClaude 32d8ad3652 fix(im): 修复addGroupMember时memberName因@JsonIgnore在Feign传输中丢失
移除MemberListDTO.memberName上的@JsonIgnore注解,确保名称能通过Feign跨服务传输。
改为在发送腾讯IM API前手动移除memberName字段,避免发送IM不接受的字段。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 17:21:41 +08:00
pengjieandClaude b5c5251913 feat(IM): 添加群成员时同步导入成员名称
addGroupMember在导入账号后逐个调用accountImport导入成员Nick名称

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 16:59:44 +08:00
pengjieandClaude 38a0aa07dd feat: 合并急救员建群逻辑到createGroupMag,新增参与工单列表接口
1. createGroupMag增加addCareUsersToGroup调用,自动拉入关爱联络员+急救员
2. 废除createGroupMagFirstAider(标记@Deprecated)
3. 新增/myparticipatedOrders接口,app端查询当前登录人参与的应急工单列表

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 15:51:33 +08:00
pengjie be701122ca fix(IM): Redis签名key加入sdkAppId,防止切换AppID后签名不匹配
- key格式: silence:im_user_sig:{sdkAppId}:{identifier}
- 切换IMConfig.sdkAppId后自动缓存miss,重新生成正确签名
- 旧key作为孤儿数据24小时后自然过期
2026-07-20 10:43:07 +08:00
pengjie 1a5a2dcb3f fix(emergency): 修复添加群成员错误提示、成员查询遗漏和日志缺少异常栈
- addGroupUser: IM添加失败时改用友好提示,内部错误记日志
- EmergencyOrderApiServiceImpl: 员工成员查询去掉operatorType过滤,补充兜底覆盖;userId去重
- EmergencyCloudManager: 微服务调用失败日志补充异常栈
2026-07-20 10:26:24 +08:00
pengjie fecfb2a355 fix(IM): addGroupMember前先multiAccountImport,防止账号未注册导致添加失败 2026-07-20 10:25:08 +08:00
lianlonggangandClaude f604d18b32 feat(消息管理): list接口支持按员工电话查询esContent
- queryPageList新增employeePhone参数
- 11位纯数字还原为3~4~4全角波浪线格式后like查询esContent
- 非11位用原值兜底like

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-16 16:25:31 +08:00
pengjieandClaude 145cf89a89 fix(emergency): addCareUsersToGroup改为员工编号匹配,解决手机号不准的TODO
数据流: 安眼手机号 → AYGC_YWK.EMP_BASIC_INFO查EMPLOYEE_NUM → getUserListByWorkNoList → 本系统用户ID

变更:
- LargeScreenDataMapper: 新增selectEmployeeNumsByPhones,INSTR模糊匹配支持多手机号字段
- EmergencyApiServiceImpl: 替换getUserByPhoneList直接匹配,改为两步反查;order_operator匹配同步调整
- 移除旧TODO注释

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-16 09:43:34 +08:00
pengjie d4f1d1a7bd fix(emergency): 修复一人一案接口VARCHAR列映射Double报类型转换异常
达梦库 EMP_PERSON_FILE 表的 BMI/HEIGHT/WEIGHT/WEIGHT_TARGET 等
列为 VARCHAR 类型,MyBatis 映射到 Double 时报类型转换异常,
将对应 VO 字段改为 String,并在 getBmi() 中解析字符串做保留一位小数。
2026-07-14 16:36:55 +08:00
pengjie 2d5198ec3f feat(emergency): 新增大屏一人一案移动端接口
根据当前登录用户身份证号,从安眼库 EMP_PERSON_FILE 关联表
查询最新档案详情,供移动端展示一人一案健康数据。
2026-07-14 16:22:05 +08:00
pengjie e844fec480 feat(emergency): 新增大屏关爱联络员身份判断接口
按当前登录人工号查询关爱关系表是否存在记录,前端用于按联络员
身份差异化展示。原 isLiaisonUser 实现固定返回 false,本次接通
真实查询逻辑。
2026-07-08 17:48:03 +08:00
pengjie 194c3b2134 feat(emergency): 推送IM消息增加resourceId文章ID字段 2026-07-08 09:20:50 +08:00
pengjie a1e5625ca7 fix(emergency): 推送时封面图相对路径拼接jeecg.path.show前缀为全路径 2026-07-07 14:54:22 +08:00
pengjie e16c0c2536 feat(emergency): 推送消息增加封面图字段messageImageUrl,去掉businessId 2026-07-07 14:24:56 +08:00
pengjie 66b77cc231 fix(im): sendGroupMsgArticle补充businessID协议字段,修复客户端显示[自定义消息] 2026-07-07 14:12:03 +08:00
pengjie 9b35cfe9c2 chore(im): 文章推送增加请求参数日志,便于验证businessId等信息 2026-07-07 13:51:58 +08:00
pengjie 4898a63b4b feat(emergency): 宣教推送IM消息改为独立方法sendGroupMsgArticle,不影响已有业务
- 新增 TencentCloudImUtil.sendGroupMsgArticle: From_Account生效、不覆盖businessID、fromAdmin动态判断
- 新增 TXController.sendGroupMsgArticle / IMHelloApi.sendGroupMsgArticle
- FirstAidPushServiceImpl 推送人改为当前登录用户,调用新方法
2026-07-07 13:43:21 +08:00
pengjie 37132eeeb6 fix(im): sendGroupMsgText取消From_Account注释,使fromUserId生效 2026-07-07 12:07:17 +08:00
pengjie 8efc5bbd56 feat(emergency): 移动端宣教详情接口增加阅读记录逻辑 2026-07-07 10:55:57 +08:00
pengjie 3c65f7eb24 fix(emergency): 宣教推送IM消息发送人改为当前登录用户,不再硬编码administrator 2026-07-07 10:39:36 +08:00
pengjieandClaude bb95f1d78f fix(emergency): add接口新增成功后返回主键ID
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 10:32:15 +08:00
pengjieandClaude 1484f621bd fix(emergency): addGroupUser 增加本人判断和IM群成员去重
- 过滤当前登录用户,不允许邀请自己入群
- 查询腾讯IM实际群成员,避免重复邀请已在群中的人
- IM查询失败时降级使用本地 order_operator 数据去重
- 去重来源:本地记录 + IM实际成员 取并集

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 10:05:04 +08:00
pengjieandClaude f10221e92c feat(emergency): push/send 接入IM消息推送,SendIm 新增 businessId 字段
- SendIm 新增 businessId 字段(String),用于区分业务来源,不影响已有调用
- FirstAidPushServiceImpl.send() 步骤5从日志占位改为实际发送IM群消息
- businessId 固定为 "article",前端可据此识别宣教推送消息
- 工单无 sessionId 时跳过IM发送并 log.warn 告警

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 09:48:37 +08:00
pengjie 945c0b46d7 @
feat(emergency): 新增腾讯IM验证的群成员列表接口,优化searchEmployee模糊搜索

- 新增 /order/getActualGroupMemberList 接口,先查本地参与人员再调腾讯IM验证,以交集为准
- 新增 GetGroupMemberInfoVO,封装腾讯IM get_group_member_info API响应
- TencentCloudImUtil 新增 getGroupMemberInfo 方法,TXController/IMHelloApi 新增对应端点
- GroupMemberVO 新增 isSelf 字段区分本人 (0/1),默认值 0
- searchEmployee 姓名/工号/手机号改为模糊搜索(* 包装实现)
- 修复 getActualGroupMemberList IM 调用失败时静默返回空列表的 bug,失败时降级使用本地成员
- 接口文档更新变更摘要,标明新接口替换原 /order/getGroupMemberList 地址

Co-Authored-By: Claude <noreply@anthropic.com>
@
2026-07-07 09:29:19 +08:00
pengjie 74ecbe6bda docs(emergency): 移动端接口文档顶部新增本次变更摘要,供前端联调 2026-07-06 17:16:55 +08:00
pengjie fd7fd32c8d feat(emergency): 新增急救员建群接口,人员接口返回手机号
- 新增 GET /api/emergency/createGroupMagFirstAider(不发起通话,自动拉关爱/急救员)
- createGroupMag 原始逻辑不受影响,新方法独立实现
- OrderOperator.operatorType 注释同步数据库(新增3/4/5)
- EmergencyProfessionUser/GroupMemberVO 新增 phone 字段
- searchExpert/getGroupMemberList 填充手机号
- API文档-移动端接口.md 同步更新
2026-07-06 17:14:48 +08:00
pengjie 4cb3411eca feat(emergency): 新增移动端宣教详情接口,整理移动端API文档
- 新增 FirstAidResourceApiController,提供 GET /api/emergency/firstAid/resource/queryById
- RequestPrefix 新增 FIRST_AID_RESOURCE 路径常量
- API文档-应急群聊拉人.md 合并入新文档 API文档-移动端接口.md(含群聊拉人 + 宣教资源)
- API文档-急救宣教资源管理v3.md 保持管理后台接口
2026-07-06 15:45:55 +08:00
pengjie 9a022aa9cc docs(emergency): 补全应急群聊拉人API文档接口地址,添加网关前缀 /health-emergency 2026-07-06 15:24:43 +08:00
lianlonggang ebc3a026d1 style(consult): banner图片获取功能调整,每次修改nacos配置代价太大了,改成缓存配置地址 2026-07-01 10:59:18 +08:00
lianlonggang 29c2be8856 style(watch): 睡眠接口午睡(短睡)兼容问题处理 2026-07-01 10:42:14 +08:00
lianlonggang 9713b242e8 style(watch): 睡眠接口午睡(短睡)兼容问题处理 2026-06-30 16:21:06 +08:00
pengjieandClaude bbb0d5b0f8 chore(emergency): 修正部署SQL,同步实体字典引用
- STATUS DEFAULT 改为 0(草稿)
- PARENT_ID 注释修正
- 删除 first_aid_resource_status/first_aid_category_type 字典
- 同步移除实体 @Excel 注解中对应 dicCode

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 15:38:08 +08:00
pengjieandClaude 3c5bfc8ad1 fix(emergency): 修复宣教模块3个状态机与兼容性缺陷
- auditStatus 新增 insertStrategy=IGNORED,修复新增/复制时存0而非NULL
- submitForReview 增加PENDING重复提交拦截
- recordRead/toggleFavorite 重复键异常catch兼容DM8数据库

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 15:09:22 +08:00
pengjieandClaude 5612fe0752 refactor(emergency): 宣教模块代码整理与问题修复
- 修复 GlobalUtils.getLoginUser() NPE 风险(send/approve/reject)
- 批量操作改为前置校验+全量执行(batchPublish/batchTakeDown/batchDeleteResources)
- 新增 deleteResource/batchDeleteResources,已发布资源不可删除
- 标签过滤 SQL 修复边界精确匹配(避免 id=2 误匹配 12/22)
- 收藏/阅读改用 DuplicateKeyException 消除并发竞态
- fillResourceExtras/queryResourceList 批量查询分类名称,避免 N+1
- 新增 validateResource 校验内容类型、分类、标签合法性
- copyResource 补全 effectiveTime/expiryTime、版本记录、校验
- 审核下发/下架/删除增加状态机保护
- send() 使用 REQUIRES_NEW 事务隔离,确保批量推送单条失败互不影响
- 版本历史上限控制 MAX_VERSIONS=20
- 分类/标签树形重构:分类不再嵌套子分类,统一为分类+标签两级
- 清理废弃方法(listCategories/listAllCategoriesAndTags 等)
- 批量删除分类/标签增加 @Transactional 事务保护

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 11:23:57 +08:00
pengjieandClaude 3d8b930d1a chore(emergency): 代码整理
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 17:37:17 +08:00
pengjie 8761a965aa feat(emergency): 急救宣教资源推送到应急群聊 2026-06-25 15:53:22 +08:00
91 changed files with 5087 additions and 839 deletions
+200
View File
@@ -0,0 +1,200 @@
-- =====================================================
-- 急救宣教资源管理 线上部署 SQL达梦 DM8
-- 包含分类/标签资源用户行为版本历史审核记录推送记录
-- 日期: 2026-06-25
-- =====================================================
-- 1. 资源分类/标签统一表
-- TYPE: 1=分类(树形), 2=标签(扁平)
CREATE TABLE "QH_EME_FIR_AID_RES_CATE" (
"ID" VARCHAR2(32) NOT NULL,
"NAME" VARCHAR2(100) NOT NULL,
"TYPE" NUMBER(1) DEFAULT 1 NOT NULL,
"PARENT_ID" VARCHAR2(32),
"DESCRIPTION" VARCHAR2(500),
"SORT_NO" NUMBER(10) DEFAULT 0,
"STATUS" NUMBER(1) DEFAULT 1,
"CREATE_BY" VARCHAR2(50),
"CREATE_TIME" TIMESTAMP,
"UPDATE_BY" VARCHAR2(50),
"UPDATE_TIME" TIMESTAMP,
"DEL_FLAG" NUMBER(1) DEFAULT 0,
"MEMO" VARCHAR2(500),
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIR_AID_RES_CATE" IS '急救宣教资源分类/标签';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."NAME" IS '名称';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."TYPE" IS '类型:1=分类,2=标签';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."PARENT_ID" IS '父分类ID(根分类为NULL标签指向所属分类ID)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."DESCRIPTION" IS '描述';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."SORT_NO" IS '排序号';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."STATUS" IS '状态(1=正常,2=冻结)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."DEL_FLAG" IS '删除标记(0=正常,1=已删除)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."MEMO" IS '备注';
CREATE INDEX "IDX_FA_CATEGORY_TYPE" ON "QH_EME_FIR_AID_RES_CATE"("TYPE");
CREATE INDEX "IDX_FA_CATEGORY_PARENT_ID" ON "QH_EME_FIR_AID_RES_CATE"("PARENT_ID");
-- 2. 资源表
CREATE TABLE "QH_EME_FIR_AID_RES" (
"ID" VARCHAR2(32) NOT NULL,
"TITLE" VARCHAR2(200) NOT NULL,
"SUMMARY" VARCHAR2(500),
"CONTENT_TYPE" VARCHAR2(32),
"CATEGORY_ID" VARCHAR2(32),
"APPLICABLE_SCENARIO" VARCHAR2(500),
"TAGS" VARCHAR2(2000),
"CONTENT" CLOB,
"COVER_IMAGE" VARCHAR2(500),
"FILE_URL" CLOB,
"EFFECTIVE_TIME" TIMESTAMP,
"EXPIRY_TIME" TIMESTAMP,
"AUDIT_STATUS" NUMBER(2),
"AUDIT_REJECT_REASON" VARCHAR2(500),
"AUDIT_BY" VARCHAR2(50),
"AUDIT_BY_ID" VARCHAR2(32),
"AUDIT_TIME" TIMESTAMP,
"VIEW_COUNT" NUMBER(10) DEFAULT 0,
"FAVORITE_COUNT" NUMBER(10) DEFAULT 0,
"STATUS" NUMBER(1) DEFAULT 0,
"CREATE_BY" VARCHAR2(50),
"CREATE_TIME" TIMESTAMP,
"UPDATE_BY" VARCHAR2(50),
"UPDATE_TIME" TIMESTAMP,
"DEL_FLAG" NUMBER(1) DEFAULT 0,
"MEMO" VARCHAR2(500),
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIR_AID_RES" IS '急救宣教资源';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."TITLE" IS '标题';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."SUMMARY" IS '摘要/简介';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CONTENT_TYPE" IS '内容类型:IMAGE_TEXT=图文/VIDEO=视频/AUDIO=音频/PDF';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CATEGORY_ID" IS '所属分类ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."APPLICABLE_SCENARIO" IS '适用场景';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."TAGS" IS '标签ID列表(逗号分隔)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CONTENT" IS '正文内容(富文本HTML)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."COVER_IMAGE" IS '封面图URL';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."FILE_URL" IS '附件URL列表(JSON数组)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."EFFECTIVE_TIME" IS '生效时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."EXPIRY_TIME" IS '失效时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_STATUS" IS '审核状态(NULL=无审核/10=待审核/11=已通过/12=已驳回)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_REJECT_REASON" IS '驳回原因';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_BY" IS '审核人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_BY_ID" IS '审核人ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_TIME" IS '审核时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."VIEW_COUNT" IS '浏览次数';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."FAVORITE_COUNT" IS '收藏次数';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."STATUS" IS '发布状态(0=草稿,1=已发布,2=已下架)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."DEL_FLAG" IS '删除标记(0=正常,1=已删除)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."MEMO" IS '备注';
CREATE INDEX "IDX_FA_RESOURCE_CATEGORY_ID" ON "QH_EME_FIR_AID_RES"("CATEGORY_ID");
CREATE INDEX "IDX_FA_RESOURCE_AUDIT_STATUS" ON "QH_EME_FIR_AID_RES"("AUDIT_STATUS");
CREATE INDEX "IDX_FA_RESOURCE_EFFECTIVE_TIME" ON "QH_EME_FIR_AID_RES"("EFFECTIVE_TIME");
-- 3. 用户行为记录表阅读+收藏
CREATE TABLE "QH_EME_FIR_AID_RES_UACT" (
"ID" VARCHAR2(32) NOT NULL,
"RESOURCE_ID" VARCHAR2(32) NOT NULL,
"USER_ID" VARCHAR2(50) NOT NULL,
"TYPE" NUMBER(1) NOT NULL,
"CREATE_TIME" TIMESTAMP,
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIR_AID_RES_UACT" IS '用户行为记录(阅读/收藏)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."USER_ID" IS '用户ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."TYPE" IS '行为类型:1=阅读,2=收藏';
CREATE UNIQUE INDEX "UK_FA_USER_ACTION" ON "QH_EME_FIR_AID_RES_UACT"("RESOURCE_ID", "USER_ID", "TYPE");
CREATE INDEX "IDX_FA_USER_ACTION_USER_TYPE" ON "QH_EME_FIR_AID_RES_UACT"("USER_ID", "TYPE");
-- 4. 版本历史表
CREATE TABLE "QH_EME_FIR_AID_RES_VER" (
"ID" VARCHAR2(32) NOT NULL,
"RESOURCE_ID" VARCHAR2(32) NOT NULL,
"VERSION_NO" NUMBER(10) NOT NULL,
"TITLE" VARCHAR2(200),
"CONTENT" CLOB,
"CHANGE_DESCRIPTION" VARCHAR2(500),
"IS_CURRENT" NUMBER(1) DEFAULT 0,
"CREATE_BY" VARCHAR2(50),
"CREATE_TIME" TIMESTAMP,
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIR_AID_RES_VER" IS '资源版本历史';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."VERSION_NO" IS '版本号';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."TITLE" IS '版本时的标题';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."CONTENT" IS '版本时的正文内容';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."CHANGE_DESCRIPTION" IS '变更描述';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."IS_CURRENT" IS '是否当前版本(1=,0=)';
CREATE INDEX "IDX_FA_VERSION_RESOURCE_ID" ON "QH_EME_FIR_AID_RES_VER"("RESOURCE_ID");
-- 5. 审核记录表
CREATE TABLE "QH_EME_FIR_AID_RES_AUD" (
"ID" VARCHAR2(32) NOT NULL,
"RESOURCE_ID" VARCHAR2(32) NOT NULL,
"AUDIT_STATUS" NUMBER(2),
"AUDIT_REJECT_REASON" VARCHAR2(500),
"AUDIT_BY" VARCHAR2(50),
"AUDIT_BY_ID" VARCHAR2(32),
"AUDIT_TIME" TIMESTAMP,
"CREATE_TIME" TIMESTAMP DEFAULT SYSDATE,
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIR_AID_RES_AUD" IS '急救宣教资源审核记录';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."RESOURCE_ID" IS '关联资源ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."AUDIT_STATUS" IS '审核状态:10=待审核,11=已通过,12=已驳回';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."AUDIT_REJECT_REASON" IS '驳回原因';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."AUDIT_BY" IS '审核人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."AUDIT_BY_ID" IS '审核人ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."AUDIT_TIME" IS '审核时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_AUD"."CREATE_TIME" IS '创建时间';
CREATE INDEX "IDX_FA_AUDIT_RESOURCE_ID" ON "QH_EME_FIR_AID_RES_AUD"("RESOURCE_ID");
-- 6. 推送记录表
CREATE TABLE "QH_EME_FIR_AID_PUSH_RD" (
"ID" VARCHAR2(32) NOT NULL,
"RESOURCE_ID" VARCHAR2(32) NOT NULL,
"RESOURCE_TITLE" VARCHAR2(200) DEFAULT NULL,
"ORDER_ID" VARCHAR2(32) NOT NULL,
"TARGET_USER_ID" VARCHAR2(32) DEFAULT NULL,
"PUSH_BY" VARCHAR2(50) DEFAULT NULL,
"PUSH_BY_ID" VARCHAR2(32) DEFAULT NULL,
"PUSH_TIME" TIMESTAMP DEFAULT NULL,
"TENANT_ID" VARCHAR2(32) DEFAULT NULL,
PRIMARY KEY ("ID"),
CONSTRAINT "UK_ORDER_RESOURCE" UNIQUE ("ORDER_ID", "RESOURCE_ID")
);
COMMENT ON TABLE "QH_EME_FIR_AID_PUSH_RD" IS '急救宣教推送记录表';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."RESOURCE_TITLE" IS '资源标题冗余';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."ORDER_ID" IS '关联应急工单ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."TARGET_USER_ID" IS '推送目标用户ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."PUSH_BY" IS '推送人';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."PUSH_BY_ID" IS '推送人ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."PUSH_TIME" IS '推送时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_PUSH_RD"."TENANT_ID" IS '租户ID';
CREATE INDEX "IDX_FA_PUSH_RECORD_ORDER_ID" ON "QH_EME_FIR_AID_PUSH_RD"("ORDER_ID");
-- 7. 字典数据
-- 7.1 内容类型
INSERT INTO "QH_SYS_DICT" ("ID", "TYPE", "DICT_NAME", "DICT_CODE", "DESCRIPTION", "DEL_FLAG", "TENANT_ID", "LOW_APP_ID", "CREATE_BY", "CREATE_TIME", "UPDATE_BY", "UPDATE_TIME")
VALUES ('1589123456789100001', 0, '内容类型', 'first_aid_content_type', '急救宣教资源内容类型', 0, 0, NULL, 'admin', SYSDATE, 'admin', SYSDATE);
INSERT INTO "QH_SYS_DICT_ITEM" ("ID", "DICT_ID", "ITEM_TEXT", "ITEM_VALUE", "DESCRIPTION", "SORT_ORDER", "STATUS", "CREATE_BY", "CREATE_TIME", "UPDATE_BY", "UPDATE_TIME") VALUES
('1589123456789100011', '1589123456789100001', '图文', 'IMAGE_TEXT', '图文内容', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE);
INSERT INTO "QH_SYS_DICT_ITEM" ("ID", "DICT_ID", "ITEM_TEXT", "ITEM_VALUE", "DESCRIPTION", "SORT_ORDER", "STATUS", "CREATE_BY", "CREATE_TIME", "UPDATE_BY", "UPDATE_TIME") VALUES
('1589123456789100012', '1589123456789100001', '视频', 'VIDEO', '视频内容', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE);
INSERT INTO "QH_SYS_DICT_ITEM" ("ID", "DICT_ID", "ITEM_TEXT", "ITEM_VALUE", "DESCRIPTION", "SORT_ORDER", "STATUS", "CREATE_BY", "CREATE_TIME", "UPDATE_BY", "UPDATE_TIME") VALUES
('1589123456789100013', '1589123456789100001', '音频', 'AUDIO', '音频内容', 3, 1, 'admin', SYSDATE, 'admin', SYSDATE);
INSERT INTO "QH_SYS_DICT_ITEM" ("ID", "DICT_ID", "ITEM_TEXT", "ITEM_VALUE", "DESCRIPTION", "SORT_ORDER", "STATUS", "CREATE_BY", "CREATE_TIME", "UPDATE_BY", "UPDATE_TIME") VALUES
('1589123456789100014', '1589123456789100001', 'PDF', 'PDF', 'PDF文档', 4, 1, 'admin', SYSDATE, 'admin', SYSDATE);
@@ -0,0 +1,28 @@
-- =====================================================
-- 缩短急救宣教相关表名达梦数据库
-- 原因原表名超过数据库限制
-- 日期2026-08-10
-- =====================================================
-- 1. 急救资源分类表
ALTER TABLE QH_EME_FIRST_AID_RESOURCE_CATEGORY RENAME TO QH_EME_FIR_AID_RES_CATE;
-- 2. 用户操作记录表
ALTER TABLE QH_EME_FIRST_AID_RESOURCE_USER_ACTION RENAME TO QH_EME_FIR_AID_RES_UACT;
-- 3. 资源版本表
ALTER TABLE QH_EME_FIRST_AID_RESOURCE_VERSION RENAME TO QH_EME_FIR_AID_RES_VER;
-- 4. 资源审核表
ALTER TABLE QH_EME_FIRST_AID_RESOURCE_AUDIT RENAME TO QH_EME_FIR_AID_RES_AUD;
-- 5. 推送记录表
ALTER TABLE QH_EME_FIRST_AID_PUSH_RECORD RENAME TO QH_EME_FIR_AID_PUSH_RD;
-- 6. 急救资源主表最后执行避免外键依赖
ALTER TABLE QH_EME_FIRST_AID_RESOURCE RENAME TO QH_EME_FIR_AID_RES;
-- =====================================================
-- 验证确认新表名已生效
-- SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME LIKE 'QH_EME_FIR_AID_%';
-- =====================================================
@@ -0,0 +1,10 @@
-- QH_WAT_MONITOR_DATA 新增 EVENT_ID事件ID LOCATE_TIME定位时间字段
-- 数据库118-DM 达梦数据库SQL 全大写规范
ALTER TABLE "QH_WAT_MONITOR_DATA" ADD "EVENT_ID" VARCHAR(64) DEFAULT NULL;
ALTER TABLE "QH_WAT_MONITOR_DATA" ADD "LOCATE_TIME" DATETIME DEFAULT NULL;
COMMENT ON COLUMN "QH_WAT_MONITOR_DATA"."EVENT_ID" IS '事件ID';
COMMENT ON COLUMN "QH_WAT_MONITOR_DATA"."LOCATE_TIME" IS '定位时间';
CREATE INDEX "IDX_EVENT_ID" ON "QH_WAT_MONITOR_DATA"("EVENT_ID");
CREATE INDEX "IDX_WATCH_NO_EVENT_ID" ON "QH_WAT_MONITOR_DATA"("WATCH_NO", "EVENT_ID");
@@ -258,8 +258,11 @@ public class AnYanClientExecutor implements AnYanClient {
return token;
}
@Override
public AuthResult publicNetworkTokenNoCache() {
/**
* @description: 旧版公网获取token实现(SM4 + AnYanResult 包裹解密),保留备份
* @author PengJ
*/
public AuthResult publicNetworkTokenNoCacheBAK() {
JSONObject param = new JSONObject();
String password = anYanProperties.getPublicPassWrod();
String userName = anYanProperties.getPublicUserNmae();
@@ -289,6 +292,47 @@ public class AnYanClientExecutor implements AnYanClient {
return null;
}
/**
* @description: 公网获取token(密码模式,调用方式与 getAuthTokenByPassword 保持一致)
* 表单提交 + 公网 host + QUERY_AUTH_TOKEN,仅 RSA 加密密码,不写 Redis(缓存由调用方负责)
* @author PengJ
*/
@Override
public AuthResult publicNetworkTokenNoCache() {
String username = anYanProperties.getPublicUserNmae();
String password = anYanProperties.getPublicPassWrod();
try {
// 仅 RSA 加密密码,与 getAuthTokenByPassword 保持一致
String encryptPassword = StringUtils.encryptByPublicKey(publicKey, password);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "password");
formData.add("username", username);
formData.add("password", encryptPassword);
formData.add("client_id", anYanProperties.getClientId());
formData.add("client_secret", anYanProperties.getClientSecret());
ResponseEntity<AuthResult> authResult = RestTemplateUtil.postExchangeForGetTokenByPassword(AnYanApiEnum.QUERY_AUTH_TOKEN, formData, headers, AuthResult.class);
if (HttpStatus.OK.equals(authResult.getStatusCode())
&& ObjectUtils.isNotEmpty(authResult.getBody())) {
return authResult.getBody();
} else {
log.error("------安眼系统获取公网token失败{}------", authResult.getBody());
ExceptionAssertsUtil.fail("------安眼系统获取公网token失败------" + authResult.getBody());
return null;
}
} catch (HttpClientErrorException e) {
log.error("------安眼系统获取公网token失败{}------", e.getMessage());
String replace = Objects.requireNonNull(e.getMessage()).replace("401 Unauthorized: ", "");
replace = replace.replace("\"", "");
AuthResult bean = JSONUtil.toBean(replace, AuthResult.class);
ExceptionAssertsUtil.fail(bean.getMessage());
} catch (Exception e) {
ExceptionAssertsUtil.fail(e.getMessage());
}
return null;
}
/**
* @description: 根据token获取用户信息
* @author PengJ
@@ -1,5 +1,6 @@
package com.renkang.anyan.config;
import com.alibaba.fastjson.JSON;
import com.renkang.anyan.enums.AnYanApiEnum;
import com.renkang.anyan.model.vo.AnYanResult;
import lombok.extern.slf4j.Slf4j;
@@ -81,7 +82,15 @@ public class RestTemplateUtil {
public static <T> ResponseEntity<T> postExchangeForGetToken(AnYanApiEnum api, Object requestBody, Class<T> responseType) {
String url = getProperties().getPublicHost() + api.getMethodPath() + api.getMethodParam();
HttpEntity<Object> requestEntity = new HttpEntity<>(requestBody);
return getRestTemplate().exchange(url, api.getHttpMethod(), requestEntity, responseType);
// 打印请求入参,便于排查
String bodyJson = JSON.toJSONString(requestBody);
log.info("安眼获取token请求入参: url={}, body={}", url, bodyJson);
// 打印等价 curlapplication/json),便于本地手动调试
log.info("安眼获取token等价curl: curl -X POST '{}' -H 'Content-Type: application/json' -d '{}'", url, bodyJson);
ResponseEntity<T> response = getRestTemplate().exchange(url, api.getHttpMethod(), requestEntity, responseType);
// 打印返回信息(状态码 + 响应体)
log.info("安眼获取token返回信息: statusCode={}, body={}", response.getStatusCode(), JSON.toJSONString(response.getBody()));
return response;
}
/**
@@ -15,24 +15,31 @@ import com.renkang.consultation.entity.ConDoctorExAndUser;
import com.renkang.consultation.service.IConDoctorService;
import com.renkang.consultation.util.TempUtil;
import com.renkang.consultation.vo.ConDoctorVO;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.bean.request.ListUser;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.DictUtil;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.util.RSAEncryptUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping(RequestPrefix.conDoctor)
public class ConDoctorApiController {
@@ -46,6 +53,8 @@ public class ConDoctorApiController {
@Autowired
private DictUtil dictUtil;
@Autowired
private IMHelloApi imHelloApi;
/**
* 分页列表查询
@@ -133,12 +142,28 @@ public class ConDoctorApiController {
}
boolean flag = conDoctorService.updateById(conDoctor);
if (flag){
// 同步IM个人资料
syncImProfile(conDoctor.getId());
return Result.ok("更新成功!");
}
return Result.error("更新失败!");
}
private void syncImProfile(String userId) {
try {
List<LoginUserNew> users = isysBaseApi.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollectionUtils.isEmpty(users)) {
return;
}
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
} catch (Exception e) {
log.error("同步IM个人资料失败, userId: {}", userId, e);
}
}
@Operation(summary = "查询医生信息", description = "查询医生信息")
@GetMapping("/selectDoctorInfo")
@@ -19,6 +19,8 @@ import com.renkang.consultation.mapper.ConSessionMapper;
import com.renkang.consultation.service.IConDoctorService;
import com.renkang.consultation.service.IConSessionService;
import com.renkang.consultation.vo.ConDoctorExportVO;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
@@ -62,10 +64,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -98,6 +97,8 @@ public class AsyncExportService {
private ConHelperMapper conHelperMapper;
@Autowired
private AsyncTaskExecutor taskExecutor;
@Autowired
private IMHelloApi imHelloApi;
@Transactional(rollbackFor = Exception.class)
public void userToDoctorExportXls(ConSessionDTO dto, CommonExportsInfo info, String empFinish, String errCode, String sheetName, String fileName) {
@@ -580,6 +581,8 @@ public class AsyncExportService {
BeanUtils.copyProperties(excelVo, conDoctor);
conDoctor.setId(userId);
conDoctorService.saveDoctor(conDoctor);
// 同步注册IM账号
importImAccount(userId);
info.setHandleMsg(index++ + "/" + userList.size());
info.setExportStatus("0");
info.setId(commonExportsInfo.getId());
@@ -598,6 +601,20 @@ public class AsyncExportService {
sysBaseAPI.updateExportsInfo(info);
}
private void importImAccount(String userId) {
try {
List<LoginUserNew> users = sysBaseAPI.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollectionUtils.isEmpty(users)) {
return;
}
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(java.util.Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
} catch (Exception e) {
log.error("导入IM账号失败, userId: {}", userId, e);
}
}
private class UserToDoctorExportHandler implements PoiExportHandler<UserToDoctorExport, SessionFilter> {
@Override
@@ -45,6 +45,7 @@ import org.jeecg.global.GlobalUtils;
import org.jeecg.modules.exports.entity.CommonExportsInfo;
import org.jeecg.modules.manager.SysCacheImpl;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.util.GlobalManager;
import org.jeecg.util.WrapperUtils;
import org.jeecgframework.poi.excel.ExcelExportUtil;
import org.jeecgframework.poi.excel.entity.ExportParams;
@@ -1267,8 +1268,16 @@ public class ConSessionServiceImpl extends ServiceImpl<ConSessionMapper, ConSess
@Value("${banner.picUrl:}")
private String bannerPicUrl;
@Autowired
private GlobalManager globalManager;
public String getPicUrl(){
// 读取配置文件的url
// 优先从缓存配置读取(运行时可动态调整),无值则回退到本地配置文件
String cacheUrl = globalManager.getConfigValueByKey("banner_pic_url");
if (StrUtil.isNotBlank(cacheUrl)) {
return cacheUrl;
}
return bannerPicUrl;
}
@@ -0,0 +1,752 @@
# 急救宣教资源管理 API 文档 v3
> **基础信息**
> 网关地址: `http://{gateway-host}:{port}`(前端加 `/health-emergency` 前缀)
> 服务直连: `http://{host}:7011`(调试用,不加前缀)
> 认证方式: 请求头 `X-Access-Token: {token}`
> Content-Type: `application/json; charset=utf-8`
> 统一响应格式: `{"success": bool, "message": "...", "code": 200, "result": {...}, "timestamp": ...}`
> 日期格式: `yyyy-MM-dd HH:mm:ss`(东八区)
---
## V3 变更说明(相比 V2
| # | 变更项 | 说明 |
|---|--------|------|
| 1 | **审批与上架分离** | 审核通过(`approve`)后不再自动发布,需单独调用 `publish` 上架;`publish` 要求 `auditStatus=11`(已通过),否则报错;新增状态"已通过" |
| 2 | **分类扁平化** | 分类仅一级(无子分类),标签绑定到分类下,`listRoot` 返回全部分类,`listChildren` 返回分类下的标签 |
| 3 | **分类标签联动** | 新增 `GET /category/listWithTags` 返回分类+嵌套标签,前端一次请求即可实现分类/标签联动选择 |
| 4 | **标签绑定分类** | 标签新增/编辑时 `parentId`(所属分类)必填;标签列表 `listAll` 支持 `parentId` 筛选 |
| 5 | **批量发布行为变更** | `batchPublish` 仅对 `auditStatus=11` 的资源生效,跳过未审核通过的 |
| 6 | **导出状态映射修正** | `auditStatus=11` 但未上架的资源导出显示"已通过"而非"已发布" |
---
## 目录
1. [通用枚举说明](#通用枚举说明)
2. [状态流转](#状态流转)
3. [分类管理](#1-分类管理-emergencyfirstaidcategory)
4. [标签管理](#2-标签管理-emergencyfirstaidtag)
5. [资源管理](#3-资源管理-emergencyfirstaidresource)
6. [审核管理](#4-审核管理-emergencyfirstaidaudit)
7. [推送管理](#5-推送管理-emergencyfirstaidpush)
8. [收藏管理](#6-收藏管理-emergencyfirstaidfavorite)
9. [版本管理](#7-版本管理-emergencyfirstaidversion)
10. [响应数据结构](#响应数据结构)
---
## 通用枚举说明
### 内容类型 `contentType`
| 值 | 说明 |
|----|------|
| `IMAGE_TEXT` | 图文 |
| `VIDEO` | 视频 |
| `AUDIO` | 音频 |
| `PDF` | PDF |
### 复合状态查询 `combinedStatus`(资源列表筛选用)
前端一个下拉框统一筛选,后端根据值域自动分发:
| 下拉选项 | 传值 | 分发逻辑 |
|---------|:---:|---------|
| 草稿 | `0` | 查 `status` |
| 已发布 | `1` | 查 `status` |
| 已下架 | `2` | 查 `status` |
| 待审核 | `10` | 查 `auditStatus` |
| 已通过 | `11` | 查 `auditStatus` |
| 已驳回 | `12` | 查 `auditStatus` |
> **规则**`combinedStatus ≤ 9` 分发到 `status``combinedStatus ≥ 10` 分发到 `auditStatus`。
> `combinedStatus` 与 `status` / `auditStatus` 参数互斥——同时传时 `combinedStatus` 优先。
### 发布状态 `status`
| 值 | 说明 |
|:--:|------|
| 0 | 草稿 |
| 1 | 已发布 |
| 2 | 已下架 |
### 审核状态 `auditStatus`
| 值 | 说明 |
|:--:|------|
| `null` | 无审核动作(新建/编辑后) |
| 10 | 待审核 |
| 11 | 已通过 |
| 12 | 已驳回 |
### 综合状态展示对照表(V3 新增)
> 前端列表的状态列按此表展示:
| auditStatus | status | 前端显示 |
|:-----------:|:------:|---------|
| `null` | 0 | 草稿 |
| 10 | 0 | 待审核 |
| 11 | 0 | **已通过**V3 新增) |
| 11 | 1 | 已发布 |
| 11 | 2 | 已下架 |
| 12 | 0 | 已驳回 |
### 分类/标签类型 `type`
| 值 | 说明 |
|:--:|------|
| 1 | 分类(扁平,仅一级) |
| 2 | 标签(绑定到分类) |
### 通用状态(分类/标签)
| 值 | 说明 |
|:--:|------|
| 1 | 正常 |
| 2 | 冻结 |
### 用户行为类型
| 值 | 说明 |
|:--:|------|
| 1 | 阅读 |
| 2 | 收藏 |
---
## 状态流转
```
新建 ──→ 草稿 (status=0, auditStatus=null)
└── 提交审核 ──→ 待审核 (status=0, auditStatus=10)
├── 审核通过 ──→ 已通过 (status=0, auditStatus=11) ← V3 变更
│ │
│ ├── 上架 ──→ 已发布 (status=1, auditStatus=11)
│ │ │
│ │ ├── 下架 ──→ 已下架 (status=2, auditStatus=11)
│ │ │ │
│ │ │ └── 上架 ──→ 已发布 (status=1, auditStatus=11)
│ │ │
│ │ └── 编辑 ──→ 草稿 (status=0, auditStatus=null)
│ │
│ └── 编辑 ──→ 草稿 (status=0, auditStatus=null)
└── 审核驳回 ──→ 已驳回 (status=0, auditStatus=12)
└── 编辑 ──→ 草稿 (status=0, auditStatus=null)
复制 ──→ 草稿 (status=0, auditStatus=null),标题+" - 副本"
回滚 ──→ 草稿 (status=0, auditStatus=null)
```
> **V3 关键变更**:审核通过后资源进入"已通过"状态,需运营人员点击"上架"才变为"已发布"。编辑/回滚后需重新提交审核。
---
## 1. 分类管理 `emergency/firstAid/category`
分类为扁平结构(仅一级),标签通过 `parentId` 绑定到分类下。
### 1.1 分页列表 `GET /category/list`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页条数 |
| name | String | 否 | - | 名称模糊搜索 |
| status | Integer | 否 | - | 状态(1=正常/2=冻结) |
```json
// 响应 result
{
"records": [
{
"id": "...",
"name": "心肺复苏",
"type": 1,
"parentId": null,
"description": "...",
"sortNo": 0,
"status": 1,
"resourceCount": 5,
"createTime": "2026-06-22 10:00:00"
}
],
"total": 6, "size": 10, "current": 1, "pages": 1
}
```
### 1.2 全部列表(树形) `GET /category/listAll`
无参数。返回全部分类及其下标签的嵌套树,`tags` 为标签数组,`resourceCount` 为使用量。
```json
[
{
"id": "cat1", "name": "心肺复苏", "resourceCount": 5,
"tags": [
{ "id": "tag1", "name": "成人", "resourceCount": 3 },
{ "id": "tag2", "name": "儿童", "resourceCount": 2 }
]
},
{
"id": "cat2", "name": "创伤急救", "resourceCount": 3,
"tags": [...]
}
]
```
### 1.3 分类列表 `GET /category/listRoot`
无参数,返回全部分类(`type=1`)扁平列表(含 `resourceCount`)。
### 1.4 标签列表(按分类) `GET /category/listChildren`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| parentId | String | 是 | 分类ID |
> 返回指定分类下的标签(`type=2`),含 `resourceCount`。
### 1.5 分类及标签嵌套列表 `GET /category/listWithTags` 🆕 V3 新增
`listAll` 返回结构相同,无参数,返回全部分类及其下标签的嵌套结构。
### 1.6 详情 `GET /category/queryById`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| id | String | 是 | 分类ID |
### 1.7 新增 `POST /category/add`
```json
{
"name": "分类名称",
"description": "描述(可选)",
"sortNo": 1,
"status": 1
}
```
> 分类不再需要 `parentId`(扁平化,仅一级)。
### 1.8 编辑 `POST /category/edit`
```json
{
"id": "分类ID",
"name": "新名称",
"description": "新描述",
"sortNo": 2,
"status": 1
}
```
### 1.9 删除 `POST /category/delete`
```json
{"id": "分类ID"}
```
> **级联校验**:分类下存在标签或资源时拒绝删除。
### 1.10 批量删除 `POST /category/deleteBatch`
```json
["id1", "id2"]
```
### 1.11 导出Excel `GET /category/exportXls`
参数与分页列表一致。返回 Excel 文件流。
### 1.12 导入Excel `POST /category/importExcel`
文件上传,格式与导出一致。
---
## 2. 标签管理 `emergency/firstAid/tag`
标签与分类共用 `QH_EME_FIR_AID_RES_CATE` 表,`type=2` 区分。标签通过 `parentId` 绑定到分类。
### 2.1 分页列表 `GET /tag/list`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页条数 |
| name | String | 否 | - | 名称模糊搜索 |
| parentId | String | 否 | - | 所属分类ID |
### 2.2 全部列表 `GET /tag/listAll`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| parentId | String | 否 | - | 所属分类ID(传则只返回该分类下的标签,不传返回全部) |
返回标签列表(含 `resourceCount` 引用数量)。
### 2.3 详情 `GET /tag/queryById`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| id | String | 是 | 标签ID |
### 2.4 新增 `POST /tag/add`
```json
{
"name": "标签名称",
"parentId": "所属分类ID(必填)",
"description": "描述(可选)",
"sortNo": 1,
"status": 1
}
```
### 2.5 编辑 `POST /tag/edit`
```json
{
"id": "标签ID",
"name": "新名称",
"parentId": "所属分类ID",
"description": "新描述",
"sortNo": 2,
"status": 1
}
```
### 2.6 删除 `POST /tag/delete`
```json
{"id": "标签ID"}
```
> **级联校验**:标签已被资源引用时拒绝删除。
### 2.7 批量删除 `POST /tag/deleteBatch`
```json
["id1", "id2"]
```
### 2.8 导出Excel `GET /tag/exportXls`
参数与分页列表一致。返回 Excel 文件流。
### 2.9 导入Excel `POST /tag/importExcel`
文件上传,格式与导出一致。
---
## 3. 资源管理 `emergency/firstAid/resource`
### 3.1 分页列表 `GET /resource/list`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页条数 |
| title | String | 否 | - | 标题模糊搜索 |
| categoryId | String | 否 | - | 分类ID精确匹配 |
| contentType | String | 否 | - | 内容类型:`IMAGE_TEXT`/`VIDEO`/`AUDIO`/`PDF` |
| combinedStatus | Integer | 否 | - | 复合状态筛选(≤9查status,≥10查auditStatus |
| status | Integer | 否 | - | 发布状态(审核页独立筛选用) |
| auditStatus | Integer | 否 | - | 审核状态(审核页独立筛选用) |
| tags | String | 否 | - | 标签ID,逗号分隔 |
> **互斥规则**`combinedStatus` 与 `status`/`auditStatus` 互斥,同时传时 `combinedStatus` 优先。
### 3.2 详情 `GET /resource/queryById`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| id | String | 是 | 资源ID |
响应额外填充:`categoryName``tagNameList``isFavorited`
### 3.3 新增 `POST /resource/add`
```json
{
"title": "资源标题(必填)",
"summary": "摘要简介",
"contentType": "IMAGE_TEXT",
"categoryId": "分类ID",
"applicableScenario": "适用场景描述",
"tags": "标签ID1,标签ID2",
"content": "<h1>富文本HTML正文</h1>",
"coverImage": "https://example.com/cover.jpg",
"fileUrl": "[\"https://example.com/file1.pdf\"]",
"effectiveTime": "2026-07-01 00:00:00",
"expiryTime": "2026-12-31 23:59:59"
}
```
> 新建后 `status=0`(草稿)、`auditStatus=null`。
### 3.4 编辑 `POST /resource/edit`
```json
{
"id": "资源ID",
"title": "新标题",
"contentType": "IMAGE_TEXT",
"categoryId": "分类ID",
"tags": "标签ID1,标签ID2",
"content": "<p>新内容</p>",
"...": "其他字段同上"
}
```
> 编辑后自动退回草稿 + 清除审核状态(`status=0, auditStatus=null`),并保存编辑前后各一条版本快照("编辑前版本"和"编辑更新")。
### 3.5 删除 `POST /resource/delete`
```json
{"id": "资源ID"}
```
> 逻辑删除(`delFlag=1`)。
### 3.6 批量删除 `POST /resource/deleteBatch`
```json
["id1", "id2"]
```
### 3.7 提交审核 `POST /resource/submitForReview`
```json
{"id": "资源ID"}
```
> `auditStatus` 变为 `10`(待审核)。
### 3.8 下架 `POST /resource/takeDown`
```json
{"id": "资源ID"}
```
> `status` 变为 `2`(已下架)。
### 3.9 上架 `POST /resource/publish` ⚠️ V3 变更
```json
{"id": "资源ID"}
```
> **V3 行为变更**:仅 `auditStatus=11`(已通过)的资源可上架,否则返回错误 `"该资源尚未通过审核,无法上架"`。上架后 `status=1`,审核字段不变。下架操作无前置校验,任意状态均可下架。
### 3.10 批量下架 `POST /resource/batchTakeDown`
```json
["id1", "id2"]
```
### 3.11 批量上架 `POST /resource/batchPublish` ⚠️ V3 变更
```json
["id1", "id2"]
```
> **V3 行为变更**:仅对 `auditStatus=11` 的资源设置 `status=1`,跳过未通过审核的。
### 3.12 记录阅读 `POST /resource/incrementView`
```json
{"id": "资源ID"}
```
> 同一用户重复阅读不累计,返回 `"已阅读过"`。
### 3.13 复制资源 `POST /resource/copy`
```json
{"id": "源资源ID"}
```
> 标题追加 `" - 副本"``status=0, auditStatus=null`,浏览/收藏归零。
### 3.14 导出Excel `GET /resource/exportXls`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| selections | String | 否 | 勾选的ID列表(逗号分隔),不传则导出全部 |
| title | String | 否 | 标题筛选 |
| categoryId | String | 否 | 分类筛选 |
| contentType | String | 否 | 类型筛选 |
| status | Integer | 否 | 状态筛选 |
导出字段:标题、摘要、分类、类型、标签、状态、阅读/收藏、创建时间。
### 3.15 导入Excel `POST /resource/importExcel`
文件上传,格式与导出一致。
---
## 4. 审核管理 `emergency/firstAid/audit`
### 4.1 待审核列表 `GET /audit/pending`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页条数 |
| title | String | 否 | - | 标题搜索 |
| categoryId | String | 否 | - | 分类筛选 |
| contentType | String | 否 | - | 内容类型筛选 |
### 4.2 已通过列表 `GET /audit/passed`
参数同上。
### 4.3 已驳回列表 `GET /audit/rejected`
参数同上。
### 4.4 审核通过 `POST /audit/approve` ⚠️ V3 变更
```json
{"id": "资源ID"}
```
> **V3 行为变更**:仅设置 `auditStatus=11`(已通过)、审核人、审核时间,并清除 `auditRejectReason`**不再同时设置 `status=1`**。审核后资源进入"已通过"状态,需单独调用上架接口发布。
### 4.5 审核驳回 `POST /audit/reject`
```json
{"id": "资源ID", "reason": "驳回原因"}
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| id | String | 是 | 资源ID |
| reason | String | 是 | 驳回原因 |
> `auditStatus` 变为 `12`,记录驳回原因和审核信息。
### 4.6 审核记录列表 `GET /audit/records`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页条数 |
| resourceTitle | String | 否 | - | 资源标题搜索 |
| auditStatus | Integer | 否 | - | 审核状态:`11`=通过/`12`=驳回 |
| startDate | String | 否 | - | 开始日期(yyyy-MM-dd |
| endDate | String | 否 | - | 结束日期(yyyy-MM-dd |
> 从审核记录表查询,按审核时间倒序。
---
## 5. 推送管理 `emergency/firstAid/push`
应急调度员在「正在应急」页面右侧面板搜索宣教资源,推送给求助者。
### 5.1 可推送资源列表 `GET /push/resourceList`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| orderId | String | 否 | - | 工单ID(用于标记推送状态) |
| keyword | String | 否 | - | 关键词搜索(标题/内容) |
| categoryId | String | 否 | - | 分类筛选 |
| contentType | String | 否 | - | 内容类型筛选 |
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 20 | 每页条数 |
> 仅展示 `status=1 && auditStatus=11` 且未失效的资源。传 `orderId` 时,列表项的 `pushed` 字段标记该工单下是否已推送。
### 5.2 推送单条 `POST /push/send`
```json
{
"resourceId": "资源ID",
"orderId": "工单ID"
}
```
> 校验资源状态为 `status=1 && auditStatus=11`,否则返回 `"仅已发布且审核通过的资源可推送"`。同一工单+同一资源不允许重复推送,重复返回 `"该资源已推送,请勿重复操作"`。
### 5.3 一键推送(批量) `POST /push/batchSend`
```json
{
"orderId": "工单ID(必填)",
"resourceIds": ["id1", "id2"],
"filterParams": {
"keyword": "搜索关键词",
"categoryId": "分类ID",
"contentType": "IMAGE_TEXT"
}
}
```
> `resourceIds` 与 `filterParams` 二选一:
> - 传 `resourceIds`:仅推送指定ID
> - 传 `filterParams`:按条件查询后全量推送
>
> 部分失败不影响其他条,响应返回成功计数 `{"count": 3}`。
---
## 6. 收藏管理 `emergency/firstAid/favorite`
### 6.1 切换收藏 `POST /favorite/toggle`
```json
{"id": "资源ID"}
```
> 已收藏则取消,未收藏则收藏。返回 `{"favorited": true/false}`。
### 6.2 查询收藏状态 `GET /favorite/isFavorited`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| resourceId | String | 是 | 资源ID |
> 返回 `{"favorited": true/false}`。
### 6.3 我的收藏列表 `GET /favorite/myList`
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:---:|--------|------|
| pageNo | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页条数 |
---
## 7. 版本管理 `emergency/firstAid/version`
### 7.1 版本历史列表 `GET /version/list`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| resourceId | String | 是 | 资源ID |
> 按版本号倒序,每条含:`id`, `resourceId`, `versionNo`, `title`, `content`, `changeDescription`, `isCurrent`, `createBy`, `createTime`。
### 7.2 回滚 `POST /version/rollback`
```json
{"id": "版本记录ID"}
```
> 将目标版本的内容覆盖到资源,重置 `status=0, auditStatus=null`,并保存一条新的版本记录(标记为"回滚到版本N")。
### 7.3 版本差异对比 `GET /version/diff`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| versionId1 | String | 是 | 版本1的ID |
| versionId2 | String | 是 | 版本2的ID(须同一资源) |
> 返回两个版本的完整信息用于前端差异对比。
---
## 响应数据结构
### 通用列表分页
```json
{
"success": true,
"message": "",
"code": 200,
"result": {
"records": [ ... ],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
},
"timestamp": 1782196719184
}
```
### 通用操作响应
```json
{
"success": true,
"message": "已上架",
"code": 200,
"result": "已上架",
"timestamp": 1782196719184
}
```
### 资源列表项关键字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 资源ID |
| title | String | 标题 |
| summary | String | 摘要 |
| contentType | String | 内容类型 |
| categoryId | String | 分类ID |
| categoryName | String | 分类名称(非持久) |
| tags | String | 标签ID(逗号分隔) |
| tagNameList | List\<String\> | 标签名称数组(非持久) |
| content | String | 富文本正文 |
| coverImage | String | 封面图URL |
| fileUrl | String | 附件JSON数组字符串 |
| status | Integer | 发布状态(0=草稿/1=已发布/2=已下架) |
| auditStatus | Integer | 审核状态(null/10=待审核/11=已通过/12=已驳回) |
| auditRejectReason | String | 驳回原因 |
| auditBy | String | 审核人 |
| auditById | String | 审核人ID |
| auditTime | Date | 审核时间 |
| viewCount | Integer | 浏览次数 |
| favoriteCount | Integer | 收藏次数 |
| isFavorited | Boolean | 当前用户是否已收藏(非持久) |
| pushed | Boolean | 当前工单下是否已推送(非持久,仅推送列表) |
| effectiveTime | Date | 生效时间 |
| expiryTime | Date | 失效时间 |
| createTime | Date | 创建时间 |
| updateTime | Date | 更新时间 |
### 分类/标签项关键字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | ID |
| name | String | 名称 |
| type | Integer | 类型(1=分类/2=标签) |
| parentId | String | 所属分类ID(标签有值,分类为null) |
| parentName | String | 所属分类名称(非持久) |
| description | String | 描述 |
| sortNo | Integer | 排序号 |
| status | Integer | 状态(1=正常/2=冻结) |
| resourceCount | Integer | 关联资源数量(非持久,listAll/listWithTags 返回) |
| tags | List\<FirstAidResourceCategory\> | 标签列表(非持久,listAll/listWithTags 返回) |
| createTime | Date | 创建时间 |
### 审核记录项关键字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 记录ID |
| resourceId | String | 资源ID |
| resourceTitle | String | 资源标题(非持久,查询时填充) |
| auditStatus | Integer | 审核状态(11=通过/12=驳回) |
| auditRejectReason | String | 驳回原因 |
| auditBy | String | 审核人 |
| auditById | String | 审核人ID |
| auditTime | Date | 审核时间 |
| createTime | Date | 记录创建时间 |
@@ -0,0 +1,422 @@
# 急救模块移动端 API 文档
## 🆕 本次变更(前端联调)
| # | 接口 | 变更类型 | 说明 |
|---|------|:---:|------------------------------------------------------------------------------|
| 1 | `GET /createGroupMagFirstAider` | **废除** | 已废除,功能由 `createGroupMag` 承接 |
| 2 | `GET /order/myParticipatedOrders` | **新增** | App端查询当前登录人参与的应急工单列表(sessionNow=1),返回被救助人信息 |
---
> **基础信息**
> 网关地址: `http://{gateway-host}:{port}`(前端加 `/health-emergency` 前缀)
> 服务直连: `http://{host}:7011`(调试用,不加前缀)
> 认证方式: 请求头 `X-Access-Token: {token}`
> Content-Type: `application/json; charset=utf-8`
> 统一响应格式: `{"success": bool, "message": "...", "code": 200, "result": {...}, "timestamp": ...}`
> 日期格式: `yyyy-MM-dd HH:mm:ss`(东八区)
---
## 目录
1. [应急群聊拉人](#1-应急群聊拉人-apiemergency)
2. [宣教资源](#2-宣教资源-apiemergencyfirstaidresource)
3. [工单列表](#3-工单列表-apiemergencyorder)
---
## 1. 应急群聊拉人 `api/emergency`
> 网关完整路径: `/health-emergency/api/emergency`
### 1.1 搜索专家(专业人员) `GET /order/searchExpert`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| realname | String | 否 | 姓名,模糊搜索 |
| centerId | String | 否 | 应急中心ID |
| excludeSessionId | String | 否 | 群组ID,用于排除已在群中的人 |
| pageNo | Integer | 否 | 页码,默认1 |
| pageSize | Integer | 否 | 每页条数,默认10 |
```json
// 响应 result
{
"records": [
{
"userId": "xxx", "realname": "张医生", "avatar": "https://...",
"type": "6", "centerId": "xxx",
"post": "主任医师", "school": "北京大学", "edu": "博士",
"goodAt": "心血管内科", "deptName": "急诊科", "phone": "13800000001",
"doctorName": "张医生", "doctorNo": "XJ-001", "photo": "https://...",
"resourceName": "新疆人民医院", "departmentId": "xxx",
"departmentName": "急诊科", "doctorTitle": "主任医师",
"doctorJob": "科长", "doctorStatus": "1", "isAccept": "1",
"doctorType": "1", "audioStatus": "1", "tfJumpHoliday": "0",
"experience": "从事心血管内科临床工作20年...",
"sort": 100, "tfRecommend": "1",
"introduction": "擅长心血管疾病的诊治..."
}
],
"total": 50, "size": 10, "current": 1
}
```
| 字段 | 说明 |
|------|------|
| userId | 用户ID,邀请时传入 memberList |
| realname | 姓名 |
| avatar | 头像 |
| type | 类型,"6"=专业人员 |
| centerId | 应急中心ID |
| post | 职称 |
| school | 毕业院校 |
| edu | 学历 |
| goodAt | 擅长领域 |
| deptName | 科室 |
| phone | 手机号 |
| **↓ 以下字段来源于咨询服务专家表(qh_con_doctor),非专家时为 null** |
| doctorName | 医生姓名 |
| doctorNo | 专家编号 |
| photo | 医生头像 |
| resourceName | 所属医院名称 |
| departmentId | 科室ID |
| departmentName | 科室名称 |
| doctorTitle | 职务(z_doct_lev 字典) |
| doctorJob | 职称(z_doct_job 字典) |
| doctorStatus | 专家状态(doc_status 字典) |
| isAccept | 是否开启咨询(z_doct_sta 字典: 1开启/3关闭) |
| doctorType | 专家类型(z_doct_typ 字典: 1收费/2免费/3其他/4义诊) |
| audioStatus | 是否提供音视频 |
| tfJumpHoliday | 是否跳过节假日 |
| experience | 执业经历 |
| sort | 排序 |
| tfRecommend | 是否推荐(0否/1是) |
| introduction | 简介 |
### 1.2 搜索员工 `GET /order/searchEmployee`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| realname | String | 否 | 姓名,模糊搜索 |
| orgCode | String | 否 | 部门编码 |
| workNo | String | 否 | 工号 |
| phone | String | 否 | 手机号 |
| excludeSessionId | String | 否 | 群组ID,用于排除已在群中的人 |
| pageNo | Integer | 否 | 页码,默认1 |
| pageSize | Integer | 否 | 每页条数,默认10 |
```json
// 响应 result
{
"records": [
{
"userId": "xxx", "realname": "张三", "avatar": "https://...",
"orgCode": "650000001001", "orgName": "新疆油田公司",
"deptName": "安全环保部", "phone": "13800000000", "workNo": "A00123"
}
],
"total": 200, "size": 10, "current": 1
}
```
| 字段 | 说明 |
|------|------|
| userId | 用户ID,邀请时传入 memberList |
| realname | 姓名 |
| avatar | 头像 |
| orgCode | 部门编码 |
| orgName | 单位名称(二级单位) |
| deptName | 部门名称 |
| phone | 手机号 |
| workNo | 工号 |
### 1.3 创建群聊 `GET /createGroupMag`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| name | String | 是 | 群聊名称 |
| longitude | String | 是 | 经度 |
| latitude | String | 是 | 纬度 |
| type | String | 否 | 类型 |
> 返回 `groupId`(群组ID),作为后续拉人接口的入参。**此接口会发起音视频通话。**
### 1.4 创建群聊-急救员 `GET /createGroupMagFirstAider` ⚠️ 已废除
> ⚠️ 本接口已废除,功能由 `createGroupMag` 承接,请勿调用。
### 1.5 邀请进群 `POST /addGroupUser`
拉专家或员工加入应急群聊。
```json
// Request Body
{
"groupId": "1907659384834752513",
"memberList": ["userId1", "userId2"],
"memberType": 1
}
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| groupId | String | 是 | 群组ID(即 `createGroupMag` 返回的 `groupId` |
| memberList | List\<String\> | 是 | 要邀请的用户ID列表,最多100个 |
| memberType | Integer | 否 | `0`=专家/专业人员, `1`=员工,**默认0** |
```json
// 响应
{"success": true, "message": "", "result": true}
```
> 已在群中的人员会自动跳过,不会报错。所有选中的人都在群中时返回失败。
### 1.6 移除群成员 `POST /removeGroupUser`
```json
// Request Body
{ "groupId": "1907659384834752513", "memberList": ["userId1"] }
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| groupId | String | 是 | 群组ID |
| memberList | List\<String\> | 是 | 要移除的用户ID列表 |
```json
// 响应
{"success": true, "message": "", "result": true}
```
> 只能移除后续拉入的协助人员,不能移除发起人和初始操作/专业人员。
### 1.7 群成员列表 `GET /order/getGroupMemberList`
> ⚠️ 本接口仅查本地 DB,不验证腾讯 IM。**建议改用 1.8 `/order/getActualGroupMemberList`。**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| sessionId | String | 是 | 群组ID |
```json
// 响应 result
[
{ "orderId": "...", "userId": "发起人id", "userName": "李四", "avatar": "https://...", "phone": "13800000001", "isGroupMember": 1, "memberType": -1 },
{ "orderId": "...", "userId": "操作人员id", "userName": "王五", "avatar": "https://...", "phone": "13800000002", "isGroupMember": 1, "memberType": 0 },
{ "orderId": "...", "userId": "专业人员id", "userName": "张医生", "avatar": "https://...", "phone": "13800000003", "isGroupMember": 1, "memberType": 1 },
{ "orderId": "...", "userId": "员工id", "userName": "张三", "avatar": "https://...", "phone": "13800000004", "isGroupMember": 1, "memberType": 2 },
{ "orderId": "...", "userId": "其他专业人id", "userName": "赵医生", "avatar": "https://...", "phone": "13800000005", "isGroupMember": 0, "memberType": 1 }
]
```
| 字段 | 说明 |
|------|------|
| orderId | 应急单ID |
| userId | 用户ID |
| userName | 姓名 |
| avatar | 头像 |
| phone | 手机号 |
| isGroupMember | `1`=在群中, `0`=可邀请 |
| memberType | `-1`=发起人, `0`=操作人员, `1`=专业人员, `2`=员工 |
### 1.8 实际群成员列表 `GET /order/getActualGroupMemberList` 🆕
> **与 1.7 的区别**:本接口先查本地参与人员,再调用腾讯 IM `get_group_member_info` API 获取群内实际成员,以 IM 成员为准取交集,确保返回的都是 IM 群中真实存在的成员。**不过滤本人,通过 `isSelf` 标识。**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| sessionId | String | 是 | 群组ID |
```json
// 响应 result(与 getGroupMemberList 格式完全一致,但不过滤本人)
[
{ "orderId": "...", "userId": "当前用户id", "userName": "我", "avatar": "https://...", "phone": "13800000000", "isGroupMember": 1, "memberType": 0, "isSelf": 1 },
{ "orderId": "...", "userId": "发起人id", "userName": "李四", "avatar": "https://...", "phone": "13800000001", "isGroupMember": 1, "memberType": -1, "isSelf": 0 },
{ "orderId": "...", "userId": "专业人员id", "userName": "张医生", "avatar": "https://...", "phone": "13800000003", "isGroupMember": 1, "memberType": 1, "isSelf": 0 }
]
```
| 字段 | 说明 |
|------|------|
| orderId | 应急单ID |
| userId | 用户ID |
| userName | 姓名 |
| avatar | 头像 |
| phone | 手机号 |
| isGroupMember | 始终为 `1`(所有返回成员均经 IM 验证在群中) |
| memberType | `-1`=发起人, `0`=操作人员, `1`=专业人员, `2`=员工 |
| isSelf | `0`=不是本人, `1`=是本人 |
> **建议**:前端优先使用本接口获取群成员列表,1.7 接口后续可淘汰。
### 1.9 当前登录人参与的应急工单列表 `GET /order/myParticipatedOrders` 🆕
> App端接口:查询当前登录人参与过的、正在应急(`sessionNow=1`)的工单列表,返回被救助人信息。
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| pageNo | Integer | 否 | 页码,默认1 |
| pageSize | Integer | 否 | 每页条数,默认10 |
```json
// 响应 result
{
"records": [
{
"orderId": "2074305914885640194",
"sessionId": "2074305910070706176",
"salvageUserName": "张三",
"salvageUserSex": "1",
"salvageUserAvatar": "https://fileserver.yg.dt.io/xxx.png",
"salvageUserAge": 35,
"salvageUserMobile": "13800000000",
"orgName": "新疆油田公司",
"deptName": "安全环保部",
"orderStatus": "1",
"initTime": "2026-07-20 10:30:00"
}
],
"total": 5, "size": 10, "current": 1
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | String | 应急单ID |
| sessionId | String | 会话ID |
| salvageUserName | String | 被救助人姓名 |
| salvageUserSex | String | 被救助人性别(字典: sex2) |
| salvageUserAvatar | String | 被救助人头像 |
| salvageUserAge | Integer | 被救助人年龄 |
| salvageUserMobile | String | 被救助人手机号 |
| orgName | String | 单位名称(二级单位) |
| deptName | String | 部门名称 |
| orderStatus | String | 应急单状态(字典: emergency_order_status |
| initTime | Date | 发起时间 |
### 1.10 memberType 字典
| 值 | 含义 | 场景 |
|:---:|------|------|
| -1 | 发起人 | 创建应急的人 |
| 0 | 操作人员 | 应急中心调度员 |
| 1 | 专业人员 | 专家/医生 |
| 2 | 员工 | 普通员工(被拉入群) |
### 典型调用流程
**应急就医(带音视频通话)**
```
1. 创建群聊
GET /health-emergency/api/emergency/createGroupMag?name=xxx&longitude=xxx&latitude=xxx
2. 查看当前成员 → 搜索要拉的人 → 确认邀请 → 移除成员 → 刷新列表
GET /health-emergency/api/emergency/order/getActualGroupMemberList?sessionId={groupId}
(以下步骤同急救员流程)
```
**急救员**
```
1. 创建群聊
GET /health-emergency/api/emergency/createGroupMag?name=xxx&longitude=xxx&latitude=xxx
2. 查看当前成员(🔀 替换原 /order/getGroupMemberList
GET /health-emergency/api/emergency/order/getActualGroupMemberList?sessionId={groupId}
→ 返回经腾讯IM验证的实际群成员,新增 isSelf 字段区分本人
3. 搜索要拉的人
GET /health-emergency/api/emergency/order/searchExpert?realname=张&excludeSessionId={groupId}
GET /health-emergency/api/emergency/order/searchEmployee?realname=李&excludeSessionId={groupId}
→ 展示搜索结果,用户勾选
4. 确认邀请
POST /health-emergency/api/emergency/addGroupUser
{"groupId": "{groupId}", "memberList": ["id1","id2"], "memberType": 0}
5. 移除成员
POST /health-emergency/api/emergency/removeGroupUser
{"groupId": "{groupId}", "memberList": ["id1"]}
6. 刷新成员列表(🔀 替换原 /order/getGroupMemberList
GET /health-emergency/api/emergency/order/getActualGroupMemberList?sessionId={groupId}
```
---
## 2. 宣教资源 `api/emergency/firstAid/resource`
> 网关完整路径: `/health-emergency/api/emergency/firstAid/resource`
### 2.1 根据宣教ID查询详情 `GET /resource/queryById`
| 参数 | 类型 | 必填 | 说明 |
|------|------|:---:|------|
| id | String | 是 | 宣教资源ID |
```json
// 响应 result
{
"id": "...",
"title": "心肺复苏指南",
"summary": "摘要简介",
"contentType": "IMAGE_TEXT",
"categoryId": "cat1",
"categoryName": "心肺复苏",
"applicableScenario": "适用场景",
"tags": "tag1,tag2",
"tagNameList": ["成人", "初级"],
"content": "<h1>富文本HTML正文</h1>",
"coverImage": "https://example.com/cover.jpg",
"fileUrl": "[\"https://example.com/file1.pdf\"]",
"viewCount": 100,
"favoriteCount": 20,
"isFavorited": false,
"effectiveTime": "2026-07-01 00:00:00",
"expiryTime": "2026-12-31 23:59:59",
"createTime": "2026-07-01 10:00:00",
"updateTime": "2026-07-05 14:30:00"
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 资源ID |
| title | String | 标题 |
| summary | String | 摘要/简介 |
| contentType | String | 内容类型:`IMAGE_TEXT`/`VIDEO`/`AUDIO`/`PDF` |
| categoryId | String | 分类ID |
| categoryName | String | 分类名称 |
| applicableScenario | String | 适用场景 |
| tags | String | 标签ID(逗号分隔) |
| tagNameList | List\<String\> | 标签名称列表 |
| content | String | 富文本正文HTML |
| coverImage | String | 封面图URL |
| fileUrl | String | 附件URL列表(JSON数组字符串) |
| viewCount | Integer | 浏览次数 |
| favoriteCount | Integer | 收藏次数 |
| isFavorited | Boolean | 当前用户是否已收藏 |
| effectiveTime | Date | 生效时间 |
| expiryTime | Date | 失效时间 |
| createTime | Date | 创建时间 |
| updateTime | Date | 更新时间 |
---
## 3. 工单列表 `/api/emergency/order`
> 网关完整路径: `/health-emergency/api/emergency/order`
### 3.1 当前登录人参与的应急工单列表 `GET /myParticipatedOrders` 🆕
**业务逻辑**
1.`QH_EME_ORDER_OPERATOR` 表获取当前登录人参与的所有工单ID(去重)
2.`QH_EME_ORDER` 表筛选 `sessionNow=1` 的工单,按创建时间倒序
3. 返回被救助人(`salvage` 字段)信息 + 单位/部门名称
> **调用时机**:登录用户在急救页面查看自己正在参与中的应急工单。仅返回 IM 会话进行中的工单(`sessionNow=1`)。
请求和响应格式见 **[1.9 当前登录人参与的应急工单列表](#19-当前登录人参与的应急工单列表-get-ordermyparticipatedorders-)**。
@@ -20,4 +20,16 @@ public class SendIm {
* 类型 1: 应急单
*/
private Integer type;
/**
* 业务标识,用于区分不同业务来源(例:"article"=宣教推送)
*/
private String businessId;
/**
* 消息图片URL(封面图全路径)
*/
private String messageImageUrl;
/**
* 文章ID(宣教资源ID)
*/
private String resourceId;
}
@@ -126,4 +126,70 @@ public class EmergencyProfessionUser implements Serializable {
// 专业人员
@TableField(exist = false)
private Integer professionNum;
// 科室名称
@TableField(exist = false)
private String deptName;
// 手机号(非持久字段,查询时填充)
@TableField(exist = false)
private String phone;
// ===== 以下字段来源于 qh_con_doctor(咨询服务医生表),查询时批量填充 =====
// 医生姓名
@TableField(exist = false)
private String doctorName;
// 专家编号
@TableField(exist = false)
private String doctorNo;
// 医生头像
@TableField(exist = false)
private String photo;
// 医院名称
@TableField(exist = false)
private String resourceName;
// 科室id
@TableField(exist = false)
private String departmentId;
// 科室名称
@TableField(exist = false)
private String departmentName;
// 职务
@TableField(exist = false)
@Dict(dicCode = "z_doct_lev")
private String doctorTitle;
// 职称
@TableField(exist = false)
@Dict(dicCode = "z_doct_job")
private String doctorJob;
// 专家状态
@TableField(exist = false)
@Dict(dicCode = "doc_status")
private String doctorStatus;
// 是否开启咨询(1开启 3关闭)
@TableField(exist = false)
@Dict(dicCode = "z_doct_sta")
private String isAccept;
// 专家类型(z_doct_typ) —— 避免与人员type冲突,命名为doctorType
@TableField(exist = false)
@Dict(dicCode = "z_doct_typ")
private String doctorType;
// 是否提供音视频
@TableField(exist = false)
private String audioStatus;
// 是否跳过节假日
@TableField(exist = false)
private String tfJumpHoliday;
// 执业经历
@TableField(exist = false)
private String experience;
// 排序
@TableField(exist = false)
private Integer sort;
// 是否推荐(0否1是)
@TableField(exist = false)
private String tfRecommend;
// 简介
@TableField(exist = false)
private String introduction;
}
@@ -19,7 +19,7 @@ import java.util.Date;
* @Date: 2026-06-24
*/
@Data
@TableName("QH_EME_FIRST_AID_PUSH_RECORD")
@TableName("QH_EME_FIR_AID_PUSH_RD")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(title = "first_aid_push_record对象", description = "急救宣教推送记录")
@@ -19,7 +19,7 @@ import lombok.experimental.Accessors;
* @Version: V1.0
*/
@Data
@TableName("QH_EME_FIRST_AID_RESOURCE")
@TableName("QH_EME_FIR_AID_RES")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(title = "first_aid_resource对象", description = "急救宣教资源")
@@ -89,10 +89,9 @@ public class FirstAidResource implements Serializable {
@Schema(title = "失效时间")
private java.util.Date expiryTime;
/** 审核状态(null=无审核,10=待审核,11=已通过,12=已驳回) */
@Excel(name = "审核状态", width = 15, dicCode = "first_aid_audit_status")
/** 审核状态(null=无审核,10=待审核,11=已通过,12=已驳回),值由 FirstAidConstants 常量控制 */
@Schema(title = "审核状态(null=无审核,10=待审核,11=已通过,12=已驳回)")
@TableField(updateStrategy = FieldStrategy.IGNORED)
@TableField(insertStrategy = FieldStrategy.IGNORED, updateStrategy = FieldStrategy.IGNORED)
private Integer auditStatus;
/** 驳回原因 */
@@ -126,7 +125,7 @@ public class FirstAidResource implements Serializable {
private Integer favoriteCount;
/** 发布状态(0=草稿,1=已发布,2=已下架) */
@Excel(name = "状态", width = 15, dicCode = "first_aid_resource_status")
@Excel(name = "状态", width = 15)
@Schema(title = "发布状态(0=草稿,1=已发布,2=已下架)")
private Integer status;
@@ -21,7 +21,7 @@ import java.util.Date;
* @Version: V1.0
*/
@Data
@TableName("QH_EME_FIRST_AID_RESOURCE_AUDIT")
@TableName("QH_EME_FIR_AID_RES_AUD")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(title = "first_aid_resource_audit对象", description = "急救宣教资源审核记录")
@@ -19,7 +19,7 @@ import lombok.experimental.Accessors;
* @Version: V1.0
*/
@Data
@TableName("QH_EME_FIRST_AID_RESOURCE_CATEGORY")
@TableName("QH_EME_FIR_AID_RES_CATE")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(title = "first_aid_resource_category对象", description = "急救宣教资源分类/标签")
@@ -37,7 +37,7 @@ public class FirstAidResourceCategory implements Serializable {
private String name;
/** 类型(1=分类,2=标签) */
@Excel(name = "类型", width = 15, dicCode = "first_aid_category_type")
@Excel(name = "类型", width = 15)
@Schema(title = "类型(1=分类,2=标签)")
private Integer type;
@@ -17,7 +17,7 @@ import lombok.experimental.Accessors;
* @Version: V1.0
*/
@Data
@TableName("QH_EME_FIRST_AID_RESOURCE_USER_ACTION")
@TableName("QH_EME_FIR_AID_RES_UACT")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(title = "first_aid_resource_user_action对象", description = "用户行为记录(阅读/收藏)")
@@ -17,7 +17,7 @@ import lombok.experimental.Accessors;
* @Version: V1.0
*/
@Data
@TableName("QH_EME_FIRST_AID_RESOURCE_VERSION")
@TableName("QH_EME_FIR_AID_RES_VER")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(title = "first_aid_resource_version对象", description = "资源版本历史")
@@ -35,9 +35,9 @@ public class OrderOperator implements Serializable {
private String orderId;
/**
* 人员类型(人员类型0:操作人员 1:专业人员)
* 人员类型(0:操作人员 1:专业人员 3:急救人员1 4:急救人员2 5:关爱联络员)
*/
@Schema(title = "人员类型(人员类型0:操作人员 1:专业人员)")
@Schema(title = "人员类型(0:操作人员 1:专业人员 2:员工 3:急救人员1 4:急救人员2 5:关爱联络员)")
private Integer operatorType;
/**
@@ -33,4 +33,10 @@ public class AddOtherMemberDTO {
@Size(min = 1, max = 100, message = "邀请成员数量必须在1-100个之间")
@Schema(title = "邀请成员id集合")
private List<String> memberList;
/**
* 邀请成员类型:0=专家/专业人员,1=员工,默认0向后兼容
*/
@Schema(title = "邀请成员类型(0:专家, 1:员工)")
private Integer memberType;
}
@@ -0,0 +1,36 @@
package com.renkang.emergency.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.util.List;
/**
* @author PengJ
* @version 1.0
* @description: 移除群成员DTO
* @date 2025/6/25
*/
@NoArgsConstructor
@Data
public class RemoveGroupMemberDTO {
/**
* 群组id
*/
@NotBlank(message = "群组id不能为空")
@Schema(title = "群组id")
private String groupId;
/**
* 移除成员id集合
*/
@NotEmpty(message = "移除成员id集合不能为空")
@Size(min = 1, max = 100, message = "移除成员数量必须在1-100个之间")
@Schema(title = "移除成员id集合")
private List<String> memberList;
}
@@ -0,0 +1,38 @@
package com.renkang.emergency.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @author PengJ
* @version 1.0
* @description: 员工搜索结果VO
* @date 2025/6/25
*/
@Data
public class EmployeeVO {
@Schema(title = "用户ID")
private String userId;
@Schema(title = "姓名")
private String realname;
@Schema(title = "头像")
private String avatar;
@Schema(title = "部门编码")
private String orgCode;
@Schema(title = "单位名称")
private String orgName;
@Schema(title = "部门名称")
private String deptName;
@Schema(title = "手机号")
private String phone;
@Schema(title = "工号")
private String workNo;
}
@@ -42,4 +42,22 @@ public class GroupMemberVO {
@Schema(title = "是否是群组成员, 0:不是群组成员, 1:是群组成员")
private Integer isGroupMember;
/**
* 成员类型:-1=发起人, 0=操作人员, 1=专业人员, 2=员工
*/
@Schema(title = "成员类型(-1:发起人, 0:操作人员, 1:专业人员, 2:员工)")
private Integer memberType;
/**
* 手机号
*/
@Schema(title = "手机号")
private String phone;
/**
* 是否是当前登录用户, 0:不是, 1:是
*/
@Schema(title = "是否是当前登录用户, 0:不是, 1:是")
private Integer isSelf = 0;
}
@@ -0,0 +1,53 @@
package com.renkang.emergency.entity.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* App端:当前登录人参与的应急工单VO
*
* @author PengJ
* @date 2026/7/20
*/
@Data
public class MyParticipatedOrderVO {
@Schema(title = "应急单ID")
private String orderId;
@Schema(title = "会话ID")
private String sessionId;
@Schema(title = "被救助人姓名")
private String salvageUserName;
@Schema(title = "被救助人性别", description = "字典: sex2")
private String salvageUserSex;
@Schema(title = "被救助人头像")
private String salvageUserAvatar;
@Schema(title = "被救助人年龄")
private Integer salvageUserAge;
@Schema(title = "被救助人手机号")
private String salvageUserMobile;
@Schema(title = "单位名称")
private String orgName;
@Schema(title = "部门名称")
private String deptName;
@Schema(title = "应急单状态", description = "字典: emergency_order_status")
private String orderStatus;
@Schema(title = "发起时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date initTime;
}
@@ -6,6 +6,8 @@ import com.renkang.emergency.dto.PushBatchSendDTO;
import com.renkang.emergency.dto.PushFilterDTO;
import com.renkang.emergency.entity.FirstAidResource;
import java.util.Map;
/**
* @Description: 急救宣教推送Service
* @Author: jeecg-boot
@@ -24,7 +26,7 @@ public interface IFirstAidPushService {
void send(String resourceId, String orderId);
/**
* 批量推送
* 批量推送,返回 {total, success, failed} 结构
*/
int batchSend(PushBatchSendDTO dto);
Map<String, Object> batchSend(PushBatchSendDTO dto);
}
@@ -12,6 +12,8 @@ public interface RequestPrefix {
String DISEASE = BASE + "/disease";
String FIRST_AID_RESOURCE = BASE + "/firstAid/resource";
// 不鉴权路径
String API = "/api";
String ANON = API + "/anon/aed";
@@ -9,6 +9,7 @@ import com.renkang.emergency.bean.response.Call;
import com.renkang.emergency.bean.response.Stat;
import com.renkang.emergency.bean.response.StatSimple;
import com.renkang.emergency.entity.dto.AddOtherMemberDTO;
import com.renkang.emergency.entity.dto.RemoveGroupMemberDTO;
import com.renkang.emergency.entity.vo.CreateGroupMagVO;
import io.swagger.v3.oas.annotations.Operation;
import org.jeecg.common.api.vo.Result;
@@ -103,6 +104,15 @@ public class EmergencyApiController {
return Result.OK(emergencyApiService.createGroupMag(name, longitude, latitude, type));
}
@Deprecated
@Operation(summary = "[已废弃] 创建群聊-应急就医-急救员,请使用 /createGroupMag")
@GetMapping(value = "/createGroupMagFirstAider")
public Result<CreateGroupMagVO> createGroupMagFirstAider(@RequestParam("name") String name,
@RequestParam("longitude") String longitude,
@RequestParam("latitude") String latitude) {
return Result.OK(emergencyApiService.createGroupMagFirstAider(name, longitude, latitude));
}
@Operation(summary = "创建群聊-干预")
@GetMapping(value = "/createGroupMagIntervene")
@@ -123,4 +133,10 @@ public class EmergencyApiController {
return Result.OK(emergencyApiService.addGroupUser(dto));
}
@Operation(summary = "移除群成员-应急")
@PostMapping(value = "/removeGroupUser")
public Result<Boolean> removeGroupUser(@RequestBody @Validated RemoveGroupMemberDTO dto) {
return Result.OK(emergencyApiService.removeGroupUser(dto));
}
}
@@ -12,10 +12,13 @@ import com.renkang.emergency.bean.request.StationUpdateOrder;
import com.renkang.emergency.bean.response.InitUserOrder;
import com.renkang.emergency.bean.response.OrderThrough;
import com.renkang.emergency.bean.response.SessionOrderUser;
import com.renkang.emergency.entity.EmergencyProfessionUser;
import com.renkang.emergency.entity.Order;
import com.renkang.emergency.entity.OrderDetail;
import com.renkang.emergency.entity.OrderSendRecord;
import com.renkang.emergency.entity.vo.EmployeeVO;
import com.renkang.emergency.entity.vo.GroupMemberVO;
import com.renkang.emergency.entity.vo.MyParticipatedOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import org.jeecg.common.api.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
@@ -296,5 +299,42 @@ public class EmergencyOrderApiController {
return Result.OK(emergencyOrderApiService.getGroupMemberList(sessionId));
}
@Operation(summary = "获取实际群成员列表(腾讯IM验证)")
@GetMapping("/getActualGroupMemberList")
public Result<List<GroupMemberVO>> getActualGroupMemberList(@RequestParam("sessionId") String sessionId) {
return Result.OK(emergencyOrderApiService.getActualGroupMemberList(sessionId));
}
@Operation(summary = "搜索专家/专业人员")
@GetMapping("/searchExpert")
public Result<IPage<EmergencyProfessionUser>> searchExpert(
@RequestParam(required = false) String realname,
@RequestParam(required = false) String centerId,
@RequestParam(required = false) String excludeSessionId,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.OK(emergencyOrderApiService.searchExpert(realname, centerId, excludeSessionId, pageNo, pageSize));
}
@Operation(summary = "搜索员工")
@GetMapping("/searchEmployee")
public Result<IPage<EmployeeVO>> searchEmployee(
@RequestParam(required = false) String realname,
@RequestParam(required = false) String orgCode,
@RequestParam(required = false) String workNo,
@RequestParam(required = false) String phone,
@RequestParam(required = false) String excludeSessionId,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.OK(emergencyOrderApiService.searchEmployee(realname, orgCode, workNo, phone, excludeSessionId, pageNo, pageSize));
}
@Operation(summary = "查询当前登录人参与的应急工单列表")
@GetMapping("/myParticipatedOrders")
public Result<IPage<MyParticipatedOrderVO>> myParticipatedOrders(
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.OK(emergencyOrderApiService.myParticipatedOrders(pageNo, pageSize));
}
}
@@ -0,0 +1,46 @@
package com.renkang.emergency.api.controller;
import com.renkang.emergency.api.RequestPrefix;
import com.renkang.emergency.entity.FirstAidResource;
import com.renkang.emergency.service.IFirstAidResourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jeecg.common.api.vo.Result;
import org.jeecg.global.GlobalUtils;
import org.jeecg.common.system.vo.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 移动端-急救宣教资源
*
* @author pengjie
* @date 2026-07-06
*/
@Tag(name = "移动端-急救宣教资源")
@RestController
@RequestMapping(RequestPrefix.FIRST_AID_RESOURCE)
public class FirstAidResourceApiController {
@Autowired
private IFirstAidResourceService resourceService;
@Operation(summary = "根据宣教ID查询详情(含阅读记录)")
@GetMapping("/queryById")
public Result<FirstAidResource> queryById(@RequestParam("id") String id) {
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
FirstAidResource entity = resourceService.getDetail(id, userId);
if (entity == null) {
return Result.error("未找到对应数据");
}
// 记录阅读(已读自动去重)
if (userId != null) {
resourceService.incrementViewCount(id, userId);
}
return Result.OK(entity);
}
}
@@ -7,6 +7,7 @@ import com.renkang.emergency.bean.response.Call;
import com.renkang.emergency.bean.response.Stat;
import com.renkang.emergency.bean.response.StatSimple;
import com.renkang.emergency.entity.dto.AddOtherMemberDTO;
import com.renkang.emergency.entity.dto.RemoveGroupMemberDTO;
import com.renkang.emergency.entity.vo.CreateGroupMagVO;
import org.jeecg.common.api.vo.Result;
@@ -32,9 +33,22 @@ public interface EmergencyApiService {
CreateGroupMagVO createGroupMag(String name, String longitude, String latitude, String type);
/**
* [已废弃] 创建群聊-应急就医-急救员,请使用 {@link #createGroupMag}
*/
@Deprecated
CreateGroupMagVO createGroupMagFirstAider(String name, String longitude, String latitude);
Result<Map<String, Object>> createGroupMagIntervene(String name, String longitude, String latitude, String type);
List<String> fixOrderStatus();
boolean addGroupUser(AddOtherMemberDTO dto);
/**
* 拉入关爱联络员和急救人员到群聊(兜底补拉用,失败不抛异常)
*/
void addCareUsersToGroup(String userId, String groupId, String orderId);
boolean removeGroupUser(RemoveGroupMemberDTO dto);
}
@@ -9,11 +9,14 @@ import com.renkang.emergency.bean.request.StationUpdateOrder;
import com.renkang.emergency.bean.response.InitUserOrder;
import com.renkang.emergency.bean.response.OrderThrough;
import com.renkang.emergency.bean.response.SessionOrderUser;
import com.renkang.emergency.entity.EmergencyProfessionUser;
import com.renkang.emergency.entity.Order;
import com.renkang.emergency.entity.OrderDetail;
import com.renkang.emergency.entity.OrderSendRecord;
import com.renkang.emergency.entity.TblHelpCentorVo;
import com.renkang.emergency.entity.vo.EmployeeVO;
import com.renkang.emergency.entity.vo.GroupMemberVO;
import com.renkang.emergency.entity.vo.MyParticipatedOrderVO;
import java.util.Date;
import java.util.List;
@@ -75,4 +78,18 @@ public interface EmergencyOrderApiService {
List<GroupMemberVO> getGroupMemberList(String sessionId);
/**
* 获取实际群成员列表(先查本地参与人员,再匹配腾讯IM实际群成员)
*/
List<GroupMemberVO> getActualGroupMemberList(String sessionId);
IPage<EmergencyProfessionUser> searchExpert(String realname, String centerId, String excludeSessionId, Integer pageNo, Integer pageSize);
IPage<EmployeeVO> searchEmployee(String realname, String orgCode, String workNo, String phone, String excludeSessionId, Integer pageNo, Integer pageSize);
/**
* 查询当前登录人参与的应急工单列表 + 自己发起/被救助的应急工单
*/
IPage<MyParticipatedOrderVO> myParticipatedOrders(Integer pageNo, Integer pageSize);
}
@@ -7,7 +7,11 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.renkang.anyan.client.AnYanClient;
import com.renkang.anyan.model.dto.EmergencyContactParam;
import com.renkang.anyan.model.vo.EmergencyContactVO;
import com.renkang.emergency.api.service.EmergencyApiService;
import com.renkang.emergency.bean.request.CallBack;
import com.renkang.emergency.bean.request.EmergencyCall;
@@ -17,7 +21,9 @@ import com.renkang.emergency.bean.response.*;
import com.renkang.emergency.constant.EmergencyConstants;
import com.renkang.emergency.entity.*;
import com.renkang.emergency.entity.dto.AddOtherMemberDTO;
import com.renkang.emergency.entity.dto.RemoveGroupMemberDTO;
import com.renkang.emergency.entity.vo.CreateGroupMagVO;
import com.renkang.emergency.largeScreen.mapper.LargeScreenDataMapper;
import com.renkang.emergency.manager.EmergencyCloudManager;
import com.renkang.emergency.mapper.*;
import com.renkang.emergency.service.IEmergencyCenterService;
@@ -27,13 +33,17 @@ import com.renkang.emergency.util.DateUtils;
import com.renkang.emergency.websocket.WebSocket;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.dto.AddGroupMemberDTO;
import com.renkang.im.dto.DeleteGroupMemberDTO;
import com.renkang.im.entity.ConSessionRequestDO;
import com.renkang.im.entity.ImGroupMsgTextReqDO;
import com.renkang.im.vo.AddGroupMemberVO;
import com.renkang.im.vo.GetGroupMemberInfoVO;
import com.renkang.im.vo.ImBaseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.bean.request.ListUser;
import org.jeecg.bean.response.BaseEmployeeInfo;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.ExceptionAssertsUtil;
@@ -42,6 +52,7 @@ import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.global.GlobalUtils;
import org.jeecg.modules.manager.ISysCache;
import org.jeecg.modules.system.entity.SysUser;
import org.parboiled.common.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -114,6 +125,10 @@ public class EmergencyApiServiceImpl implements EmergencyApiService {
private OrderOperatorMapper orderOperatorMapper;
@Autowired
private EmergencyProfessionUserMapper emergencyProfessionUserMapper;
@Autowired
private AnYanClient anYanClient;
@Autowired
private LargeScreenDataMapper largeScreenDataMapper;
@Override
@@ -875,6 +890,8 @@ public class EmergencyApiServiceImpl implements EmergencyApiService {
String orderId = this.call(emergencyCall);
//保存参与应急的人员信息 PS: 不想大改之前的代码逻辑此处 没有将同一条记录的赋值逻辑合并
saveOderOperationInfo(orderId, sessionId, majorsId, operatorsId, majorsTwoId, rs.getCenterId(), conSessionRequestDO.getAccountList());
// 自动拉入关爱联络员+急救员1+急救员2(不阻断主流程)
addCareUsersToGroup(sysUser.getId(), sessionId, orderId);
imrs.getResult().put("member", userList);
imrs.getResult().put("tfNew", "1");
imrs.getResult().put("id", orderId);
@@ -884,6 +901,233 @@ public class EmergencyApiServiceImpl implements EmergencyApiService {
return createGroupMagVO;
}
@Deprecated
@Transactional(rollbackFor = Exception.class)
@Override
public CreateGroupMagVO createGroupMagFirstAider(String name, String longitude, String latitude) {
CreateGroupMagVO createGroupMagVO = new CreateGroupMagVO();
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//查是否有已经存在的应急工单,有则直接返回相关信息
Order order = emergencyResourceApiServiceImpl.sessionNowSession(sysUser.getId());
List<String> userList = new ArrayList<>();
if (order != null) {
createGroupMagVO.setUserId(sysUser.getId());
createGroupMagVO.setGroupId(order.getSessionId());
createGroupMagVO.setTfNew("0");
userList.add(order.getOperationUserId());
userList.add(order.getMajorUserId());
if (null != order.getMajorUserTwoId()) {
userList.add(order.getMajorUserTwoId());
}
createGroupMagVO.setMember(userList);
createGroupMagVO.setId(order.getId());
return createGroupMagVO;
}
// 查询当前应急中心值班人员
Call rs = this.officerOnDuty();
if (rs == null || CollectionUtils.isEmpty(rs.getMajors()) || CollectionUtils.isEmpty(rs.getOperators())) {
ExceptionAssertsUtil.fail("没有接受方人员!");
}
String majorsId = rs.getMajors().get(0).getUserId();
String operatorsId = rs.getOperators().get(0).getUserId();
String majorsTwoId = null;
userList.add(operatorsId);
userList.add(majorsId);
List<String> yesIds = new ArrayList<>();
yesIds.add(sysUser.getId());
yesIds.add(majorsId);
yesIds.add(operatorsId);
//获取参与应急的人员信息
ConSessionRequestDO conSessionRequestDO = selectConSessionRequestDO(yesIds, "", name);
conSessionRequestDO.setUserId(sysUser.getId());
//调用IM创建群组
Result<Map<String, Object>> imrs = iMHelloApi.creatGroupMagNew(conSessionRequestDO);
if (!imrs.isSuccess()) {
ExceptionAssertsUtil.fail("应急单创建失败");
}
String sessionId = (String) imrs.getResult().get("groupId");
createGroupMagVO.setGroupId(sessionId);
//直接保存应急工单(不发起点对点音视频通话)
String orderId = insertOrderForFirstAider(sysUser, Double.valueOf(longitude), Double.valueOf(latitude), rs.getCenterId());
//拉入关爱联络员和急救人员
addCareUsersToGroup(sysUser.getId(), sessionId, orderId);
//保存参与应急的人员信息
saveOderOperationInfo(orderId, sessionId, majorsId, operatorsId, majorsTwoId, rs.getCenterId(), conSessionRequestDO.getAccountList());
imrs.getResult().put("member", userList);
imrs.getResult().put("tfNew", "1");
imrs.getResult().put("id", orderId);
createGroupMagVO.setMember(userList);
createGroupMagVO.setTfNew("1");
createGroupMagVO.setId(orderId);
return createGroupMagVO;
}
/**
* 急救员场景插入工单(不触发大屏推送和短信)
*/
private String insertOrderForFirstAider(LoginUser loginUser, Double longitude, Double latitude, String centerId) {
Order order = new Order();
order.setInitiatorType("1");
order.setIsFill(EmergencyConstants.ORDER_IS_FILL_0);
// 发起人
order.setInitiatorUserId(loginUser.getId());
order.setInitiatorUserSex(String.valueOf(loginUser.getSex()));
order.setInitiatorUserName(loginUser.getRealname());
order.setInitiatorUserMobile(loginUser.getPhone());
order.setInitiatorUserIdCard(loginUser.getIdCard());
order.setInitiatorLongitude(BigDecimal.valueOf(longitude));
order.setInitiatorLatitude(BigDecimal.valueOf(latitude));
order.setInitiatorUserDepart(loginUser.getOrgCode());
order.setInitiatorUserAvatar(loginUser.getAvatar());
// 救助人
order.setSalvageUserId(loginUser.getId());
order.setSalvageUserSex(String.valueOf(loginUser.getSex()));
order.setSalvageUserName(loginUser.getRealname());
order.setSalvageUserMobile(loginUser.getPhone());
order.setSalvageUserIdCard(loginUser.getIdCard());
order.setSalvageUserDepart(loginUser.getOrgCode());
order.setSalvageUserAvatar(loginUser.getAvatar());
order.setCenterId(centerId);
orderMapper.insert(order);
return order.getId();
}
/**
* 拉入关爱联络员和急救人员到群聊
* <p>通过安眼返回的手机号 → EMP_BASIC_INFO查询员工编号 → 本系统用户ID</p>
*
* @param userId 发起人ID
* @param groupId IM群组ID
* @param orderId 应急工单ID
*/
public void addCareUsersToGroup(String userId, String groupId, String orderId) {
try {
// 1. 获取发起人工号
List<BaseEmployeeInfo> employeeInfos = sysBaseAPI.queryBaseEmployeeInfo(Collections.singleton(userId));
if (CollUtil.isEmpty(employeeInfos)) {
log.warn("未找到发起人的员工信息,跳过拉入关爱/急救人员, userId={}", userId);
return;
}
String workNo = employeeInfos.get(0).getWorkNo();
if (StrUtil.isBlank(workNo)) {
log.warn("发起人工号为空,跳过拉入关爱/急救人员, userId={}", userId);
return;
}
// 2. 查安眼系统获取关爱联络员和急救人员信息
EmergencyContactParam param = new EmergencyContactParam();
param.setEmployeeNum(workNo);
IPage<EmergencyContactVO> page = anYanClient.queryEmergencyContact(param);
if (page == null || CollUtil.isEmpty(page.getRecords())) {
log.warn("安眼系统未查到关爱/急救人员信息, workNo={}", workNo);
return;
}
EmergencyContactVO contact = page.getRecords().get(0);
// 3. 收集3个人的手机号 → 角色映射
Map<String, Integer> phoneRoleMap = new LinkedHashMap<>();
if (StrUtil.isNotBlank(contact.getRelationEmpPhone())) {
phoneRoleMap.put(contact.getRelationEmpPhone(), 5); // 关爱联络员
}
if (StrUtil.isNotBlank(contact.getFirstAiderPhone())) {
phoneRoleMap.put(contact.getFirstAiderPhone(), 3); // 急救人员1
}
if (StrUtil.isNotBlank(contact.getFirstAiderPhone2())) {
phoneRoleMap.put(contact.getFirstAiderPhone2(), 4); // 急救人员2
}
if (phoneRoleMap.isEmpty()) {
log.warn("关爱/急救人员手机号均为空, workNo={}", workNo);
return;
}
// 4. 手机号 → EMP_BASIC_INFO 反查员工编号
List<String> phones = new ArrayList<>(phoneRoleMap.keySet());
List<Map<String, String>> empBasicList = largeScreenDataMapper.selectEmployeeNumsByPhones(phones);
if (CollUtil.isEmpty(empBasicList)) {
log.warn("关爱/急救人员手机号均未匹配到EMP_BASIC_INFO, phones={}", phones);
return;
}
// 构建 phone → employeeNum 映射(拆分多手机号字段,\D+匹配任意非数字分隔符)
Map<String, String> phoneWorkNoMap = new LinkedHashMap<>();
for (Map<String, String> row : empBasicList) {
String phoneField = row.get("phone");
String employeeNum = row.get("employeeNum");
if (StrUtil.isNotBlank(phoneField) && StrUtil.isNotBlank(employeeNum)) {
for (String singlePhone : phoneField.split("\\D+")) {
if (StrUtil.isNotBlank(singlePhone)) {
phoneWorkNoMap.put(singlePhone, employeeNum);
}
}
}
}
log.info("手机号匹配到{}个EMP_BASIC_INFO员工编号, phones={}", phoneWorkNoMap.size(), phones);
// 5. 员工编号 → 本系统用户ID
List<String> workNoList = new ArrayList<>(phoneWorkNoMap.values());
List<SysUser> sysUsers = sysBaseAPI.getUserListByWorkNoList(workNoList);
if (CollUtil.isEmpty(sysUsers)) {
log.warn("关爱/急救人员员工编号均未匹配到本系统用户, workNos={}", workNoList);
return;
}
log.info("关爱/急救人员员工编号匹配到{}个系统用户, workNos={}", sysUsers.size(), workNoList);
// 6. 拉入IM群聊
Map<String, SysUser> sysUserMap = sysUsers.stream().collect(Collectors.toMap(SysUser::getId, u -> u, (a, b) -> a));
List<String> memberAccounts = new ArrayList<>(sysUserMap.keySet());
AddGroupMemberDTO addDto = new AddGroupMemberDTO();
addDto.setGroupId(groupId);
addDto.setSilence(1);
addDto.setMemberList(memberAccounts.stream().map(account -> {
AddGroupMemberDTO.MemberListDTO m = new AddGroupMemberDTO.MemberListDTO();
m.setMemberAccount(account);
SysUser u = sysUserMap.get(account);
if (u != null && StrUtil.isNotBlank(u.getRealname())) {
m.setMemberName(u.getRealname());
}
return m;
}).collect(Collectors.toList()));
try {
iMHelloApi.addGroupMember(addDto);
log.info("关爱/急救人员拉入群聊成功, groupId={}, userIds={}", groupId, memberAccounts);
} catch (Exception e) {
log.error("关爱/急救人员拉入群聊失败, groupId={}, userIds={}", groupId, memberAccounts, e);
// IM拉人失败不影响建群流程,继续写入 order_operator
}
// 7. 写入 order_operator 表
Date now = new Date();
Map<String, Integer> userIdRoleMap = new HashMap<>();
for (SysUser sysUser : sysUsers) {
String sysUserWorkNo = sysUser.getWorkNo();
if (StrUtil.isBlank(sysUserWorkNo)) {
continue;
}
// 遍历 phoneRoleMap 反查:phone → phoneWorkNoMap.get(phone) → 匹配 workNo
for (Map.Entry<String, Integer> roleEntry : phoneRoleMap.entrySet()) {
String mappedWorkNo = phoneWorkNoMap.get(roleEntry.getKey());
if (sysUserWorkNo.equals(mappedWorkNo)) {
userIdRoleMap.put(sysUser.getId(), roleEntry.getValue());
break;
}
}
}
for (Map.Entry<String, Integer> entry : userIdRoleMap.entrySet()) {
OrderOperator orderOperator = new OrderOperator();
orderOperator.setOrderId(orderId);
orderOperator.setOperatorType(entry.getValue());
orderOperator.setUserId(entry.getKey());
orderOperator.setJoinType(1);
orderOperator.setJoinDate(now);
orderOperatorMapper.insert(orderOperator);
log.info("写入order_operator, orderId={}, userId={}, operatorType={}", orderId, entry.getKey(), entry.getValue());
}
} catch (Exception e) {
log.error("拉入关爱/急救人员异常,不影响建群主流程, userId={}, groupId={}, orderId={}", userId, groupId, orderId, e);
}
}
public void saveOderOperationInfo(String orderId, String sessionId, String majorsId, String operatorsId, String majorsTwoId,
String centerId, List<LoginUserNew> accountList) {
Map<String, LoginUserNew> userMap = accountList.stream().collect(Collectors.toMap(LoginUserNew::getId, user -> user));
@@ -1079,34 +1323,135 @@ public class EmergencyApiServiceImpl implements EmergencyApiService {
ExceptionAssertsUtil.fail("应急单不存在");
}
String orderId = order.getId();
dto.getMemberList().forEach(userId -> {
OrderOperator orderOperator = orderOperatorMapper.selectOne(new LambdaQueryWrapper<OrderOperator>()
.eq(OrderOperator::getOrderId, orderId)
.eq(OrderOperator::getUserId, userId)
);
if (orderOperator != null) {
// 0. 获取当前登录用户,过滤掉本人
LoginUser loginUser = GlobalUtils.getLoginUser();
String currentUserId = loginUser.getId();
List<String> candidateList = dto.getMemberList().stream()
.filter(userId -> !userId.equals(currentUserId))
.collect(Collectors.toList());
if (candidateList.isEmpty()) {
ExceptionAssertsUtil.fail("不能邀请自己入群");
}
// 1. 查询本地 order_operator 和腾讯IM实际群成员,取并集作为已在群中的用户
Set<String> alreadyInGroup = new HashSet<>();
// 1a. 本地已有记录
List<OrderOperator> existingOps = orderOperatorMapper.selectList(
new LambdaQueryWrapper<OrderOperator>()
.eq(OrderOperator::getOrderId, orderId));
existingOps.forEach(op -> alreadyInGroup.add(op.getUserId()));
alreadyInGroup.add(order.getInitiatorUserId());
// 1b. 腾讯IM实际群成员
try {
Result<GetGroupMemberInfoVO> imResult = iMHelloApi.getGroupMemberInfo(groupId);
if (imResult != null && imResult.isSuccess() && imResult.getResult() != null) {
GetGroupMemberInfoVO vo = imResult.getResult();
if ("OK".equals(vo.getActionStatus()) && vo.getMemberList() != null) {
vo.getMemberList().forEach(m -> alreadyInGroup.add(m.getMemberAccount()));
}
}
} catch (Exception e) {
log.warn("获取腾讯IM群成员失败,仅使用本地数据进行去重, groupId: {}", groupId, e);
}
// 2. 群成员上限校验:最多9人
int currentGroupSize = alreadyInGroup.size();
int newMemberCount = (int) candidateList.stream()
.filter(userId -> !alreadyInGroup.contains(userId)).count();
if (currentGroupSize + newMemberCount > 9) {
ExceptionAssertsUtil.fail("群成员已达上限");
}
// memberType: 0=专家/专业人员(默认), 1=员工
Integer memberType = dto.getMemberType();
Integer operatorType = (memberType != null && memberType == 1) ? 2 : 1;
Date now = new Date();
List<String> newMemberList = new ArrayList<>();
candidateList.forEach(userId -> {
if (alreadyInGroup.contains(userId)) {
return;
}
orderOperator = new OrderOperator();
OrderOperator orderOperator = new OrderOperator();
orderOperator.setOrderId(orderId);
orderOperator.setOperatorType(1);
orderOperator.setOperatorType(operatorType);
orderOperator.setUserId(userId);
orderOperator.setJoinType(1);
orderOperator.setJoinDate(new Date());
orderOperator.setJoinDate(now);
orderOperatorMapper.insert(orderOperator);
newMemberList.add(userId);
});
if (newMemberList.isEmpty()) {
ExceptionAssertsUtil.fail("所选成员已在群聊中");
}
// 查询成员名称和头像用于IM账号导入
Map<String, String> memberNameMap = new HashMap<>();
Map<String, String> memberAvatarMap = new HashMap<>();
List<EmergencyProfessionUser> profUsers = emergencyProfessionUserMapper.selectList(
new LambdaQueryWrapper<EmergencyProfessionUser>().in(EmergencyProfessionUser::getUserId, newMemberList));
profUsers.forEach(u -> {
memberNameMap.put(u.getUserId(), u.getRealname());
if (StrUtil.isNotBlank(u.getAvatar())) {
memberAvatarMap.put(u.getUserId(), u.getAvatar());
}
});
List<String> remainingIds = newMemberList.stream()
.filter(id -> !memberNameMap.containsKey(id)).collect(Collectors.toList());
if (!remainingIds.isEmpty()) {
emergencyCloudManager.listUser(remainingIds).forEach((id, u) -> {
if (StrUtil.isNotBlank(u.getRealname())) {
memberNameMap.put(id, u.getRealname());
}
if (StrUtil.isNotBlank(u.getAvatar())) {
memberAvatarMap.put(id, u.getAvatar());
}
});
}
AddGroupMemberDTO addGroupMemberDTO = new AddGroupMemberDTO();
addGroupMemberDTO.setGroupId(dto.getGroupId());
addGroupMemberDTO.setMemberList(dto.getMemberList().stream().map(userId -> {
addGroupMemberDTO.setMemberList(newMemberList.stream().map(userId -> {
AddGroupMemberDTO.MemberListDTO memberListDTO = new AddGroupMemberDTO.MemberListDTO();
memberListDTO.setMemberAccount(userId);
memberListDTO.setMemberName(memberNameMap.get(userId));
memberListDTO.setMemberAvatar(memberAvatarMap.get(userId));
return memberListDTO;
}).collect(Collectors.toList()));
//添加群成员
Result<AddGroupMemberVO> addGroupMemberVoResult = iMHelloApi.addGroupMember(addGroupMemberDTO);
String actionStatus = addGroupMemberVoResult.getResult().getActionStatus();
if (!"OK".equals(actionStatus)) {
ExceptionAssertsUtil.fail(addGroupMemberVoResult.getResult().getErrorInfo());
log.error("添加群成员失败, actionStatus: {}, errorInfo: {}", actionStatus, addGroupMemberVoResult.getResult().getErrorInfo());
ExceptionAssertsUtil.fail("添加群成员失败");
}
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean removeGroupUser(RemoveGroupMemberDTO dto) {
String groupId = dto.getGroupId();
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
.eq(Order::getSessionId, groupId)
);
if (order == null) {
ExceptionAssertsUtil.fail("应急单不存在");
}
String orderId = order.getId();
// 只允许移除协助人员(joinType=1),不能移除初始处理人员(joinType=0)
for (String userId : dto.getMemberList()) {
orderOperatorMapper.delete(new LambdaQueryWrapper<OrderOperator>()
.eq(OrderOperator::getOrderId, orderId)
.eq(OrderOperator::getUserId, userId)
.eq(OrderOperator::getJoinType, 1)
);
}
// 调用IM删除群成员
DeleteGroupMemberDTO deleteDTO = new DeleteGroupMemberDTO();
deleteDTO.setGroupId(groupId);
deleteDTO.setMemberToDelAccount(dto.getMemberList());
Result<ImBaseResult> result = iMHelloApi.deleteGroupMember(deleteDTO);
if (!result.isSuccess()) {
ExceptionAssertsUtil.fail("IM删除群成员失败");
}
return true;
}
@@ -19,19 +19,29 @@ import com.renkang.emergency.bean.request.StationUpdateOrder;
import com.renkang.emergency.bean.response.*;
import com.renkang.emergency.constant.EmergencyConstants;
import com.renkang.emergency.entity.*;
import com.renkang.emergency.entity.vo.EmployeeVO;
import com.renkang.emergency.entity.vo.GroupMemberVO;
import com.renkang.emergency.entity.vo.MyParticipatedOrderVO;
import com.renkang.emergency.manager.EmergencyCacheManager;
import com.renkang.emergency.manager.EmergencyCloudManager;
import com.renkang.emergency.mapper.*;
import com.renkang.emergency.service.IOrderDetailService;
import com.renkang.emergency.service.IOrderService;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.dto.AddGroupMemberDTO;
import com.renkang.im.entity.ImGroupMsgTextReqDO;
import com.renkang.im.vo.AddGroupMemberVO;
import com.renkang.im.vo.GetGroupMemberInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.global.GlobalUtils;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.manager.ISysCache;
import org.jeecg.modules.system.entity.SysDepart;
import org.springframework.beans.factory.annotation.Autowired;
@@ -69,6 +79,14 @@ public class EmergencyOrderApiServiceImpl implements EmergencyOrderApiService {
private OrderOperatorMapper orderOperatorMapper;
@Autowired
private EmergencyProfessionUserMapper emergencyProfessionUserMapper;
@Autowired
private ConDoctorMapper conDoctorMapper;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
private EmergencyCloudManager emergencyCloudManager;
@Autowired
private com.renkang.emergency.api.service.EmergencyApiService emergencyApiService;
@Override
public IPage<InitUserOrder> initUserOrderPage(Page<Order> page) {
@@ -608,12 +626,13 @@ public class EmergencyOrderApiServiceImpl implements EmergencyOrderApiService {
ExceptionAssertsUtil.fail("应急单不存在");
}
String orderId = order.getId();
// 添加发起人信息
// 添加发起人信息memberType=-1
handleGroupMember(groupMemberVos, orderId,
order.getInitiatorUserId(),
order.getInitiatorUserName(),
order.getInitiatorUserAvatar(),
1);
order.getInitiatorUserMobile(),
1, -1);
// 已加入应急单人员
List<OrderOperator> orderOperators = orderOperatorMapper.selectList(
@@ -625,22 +644,52 @@ public class EmergencyOrderApiServiceImpl implements EmergencyOrderApiService {
.map(OrderOperator::getUserId)
.collect(Collectors.toList());
// 查询已加入人员的专业信息
// 查询已加入人员的专业信息(从EmergencyProfessionUser
Map<String, EmergencyProfessionUser> profUserMap = new HashMap<>();
if (!userIds.isEmpty()) {
List<EmergencyProfessionUser> emergencyProfessionUsers = emergencyProfessionUserMapper.selectList(
new LambdaQueryWrapper<EmergencyProfessionUser>()
.in(EmergencyProfessionUser::getUserId, userIds)
);
emergencyProfessionUsers.forEach(emergencyProfessionUser ->
handleGroupMember(groupMemberVos, orderId,
emergencyProfessionUser.getUserId(),
emergencyProfessionUser.getRealname(),
emergencyProfessionUser.getAvatar(),
1)
);
for (EmergencyProfessionUser u : emergencyProfessionUsers) {
profUserMap.put(u.getUserId(), u);
}
}
// 其他专业人员(排除已在应急单中的用户)
// 查询员工成员(operatorType=2)的系统用户信息
List<String> employeeUserIds = orderOperators.stream()
.filter(op -> op.getOperatorType() != null && op.getOperatorType() == 2)
.map(OrderOperator::getUserId)
.filter(uid -> !profUserMap.containsKey(uid))
.collect(Collectors.toList());
Map<String, LoginUserNew> sysUserMap = new HashMap<>();
if (!employeeUserIds.isEmpty()) {
sysUserMap = emergencyCloudManager.listUser(employeeUserIds);
}
// 遍历OrderOperator,填充已加入的群成员
for (OrderOperator op : orderOperators) {
String userName = "";
String avatar = "";
String phone = "";
EmergencyProfessionUser profUser = profUserMap.get(op.getUserId());
if (profUser != null) {
userName = profUser.getRealname();
avatar = profUser.getAvatar();
phone = profUser.getPhone();
} else {
LoginUserNew sysUser = sysUserMap.get(op.getUserId());
if (sysUser != null) {
userName = sysUser.getRealname();
avatar = sysUser.getAvatar();
phone = sysUser.getPhone();
}
}
handleGroupMember(groupMemberVos, orderId,
op.getUserId(), userName, avatar, phone, 1, op.getOperatorType());
}
// 其他专业人员(排除已在应急单中的用户),memberType=1
LambdaQueryWrapper<EmergencyProfessionUser> otherMajorQuery = new LambdaQueryWrapper<>();
otherMajorQuery.eq(EmergencyProfessionUser::getType, "6");
if (!userIds.isEmpty()) {
@@ -648,26 +697,467 @@ public class EmergencyOrderApiServiceImpl implements EmergencyOrderApiService {
}
List<EmergencyProfessionUser> otherMajorMemberList = emergencyProfessionUserMapper.selectList(otherMajorQuery);
// 批量查手机号
Map<String, String> otherMajorPhoneMap = new HashMap<>();
if (!otherMajorMemberList.isEmpty()) {
List<String> otherMajorUserIds = otherMajorMemberList.stream()
.map(EmergencyProfessionUser::getUserId).collect(Collectors.toList());
Map<String, LoginUserNew> otherUserMap = emergencyCloudManager.listUser(otherMajorUserIds);
for (EmergencyProfessionUser m : otherMajorMemberList) {
LoginUserNew u = otherUserMap.get(m.getUserId());
if (u != null) {
otherMajorPhoneMap.put(m.getUserId(), u.getPhone());
}
}
}
otherMajorMemberList.forEach(otherMajorMember ->
handleGroupMember(groupMemberVos, orderId,
otherMajorMember.getUserId(),
otherMajorMember.getRealname(),
otherMajorMember.getAvatar(),
0)
otherMajorPhoneMap.get(otherMajorMember.getUserId()),
0, 1)
);
return groupMemberVos.stream().filter(groupMemberVO -> !groupMemberVO.getUserId().equals(loginUser.getId())).collect(Collectors.toList());
}
public void handleGroupMember(List<GroupMemberVO> groupMemberVos, String orderId, String userId, String userName, String avatar, Integer isGroupMember) {
/**
* @description: 获取实际群成员列表(先查本地参与人员,再匹配腾讯IM实际群成员)
* @author PengJ
* @date 2026/7/7
*/
@Override
public List<GroupMemberVO> getActualGroupMemberList(String sessionId) {
List<GroupMemberVO> groupMemberVos = new ArrayList<>();
LoginUser loginUser = GlobalUtils.getLoginUser();
// 1. 校验订单是否存在
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
.eq(Order::getSessionId, sessionId));
if (order == null) {
ExceptionAssertsUtil.fail("应急单不存在");
}
String orderId = order.getId();
// 2. 兜底补拉关爱联络员+急救人员1+急救人员2(重新查询安眼系统)
// 发起应急时这些人员可能还未注册,此处兜底
try {
emergencyApiService.addCareUsersToGroup(order.getInitiatorUserId(), sessionId, orderId);
} catch (Exception e) {
log.warn("兜底补拉关爱/急救人员异常, sessionId={}", sessionId, e);
}
// 3. 查order_operator表获取本地参与人员
List<OrderOperator> orderOperators = orderOperatorMapper.selectList(
new LambdaQueryWrapper<OrderOperator>()
.eq(OrderOperator::getOrderId, orderId));
List<String> localUserIds = new ArrayList<>();
if (CollectionUtils.isNotEmpty(orderOperators)) {
localUserIds = orderOperators.stream()
.map(OrderOperator::getUserId)
.collect(Collectors.toList());
}
// 4. 调腾讯IM获取实际群成员
Set<String> imMemberAccounts = new HashSet<>();
boolean imSuccess = false;
try {
Result<GetGroupMemberInfoVO> imResult = imHelloApi.getGroupMemberInfo(sessionId);
if (imResult != null && imResult.isSuccess() && imResult.getResult() != null) {
GetGroupMemberInfoVO vo = imResult.getResult();
if ("OK".equals(vo.getActionStatus()) && vo.getMemberList() != null) {
vo.getMemberList().forEach(m -> imMemberAccounts.add(m.getMemberAccount()));
imSuccess = true;
}
}
} catch (Exception e) {
log.error("获取腾讯IM群成员失败,sessionId: {},降级使用本地参与人员", sessionId, e);
}
// 5. 自动补拉其他遗漏的预设成员(仅IM可用时)
if (imSuccess) {
autoInvitePresetMembers(sessionId, order, imMemberAccounts, localUserIds, loginUser);
}
// 6. 如果IM调用失败,降级使用本地成员全集
final Set<String> validAccounts = imSuccess ? imMemberAccounts : new HashSet<>(localUserIds);
if (!imSuccess) {
log.warn("腾讯IM调用失败,降级返回本地参与人员 {} 人, sessionId: {}", localUserIds.size(), sessionId);
}
// 7. 发起人:匹配验证集
if (validAccounts.contains(order.getInitiatorUserId())) {
handleGroupMember(groupMemberVos, orderId,
order.getInitiatorUserId(),
order.getInitiatorUserName(),
order.getInitiatorUserAvatar(),
order.getInitiatorUserMobile(),
1, -1);
}
// 8. 查order_operator表,匹配验证集
if (CollectionUtils.isNotEmpty(orderOperators)) {
// 查询专业用户信息
Map<String, EmergencyProfessionUser> profUserMap = new HashMap<>();
if (!localUserIds.isEmpty()) {
List<EmergencyProfessionUser> profUsers = emergencyProfessionUserMapper.selectList(
new LambdaQueryWrapper<EmergencyProfessionUser>()
.in(EmergencyProfessionUser::getUserId, localUserIds));
profUsers.forEach(u -> profUserMap.put(u.getUserId(), u));
}
// 查询员工成员的系统用户信息(所有profUserMap未命中的userId都走SysUser兜底)
List<String> employeeUserIds = orderOperators.stream()
.map(OrderOperator::getUserId)
.filter(uid -> !profUserMap.containsKey(uid))
.distinct()
.collect(Collectors.toList());
Map<String, LoginUserNew> sysUserMap = new HashMap<>();
if (!employeeUserIds.isEmpty()) {
sysUserMap = emergencyCloudManager.listUser(employeeUserIds);
}
// 遍历order_operator,只保留验证集中的成员
for (OrderOperator op : orderOperators) {
if (!validAccounts.contains(op.getUserId())) {
continue;
}
String userName = "";
String avatar = "";
String phone = "";
EmergencyProfessionUser profUser = profUserMap.get(op.getUserId());
if (profUser != null) {
userName = profUser.getRealname();
avatar = profUser.getAvatar();
phone = profUser.getPhone();
} else {
LoginUserNew sysUser = sysUserMap.get(op.getUserId());
if (sysUser != null) {
userName = sysUser.getRealname();
avatar = sysUser.getAvatar();
phone = sysUser.getPhone();
}
}
handleGroupMember(groupMemberVos, orderId,
op.getUserId(), userName, avatar, phone, 1, op.getOperatorType());
}
}
// 9. 标记本人
String currentUserId = loginUser.getId();
groupMemberVos.forEach(vo -> vo.setIsSelf(currentUserId.equals(vo.getUserId()) ? 1 : 0));
return groupMemberVos;
}
public void handleGroupMember(List<GroupMemberVO> groupMemberVos, String orderId, String userId, String userName, String avatar, String phone, Integer isGroupMember, Integer memberType) {
GroupMemberVO groupMemberVO = new GroupMemberVO();
groupMemberVO.setOrderId(orderId);
groupMemberVO.setUserId(userId);
groupMemberVO.setUserName(userName);
groupMemberVO.setAvatar(avatar);
groupMemberVO.setPhone(phone);
groupMemberVO.setIsGroupMember(isGroupMember);
groupMemberVO.setMemberType(memberType);
groupMemberVO.setIsSelf(0);
groupMemberVos.add(groupMemberVO);
}
/**
* 自动补拉预设成员入群
* 预设成员:被救助人 + order_operator 所有参与人员
* 群成员总数不超过9人
*/
private void autoInvitePresetMembers(String sessionId, Order order, Set<String> imMemberAccounts,
List<String> localUserIds, LoginUser loginUser) {
Set<String> presetMemberIds = new LinkedHashSet<>();
// 被救助人
if (StrUtil.isNotBlank(order.getSalvageUserId())) {
presetMemberIds.add(order.getSalvageUserId());
}
// order_operator 中所有参与人员(操作人员、专业人员、紧急联系人、健康联络员)
if (CollectionUtils.isNotEmpty(localUserIds)) {
presetMemberIds.addAll(localUserIds);
}
// 排除当前登录用户(不能邀请自己)
presetMemberIds.remove(loginUser.getId());
// 找出不在群中的预设成员
List<String> missingPresetMembers = presetMemberIds.stream()
.filter(id -> !imMemberAccounts.contains(id))
.collect(Collectors.toList());
if (missingPresetMembers.isEmpty()) {
return;
}
// 控制群成员不超过9人
int currentCount = imMemberAccounts.size();
int maxInvite = Math.max(0, 9 - currentCount);
if (maxInvite <= 0) {
log.warn("群成员已达上限9人,无法自动补拉预设成员, sessionId={}, current={}", sessionId, currentCount);
return;
}
List<String> toInvite = missingPresetMembers.subList(0, Math.min(missingPresetMembers.size(), maxInvite));
try {
AddGroupMemberDTO addDto = new AddGroupMemberDTO();
addDto.setGroupId(sessionId);
addDto.setSilence(1);
addDto.setMemberList(toInvite.stream().map(userId -> {
AddGroupMemberDTO.MemberListDTO m = new AddGroupMemberDTO.MemberListDTO();
m.setMemberAccount(userId);
return m;
}).collect(Collectors.toList()));
Result<AddGroupMemberVO> result = imHelloApi.addGroupMember(addDto);
if (result != null && result.isSuccess() && result.getResult() != null
&& "OK".equals(result.getResult().getActionStatus())) {
imMemberAccounts.addAll(toInvite);
log.info("自动补拉预设成员入群成功, sessionId={}, userIds={}, currentCount={}",
sessionId, toInvite, currentCount + toInvite.size());
} else {
log.warn("自动补拉预设成员入群失败, sessionId={}, userIds={}", sessionId, toInvite);
}
} catch (Exception e) {
log.error("自动补拉预设成员入群异常, sessionId={}", sessionId, e);
}
}
@Override
public IPage<EmergencyProfessionUser> searchExpert(String realname, String centerId, String excludeSessionId, Integer pageNo, Integer pageSize) {
// ---- 1. 收集需要排除的userId(已在群中的人) ----
Set<String> excludeUserIds = new HashSet<>();
if (StrUtil.isNotBlank(excludeSessionId)) {
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
.eq(Order::getSessionId, excludeSessionId));
if (order != null) {
List<OrderOperator> ops = orderOperatorMapper.selectList(
new LambdaQueryWrapper<OrderOperator>()
.eq(OrderOperator::getOrderId, order.getId()));
excludeUserIds.addAll(ops.stream().map(OrderOperator::getUserId).collect(Collectors.toList()));
}
}
// ---- 2. 查询 QH_EME_PROFESS_USER(急救专业人员) ----
LambdaQueryWrapper<EmergencyProfessionUser> profQuery = new LambdaQueryWrapper<>();
profQuery.eq(EmergencyProfessionUser::getType, "6");
if (StrUtil.isNotBlank(realname)) {
profQuery.like(EmergencyProfessionUser::getRealname, realname);
}
if (StrUtil.isNotBlank(centerId)) {
profQuery.eq(EmergencyProfessionUser::getCenterId, centerId);
}
if (!excludeUserIds.isEmpty()) {
profQuery.notIn(EmergencyProfessionUser::getUserId, excludeUserIds);
}
List<EmergencyProfessionUser> profUsers = emergencyProfessionUserMapper.selectList(profQuery);
// ---- 3. 查询 qh_con_doctor(咨询专家),排除已在 profUsers 或已在群中的 ----
List<String> skipIds = new ArrayList<>(excludeUserIds);
profUsers.forEach(u -> skipIds.add(u.getUserId()));
List<EmergencyProfessionUser> doctorUsers = conDoctorMapper.searchDoctorByName(realname, skipIds);
// ---- 4. 合并两条结果(profUsers 优先,doctorUsers 追加) ----
Map<String, EmergencyProfessionUser> mergedMap = new LinkedHashMap<>();
for (EmergencyProfessionUser u : profUsers) {
mergedMap.put(u.getUserId(), u);
}
// 从 qh_con_doctor 来的记录,不入 mergedMap 时用 doctorName 兜底 realname/avatar
for (EmergencyProfessionUser u : doctorUsers) {
mergedMap.putIfAbsent(u.getUserId(), u);
}
List<EmergencyProfessionUser> allRecords = new ArrayList<>(mergedMap.values());
// ---- 5. 批量查询 sys_user,统一填充 realname/avatar/phone/deptName ----
List<String> allUserIds = allRecords.stream()
.map(EmergencyProfessionUser::getUserId).collect(Collectors.toList());
if (!allUserIds.isEmpty()) {
Map<String, LoginUserNew> userMap = emergencyCloudManager.listUser(allUserIds);
allRecords.forEach(prof -> {
LoginUserNew sysUser = userMap.get(prof.getUserId());
if (sysUser != null) {
// 兜底:从 qh_con_doctor 来的记录没有 realname/avatar,用 sys_user 填充
if (StrUtil.isBlank(prof.getRealname())) {
prof.setRealname(sysUser.getRealname());
}
if (StrUtil.isBlank(prof.getAvatar())) {
prof.setAvatar(sysUser.getAvatar());
}
if (StrUtil.isNotBlank(sysUser.getOrgCode())) {
prof.setDeptName(emergencyCacheManager.getUserDepart(sysUser.getOrgCode()).getDepartName());
}
if (StrUtil.isBlank(prof.getPhone())) {
prof.setPhone(sysUser.getPhone());
}
}
});
}
// ---- 6. 对 profUsers 补充 qh_con_doctor 专家字段 ----
List<String> profUserIds = profUsers.stream()
.map(EmergencyProfessionUser::getUserId).collect(Collectors.toList());
if (!profUserIds.isEmpty()) {
List<EmergencyProfessionUser> doctorList = conDoctorMapper.selectDoctorByIds(profUserIds);
if (CollectionUtils.isNotEmpty(doctorList)) {
Map<String, EmergencyProfessionUser> doctorMap = doctorList.stream()
.collect(Collectors.toMap(EmergencyProfessionUser::getUserId, e -> e, (a, b) -> a));
profUsers.forEach(prof -> {
EmergencyProfessionUser doc = doctorMap.get(prof.getUserId());
if (doc == null) return;
copyDoctorFields(doc, prof);
});
}
}
// ---- 7. 内存分页 ----
int total = allRecords.size();
int pn = pageNo != null ? pageNo : 1;
int ps = pageSize != null ? pageSize : 10;
int fromIndex = Math.min((pn - 1) * ps, total);
int toIndex = Math.min(fromIndex + ps, total);
List<EmergencyProfessionUser> pageRecords = allRecords.subList(fromIndex, toIndex);
IPage<EmergencyProfessionUser> result = new Page<>(pn, ps, total);
result.setRecords(pageRecords);
return result;
}
private void copyDoctorFields(EmergencyProfessionUser doc, EmergencyProfessionUser prof) {
prof.setDoctorName(doc.getDoctorName());
prof.setDoctorNo(doc.getDoctorNo());
prof.setPhoto(doc.getPhoto());
prof.setResourceName(doc.getResourceName());
prof.setDepartmentId(doc.getDepartmentId());
prof.setDepartmentName(doc.getDepartmentName());
prof.setDoctorTitle(doc.getDoctorTitle());
prof.setDoctorJob(doc.getDoctorJob());
prof.setDoctorStatus(doc.getDoctorStatus());
prof.setIsAccept(doc.getIsAccept());
prof.setDoctorType(doc.getDoctorType());
prof.setAudioStatus(doc.getAudioStatus());
prof.setTfJumpHoliday(doc.getTfJumpHoliday());
prof.setExperience(doc.getExperience());
prof.setSort(doc.getSort());
prof.setTfRecommend(doc.getTfRecommend());
prof.setIntroduction(doc.getIntroduction());
if (StrUtil.isNotBlank(doc.getGoodAt())) prof.setGoodAt(doc.getGoodAt());
if (StrUtil.isNotBlank(doc.getSchool())) prof.setSchool(doc.getSchool());
if (StrUtil.isNotBlank(doc.getEdu())) prof.setEdu(doc.getEdu());
}
@Override
public IPage<EmployeeVO> searchEmployee(String realname, String orgCode, String workNo, String phone, String excludeSessionId, Integer pageNo, Integer pageSize) {
// 收集需要在结果中排除的用户ID
final Set<String> excludeUserIds = new HashSet<>();
if (StrUtil.isNotBlank(excludeSessionId)) {
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
.eq(Order::getSessionId, excludeSessionId));
if (order != null) {
List<OrderOperator> ops = orderOperatorMapper.selectList(
new LambdaQueryWrapper<OrderOperator>()
.eq(OrderOperator::getOrderId, order.getId()));
excludeUserIds.addAll(ops.stream().map(OrderOperator::getUserId).collect(Collectors.toList()));
}
}
// 构建查询条件(姓名、工号、手机号拼接 * 实现模糊搜索)
SysUser filter = new SysUser();
filter.setRealname(StrUtil.isNotBlank(realname) ? "*" + realname + "*" : null);
filter.setOrgCode(orgCode);
filter.setWorkNo(StrUtil.isNotBlank(workNo) ? "*" + workNo + "*" : null);
filter.setPhone(StrUtil.isNotBlank(phone) ? "*" + phone + "*" : null);
Page<SysUser> srcPage = sysBaseAPI.pageUser(filter, pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10, null);
Page<EmployeeVO> targetPage = new Page<>(srcPage.getCurrent(), srcPage.getSize(), srcPage.getTotal());
targetPage.setRecords(srcPage.getRecords().stream()
.filter(u -> !excludeUserIds.contains(u.getId()))
.map(u -> {
EmployeeVO vo = new EmployeeVO();
vo.setUserId(u.getId());
vo.setRealname(u.getRealname());
vo.setAvatar(u.getAvatar());
vo.setOrgCode(u.getOrgCode());
vo.setPhone(u.getPhone());
vo.setWorkNo(u.getWorkNo());
// 填充单位名称和部门名称
if (StrUtil.isNotBlank(u.getOrgCode())) {
vo.setOrgName(emergencyCacheManager.getUserSecondDepart(u.getOrgCode()).getDepartName());
vo.setDeptName(emergencyCacheManager.getUserDepart(u.getOrgCode()).getDepartName());
}
return vo;
}).collect(Collectors.toList()));
return targetPage;
}
@Override
public IPage<MyParticipatedOrderVO> myParticipatedOrders(Integer pageNo, Integer pageSize) {
String userId = GlobalUtils.getLoginUser().getId();
// 1. 查当前登录人参与的工单ID(从order_operator表)
List<String> participatedOrderIds = orderOperatorMapper.selectList(
new LambdaQueryWrapper<OrderOperator>()
.select(OrderOperator::getOrderId)
.eq(OrderOperator::getUserId, userId)
).stream().map(OrderOperator::getOrderId).distinct().collect(Collectors.toList());
// 2. 查当前用户自己发起的或被救助的应急工单ID(从order表)
List<String> ownOrderIds = orderMapper.selectList(
new LambdaQueryWrapper<Order>()
.select(Order::getId)
.and(w -> w.eq(Order::getInitiatorUserId, userId).or().eq(Order::getSalvageUserId, userId))
.eq(Order::getDelFlag, CommonConstant.DEL_FLAG_0)
.eq(Order::getStatus, 1)
).stream().map(Order::getId).distinct().collect(Collectors.toList());
// 3. 合并去重
Set<String> allOrderIds = new LinkedHashSet<>();
if (CollectionUtils.isNotEmpty(participatedOrderIds)) {
allOrderIds.addAll(participatedOrderIds);
}
if (CollectionUtils.isNotEmpty(ownOrderIds)) {
allOrderIds.addAll(ownOrderIds);
}
if (CollectionUtils.isEmpty(allOrderIds)) {
return new Page<>(pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10);
}
// 4. 分页查询正在应急的工单
Page<Order> page = new Page<>(pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10);
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.in(Order::getId, new ArrayList<>(allOrderIds));
wrapper.eq(Order::getSessionNow, true);
wrapper.eq(Order::getDelFlag, CommonConstant.DEL_FLAG_0);
wrapper.eq(Order::getStatus, 1);
wrapper.orderByDesc(Order::getCreateTime);
IPage<Order> srcPage = orderMapper.selectPage(page, wrapper);
// 5. 映射VO
Page<MyParticipatedOrderVO> targetPage = new Page<>(srcPage.getCurrent(), srcPage.getSize(), srcPage.getTotal());
if (CollectionUtils.isEmpty(srcPage.getRecords())) {
return targetPage;
}
List<MyParticipatedOrderVO> records = srcPage.getRecords().stream().map(order -> {
MyParticipatedOrderVO vo = new MyParticipatedOrderVO();
vo.setOrderId(order.getId());
vo.setSessionId(order.getSessionId());
vo.setSalvageUserName(order.getSalvageUserName());
vo.setSalvageUserSex(order.getSalvageUserSex());
vo.setSalvageUserAvatar(order.getSalvageUserAvatar());
vo.setSalvageUserAge(order.getSalvageUserAge());
vo.setSalvageUserMobile(order.getSalvageUserMobile());
vo.setOrderStatus(order.getOrderStatus());
vo.setInitTime(order.getCreateTime());
// 解析部门(防NPE
if (StrUtil.isNotBlank(order.getSalvageUserDepart())) {
SysDepart secondDepart = emergencyCacheManager.getUserSecondDepart(order.getSalvageUserDepart());
SysDepart depart = emergencyCacheManager.getUserDepart(order.getSalvageUserDepart());
if (secondDepart != null) {
vo.setOrgName(secondDepart.getDepartName());
}
if (depart != null) {
vo.setDeptName(depart.getDepartName());
}
}
return vo;
}).collect(Collectors.toList());
targetPage.setRecords(records);
return targetPage;
}
private List<SessionOrderUser> convertOrder(List<Order> orders) {
if (CollectionUtils.isEmpty(orders)) {
return Collections.emptyList();
@@ -14,7 +14,6 @@ import org.jeecg.common.aspect.annotation.AutoLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
@@ -81,17 +80,15 @@ public class FirstAidPushController {
* 一键推送(批量)
*/
@AutoLog(value = "急救宣教资源-批量推送")
@Operation(summary = "一键推送", description = "批量推送资源,支持指定ID列表或按筛选条件全量推送")
@Operation(summary = "一键推送", description = "批量推送资源,支持指定ID列表或按筛选条件全量推送,返回成功/失败明细")
@PostMapping(value = "/batchSend")
public Result<Map<String, Object>> batchSend(@RequestBody PushBatchSendDTO dto) {
if (dto.getOrderId() == null || dto.getOrderId().isEmpty()) {
return Result.error("工单ID不能为空");
}
try {
int count = pushService.batchSend(dto);
Map<String, Object> result = new HashMap<>();
result.put("count", count);
return Result.OK("已推送 " + count + "", result);
Map<String, Object> result = pushService.batchSend(dto);
return Result.OK(result);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
@@ -58,13 +58,13 @@ public class FirstAidResourceCategoryController extends JeecgController<FirstAid
return Result.OK(categoryService.listCategoriesWithTags());
}
@Operation(summary = "急救宣教资源分类-分类列表")
@Operation(summary = "急救宣教资源分类-分类列表")
@GetMapping(value = "/listRoot")
public Result<List<FirstAidResourceCategory>> listRoot() {
return Result.OK(categoryService.listRootCategories());
}
@Operation(summary = "急救宣教资源分类-子分类列表")
@Operation(summary = "急救宣教资源分类-标签列表(按分类)")
@GetMapping(value = "/listChildren")
public Result<List<FirstAidResourceCategory>> listChildren(@RequestParam(name = "parentId") String parentId) {
return Result.OK(categoryService.listChildrenByParentId(parentId));
@@ -128,13 +128,10 @@ public class FirstAidResourceCategoryController extends JeecgController<FirstAid
@AutoLog(value = "急救宣教资源分类-批量删除")
//@RequiresPermissions("emergency:firstAidCategory:deleteBatch")
@PostMapping(value = "/deleteBatch")
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public Result<String> deleteBatch(@RequestBody List<String> ids) {
for (String id : ids) {
try {
categoryService.deleteWithCheck(id);
} catch (RuntimeException e) {
return Result.error("ID[" + id + "]" + e.getMessage());
}
}
return Result.OK("批量删除成功!");
}
@@ -61,7 +61,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
entity.setStatus(status);
entity.setTags(tags);
Page<FirstAidResource> page = new Page<>(pageNo, pageSize);
String userId = GlobalUtils.getLoginUser().getId();
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
return Result.OK(resourceService.queryPageList(entity, page, userId));
}
@@ -73,7 +74,7 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
return Result.error("标题不能为空");
}
resourceService.saveResource(entity);
return Result.OK("添加成功!");
return Result.OK("添加成功!", entity.getId());
}
@Operation(summary = "急救宣教资源-编辑")
@@ -91,7 +92,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-删除")
@PostMapping(value = "/delete")
public Result<String> delete(@RequestBody IdDTO dto) {
resourceService.removeById(dto.getId());
try {
resourceService.deleteResource(dto.getId());
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("删除成功!");
}
@@ -99,7 +104,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量删除")
@PostMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestBody List<String> ids) {
resourceService.removeByIds(ids);
try {
resourceService.batchDeleteResources(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量删除成功!");
}
@@ -107,7 +116,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量发布")
@PostMapping(value = "/batchPublish")
public Result<String> batchPublish(@RequestBody List<String> ids) {
try {
resourceService.batchPublish(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量发布成功!");
}
@@ -115,7 +128,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量下架")
@PostMapping(value = "/batchTakeDown")
public Result<String> batchTakeDown(@RequestBody List<String> ids) {
try {
resourceService.batchTakeDown(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量下架成功!");
}
@@ -158,7 +175,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@Operation(summary = "急救宣教资源-通过id查询详情")
@GetMapping(value = "/queryById")
public Result<FirstAidResource> queryById(@RequestParam(name = "id") String id) {
String userId = GlobalUtils.getLoginUser().getId();
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
FirstAidResource entity = resourceService.getDetail(id, userId);
return entity == null ? Result.error("未找到对应数据") : Result.OK(entity);
}
@@ -167,7 +185,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-记录阅读")
@PostMapping(value = "/incrementView")
public Result<String> incrementView(@RequestBody IdDTO dto) {
String userId = GlobalUtils.getLoginUser().getId();
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
boolean firstRead = resourceService.incrementViewCount(dto.getId(), userId);
return firstRead ? Result.OK("阅读记录成功") : Result.OK("已阅读过");
}
@@ -128,13 +128,10 @@ public class FirstAidResourceTagController extends JeecgController<FirstAidResou
@AutoLog(value = "急救宣教资源标签-批量删除")
//@RequiresPermissions("emergency:firstAidTag:deleteBatch")
@PostMapping(value = "/deleteBatch")
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public Result<String> deleteBatch(@RequestBody List<String> ids) {
for (String id : ids) {
try {
categoryService.deleteWithCheck(id);
} catch (RuntimeException e) {
return Result.error("ID[" + id + "]" + e.getMessage());
}
}
return Result.OK("批量删除成功!");
}
@@ -251,7 +251,8 @@ public class LargeScreenNewController {
* 关爱趋势某一条详情
*/
@GetMapping("loveTrendByOne")
public Result<CareUserListVo> loveTrendByOne(@RequestParam(name = "anYanUserId", required = false)String anYanUserId,
public Result<CareUserListVo>
loveTrendByOne(@RequestParam(name = "anYanUserId", required = false)String anYanUserId,
@RequestParam(name = "employeeNum", required = false)String employeeNum){
return Result.ok(largeScreenDataService.loveTrendByOne(anYanUserId,employeeNum));
}
@@ -299,6 +300,23 @@ public class LargeScreenNewController {
return Result.ok(largeScreenDataService.careUser(userId,workNo));
}
/**
* 根据当前登录用户工号判断是否为联络人员
*/
@GetMapping("isLiaisonUser")
public Result<Boolean> isLiaisonUser(){
return Result.ok(largeScreenDataService.isLiaisonUser());
}
/**
* 一人一案(移动端)
*/
@GetMapping("onePersonCase")
public Result<OnePersonCaseVO> onePersonCase(){
return Result.ok(largeScreenDataService.onePersonCase());
}
/**
* 或者安眼平台访问token
* @return
@@ -0,0 +1,285 @@
package com.renkang.emergency.largeScreen.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
/**
* 一人一案详情出参
* 数据来源:安眼库 AYGC_YWK.EMP_PERSON_FILE 及关联表
*/
@Data
public class OnePersonCaseVO {
@Schema(title = "ID")
private String id;
@Schema(title = "员工ID")
private String empId;
@Schema(title = "员工姓名")
private String name;
@Schema(title = "年龄")
private Integer age;
@Schema(title = "性别")
private String sexCode;
@Schema(title = "身份证号")
private String idNo;
@Schema(title = "手机号")
private String phone;
@Schema(title = "关爱状态")
private String careStatus;
@Schema(title = "工作单位")
private String workUnit ;
@Schema(title = "单位ID")
private String workUnitId;
@Schema(title = "创建时间")
private String createDate;
@Schema(title = "修改时间")
private String updateDate;
@Schema(title = "数据级别")
private Integer dataLevel;
@Schema(title = "负面清单")
private String negativeList;
@Schema(title = "一人一案")
private String caseInfo;
@Schema(title = "肥胖人员标识(1,是,2-否)")
private Integer flagFat;
@Schema(title = "BMI")
private String bmi;
@Schema(title = "身高")
private String height;
@Schema(title = "体重")
private String weight;
@Schema(title = "体重管理目标")
private String targetWeight;
@Schema(title = "目标BMI")
private String targetBmi;
@Schema(title = "目前BMI监测值")
private String bmiCurrent;
@Schema(title = "最新腰围")
private String waistNew;
@Schema(title = "最新体脂率")
private String bodyFatPercentNew;
@Schema(title = "体重风险类型")
private String weightRiskType;
@Schema(title = "高血压(0-否,1-是)")
private Integer hypertension;
@Schema(title = "收缩压均值")
private String systolicPressureAvg;
@Schema(title = "收缩压控制目标")
private String systolicPressureTarget;
@Schema(title = "目前收缩压检测值")
private String systolicPressureCurrent;
@Schema(title = "舒张压平均值")
private String diastolicPressureAvg;
@Schema(title = "舒张压控制目标值")
private String diastolicPressureTarget;
@Schema(title = "舒张压当前检测值")
private String diastolicPressureCurrent;
@Schema(title = "糖尿病标识(0-否,1-是)")
private Integer diabetes;
@Schema(title = "空腹葡萄糖(GLU)")
private String glu;
@Schema(title = "血糖控制目标值")
private String bloodSugarTarget;
@Schema(title = "目前血糖监测值:")
private String bloodSugarCurrent;
@Schema(title = "低密度脂蛋白")
private String ldl;
@Schema(title = "低密度脂蛋白目标控制值")
private String ldlTarget;
@Schema(title = "低密度脂蛋白当前检测值")
private String ldlCurrent;
@Schema(title = "总胆固醇")
private String tc;
@Schema(title = "总胆固醇目标控制值")
private String tcTarget;
@Schema(title = "总胆固醇目前监测值")
private String tcCurrent;
@Schema(title = "甘油三酯")
private String tg;
@Schema(title = "甘油三酯目标控制值")
private String tgTarget;
@Schema(title = "甘油三酯当前监测值")
private String tgCurrent;
@Schema(title = "尿酸")
private String uricAcid;
@Schema(title = "尿酸目标控制值")
private String uricAcidTarget;
@Schema(title = "尿酸当前监测值")
private String uricAcidCurrent;
@Schema(title = "同型半胱氨酸")
private String homocysteine;
@Schema(title = "同型半胱氨酸目标控制值")
private String homocysteineTarget;
@Schema(title = "同型半胱氨酸当前监测值")
private String homocysteineCurrent;
@Schema(title = "血钾")
private String bloodK;
@Schema(title = "冠心病")
private String chd;
@Schema(title = "脑血管")
private String cvd;
@Schema(title = "心房颤动")
private String af;
@Schema(title = "恶性肿瘤")
private String ca;
@Schema(title = "检测指标列表")
private String indicatorListStr;
@Schema(title = "导入指标列表")
private String indicatorListStrOrigin;
@Schema(title = "风险等级")
private String riskLevel;
@Schema(title = "年度")
private String year;
@Schema(title = "筛查项目汇总")
private String screeningProjects;
@Schema(title = "关爱联络员姓名")
private String careLiaisonName;
@Schema(title = "治疗方案")
private String treatPlan;
@Schema(title = "治疗方案反馈")
private String treatPlanResponse;
@Schema(title = "饮食建议")
private String dietSuggestion;
@Schema(title = "饮食建议反馈")
private String dietSuggestionResponse;
@Schema(title = "运动指导")
private String sportGuide;
@Schema(title = "运动指导反馈")
private String sportGuideResponse;
@Schema(title = "日常生活管理")
private String lifeManage;
@Schema(title = "日常生活管理反馈")
private String lifeManageResponse;
@Schema(title = "小屋意见")
private String cabinOpinion;
@Schema(title = "小屋意见时间")
private String cabinOpinionTime;
@Schema(title = "小屋意见员工姓名")
private String cabinOpinionUserName;
@Schema(title = "专家意见")
private String expertOpinion;
@Schema(title = "专家意见时间")
private String expertOpinionTime;
@Schema(title = "专家意见员工姓名")
private String expertOpinionUserName;
/**
* BMI 保留一位小数
* 数据库 BMI 列为 VARCHAR,这里解析为数值后四舍五入
*/
public String getBmi() {
if (bmi == null || bmi.isEmpty()) {
return null;
}
try {
double v = Double.parseDouble(bmi.trim());
return String.valueOf(Math.round(v * 10) / 10.0);
} catch (NumberFormatException e) {
return bmi;
}
}
/**
* 年龄:优先取库中值,为空时根据身份证号计算
*/
public Integer getAge() {
if (age != null) {
return age;
}
if (idNo == null || idNo.length() != 18) {
return null; // 身份证号无效时返回null
}
try {
// 截取出生日期字符串
String birthDateStr = idNo.substring(6, 14);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate birthDate = LocalDate.parse(birthDateStr, formatter);
LocalDate now = LocalDate.now();
if (birthDate.isAfter(now)) {
return null; // 出生日期在未来,返回null
}
return Period.between(birthDate, now).getYears();
} catch (Exception e) {
return null; // 解析异常返回null
}
}
}
@@ -2,6 +2,12 @@ package com.renkang.emergency.largeScreen.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.renkang.emergency.entity.LargeScreenData;
import com.renkang.emergency.largeScreen.entity.vo.OnePersonCaseVO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* @Description: large_screen
@@ -11,4 +17,40 @@ import com.renkang.emergency.entity.LargeScreenData;
*/
public interface LargeScreenDataMapper extends MPJBaseMapper<LargeScreenData> {
/**
* 根据工号查询关爱联络员关系表记录数
*
* @param workNo 工号
* @return 命中记录数,大于 0 表示该工号是关爱联络员
*/
@Select("SELECT COUNT(1) FROM AYGC_YWK.HYT_RELATION_CARE_NEW WHERE ADMIN_NUM = #{workNo}")
int countByAdminNum(@Param("workNo") String workNo);
/**
* 一人一案:根据工号查询最新一条档案详情
* SQL 见同包 xml/LargeScreenDataMapper.xml
*
* @param workNo 工号
* @return 一人一案详情,无数据时返回 null
*/
OnePersonCaseVO selectOnePersonCase(@Param("workNo") String workNo);
/**
* 根据手机号批量查询 EMP_BASIC_INFO 的员工编号
* SQL 见同包 xml/LargeScreenDataMapper.xml
*
* @param phones 手机号集合
* @return 手机号→员工编号映射列表 (key: PHONE, EMPLOYEE_NUM)
*/
List<Map<String, String>> selectEmployeeNumsByPhones(@Param("phones") List<String> phones);
/**
* 查询 EMP_CARE_EMP 表中 FIRST_AIDER_INFO 是否包含指定工号
*
* @param workNo 工号
* @return 命中记录数,大于 0 表示该工号是急救人员
*/
@Select("SELECT COUNT(1) FROM AYGC_YWK.EMP_CARE_EMP WHERE DEL_FLAG = '0' AND FIRST_AIDER_INFO LIKE CONCAT('%', #{workNo}, '%')")
int countFirstAiderByWorkNo(@Param("workNo") String workNo);
}
@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.renkang.emergency.largeScreen.mapper.LargeScreenDataMapper">
<!-- 一人一案:根据工号取最新一条档案详情 -->
<select id="selectOnePersonCase" resultType="com.renkang.emergency.largeScreen.entity.vo.OnePersonCaseVO">
SELECT
f.ID AS "id",
f.EMP_ID AS "empId",
b.NAME AS "name",
b.ID_NO AS "idNo",
b.AGE AS "age",
b.PHONE AS "phone",
b.IS_HEART_BRAIN_BLOOD_CARE_EMP AS "careStatus",
f.DATA_LEVEL AS "dataLevel",
f.NEGATIVE_LIST AS "negativeList",
f.CASE_INFO AS "caseInfo",
f.FLAG_FAT AS "flagFat",
f.BMI AS "bmi",
f.HEIGHT AS "height",
f.WEIGHT AS "weight",
f.WEIGHT_TARGET AS "targetWeight",
CASE
WHEN f.HEIGHT > 0
AND REGEXP_LIKE (f.WEIGHT_TARGET, '^[0-9]+(\.[0-9]+)?$') THEN ROUND(
TO_NUMBER (f.WEIGHT_TARGET) / POWER(f.HEIGHT / 100.0, 2),
2
)
ELSE NULL
END AS "targetBmi",
f.HYPERTENSION AS "hypertension",
f.SYSTOLIC_PRESSURE_AVG AS "systolicPressureAvg",
f.SYSTOLIC_PRESSURE_TARGET AS "systolicPressureTarget",
f.DIASTOLIC_PRESSURE_AVG AS "diastolicPressureAvg",
f.DIASTOLIC_PRESSURE_TARGET AS "diastolicPressureTarget",
f.DIABETES AS "diabetes",
f.GLU AS "glu",
f.BLOOD_SUGAR_TARGET AS "bloodSugarTarget",
f.LDL AS "ldl",
f.LDL_TARGET AS "ldlTarget",
f.TC AS "tc",
f.TC_TARGET AS "tcTarget",
f.TG AS "tg",
f.TG_TARGET AS "tgTarget",
f.URIC_ACID AS "uricAcid",
f.URIC_ACID_TARGET AS "uricAcidTarget",
f.HOMOCYSTEINE AS "homocysteine",
f.HOMOCYSTEINE_TARGET AS "homocysteineTarget",
f.BLOOD_K AS "bloodK",
f.CHD AS "chd",
f.CVD AS "cvd",
f.AF AS "af",
f.CA AS "ca",
f.INDICATOR_LIST_STR AS "indicatorListStr",
f.INDICATOR_LIST_STR_ORIGIN AS "indicatorListStrOrigin",
f.TREAT_PLAN AS "treatPlan",
f.TREAT_PLAN_RESPONSE AS "treatPlanResponse",
f.DIET_SUGGESTION AS "dietSuggestion",
f.DIET_SUGGESTION_RESPONSE AS "dietSuggestionResponse",
f.SPORT_GUIDE AS "sportGuide",
f.SPORT_GUIDE_RESPONSE AS "sportGuideResponse",
f.LIFE_MANAGE AS "lifeManage",
f.LIFE_MANAGE_RESPONSE AS "lifeManageResponse",
f.RISK_LEVEL AS "riskLevel",
f.YEAR AS "year",
w.BMI_NEW AS "bmiCurrent",
w.WAIST_NEW AS "waistNew",
w.BODY_FAT_PERCENT_NEW AS "bodyFatPercentNew",
w.TYPE AS "weightRiskType",
ft.PROJECTS AS "screeningProjects",
-- 小屋意见
(
SELECT d.RESPONSE_CONTENT
FROM (
SELECT
RESPONSE_CONTENT,
CREATE_DATE,
CREATE_BY,
ROW_NUMBER() OVER (ORDER BY CREATE_DATE DESC) as rn
FROM AYGC_YWK.EMP_PERSON_FILE_DETAIL
WHERE EMP_ID = f.EMP_ID
AND DEL_FLAG = '0'
AND RESPONSE_TYPE = '小屋意见'
) d
WHERE d.rn = 1
) AS "cabinOpinion",
-- 小屋意见时间
(
SELECT d.CREATE_DATE
FROM (
SELECT
RESPONSE_CONTENT,
CREATE_DATE,
CREATE_BY,
ROW_NUMBER() OVER (ORDER BY CREATE_DATE DESC) as rn
FROM AYGC_YWK.EMP_PERSON_FILE_DETAIL
WHERE EMP_ID = f.EMP_ID
AND DEL_FLAG = '0'
AND RESPONSE_TYPE = '小屋意见'
) d
WHERE d.rn = 1
) AS "cabinOpinionTime",
-- 小屋意见员工姓名
(
SELECT u.NAME
FROM (
SELECT
CREATE_BY,
ROW_NUMBER() OVER (ORDER BY CREATE_DATE DESC) as rn
FROM AYGC_YWK.EMP_PERSON_FILE_DETAIL
WHERE EMP_ID = f.EMP_ID
AND DEL_FLAG = '0'
AND RESPONSE_TYPE = '小屋意见'
) d
LEFT JOIN AYGC_XTK.SYS_USER u ON u.ID = d.CREATE_BY
WHERE d.rn = 1
) AS "cabinOpinionUserName",
-- 专家意见
(
SELECT d.RESPONSE_CONTENT
FROM (
SELECT
RESPONSE_CONTENT,
CREATE_DATE,
CREATE_BY,
ROW_NUMBER() OVER (ORDER BY CREATE_DATE DESC) as rn
FROM AYGC_YWK.EMP_PERSON_FILE_DETAIL
WHERE EMP_ID = f.EMP_ID
AND DEL_FLAG = '0'
AND RESPONSE_TYPE = '专家意见'
) d
WHERE d.rn = 1
) AS "expertOpinion",
-- 专家意见时间
(
SELECT d.CREATE_DATE
FROM (
SELECT
RESPONSE_CONTENT,
CREATE_DATE,
CREATE_BY,
ROW_NUMBER() OVER (ORDER BY CREATE_DATE DESC) as rn
FROM AYGC_YWK.EMP_PERSON_FILE_DETAIL
WHERE EMP_ID = f.EMP_ID
AND DEL_FLAG = '0'
AND RESPONSE_TYPE = '专家意见'
) d
WHERE d.rn = 1
) AS "expertOpinionTime",
-- 专家意见员工姓名
(
SELECT u.NAME
FROM (
SELECT
CREATE_BY,
ROW_NUMBER() OVER (ORDER BY CREATE_DATE DESC) as rn
FROM AYGC_YWK.EMP_PERSON_FILE_DETAIL
WHERE EMP_ID = f.EMP_ID
AND DEL_FLAG = '0'
AND RESPONSE_TYPE = '专家意见'
) d
LEFT JOIN AYGC_XTK.SYS_USER u ON u.ID = d.CREATE_BY
WHERE d.rn = 1
) AS "expertOpinionUserName"
FROM
AYGC_YWK.EMP_PERSON_FILE f
LEFT JOIN AYGC_YWK.EMP_BASIC_INFO b ON b.ID = f.EMP_ID AND b.DEL_FLAG = '0'
LEFT JOIN AYGC_YWK.WEIGHT_MANAGEMENT w ON w.EMP_ID = f.EMP_ID AND w.DEL_FLAG = '2'
AND w.YEAR = f.YEAR
LEFT JOIN (
SELECT
EMP_ID,
LISTAGG (PROJECT || ':' || NVL (CHECK_RESULT, ''), '') WITHIN GROUP (
ORDER BY
CHECK_DATE DESC
) AS PROJECTS
FROM
AYGC_YWK.EMP_FIVE_THREE
WHERE
DEL_FLAG = 2
GROUP BY
EMP_ID
) ft ON ft.EMP_ID = f.EMP_ID
WHERE
f.DEL_FLAG = '0'
AND b.EMPLOYEE_NUM = #{workNo}
ORDER BY f.CREATE_DATE DESC
FETCH FIRST 1 ROWS ONLY
</select>
<!-- 根据手机号批量查询EMP_BASIC_INFO的员工编号,INSTR模糊匹配支持任意分隔符的多手机号场景 -->
<select id="selectEmployeeNumsByPhones" resultType="java.util.HashMap">
SELECT
b.PHONE AS "phone",
b.EMPLOYEE_NUM AS "employeeNum"
FROM
AYGC_YWK.EMP_BASIC_INFO b
WHERE
b.DEL_FLAG = '0'
AND (
<foreach collection="phones" item="phone" separator=" OR " open="(" close=")">
INSTR(b.PHONE, #{phone}) &gt; 0
</foreach>
)
</select>
</mapper>
@@ -13,6 +13,7 @@ import com.renkang.emergency.entity.dto.LargeScreenDataValueDTO;
import com.renkang.emergency.entity.vo.DataDetailVO;
import com.renkang.emergency.entity.vo.MedicalDataVO;
import com.renkang.emergency.largeScreen.entity.dto.TodayEmergencyDTO;
import com.renkang.emergency.largeScreen.entity.vo.OnePersonCaseVO;
import com.renkang.emergency.largeScreen.entity.vo.TodayEmergencyVO;
import java.util.List;
@@ -51,6 +52,10 @@ public interface ILargeScreenDataService extends IService<LargeScreenData> {
CareUserVO careUser(String userId,String workNo);
Boolean isLiaisonUser();
OnePersonCaseVO onePersonCase();
IPage<AnYanHealthCheckNew> queryAnYanHealthCheckNew(AnYanHealthCheckNewParam param);
AnYanEmpHealthCheckNew queryCheckNewByCheckId(String id);
@@ -22,6 +22,7 @@ import com.renkang.emergency.entity.vo.DataDetailVO;
import com.renkang.emergency.entity.vo.MedicalDataVO;
import com.renkang.emergency.largeScreen.entity.dto.TodayEmergencyDTO;
import com.renkang.emergency.largeScreen.entity.vo.LargeScreenMap;
import com.renkang.emergency.largeScreen.entity.vo.OnePersonCaseVO;
import com.renkang.emergency.largeScreen.entity.vo.TodayEmergencyVO;
import com.renkang.emergency.largeScreen.mapper.LargeScreenDataMapper;
import com.renkang.emergency.largeScreen.service.ILargeScreenDataService;
@@ -928,6 +929,26 @@ public class LargeScreenDataServiceImpl extends ServiceImpl<LargeScreenDataMappe
return careUserVO;
}
@Override
public Boolean isLiaisonUser(){
LoginUser loginUser = GlobalUtils.getLoginUser();
String workNo = loginUser.getWorkNo();
if (StrUtil.isEmpty(workNo)) {
return false;
}
return baseMapper.countFirstAiderByWorkNo(workNo) > 0;
}
@Override
public OnePersonCaseVO onePersonCase(){
LoginUser loginUser = GlobalUtils.getLoginUser();
String workNo = loginUser.getWorkNo();
if (StrUtil.isEmpty(workNo)) {
return null;
}
return baseMapper.selectOnePersonCase(workNo);
}
@Override
public IPage<AnYanHealthCheckNew> queryAnYanHealthCheckNew(AnYanHealthCheckNewParam param) {
return anYanClient.queryAnYanHealthCheckNew(param);
@@ -46,7 +46,7 @@ public class EmergencyCloudManager {
// TODO 微服务调用会发送post请求 暂时将 listUserByIds修改为post
list = sysBaseAPI.listUserByIds(listUser);
} catch (Exception e) {
logger.error("微服务调用失败(system listUserByIds)");
logger.error("微服务调用失败(system listUserByIds)", e);
}
if (CollectionUtils.isEmpty(list)) {
return map;
@@ -0,0 +1,65 @@
package com.renkang.emergency.mapper;
import com.renkang.emergency.entity.EmergencyProfessionUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 咨询服务医生表(qh_con_doctor) 查询
* 说明:health-emergency 与 health-consultation 共用同一数据库,
* 但模块间无依赖,故在应急模块内直接查询 qh_con_doctor。
*/
@Mapper
public interface ConDoctorMapper {
/**
* 根据医生id(=应急用户userId)批量查询专家资料
* 仅返回有效医生:del_flag='0' 且 status='1'
*
* @param doctorIds 医生id集合
* @return 以 EmergencyProfessionUser 承载 con_doctor 字段(仅填充新增的 exist=false 字段与 goodAt/school/edu
*/
@Select("<script>" +
"SELECT id AS userId, " +
"doctor_name AS doctorName, doctor_no AS doctorNo, photo, " +
"resource_name AS resourceName, department_id AS departmentId, department_name AS departmentName, " +
"doctor_title AS doctorTitle, doctor_job AS doctorJob, doctor_status AS doctorStatus, " +
"is_accept AS isAccept, type AS doctorType, audio_status AS audioStatus, tf_jump_holiday AS tfJumpHoliday, " +
"experience, good_at AS goodAt, school, e_q AS edu, sort, " +
"tf_recommend AS tfRecommend, introduction " +
"FROM qh_con_doctor " +
"WHERE del_flag = '0' AND status = '1' AND id IN " +
"<foreach collection='doctorIds' item='id' open='(' separator=',' close=')'>#{id}</foreach>" +
"</script>")
List<EmergencyProfessionUser> selectDoctorByIds(@Param("doctorIds") List<String> doctorIds);
/**
* 搜索专家(从 qh_con_doctor 表),排除已在应急专业人员表或已在群中的用户
*
* @param name 医生姓名,模糊匹配
* @param excludeIds 需要排除的用户ID集合(已从 QH_EME_PROFESS_USER 查到 + 已在群中的)
* @return 以 EmergencyProfessionUser 承载结果,基础字段由 SQL 别名映射
*/
@Select("<script>" +
"SELECT id AS userId, doctor_name AS realname, photo AS avatar, " +
"doctor_name AS doctorName, doctor_no AS doctorNo, " +
"resource_name AS resourceName, department_id AS departmentId, department_name AS departmentName, " +
"doctor_title AS doctorTitle, doctor_job AS doctorJob, doctor_status AS doctorStatus, " +
"is_accept AS isAccept, type AS doctorType, audio_status AS audioStatus, tf_jump_holiday AS tfJumpHoliday, " +
"experience, good_at AS goodAt, school, e_q AS edu, sort, " +
"tf_recommend AS tfRecommend, introduction " +
"FROM qh_con_doctor " +
"WHERE del_flag = '0' AND status = '1' " +
"<if test='name != null and name != \"\"'>" +
"AND doctor_name LIKE CONCAT('%', CONCAT(#{name}, '%')) " +
"</if>" +
"<if test='excludeIds != null and excludeIds.size() > 0'>" +
"AND id NOT IN " +
"<foreach collection='excludeIds' item='id' open='(' separator=',' close=')'>#{id}</foreach>" +
"</if>" +
"</script>")
List<EmergencyProfessionUser> searchDoctorByName(@Param("name") String name, @Param("excludeIds") List<String> excludeIds);
}
@@ -18,61 +18,49 @@ import java.util.List;
public interface FirstAidResourceCategoryMapper extends BaseMapper<FirstAidResourceCategory> {
/**
* 查询某分类下的子分类/标签数量(级联删除校验用,含TYPE=1子分类和TYPE=2标签
* 查询某分类下的标签数量(级联删除校验用,TYPE=2)
*/
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND DEL_FLAG = 0")
@Select("SELECT COUNT(*) FROM QH_EME_FIR_AID_RES_CATE WHERE PARENT_ID = #{parentId} AND TYPE = 2 AND DEL_FLAG = 0")
Integer countChildrenByParentId(@Param("parentId") String parentId);
/**
* 查询某分类下的资源数量(级联删除校验用)
*/
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE WHERE CATEGORY_ID = #{categoryId} AND DEL_FLAG = 0")
@Select("SELECT COUNT(*) FROM QH_EME_FIR_AID_RES WHERE CATEGORY_ID = #{categoryId} AND DEL_FLAG = 0")
Integer countResourcesByCategoryId(@Param("categoryId") String categoryId);
/**
* 查询引用某标签的资源数量(标签删除校验用)
*/
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE WHERE DEL_FLAG = 0 AND TAGS LIKE CONCAT('%', #{tagId}, '%')")
@Select("SELECT COUNT(*) FROM QH_EME_FIR_AID_RES WHERE DEL_FLAG = 0 AND CONCAT(',', TAGS, ',') LIKE CONCAT('%,', #{tagId}, ',%')")
Integer countResourcesByTagId(@Param("tagId") String tagId);
/**
* 查询所有有效分类和标签(树形组装用,含parentId
* 按父分类ID查询标签列表(TYPE=2
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE STATUS = 1 AND DEL_FLAG = 0 ORDER BY TYPE ASC, SORT_NO ASC")
List<FirstAidResourceCategory> selectAllCategoriesAndTags();
/**
* 按父ID查询子节点(含子分类TYPE=1和标签TYPE=2
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY TYPE ASC, SORT_NO ASC")
@Select("SELECT * FROM QH_EME_FIR_AID_RES_CATE WHERE PARENT_ID = #{parentId} AND TYPE = 2 AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC")
List<FirstAidResourceCategory> selectByParentId(@Param("parentId") String parentId);
/**
* 按类型查询分类/标签列表
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE TYPE = #{type} AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC, CREATE_TIME DESC")
@Select("SELECT * FROM QH_EME_FIR_AID_RES_CATE WHERE TYPE = #{type} AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC, CREATE_TIME DESC")
List<FirstAidResourceCategory> selectByType(@Param("type") Integer type);
/**
* 按类型+父分类查询标签列表
*/
@Select("<script>" +
"SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE TYPE = #{type} AND STATUS = 1 AND DEL_FLAG = 0" +
"SELECT * FROM QH_EME_FIR_AID_RES_CATE WHERE TYPE = #{type} AND STATUS = 1 AND DEL_FLAG = 0" +
"<if test='parentId != null and parentId != \"\"'> AND PARENT_ID = #{parentId}</if>" +
" ORDER BY SORT_NO ASC, CREATE_TIME DESC" +
"</script>")
List<FirstAidResourceCategory> selectByTypeAndParent(@Param("type") Integer type, @Param("parentId") String parentId);
/**
* 按父分类ID查询子分类列表(树形结构用)
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND TYPE = 1 AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC")
List<FirstAidResourceCategory> selectChildrenByParentId(@Param("parentId") String parentId);
/**
* 查询所有根分类(PARENT_ID IS NULL
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE TYPE = 1 AND PARENT_ID IS NULL AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC")
@Select("SELECT * FROM QH_EME_FIR_AID_RES_CATE WHERE TYPE = 1 AND PARENT_ID IS NULL AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC")
List<FirstAidResourceCategory> selectRootCategories();
}
@@ -19,24 +19,24 @@ public interface FirstAidResourceMapper extends BaseMapper<FirstAidResource> {
/**
* 查询某分类下的资源数量(级联删除校验用)
*/
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE WHERE CATEGORY_ID = #{categoryId} AND DEL_FLAG = 0")
@Select("SELECT COUNT(*) FROM QH_EME_FIR_AID_RES WHERE CATEGORY_ID = #{categoryId} AND DEL_FLAG = 0")
Integer countByCategoryId(@Param("categoryId") String categoryId);
/**
* 浏览计数+1
*/
@Update("UPDATE QH_EME_FIRST_AID_RESOURCE SET VIEW_COUNT = VIEW_COUNT + 1 WHERE ID = #{id}")
@Update("UPDATE QH_EME_FIR_AID_RES SET VIEW_COUNT = VIEW_COUNT + 1 WHERE ID = #{id}")
void incrementViewCount(@Param("id") String id);
/**
* 收藏计数+1
*/
@Update("UPDATE QH_EME_FIRST_AID_RESOURCE SET FAVORITE_COUNT = FAVORITE_COUNT + 1 WHERE ID = #{id}")
@Update("UPDATE QH_EME_FIR_AID_RES SET FAVORITE_COUNT = FAVORITE_COUNT + 1 WHERE ID = #{id}")
void incrementFavoriteCount(@Param("id") String id);
/**
* 收藏计数-1
*/
@Update("UPDATE QH_EME_FIRST_AID_RESOURCE SET FAVORITE_COUNT = FAVORITE_COUNT - 1 WHERE ID = #{id} AND FAVORITE_COUNT > 0")
@Update("UPDATE QH_EME_FIR_AID_RES SET FAVORITE_COUNT = FAVORITE_COUNT - 1 WHERE ID = #{id} AND FAVORITE_COUNT > 0")
void decrementFavoriteCount(@Param("id") String id);
}
@@ -24,7 +24,7 @@ public interface FirstAidResourceUserActionMapper extends BaseMapper<FirstAidRes
* 查询用户对一批资源的收藏状态
*/
@Select("<script>" +
"SELECT RESOURCE_ID FROM QH_EME_FIRST_AID_RESOURCE_USER_ACTION " +
"SELECT RESOURCE_ID FROM QH_EME_FIR_AID_RES_UACT " +
"WHERE USER_ID = #{userId} AND TYPE = 2 " +
"AND RESOURCE_ID IN " +
"<foreach collection='resourceIds' item='id' open='(' separator=',' close=')'>" +
@@ -37,8 +37,8 @@ public interface FirstAidResourceUserActionMapper extends BaseMapper<FirstAidRes
/**
* 查询用户的收藏资源列表(分页)
*/
@Select("SELECT R.* FROM QH_EME_FIRST_AID_RESOURCE R " +
"INNER JOIN QH_EME_FIRST_AID_RESOURCE_USER_ACTION A ON R.ID = A.RESOURCE_ID " +
@Select("SELECT R.* FROM QH_EME_FIR_AID_RES R " +
"INNER JOIN QH_EME_FIR_AID_RES_UACT A ON R.ID = A.RESOURCE_ID " +
"WHERE A.USER_ID = #{userId} AND A.TYPE = 2 AND R.DEL_FLAG = 0 " +
"ORDER BY A.CREATE_TIME DESC")
IPage<FirstAidResource> selectFavoritePage(Page<FirstAidResource> page, @Param("userId") String userId);
@@ -20,12 +20,18 @@ public interface FirstAidResourceVersionMapper extends BaseMapper<FirstAidResour
/**
* 查询某资源的最高版本号
*/
@Select("SELECT MAX(VERSION_NO) FROM QH_EME_FIRST_AID_RESOURCE_VERSION WHERE RESOURCE_ID = #{resourceId}")
@Select("SELECT MAX(VERSION_NO) FROM QH_EME_FIR_AID_RES_VER WHERE RESOURCE_ID = #{resourceId}")
Integer selectMaxVersionNo(@Param("resourceId") String resourceId);
/**
* 查询某资源的所有版本历史(按版本号倒序)
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_VERSION WHERE RESOURCE_ID = #{resourceId} ORDER BY VERSION_NO DESC")
@Select("SELECT * FROM QH_EME_FIR_AID_RES_VER WHERE RESOURCE_ID = #{resourceId} ORDER BY VERSION_NO DESC")
List<FirstAidResourceVersion> selectByResourceId(@Param("resourceId") String resourceId);
/**
* 查询某资源的所有非当前版本ID(按版本号降序),用于清理旧版本
*/
@Select("SELECT ID FROM QH_EME_FIR_AID_RES_VER WHERE RESOURCE_ID = #{resourceId} AND IS_CURRENT = 0 ORDER BY VERSION_NO DESC")
List<String> selectNonCurrentVersionIds(@Param("resourceId") String resourceId);
}
@@ -14,27 +14,17 @@ import java.util.List;
public interface IFirstAidResourceCategoryService extends IService<FirstAidResourceCategory> {
/**
* 查询所有有效分类(扁平,一级分类
*/
List<FirstAidResourceCategory> listCategories();
/**
* 查询所有分类和标签(扁平,通过parentId组装树)
*/
List<FirstAidResourceCategory> listAllCategoriesAndTags();
/**
* 按父ID查询子节点(含子分类和标签,树形展开用)
* 按父分类ID查询标签列表(TYPE=2
*/
List<FirstAidResourceCategory> listByParentId(String parentId);
/**
* 查询根分类列表(树形结构顶层
* 查询所有分类(扁平,无子分类
*/
List<FirstAidResourceCategory> listRootCategories();
/**
* 按父分类ID查询子分类
* 按父分类ID查询标签(树形展开用)
*/
List<FirstAidResourceCategory> listChildrenByParentId(String parentId);
@@ -37,10 +37,10 @@ public interface IFirstAidResourceService extends IService<FirstAidResource> {
/** 审核驳回 */
void reject(String id, String rejectReason);
/** 批量发布 */
/** 批量发布(前置校验:全部审核通过才执行,否则抛异常) */
void batchPublish(List<String> ids);
/** 批量下架 */
/** 批量下架(前置校验:全部已发布才执行,否则抛异常) */
void batchTakeDown(List<String> ids);
IPage<FirstAidResource> queryPageList(FirstAidResource resource, Page<FirstAidResource> page, String userId);
@@ -49,6 +49,12 @@ public interface IFirstAidResourceService extends IService<FirstAidResource> {
FirstAidResource getDetail(String id, String userId);
/** 删除资源(已发布状态不可删除) */
void deleteResource(String id);
/** 批量删除资源(前置校验:全部非已发布才执行,否则抛异常) */
void batchDeleteResources(List<String> ids);
/** 批量转换实体为导出VO(含分类、标签翻译) */
List<FirstAidResourceExportVO> convertToExportVO(List<FirstAidResource> resources);
}
@@ -14,8 +14,14 @@ import com.renkang.emergency.entity.EmergencyCenterAdmin;
import com.renkang.emergency.manager.EmergencyCloudManager;
import com.renkang.emergency.mapper.EmergencyCenterAdminMapper;
import com.renkang.emergency.service.IEmergencyCenterAdminService;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.bean.request.ListUser;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.common.system.vo.SysUserModel;
import org.jeecg.common.util.CheckPasswordUtil;
import org.jeecg.common.util.PasswordUtil;
@@ -37,6 +43,7 @@ import java.util.List;
* @Author YangYuanChen
* @Data 20242024/8/21下午2:06
*/
@Slf4j
@Service
public class EmergencyCenterAdminServiceImpl extends ServiceImpl<EmergencyCenterAdminMapper, EmergencyCenterAdmin> implements IEmergencyCenterAdminService {
@@ -44,6 +51,10 @@ public class EmergencyCenterAdminServiceImpl extends ServiceImpl<EmergencyCenter
private EmergencyCloudManager cloudManager;
@Autowired
private RedisStreamUtil redisStreamUtil;
@Autowired
private IMHelloApi imHelloApi;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Override
public IPage<EmergencyCenterAdmin> customPage(String centerId, String type, String realName, Integer pageNo, Integer pageSize) {
@@ -101,6 +112,21 @@ public class EmergencyCenterAdminServiceImpl extends ServiceImpl<EmergencyCenter
// 修改system
updateUserStream(byId.getUserId(), admin.getRealname());
// 同步IM个人资料
syncImProfile(byId.getUserId());
}
private void syncImProfile(String userId) {
try {
List<LoginUserNew> users = sysBaseAPI.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollUtil.isNotEmpty(users)) {
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
}
} catch (Exception e) {
log.error("同步IM个人资料失败, userId: {}", userId, e);
}
}
@Override
@@ -17,8 +17,14 @@ import com.renkang.emergency.manager.PermissionManager;
import com.renkang.emergency.mapper.EmergencyProfessionUserMapper;
import com.renkang.emergency.mapper.EmergencySchedulesNewItemMapper;
import com.renkang.emergency.service.EmergencyProfessionUserService;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.bean.request.ListUser;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.common.system.vo.SysUserModel;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.redis.RedisStreamKeyEnum;
@@ -27,6 +33,7 @@ import org.jeecg.util.RSAEncryptUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@@ -37,6 +44,7 @@ import java.util.stream.Collectors;
* @author stan
* @since 2024-09-12 09:38:13
*/
@Slf4j
@Service
public class EmergencyProfessionUserServiceImpl extends ServiceImpl<EmergencyProfessionUserMapper, EmergencyProfessionUser> implements EmergencyProfessionUserService {
@@ -48,6 +56,10 @@ public class EmergencyProfessionUserServiceImpl extends ServiceImpl<EmergencyPro
private PermissionManager permissionManager;
@Autowired
private EmergencySchedulesNewItemMapper emergencySchedulesNewItemMapper;
@Autowired
private IMHelloApi imHelloApi;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Override
public IPage<EmergencyProfessionUser> customPage(String name, String centerId, int type, int pageNo, int pageSize) {
@@ -145,6 +157,21 @@ public class EmergencyProfessionUserServiceImpl extends ServiceImpl<EmergencyPro
}
// 修改system
updateUserStream(oldData.getUserId(), data.getRealname(), data.getAvatar());
// 同步IM个人资料
syncImProfile(oldData.getUserId());
}
private void syncImProfile(String userId) {
try {
List<LoginUserNew> users = sysBaseAPI.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollUtil.isNotEmpty(users)) {
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
}
} catch (Exception e) {
log.error("同步IM个人资料失败, userId: {}", userId, e);
}
}
@Override
@@ -2,9 +2,11 @@ package com.renkang.emergency.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.renkang.emergency.bean.response.SendIm;
import com.renkang.emergency.constant.FirstAidConstants;
import com.renkang.emergency.dto.PushBatchSendDTO;
import com.renkang.emergency.dto.PushFilterDTO;
@@ -17,12 +19,17 @@ import com.renkang.emergency.mapper.FirstAidResourceMapper;
import com.renkang.emergency.mapper.FirstAidResourceCategoryMapper;
import com.renkang.emergency.mapper.OrderMapper;
import com.renkang.emergency.service.IFirstAidPushService;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ImGroupMsgTextReqDO;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.global.GlobalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@@ -53,6 +60,15 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
@Autowired
private FirstAidPushServiceImpl self;
@Autowired
private IMHelloApi imHelloApi;
/**
* 文件服务基础URL,用于拼接封面图全路径
*/
@Value("${jeecg.path.show:}")
private String fileBaseUrl;
@Override
public IPage<FirstAidResource> queryResourceList(String orderId, PushFilterDTO filter, Page<FirstAidResource> page) {
LambdaQueryWrapper<FirstAidResource> wrapper = buildFilterWrapper(filter);
@@ -63,14 +79,22 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
// 查询当前工单已推送的资源ID集合
Set<String> pushedIds = getPushedResourceIds(orderId);
// 批量查询分类名称,避免 N+1
List<String> categoryIds = result.getRecords().stream()
.map(FirstAidResource::getCategoryId)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
java.util.Map<String, String> categoryNameMap = CollectionUtil.isEmpty(categoryIds)
? Collections.emptyMap()
: categoryMapper.selectBatchIds(categoryIds).stream()
.collect(Collectors.toMap(FirstAidResourceCategory::getId, FirstAidResourceCategory::getName, (a, b) -> a));
// 填充推送状态 + 分类名称
for (FirstAidResource resource : result.getRecords()) {
resource.setPushed(pushedIds.contains(resource.getId()));
if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId());
if (category != null) {
resource.setCategoryName(category.getName());
}
resource.setCategoryName(categoryNameMap.get(resource.getCategoryId()));
}
}
@@ -78,7 +102,7 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
}
@Override
@Transactional(rollbackFor = Exception.class)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void send(String resourceId, String orderId) {
// 1. 校验资源
FirstAidResource resource = resourceMapper.selectById(resourceId);
@@ -98,35 +122,61 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
throw new RuntimeException("该资源已推送,请勿重复操作");
}
// 3. 获取推送目标
// 3. 获取工单信息
String targetUserId = null;
Order order = null;
if (StrUtil.isNotBlank(orderId)) {
Order order = orderMapper.selectById(orderId);
order = orderMapper.selectById(orderId);
if (order != null) {
targetUserId = order.getSalvageUserId();
}
}
// 4. 保存推送记录
LoginUser loginUser = GlobalUtils.getLoginUser();
FirstAidPushRecord record = new FirstAidPushRecord();
record.setResourceId(resourceId);
record.setResourceTitle(resource.getTitle());
record.setOrderId(orderId);
record.setTargetUserId(targetUserId);
record.setPushBy(GlobalUtils.getLoginUser().getRealname());
record.setPushById(GlobalUtils.getLoginUser().getId());
if (loginUser != null) {
record.setPushBy(loginUser.getRealname());
record.setPushById(loginUser.getId());
}
record.setPushTime(new Date());
pushRecordMapper.insert(record);
// 5. 推送通知(预留,当前记录日志)
log.info("急救宣教推送: resourceId={}, title={}, orderId={}, targetUserId={}, pushById={}, pushByName={}",
resourceId, resource.getTitle(), orderId, targetUserId,
GlobalUtils.getLoginUser().getId(), GlobalUtils.getLoginUser().getRealname());
// 5. 构建推送IM消息并发送
if (order != null && StrUtil.isNotBlank(order.getSessionId())) {
SendIm sendIm = new SendIm();
sendIm.setType(1);
sendIm.setTitle(resource.getTitle());
sendIm.setContent(StrUtil.isNotBlank(resource.getSummary()) ? resource.getSummary() : resource.getTitle());
String coverImage = resource.getCoverImage();
if (StrUtil.isNotBlank(coverImage) && !coverImage.startsWith("http")) {
coverImage = StrUtil.removeSuffix(fileBaseUrl, "/") + "/" + StrUtil.removePrefix(coverImage, "/");
}
sendIm.setMessageImageUrl(coverImage);
sendIm.setResourceId(resourceId);
ImGroupMsgTextReqDO im = new ImGroupMsgTextReqDO();
im.setGroupId(order.getSessionId());
im.setFromUserId(loginUser != null ? loginUser.getId() : "administrator");
im.setContent(JSONObject.toJSON(sendIm).toString());
imHelloApi.sendGroupMsgArticle(im);
log.info("急救宣教推送IM消息已发送: resourceId={}, title={}, orderId={}, sessionId={}, pushBy={}, businessId=article",
resourceId, resource.getTitle(), orderId, order.getSessionId(),
loginUser != null ? loginUser.getRealname() : "unknown");
} else {
log.warn("急救宣教推送IM消息未发送(工单无群聊sessionId: resourceId={}, orderId={}",
resourceId, orderId);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public int batchSend(PushBatchSendDTO dto) {
public Map<String, Object> batchSend(PushBatchSendDTO dto) {
List<String> resourceIds = dto.getResourceIds();
// 方式二:按筛选条件查询
@@ -141,7 +191,7 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
}
int successCount = 0;
List<String> errors = new ArrayList<>();
List<Map<String, String>> errors = new ArrayList<>();
for (String resourceId : resourceIds) {
try {
@@ -149,19 +199,29 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
self.send(resourceId, dto.getOrderId());
successCount++;
} catch (RuntimeException e) {
errors.add(resourceId + "" + e.getMessage());
Map<String, String> err = new HashMap<>();
err.put("resourceId", resourceId);
err.put("message", e.getMessage());
errors.add(err);
}
}
if (!errors.isEmpty()) {
log.warn("批量推送部分失败: {}/{} 条, 失败原因: {}",
successCount, resourceIds.size(), String.join("; ", errors));
successCount, resourceIds.size(), errors);
}
return successCount;
Map<String, Object> result = new HashMap<>();
result.put("total", resourceIds.size());
result.put("success", successCount);
result.put("failed", errors);
return result;
}
private Set<String> getPushedResourceIds(String orderId) {
if (StrUtil.isBlank(orderId)) {
return Collections.emptySet();
}
LambdaQueryWrapper<FirstAidPushRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FirstAidPushRecord::getOrderId, orderId)
.select(FirstAidPushRecord::getResourceId);
@@ -2,6 +2,7 @@ package com.renkang.emergency.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.renkang.emergency.constant.FirstAidConstants;
import org.springframework.transaction.annotation.Transactional;
import com.renkang.emergency.entity.FirstAidResourceCategory;
import com.renkang.emergency.mapper.FirstAidResourceCategoryMapper;
import com.renkang.emergency.mapper.FirstAidResourceMapper;
@@ -26,39 +27,13 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
@Autowired
private FirstAidResourceMapper resourceMapper;
@Override
public List<FirstAidResourceCategory> listCategories() {
List<FirstAidResourceCategory> list = categoryMapper.selectByType(FirstAidConstants.TYPE_CATEGORY);
fillResourceCounts(list);
return list;
}
@Override
public List<FirstAidResourceCategory> listAllCategoriesAndTags() {
List<FirstAidResourceCategory> list = categoryMapper.selectAllCategoriesAndTags();
for (FirstAidResourceCategory item : list) {
if (FirstAidConstants.TYPE_CATEGORY.equals(item.getType())) {
Integer count = resourceMapper.countByCategoryId(item.getId());
item.setResourceCount(count != null ? count : 0);
} else {
Integer count = categoryMapper.countResourcesByTagId(item.getId());
item.setResourceCount(count != null ? count : 0);
}
}
return list;
}
@Override
public List<FirstAidResourceCategory> listByParentId(String parentId) {
// selectByParentId 仅查 TYPE=2(标签),无子分类
List<FirstAidResourceCategory> list = categoryMapper.selectByParentId(parentId);
for (FirstAidResourceCategory item : list) {
if (FirstAidConstants.TYPE_CATEGORY.equals(item.getType())) {
Integer count = resourceMapper.countByCategoryId(item.getId());
item.setResourceCount(count != null ? count : 0);
} else {
Integer count = categoryMapper.countResourcesByTagId(item.getId());
item.setResourceCount(count != null ? count : 0);
}
for (FirstAidResourceCategory tag : list) {
Integer count = categoryMapper.countResourcesByTagId(tag.getId());
tag.setResourceCount(count != null ? count : 0);
}
return list;
}
@@ -117,6 +92,7 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteWithCheck(String id) {
FirstAidResourceCategory record = getById(id);
if (record == null) {
@@ -125,8 +101,8 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
if (FirstAidConstants.TYPE_CATEGORY.equals(record.getType())) {
// 分类:检查是否有标签
Integer childCount = categoryMapper.countChildrenByParentId(id);
if (childCount != null && childCount > 0) {
Integer tagCount = categoryMapper.countChildrenByParentId(id);
if (tagCount != null && tagCount > 0) {
throw new RuntimeException("该分类下存在标签,无法删除");
}
// 分类:检查是否有资源
@@ -17,15 +17,19 @@ import com.renkang.emergency.service.IFirstAidResourceUserActionService;
import com.renkang.emergency.service.IFirstAidResourceVersionService;
import com.renkang.emergency.vo.FirstAidResourceExportVO;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.global.GlobalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -63,6 +67,7 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
resource.setStatus(FirstAidConstants.STATUS_DRAFT);
resource.setAuditStatus(null);
validateResource(resource);
save(resource);
// 保存初始版本,标记为当前版本
versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "初始版本", true);
@@ -81,6 +86,7 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
resource.setStatus(FirstAidConstants.STATUS_DRAFT);
resource.setAuditStatus(null);
resource.setAuditRejectReason(null);
validateResource(resource);
updateById(resource);
// 保存编辑后的新内容为当前版本
versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "编辑更新", true);
@@ -103,11 +109,16 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
copy.setContent(source.getContent());
copy.setCoverImage(source.getCoverImage());
copy.setFileUrl(source.getFileUrl());
copy.setEffectiveTime(source.getEffectiveTime());
copy.setExpiryTime(source.getExpiryTime());
copy.setStatus(FirstAidConstants.STATUS_DRAFT);
copy.setAuditStatus(null);
copy.setViewCount(0);
copy.setFavoriteCount(0);
validateResource(copy);
save(copy);
// 保存初始版本
versionService.saveVersion(copy.getId(), copy.getTitle(), copy.getContent(), "复制自「" + source.getTitle() + "", true);
return copy;
}
@@ -118,6 +129,16 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
if (resource == null) {
throw new RuntimeException("资源不存在");
}
// 已提交审核的不允许重复提交
if (FirstAidConstants.AUDIT_STATUS_PENDING.equals(resource.getAuditStatus())) {
throw new RuntimeException("已提交审核,请勿重复操作");
}
// 仅草稿或已驳回状态可提交审核
if (!FirstAidConstants.STATUS_DRAFT.equals(resource.getStatus())) {
if (!FirstAidConstants.AUDIT_STATUS_REJECTED.equals(resource.getAuditStatus())) {
throw new RuntimeException("当前状态不可提交审核");
}
}
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_PENDING);
resource.setAuditRejectReason(null);
updateById(resource);
@@ -130,6 +151,9 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
if (resource == null) {
throw new RuntimeException("资源不存在");
}
if (!FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("仅已发布状态的资源可下架");
}
resource.setStatus(FirstAidConstants.STATUS_DOWN);
updateById(resource);
}
@@ -157,8 +181,11 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_APPROVED);
resource.setAuditRejectReason(null);
resource.setAuditBy(GlobalUtils.getLoginUser().getRealname());
resource.setAuditById(GlobalUtils.getLoginUser().getId());
LoginUser loginUser = GlobalUtils.getLoginUser();
if (loginUser != null) {
resource.setAuditBy(loginUser.getRealname());
resource.setAuditById(loginUser.getId());
}
resource.setAuditTime(new Date());
updateById(resource);
// 记录审核日志
@@ -174,8 +201,11 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_REJECTED);
resource.setAuditRejectReason(rejectReason);
resource.setAuditBy(GlobalUtils.getLoginUser().getRealname());
resource.setAuditById(GlobalUtils.getLoginUser().getId());
LoginUser loginUser = GlobalUtils.getLoginUser();
if (loginUser != null) {
resource.setAuditBy(loginUser.getRealname());
resource.setAuditById(loginUser.getId());
}
resource.setAuditTime(new Date());
updateById(resource);
// 记录审核日志
@@ -185,14 +215,18 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
@Override
@Transactional(rollbackFor = Exception.class)
public void batchPublish(List<String> ids) {
// 前置校验:全部审核通过才允许批量发布
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource == null) {
continue;
throw new RuntimeException("资源[" + id + "]不存在");
}
if (!FirstAidConstants.AUDIT_STATUS_APPROVED.equals(resource.getAuditStatus())) {
continue;
throw new RuntimeException("资源[" + resource.getTitle() + "]尚未通过审核,无法发布");
}
}
for (String id : ids) {
FirstAidResource resource = getById(id);
resource.setStatus(FirstAidConstants.STATUS_PUBLISHED);
updateById(resource);
}
@@ -201,13 +235,52 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
@Override
@Transactional(rollbackFor = Exception.class)
public void batchTakeDown(List<String> ids) {
// 前置校验:全部已发布才允许批量下架
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource == null) {
throw new RuntimeException("资源[" + id + "]不存在");
}
if (!FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("资源[" + resource.getTitle() + "]非已发布状态,无法下架");
}
}
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource != null) {
resource.setStatus(FirstAidConstants.STATUS_DOWN);
updateById(resource);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteResource(String id) {
FirstAidResource resource = getById(id);
if (resource == null) {
throw new RuntimeException("资源不存在");
}
if (FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("已发布的资源不可删除,请先下架");
}
removeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDeleteResources(List<String> ids) {
// 前置校验:全部非已发布才允许删除
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource == null) {
throw new RuntimeException("资源[" + id + "]不存在");
}
if (FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("资源[" + resource.getTitle() + "]已发布,请先下架再删除");
}
}
for (String id : ids) {
removeById(id);
}
}
@Override
@@ -240,18 +313,19 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
queryWrapper.eq(FirstAidResource::getAuditStatus, resource.getAuditStatus());
}
}
// 标签过滤
// 标签过滤(边界精确匹配,避免 id=2 误匹配 id=12/22
if (StrUtil.isNotBlank(resource.getTags())) {
String[] tagIds = resource.getTags().split(",");
for (String tagId : tagIds) {
if (StrUtil.isNotBlank(tagId)) {
queryWrapper.like(FirstAidResource::getTags, tagId.trim());
queryWrapper.apply("CONCAT(',', TAGS, ',') LIKE CONCAT('%,', {0}, ',%')", tagId.trim());
}
}
}
queryWrapper.eq(FirstAidResource::getDelFlag, CommonConstant.DEL_FLAG_0);
queryWrapper.orderByDesc(FirstAidResource::getCreateTime);
queryWrapper.orderByDesc(FirstAidResource::getId);
IPage<FirstAidResource> result = page(page, queryWrapper);
fillResourceExtras(result.getRecords(), userId);
@@ -301,22 +375,39 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
? userActionService.getFavoritedResourceIds(userId, resourceIds)
: Collections.emptyList();
// 批量收集所有分类ID和标签ID
List<String> allCategoryIds = new ArrayList<>();
for (FirstAidResource resource : resources) {
if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId());
if (category != null) {
resource.setCategoryName(category.getName());
allCategoryIds.add(resource.getCategoryId());
}
if (StrUtil.isNotBlank(resource.getTags())) {
for (String tagId : resource.getTags().split(",")) {
String trimmed = tagId.trim();
if (StrUtil.isNotBlank(trimmed)) {
allCategoryIds.add(trimmed);
}
}
}
}
// 批量查询,避免 N+1
java.util.Map<String, String> nameMap = CollectionUtil.isEmpty(allCategoryIds)
? Collections.emptyMap()
: categoryMapper.selectBatchIds(allCategoryIds).stream()
.collect(Collectors.toMap(FirstAidResourceCategory::getId, FirstAidResourceCategory::getName, (a, b) -> a));
for (FirstAidResource resource : resources) {
if (StrUtil.isNotBlank(resource.getCategoryId())) {
resource.setCategoryName(nameMap.get(resource.getCategoryId()));
}
if (StrUtil.isNotBlank(resource.getTags())) {
List<String> tagNames = new ArrayList<>();
String[] tagIds = resource.getTags().split(",");
for (String tagId : tagIds) {
String trimmedId = tagId.trim();
if (StrUtil.isNotBlank(trimmedId)) {
FirstAidResourceCategory tag = categoryMapper.selectById(trimmedId);
if (tag != null) {
tagNames.add(tag.getName());
for (String tagId : resource.getTags().split(",")) {
String trimmed = tagId.trim();
if (StrUtil.isNotBlank(trimmed)) {
String tagName = nameMap.get(trimmed);
if (tagName != null) {
tagNames.add(tagName);
}
}
}
@@ -326,6 +417,53 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
}
private static final Set<String> VALID_CONTENT_TYPES = new HashSet<>(Arrays.asList(
FirstAidConstants.CONTENT_TYPE_IMAGE_TEXT,
FirstAidConstants.CONTENT_TYPE_VIDEO,
FirstAidConstants.CONTENT_TYPE_AUDIO,
FirstAidConstants.CONTENT_TYPE_PDF
));
/**
* 校验资源的分类和标签引用合法性
*/
private void validateResource(FirstAidResource resource) {
// 校验内容类型
if (StrUtil.isNotBlank(resource.getContentType())
&& !VALID_CONTENT_TYPES.contains(resource.getContentType())) {
throw new RuntimeException("无效的内容类型: " + resource.getContentType());
}
// 校验分类存在且为分类类型
if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId());
if (category == null || !FirstAidConstants.TYPE_CATEGORY.equals(category.getType())) {
throw new RuntimeException("所选分类不存在");
}
}
// 校验标签存在且为标签类型
if (StrUtil.isNotBlank(resource.getTags())) {
String[] parts = resource.getTags().split(",");
Set<String> tagIdSet = new HashSet<>();
for (String p : parts) {
String trimmed = p.trim();
if (StrUtil.isNotBlank(trimmed)) {
tagIdSet.add(trimmed);
}
}
if (CollectionUtil.isNotEmpty(tagIdSet)) {
List<FirstAidResourceCategory> tags = categoryMapper.selectBatchIds(new ArrayList<>(tagIdSet));
if (tags.size() != tagIdSet.size()) {
throw new RuntimeException("所选标签不存在");
}
for (FirstAidResourceCategory tag : tags) {
if (!FirstAidConstants.TYPE_TAG.equals(tag.getType())) {
throw new RuntimeException("标签[" + tag.getName() + "]类型不正确");
}
}
}
}
}
/**
* 计算导出用的状态文本(审核状态优先于发布状态)
*/
@@ -37,23 +37,16 @@ public class FirstAidResourceUserActionServiceImpl extends ServiceImpl<FirstAidR
@Override
@Transactional(rollbackFor = Exception.class)
public boolean recordRead(String resourceId, String userId) {
// 检查是否已有阅读记录
LambdaQueryWrapper<FirstAidResourceUserAction> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FirstAidResourceUserAction::getResourceId, resourceId)
.eq(FirstAidResourceUserAction::getUserId, userId)
.eq(FirstAidResourceUserAction::getType, FirstAidConstants.ACTION_TYPE_READ);
if (ObjectUtil.isNotNull(getOne(queryWrapper, false))) {
return false;
}
// 插入阅读记录
FirstAidResourceUserAction action = new FirstAidResourceUserAction();
action.setResourceId(resourceId);
action.setUserId(userId);
action.setType(FirstAidConstants.ACTION_TYPE_READ);
try {
save(action);
// 浏览计数+1
} catch (Exception e) {
// 重复键异常(兼容DM8/MySQL/PG等不同数据库驱动的异常类型差异)
return false;
}
resourceMapper.incrementViewCount(resourceId);
return true;
}
@@ -61,27 +54,23 @@ public class FirstAidResourceUserActionServiceImpl extends ServiceImpl<FirstAidR
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleFavorite(String resourceId, String userId) {
// 查是否已收藏
LambdaQueryWrapper<FirstAidResourceUserAction> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FirstAidResourceUserAction::getResourceId, resourceId)
.eq(FirstAidResourceUserAction::getUserId, userId)
.eq(FirstAidResourceUserAction::getType, FirstAidConstants.ACTION_TYPE_FAVORITE);
FirstAidResourceUserAction existing = getOne(queryWrapper, false);
if (ObjectUtil.isNotNull(existing)) {
// 已收藏 → 取消收藏
removeById(existing.getId());
resourceMapper.decrementFavoriteCount(resourceId);
return false;
} else {
// 未收藏 → 收藏
FirstAidResourceUserAction action = new FirstAidResourceUserAction();
action.setResourceId(resourceId);
action.setUserId(userId);
action.setType(FirstAidConstants.ACTION_TYPE_FAVORITE);
try {
save(action);
resourceMapper.incrementFavoriteCount(resourceId);
return true;
} catch (Exception e) {
// 重复键异常(兼容DM8/MySQL/PG等不同数据库驱动的异常类型差异)
LambdaQueryWrapper<FirstAidResourceUserAction> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FirstAidResourceUserAction::getResourceId, resourceId)
.eq(FirstAidResourceUserAction::getUserId, userId)
.eq(FirstAidResourceUserAction::getType, FirstAidConstants.ACTION_TYPE_FAVORITE);
remove(queryWrapper);
resourceMapper.decrementFavoriteCount(resourceId);
return false;
}
}
@@ -1,5 +1,6 @@
package com.renkang.emergency.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.renkang.emergency.constant.FirstAidConstants;
@@ -23,6 +24,9 @@ import java.util.List;
@Service
public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidResourceVersionMapper, FirstAidResourceVersion> implements IFirstAidResourceVersionService {
/** 每个资源最多保留的版本数(含当前版本) */
private static final int MAX_VERSIONS = 20;
@Autowired
private FirstAidResourceVersionMapper versionMapper;
@@ -52,6 +56,24 @@ public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidReso
version.setChangeDescription(changeDescription);
version.setIsCurrent(isCurrent);
save(version);
// 清理超出上限的旧版本(当前版本 + 最近 MAX_VERSIONS-1 条非当前版本,其余删除)
cleanOldVersions(resourceId);
}
/**
* 清理超出保留上限的旧版本,保留当前版本 + 最近N条非当前版本
*/
private void cleanOldVersions(String resourceId) {
List<String> nonCurrentIds = versionMapper.selectNonCurrentVersionIds(resourceId);
if (nonCurrentIds.size() <= MAX_VERSIONS - 1) {
return; // 未超上限,无需清理
}
// 保留最近 MAX_VERSIONS-1 条非当前版本,删除其余
List<String> toDelete = nonCurrentIds.subList(MAX_VERSIONS - 1, nonCurrentIds.size());
if (CollUtil.isNotEmpty(toDelete)) {
removeByIds(toDelete);
}
}
@Override
@@ -1,520 +1,4 @@
[
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-08-01 12:43:33",
"updateDate": "2025-08-05 10:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 36,
"sex": "男",
"name": "连龙刚",
"careLiaisonName": "刘一"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-09-05 11:43:33",
"updateDate": "2025-09-05 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 41,
"sex": "男",
"name": "李苏望",
"careLiaisonName": "陈二"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-07-05 11:43:33",
"updateDate": "2025-07-05 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 29,
"sex": "男",
"name": "王先森",
"careLiaisonName": "张三"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2024-09-05 11:43:33",
"updateDate": "2024-09-05 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 50,
"sex": "男",
"name": "阿尔麦江.买买提",
"careLiaisonName": "李四"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-06-05 11:43:33",
"updateDate": "2025-06-05 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 24,
"sex": "女",
"name": "古力娜扎.艾卡尔",
"careLiaisonName": "王五"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2024-09-15 11:43:33",
"updateDate": "2024-09-15 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 37,
"sex": "男",
"name": "梁文斌",
"careLiaisonName": "赵六"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-01-05 11:43:33",
"updateDate": "2025-01-05 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 46,
"sex": "男",
"name": "加海洋",
"careLiaisonName": "孙七"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-09-02 11:43:33",
"updateDate": "2025-09-02 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 41,
"sex": "女",
"name": "周八"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-09-07 11:43:33",
"updateDate": "2025-09-07 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 28,
"sex": "女",
"name": "吴九"
},
{
"id": 10641,
"empId": "f497b917da064874b022e036ddb7804b",
"treatPlanResponse": "",
"dietSuggestionResponse": "",
"sportGuideResponse": "",
"lifeManageResponse": "",
"createDate": "2025-09-06 11:43:33",
"updateDate": "2025-09-06 11:43:33",
"createBy": "13263",
"updateBy": "13263",
"delFlag": "0",
"dataLevel": "0",
"negativeList": "",
"flagFat": 2,
"bmi": 27.02,
"height": 176,
"weight": 81.8,
"bmiCurrent": 26.4,
"hypertension": 1,
"systolicPressureAvg": 106,
"systolicPressureTarget": 120,
"systolicPressureCurrent": 122,
"diastolicPressureAvg": 71,
"diastolicPressureTarget": 90,
"diastolicPressureCurrent": 87,
"diabetes": 1,
"glu": 5.79,
"bloodSugarTarget": "6.1",
"bloodSugarCurrent": 6.2,
"ldl": 0,
"ldlTarget": "3.4",
"ldlCurrent": 0,
"tc": 4.71,
"tcTarget": "5.2",
"tcCurrent": 4.53,
"tg": 0,
"tgTarget": "1.7",
"tgCurrent": 0,
"uricAcid": 0,
"uricAcidTarget": "420",
"uricAcidCurrent": 350,
"homocysteine": 15.04,
"homocysteineTarget": "13.54",
"homocysteineCurrent": 0,
"workUnit": "新疆油田储气库有限公司-机关",
"workUnitId": "103074",
"age": 36,
"sex": "男",
"name": "郑十"
}
]
@@ -7,7 +7,7 @@
-- TYPE: 1=分类, 2=标签
-- 分类支持多级树形PARENT_ID 指向父分类NULL=根分类
-- 标签为全局独立不归属分类
CREATE TABLE "QH_EME_FIRST_AID_RESOURCE_CATEGORY" (
CREATE TABLE "QH_EME_FIR_AID_RES_CATE" (
"ID" VARCHAR2(32) NOT NULL,
"NAME" VARCHAR2(100) NOT NULL,
"TYPE" NUMBER(1) DEFAULT 1 NOT NULL,
@@ -24,26 +24,26 @@ CREATE TABLE "QH_EME_FIRST_AID_RESOURCE_CATEGORY" (
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIRST_AID_RESOURCE_CATEGORY" IS '急救宣教资源分类/标签';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."NAME" IS '名称';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."TYPE" IS '类型:1=分类,2=标签';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."PARENT_ID" IS '父分类ID(分类树形结构,标签为NULL)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."DESCRIPTION" IS '描述';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."SORT_NO" IS '排序号';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."STATUS" IS '状态(1=正常,2=冻结)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."CREATE_BY" IS '创建人';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."CREATE_TIME" IS '创建时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."UPDATE_BY" IS '更新人';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."UPDATE_TIME" IS '更新时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."DEL_FLAG" IS '删除标记(0=正常,1=已删除)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_CATEGORY"."MEMO" IS '备注';
COMMENT ON TABLE "QH_EME_FIR_AID_RES_CATE" IS '急救宣教资源分类/标签';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."NAME" IS '名称';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."TYPE" IS '类型:1=分类,2=标签';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."PARENT_ID" IS '父分类ID(分类树形结构,标签为NULL)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."DESCRIPTION" IS '描述';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."SORT_NO" IS '排序号';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."STATUS" IS '状态(1=正常,2=冻结)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."CREATE_BY" IS '创建人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."CREATE_TIME" IS '创建时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."UPDATE_BY" IS '更新人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."UPDATE_TIME" IS '更新时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."DEL_FLAG" IS '删除标记(0=正常,1=已删除)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_CATE"."MEMO" IS '备注';
CREATE INDEX "IDX_FA_CATEGORY_TYPE" ON "QH_EME_FIRST_AID_RESOURCE_CATEGORY"("TYPE");
CREATE INDEX "IDX_FA_CATEGORY_PARENT_ID" ON "QH_EME_FIRST_AID_RESOURCE_CATEGORY"("PARENT_ID");
CREATE INDEX "IDX_FA_CATEGORY_TYPE" ON "QH_EME_FIR_AID_RES_CATE"("TYPE");
CREATE INDEX "IDX_FA_CATEGORY_PARENT_ID" ON "QH_EME_FIR_AID_RES_CATE"("PARENT_ID");
-- 2. 急救宣教资源表
CREATE TABLE "QH_EME_FIRST_AID_RESOURCE" (
CREATE TABLE "QH_EME_FIR_AID_RES" (
"ID" VARCHAR2(32) NOT NULL,
"TITLE" VARCHAR2(200) NOT NULL,
"SUMMARY" VARCHAR2(500),
@@ -72,40 +72,40 @@ CREATE TABLE "QH_EME_FIRST_AID_RESOURCE" (
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIRST_AID_RESOURCE" IS '急救宣教资源';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."TITLE" IS '标题';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."SUMMARY" IS '摘要/简介(列表展示用)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."CONTENT_TYPE" IS '内容类型:IMAGE_TEXT(图文)/VIDEO(视频)/AUDIO(音频)/PDF(PDF)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."CATEGORY_ID" IS '所属分类ID';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."APPLICABLE_SCENARIO" IS '适用场景';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."TAGS" IS '标签ID列表(逗号分隔)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."CONTENT" IS '正文内容(富文本HTML)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."COVER_IMAGE" IS '封面图URL';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."FILE_URL" IS '附件URL列表(JSON数组)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."EFFECTIVE_TIME" IS '生效时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."EXPIRY_TIME" IS '失效时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."AUDIT_STATUS" IS '审核状态(NULL=无审核/10=待审核/11=已通过/12=已驳回)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."AUDIT_REJECT_REASON" IS '驳回原因';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."AUDIT_BY" IS '审核人';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."AUDIT_TIME" IS '审核时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."VIEW_COUNT" IS '浏览次数';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."FAVORITE_COUNT" IS '收藏次数';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."STATUS" IS '发布状态(0=草稿,1=已发布,2=已下架)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."CREATE_BY" IS '创建人';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."CREATE_TIME" IS '创建时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."UPDATE_BY" IS '更新人';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."UPDATE_TIME" IS '更新时间';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."DEL_FLAG" IS '删除标记(0=正常,1=已删除)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."MEMO" IS '备注';
COMMENT ON TABLE "QH_EME_FIR_AID_RES" IS '急救宣教资源';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."TITLE" IS '标题';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."SUMMARY" IS '摘要/简介(列表展示用)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CONTENT_TYPE" IS '内容类型:IMAGE_TEXT(图文)/VIDEO(视频)/AUDIO(音频)/PDF(PDF)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CATEGORY_ID" IS '所属分类ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."APPLICABLE_SCENARIO" IS '适用场景';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."TAGS" IS '标签ID列表(逗号分隔)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CONTENT" IS '正文内容(富文本HTML)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."COVER_IMAGE" IS '封面图URL';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."FILE_URL" IS '附件URL列表(JSON数组)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."EFFECTIVE_TIME" IS '生效时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."EXPIRY_TIME" IS '失效时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_STATUS" IS '审核状态(NULL=无审核/10=待审核/11=已通过/12=已驳回)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_REJECT_REASON" IS '驳回原因';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_BY" IS '审核人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_TIME" IS '审核时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."VIEW_COUNT" IS '浏览次数';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."FAVORITE_COUNT" IS '收藏次数';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."STATUS" IS '发布状态(0=草稿,1=已发布,2=已下架)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CREATE_BY" IS '创建人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."CREATE_TIME" IS '创建时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."UPDATE_BY" IS '更新人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."UPDATE_TIME" IS '更新时间';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."DEL_FLAG" IS '删除标记(0=正常,1=已删除)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."MEMO" IS '备注';
CREATE INDEX "IDX_FA_RESOURCE_CATEGORY_ID" ON "QH_EME_FIRST_AID_RESOURCE"("CATEGORY_ID");
CREATE INDEX "IDX_FA_RESOURCE_AUDIT_STATUS" ON "QH_EME_FIRST_AID_RESOURCE"("AUDIT_STATUS");
CREATE INDEX "IDX_FA_RESOURCE_EFFECTIVE_TIME" ON "QH_EME_FIRST_AID_RESOURCE"("EFFECTIVE_TIME");
CREATE INDEX "IDX_FA_RESOURCE_CATEGORY_ID" ON "QH_EME_FIR_AID_RES"("CATEGORY_ID");
CREATE INDEX "IDX_FA_RESOURCE_AUDIT_STATUS" ON "QH_EME_FIR_AID_RES"("AUDIT_STATUS");
CREATE INDEX "IDX_FA_RESOURCE_EFFECTIVE_TIME" ON "QH_EME_FIR_AID_RES"("EFFECTIVE_TIME");
-- 3. 用户行为记录表阅读+收藏合一
-- TYPE: 1=阅读, 2=收藏
CREATE TABLE "QH_EME_FIRST_AID_RESOURCE_USER_ACTION" (
CREATE TABLE "QH_EME_FIR_AID_RES_UACT" (
"ID" VARCHAR2(32) NOT NULL,
"RESOURCE_ID" VARCHAR2(32) NOT NULL,
"USER_ID" VARCHAR2(50) NOT NULL,
@@ -114,20 +114,20 @@ CREATE TABLE "QH_EME_FIRST_AID_RESOURCE_USER_ACTION" (
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIRST_AID_RESOURCE_USER_ACTION" IS '用户行为记录(阅读/收藏)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"."USER_ID" IS '用户ID';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"."TYPE" IS '行为类型:1=阅读,2=收藏';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"."CREATE_TIME" IS '创建时间';
COMMENT ON TABLE "QH_EME_FIR_AID_RES_UACT" IS '用户行为记录(阅读/收藏)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."USER_ID" IS '用户ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."TYPE" IS '行为类型:1=阅读,2=收藏';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_UACT"."CREATE_TIME" IS '创建时间';
CREATE UNIQUE INDEX "UK_FA_USER_ACTION"
ON "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"("RESOURCE_ID", "USER_ID", "TYPE");
CREATE INDEX "IDX_FA_USER_ACTION_USER_TYPE" ON "QH_EME_FIRST_AID_RESOURCE_USER_ACTION"("USER_ID", "TYPE");
ON "QH_EME_FIR_AID_RES_UACT"("RESOURCE_ID", "USER_ID", "TYPE");
CREATE INDEX "IDX_FA_USER_ACTION_USER_TYPE" ON "QH_EME_FIR_AID_RES_UACT"("USER_ID", "TYPE");
-- 4. 版本历史表
-- 每次编辑资源时自动保存版本快照支持回滚
CREATE TABLE "QH_EME_FIRST_AID_RESOURCE_VERSION" (
CREATE TABLE "QH_EME_FIR_AID_RES_VER" (
"ID" VARCHAR2(32) NOT NULL,
"RESOURCE_ID" VARCHAR2(32) NOT NULL,
"VERSION_NO" NUMBER(10) NOT NULL,
@@ -139,17 +139,17 @@ CREATE TABLE "QH_EME_FIRST_AID_RESOURCE_VERSION" (
PRIMARY KEY ("ID")
);
COMMENT ON TABLE "QH_EME_FIRST_AID_RESOURCE_VERSION" IS '资源版本历史';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."VERSION_NO" IS '版本号(1,2,3...)';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."TITLE" IS '版本时的标题';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."CONTENT" IS '版本时的正文内容';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."CHANGE_DESCRIPTION" IS '变更描述';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."CREATE_BY" IS '创建人';
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE_VERSION"."CREATE_TIME" IS '创建时间';
COMMENT ON TABLE "QH_EME_FIR_AID_RES_VER" IS '资源版本历史';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."ID" IS '主键';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."RESOURCE_ID" IS '资源ID';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."VERSION_NO" IS '版本号(1,2,3...)';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."TITLE" IS '版本时的标题';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."CONTENT" IS '版本时的正文内容';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."CHANGE_DESCRIPTION" IS '变更描述';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."CREATE_BY" IS '创建人';
COMMENT ON COLUMN "QH_EME_FIR_AID_RES_VER"."CREATE_TIME" IS '创建时间';
CREATE INDEX "IDX_FA_VERSION_RESOURCE_ID" ON "QH_EME_FIRST_AID_RESOURCE_VERSION"("RESOURCE_ID");
CREATE INDEX "IDX_FA_VERSION_RESOURCE_ID" ON "QH_EME_FIR_AID_RES_VER"("RESOURCE_ID");
-- =====================================================
-- 字典数据需在应用启动后执行
@@ -16,7 +16,7 @@
-- 需用户在系统内重新提交审核
-- 如需保留为"待审核"状态将下方的 NULL 改为 10 即可
UPDATE "QH_EME_FIRST_AID_RESOURCE"
UPDATE "QH_EME_FIR_AID_RES"
SET "AUDIT_STATUS" = CASE
WHEN "AUDIT_STATUS" = 0 THEN NULL -- 草稿无法区分是否已提交保守处理
WHEN "AUDIT_STATUS" = 1 THEN 11 -- 已通过
@@ -62,7 +62,7 @@ SELECT
ELSE '未知状态'
END AS "审核状态描述",
COUNT(*) AS "数量"
FROM "QH_EME_FIRST_AID_RESOURCE"
FROM "QH_EME_FIR_AID_RES"
WHERE "DEL_FLAG" = 0
GROUP BY "AUDIT_STATUS"
ORDER BY "AUDIT_STATUS" ASC NULLS FIRST;
@@ -71,7 +71,7 @@ ORDER BY "AUDIT_STATUS" ASC NULLS FIRST;
-- =====================================================
-- 4. 更新表字段注释
-- =====================================================
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."AUDIT_STATUS" IS
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_STATUS" IS
'审核状态(NULL=无审核/10=待审核/11=已通过/12=已驳回)';
COMMIT;
@@ -5,7 +5,7 @@
-- =====================================================
-- 注意: null(草稿) 回退为 0但此时无法区分"草稿""待审核"
UPDATE "QH_EME_FIRST_AID_RESOURCE"
UPDATE "QH_EME_FIR_AID_RES"
SET "AUDIT_STATUS" = CASE
WHEN "AUDIT_STATUS" IS NULL THEN 0 -- 草稿 0(待审核)
WHEN "AUDIT_STATUS" = 10 THEN 0 -- 待审核 0
@@ -18,7 +18,7 @@ WHERE "AUDIT_STATUS" IS NULL OR "AUDIT_STATUS" IN (10, 11, 12);
COMMIT;
-- 恢复表字段注释
COMMENT ON COLUMN "QH_EME_FIRST_AID_RESOURCE"."AUDIT_STATUS" IS
COMMENT ON COLUMN "QH_EME_FIR_AID_RES"."AUDIT_STATUS" IS
'审核状态(0=待审核,1=已通过,2=已驳回)';
COMMIT;
@@ -0,0 +1,505 @@
-- =====================================================
-- 急救宣教资源 - 种子数据达梦数据库
-- 包含分类扁平一层标签归属分类下资源内容
-- 执行方式在达梦客户端中直接执行本脚本
-- 注意请先确认 QH_EME_FIR_AID_RES_CATE QH_EME_FIR_AID_RES 表已创建
-- =====================================================
-- =====================================================
-- 分类数据TYPE=1扁平一层不设二级
-- =====================================================
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT001', '心肺复苏术CPR', 1, NULL, '成人和儿童心肺复苏技术包括胸外按压人工呼吸及AED使用', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT002', '常见急症急救', 1, NULL, '心脏病脑卒中癫痫过敏等突发急症的识别与院前急救', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT003', '创伤急救处理', 1, NULL, '止血包扎固定搬运等创伤急救四大技术', 3, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT004', '意外伤害急救', 1, NULL, '溺水触电气道异物梗阻坠落伤等意外伤害的应急处置', 4, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT005', '环境相关急症', 1, NULL, '中暑失温冻伤高原反应等环境因素所致急症的急救', 5, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT006', '中毒急救', 1, NULL, '食物中毒煤气中毒化学品中毒及蛇虫咬伤的急救处理', 6, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT007', '特殊人群急救', 1, NULL, '老年人孕产妇婴幼儿等特殊群体的急救要点与注意事项', 7, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('CAT008', '灾害事故急救', 1, NULL, '地震火灾交通事故等大规模灾害事故的自救互救知识', 8, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- =====================================================
-- 标签数据TYPE=2PARENT_ID 指向所属分类
-- =====================================================
-- === 心肺复苏术CPR的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG001', '成人CPR', 2, 'CAT001', '成人12岁及以上心肺复苏操作流程与要点', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG002', '儿童与婴儿CPR', 2, 'CAT001', '儿童和婴儿心肺复苏的差异化操作', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG003', 'AED使用', 2, 'CAT001', '自动体外除颤器AED的操作方法和使用场景', 3, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG004', 'AHA指南', 2, 'CAT001', '参考美国心脏协会AHA最新指南编写', 4, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 常见急症急救的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG005', '心脑血管急症', 2, 'CAT002', '急性心肌梗死脑卒中的早期识别与急救', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG006', '过敏与休克', 2, 'CAT002', '严重过敏反应的紧急识别与肾上腺素笔使用', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG007', '突发意识障碍', 2, 'CAT002', '癫痫发作低血糖昏迷晕厥等现场急救', 3, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 创伤急救处理的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG008', '止血与包扎', 2, 'CAT003', '各类外伤出血的止血方法和包扎技术', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG009', '骨折与固定', 2, 'CAT003', '骨折关节脱位的识别及现场固定搬运技术', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG010', '烧烫伤', 2, 'CAT003', '热烧伤化学烧伤电烧伤的急救和创面处理', 3, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 意外伤害急救的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG011', '气道异物梗阻', 2, 'CAT004', '海姆立克急救法各年龄段气道梗阻解除', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG012', '溺水与触电', 2, 'CAT004', '溺水救援触电脱离及继发心脏骤停的急救', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 环境相关急症的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG013', '高温与中暑', 2, 'CAT005', '热痉挛热衰竭热射病的识别与分级急救', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG014', '低温损伤', 2, 'CAT005', '失温低体温症冻伤的分级急救与复温技术', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 中毒急救的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG015', '食物与气体中毒', 2, 'CAT006', '食物中毒一氧化碳中毒煤气中毒的识别与急救', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG016', '动物咬伤与蜇伤', 2, 'CAT006', '蛇咬伤犬咬伤蜂蜇伤蜱虫叮咬的应急处理', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 特殊人群急救的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG017', '老年人急救', 2, 'CAT007', '老年人跌倒突发疾病及用药过量的应急处理', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG018', '婴幼儿急救', 2, 'CAT007', '婴幼儿高热惊厥窒息烫伤跌落等常见意外的急救', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- === 灾害事故急救的标签 ===
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG019', '火灾与地震', 2, 'CAT008', '火灾逃生地震避震及次生灾害的应急处置', 1, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
INSERT INTO QH_EME_FIR_AID_RES_CATE (ID, NAME, TYPE, PARENT_ID, DESCRIPTION, SORT_NO, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES ('TAG020', '交通事故急救', 2, 'CAT008', '道路交通事故现场的检伤分类伤员解救和转运要领', 2, 1, 'admin', SYSDATE, 'admin', SYSDATE, 0);
-- =====================================================
-- 资源数据
-- 审核状态: 11=已通过 状态: 1=已发布
-- CATEGORY_ID 指向分类TAGS 为逗号分隔的标签ID
-- =====================================================
-- ===================== 心肺复苏术CPR =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES001',
'成人心肺复苏标准操作流程2025 AHA指南',
'详细讲解成人心脏骤停的识别胸外按压人工呼吸及AED使用的完整流程依据美国心脏协会最新指南编写',
'IMAGE_TEXT',
'CAT001',
'发现成人突然倒地无反应无呼吸或濒死喘息时',
'TAG001,TAG003,TAG004',
'<h2>确保现场安全</h2><p>施救前先观察环境确认现场对施救者和患者均安全如存在触电火灾有毒气体等危险先将患者转移到安全区域</p><h2>判断意识和呼吸</h2><ul><li>轻拍患者双肩大声呼喊"喂!你怎么了?"</li><li>观察胸部是否有起伏判断呼吸是否正常5-10秒</li><li>无反应 + 无呼吸/濒死喘息 立即启动应急反应系统</li></ul><h2>呼叫急救</h2><ul><li>指定现场某人拨打120并取AED</li><li>如果独自一人先拨打120开免提再开始CPR</li></ul><h2>胸外按压C - Circulation</h2><ul><li><strong>位置</strong>两乳头连线中点胸骨中下段</li><li><strong>手法</strong>双手十指交叉掌根重叠手臂伸直肩肘腕垂直</li><li><strong>深度</strong>5-6厘米</li><li><strong>频率</strong>100-120次/分钟</li><li><strong>回弹</strong>每次按压后让胸廓完全回弹掌根不离胸壁</li><li>按压/放松时间比1:1</li></ul><h2>开放气道A - Airway</h2><ul><li>仰头提颏法一手压前额一手提下颌</li><li>如有可见异物用手指清除</li></ul><h2>人工呼吸B - Breathing</h2><ul><li>捏住患者鼻翼口对口吹气每次约1秒</li><li>见胸廓隆起为有效</li><li>按压:通气 = 30:2</li><li><strong>如不愿或不方便做人工呼吸可只做单纯胸外按压</strong></li></ul><h2>AED到达后</h2><ul><li>立即开机按语音提示操作</li><li>电极片贴放位置右锁骨下 + 左腋前线第5肋间</li><li>电击后立即继续CPR不要耽误</li></ul><h2>持续至</h2><ul><li>患者恢复自主呼吸和意识</li><li>医务人员到达接管</li><li>施救者精疲力竭</li><li>AED提示不得电击且无需CPR</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2856, 423, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES002',
'儿童与婴儿CPR差异化操作要点',
'儿童1-12岁和婴儿<1岁的心肺复苏与成人不同本节详细对比差异帮助家长和幼儿工作者掌握正确的急救方法',
'IMAGE_TEXT',
'CAT001',
'儿童或婴儿突发意识丧失无反应无正常呼吸时',
'TAG002,TAG004',
'<h2>儿童CPR1岁至青春期</h2><h3>按压手法</h3><ul><li><strong>单手或双手</strong>根据儿童体型和施救者力量选择</li><li>单手掌根置于胸骨中下段</li><li>双手同成人手法</li></ul><h3>按压深度</h3><ul><li>至少胸廓前后径的1/3约5厘米</li></ul><h3>按压:通气比</h3><ul><li><strong>单人施救30:2</strong></li><li><strong>双人施救15:2</strong></li></ul><h3>AED</h3><ul><li>1-8岁儿童首选儿童电极片/儿童模式</li><li>没有儿童电极片时可用成人电极片代替</li></ul><hr><h2>婴儿CPR<1岁</h2><h3>判断意识</h3><ul><li>轻拍婴儿足底观察反应</li></ul><h3>按压手法</h3><ul><li><strong>双指法</strong>单人施救时用食指和中指指尖垂直按压</li><li><strong>双手环抱拇指法</strong>双人施救时双手环抱婴儿胸部双拇指并排按压</li></ul><h3>按压位置</h3><ul><li>两乳头连线中点稍下方胸骨中段</li></ul><h3>按压深度</h3><ul><li>至少胸廓前后径的1/3约4厘米</li></ul><h3>人工呼吸</h3><ul><li>口对口鼻共同吹气</li><li>每次吹气量以胸廓微微抬起为准不可过度用力</li></ul><h3>按压:通气比</h3><ul><li>单人30:2</li><li>双人15:2</li></ul><h3>AED</h3><ul><li>婴儿首选手动除颤器</li><li>AED仅在没有手动除颤器且确有必要时使用</li></ul><hr><h2>成人与儿童/婴儿CPR差异速查表</h2><table border="1"><tr><th>项目</th><th>成人</th><th>儿童</th><th>婴儿</th></tr><tr><td>按压深度</td><td>5-6cm</td><td>~5cm</td><td>~4cm</td></tr><tr><td>按压手法</td><td>双手</td><td>单手/双手</td><td>双指/双拇指</td></tr><tr><td>单人按压:通气</td><td>30:2</td><td>30:2</td><td>30:2</td></tr><tr><td>双人按压:通气</td><td>30:2</td><td>15:2</td><td>15:2</td></tr></table>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1678, 312, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES003',
'AED自动体外除颤器完全使用指南',
'AED是什么在哪里能找到怎么用从开机到电击的全流程图解指南AED是抢救心脏骤停的关键设备普通人经过简单学习即可使用',
'IMAGE_TEXT',
'CAT001',
'心脏骤停患者CPR过程中AED到达现场时公共场所发现AED设备时',
'TAG003,TAG004',
'<h2>什么是AED</h2><p>自动体外除颤器Automated External Defibrillator是一种便携式医疗设备可以自动分析心律判断是否需要电击除颤它配有语音提示非专业人员经过简单培训即可安全使用</p><h2>为什么要用AED</h2><ul><li>心脏骤停最常见的初始心律是<strong>心室颤动VF</strong></li><li>电击除颤是终止室颤的唯一有效方法</li><li><strong>每延迟1分钟除颤生存率下降7%-10%</strong></li><li>CPR+AED可将院外心脏骤停生存率提高至50%以上</li></ul><h2>哪里有AED</h2><ul><li>机场火车站地铁站</li><li>大型商场体育场馆</li><li>学校写字楼</li><li>微信搜索"AED"或使用"AED地图"小程序</li></ul><h2>AED操作步骤</h2><h3>第1步开机</h3><ul><li>打开机盖或按下电源键之后全程<strong>按照语音提示操作</strong></li></ul><h3>第2步贴电极片</h3><ul><li>脱去患者胸前衣物擦干皮肤如有水或汗</li><li>将电极片按包装图示贴在裸露的胸前</li><li><strong>位置一</strong>右侧锁骨下方右胸上部</li><li><strong>位置二</strong>左侧腋前线第5肋间左胸下部侧面</li><li>将电极片插头插入主机如已预连则跳过</li></ul><h3>第3步分析心律</h3><ul><li>AED自动分析<strong>任何人不得接触患者</strong></li></ul><h3>第4步电击</h3><ul><li>如需电击AED自动充电并发声警告</li><li>确认<strong>无人接触患者</strong>按下闪烁的放电按钮</li><li>电击后<strong>立即恢复CPR</strong>从胸外按压开始</li></ul><h2>注意事项</h2><ul><li>贴电极片前确认皮肤干燥无过多胸毛</li><li>患者胸部有膏药需撕下并擦净</li><li>体内有起搏器/ICD的患者电极片应避开设备至少2.5厘米</li><li>在水中或金属表面时将患者移至干燥地面再使用</li></ul><h2>AED与CPR配合节奏</h2><p>CPR不间断 AED到达 开机贴片 分析 电击如需 立即CPR 2分钟后AED再次分析 循环</p>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2103, 388, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 常见急症急救 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES004',
'急性心肌梗死的早期识别与院前急救',
'心梗是最常见的致命急症之一本文教您识别心梗的典型和非典型症状以及在救护车到达前如何正确处置',
'IMAGE_TEXT',
'CAT002',
'突感胸痛胸闷出冷汗疑似心脏病发作时',
'TAG005',
'<h2>什么是急性心肌梗死</h2><p>冠状动脉突然堵塞导致心肌缺血坏死黄金抢救时间窗口仅2小时<strong>时间就是心肌时间就是生命</strong></p><h2>典型症状</h2><ul><li><strong>胸痛</strong>胸骨后压榨性闷胀性疼痛持续不缓解>20分钟</li><li><strong>放射痛</strong>疼痛可向左肩左臂内侧颈部下颌背部放射</li><li><strong>伴随症状</strong>出冷汗恶心呕吐呼吸困难濒死感</li></ul><h2>非典型症状尤其警惕</h2><ul><li><strong>女性</strong>可能仅表现为极度疲劳恶心背部或下颌疼痛</li><li><strong>老年人</strong>可能表现为气短乏力意识模糊</li><li><strong>糖尿病患者</strong>可能无痛无痛性心梗仅表现为突发心衰</li></ul><h2>现场急救步骤</h2><ol><li><strong>立即停止一切活动</strong>让患者保持安静取半卧位或坐位切勿平卧</li><li><strong>拨打120</strong>清楚告知"疑似心脏病发作"和具体地址</li><li><strong>服用药物如患者自备</strong>硝酸甘油舌下含服1片5分钟后不缓解可再含1片最多3片阿司匹林嚼服300mg非肠溶片除非有禁忌</li><li><strong>保持气道通畅</strong>解开衣领和腰带</li><li><strong>持续观察</strong>如患者意识丧失无呼吸立即开始CPR</li></ol><h2>禁忌事项</h2><ul><li><strong> 不要让患者自行走动</strong></li><li><strong> 不要自行开车送医院</strong>途中可能发生室颤</li><li><strong> 不要给非患者本人的药物</strong></li><li><strong> 不要用力咳嗽"咳嗽CPR"未被指南推荐</strong></li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 3201, 567, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES005',
'脑卒中中风"120"识别法与急救',
'中风抢救争分夺秒学会"1-2-0"快速识别法及时发现中风前兆"黄金4.5小时"内送医溶栓',
'IMAGE_TEXT',
'CAT002',
'突发面部不对称单侧肢体无力言语不清等疑似中风症状',
'TAG005',
'<h2>认识脑卒中</h2><p>脑卒中分为<strong>缺血性脑梗死占70%</strong><strong>出血性脑出血占30%</strong>无论哪种都需要紧急就医溶栓治疗时间窗仅<strong>4.5小时</strong></p><h2>"中风120"快速识别法</h2><table border="1"><tr><th>数字</th><th>含义</th><th>检查方法</th></tr><tr><td><strong>1</strong></td><td><strong>1</strong>张脸</td><td>让患者微笑观察面部是否对称有无口角歪斜</td></tr><tr><td><strong>2</strong></td><td><strong>2</strong>只胳膊</td><td>让患者平举双臂观察是否有单侧无力或下垂</td></tr><tr><td><strong>0</strong></td><td><strong></strong>听语言</td><td>让患者说一句简单的话观察是否含糊不清或无法表达</td></tr></table><p>以上三项中<strong>任何一项异常</strong>中风可能性约72%应立即拨打120</p><h2>其他警示症状</h2><ul><li>突发剧烈头痛"一生中最剧烈的头痛"</li><li>突发单眼或双眼视力模糊视野缺损</li><li>突发眩晕行走困难平衡失调</li><li>突发意识障碍或晕厥</li></ul><h2>现场急救步骤</h2><ol><li><strong>立即拨打120</strong>记录发病时间必须精确到分钟</li><li>让患者平卧头偏向一侧防呕吐误吸</li><li>解开衣领和腰带保持气道通畅</li><li>如有呕吐清除口腔呕吐物</li><li>安抚患者情绪减少不必要的搬动</li><li>如意识丧失无呼吸立即CPR</li></ol><h2>绝对禁忌</h2><ul><li><strong> 不要给患者喂水喂食喂药</strong>可能吞咽困难导致窒息</li><li><strong> 不要摇晃患者</strong></li><li><strong> 不要掐人中针刺指尖放血等偏方</strong></li><li><strong> 不要让患者自行去厕所</strong></li><li><strong> 不要等待"观察一下"</strong>时间就是大脑</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2567, 445, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES006',
'严重过敏反应与肾上腺素笔使用',
'食物药物蜂蜇等引发的过敏性休克可在数分钟内致命学会识别过敏反应升级信号掌握肾上腺素笔的正确使用方法',
'IMAGE_TEXT',
'CAT002',
'进食服药后突发皮疹喉头水肿呼吸困难血压下降等症状',
'TAG006',
'<h2>认识严重过敏反应</h2><p>严重过敏反应Anaphylaxis是IgE介导的全身性超敏反应<strong>可在数分钟内出现气道阻塞和休克</strong>常见诱因食物花生海鲜鸡蛋药物青霉素昆虫蜇伤乳胶</p><h2>识别过敏反应的升级信号</h2><ul><li><strong>皮肤</strong>荨麻疹潮红瘙痒血管性水肿尤其是口唇眼睑肿胀</li><li><strong>呼吸系统</strong>喉咙发紧声音嘶哑喘鸣呼吸困难</li><li><strong>消化系统</strong>恶心呕吐腹痛腹泻</li><li><strong>心血管系统</strong>头晕心动过速血压下降意识模糊</li><li><strong>注意</strong>皮肤症状可能缺失约10%-20%的严重过敏反应<strong>没有皮疹</strong></li></ul><h2>肾上腺素笔使用指南</h2><p>肾上腺素是唯一能逆转严重过敏反应的药物<strong>确诊后应立即注射刻不容缓</strong></p><h3>操作步骤</h3><ol><li><strong>取下安全帽蓝色</strong>不要触碰橙色针头端</li><li><strong>握拳包裹</strong>橙色端垂直紧贴大腿外侧中部可隔着裤子</li><li><strong>用力下压</strong>听到"咔嗒"声后保持10秒让药液完全注入</li><li><strong>拔出按摩</strong>在注射部位按摩10秒</li><li><strong>拨打120</strong>即使症状缓解也必须就医可能发生迟发性反应</li></ol><h2>重要提示</h2><ul><li>肾上腺素应注射在大腿外侧肌肉最丰厚处<strong>不要注射在臀部或手臂</strong></li><li>如症状无缓解5-15分钟后可注射第二支</li><li>过敏体质者应随身携带2支肾上腺素笔</li><li>不要将肾上腺素笔存放在高温或阳光直射处</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1456, 289, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES007',
'癫痫发作的现场急救正确姿势',
'癫痫发作时许多"常识性"做法其实有害无益本文纠正常见误区告诉您什么该做什么绝对不该做',
'IMAGE_TEXT',
'CAT002',
'身边有人突发全身抽搐意识丧失时',
'TAG007',
'<h2>正确做法4要4不要</h2><h3> 要做的事</h3><ol><li><strong>保持镇静记录发作时间</strong></li><li><strong>清除周围危险物品</strong>桌椅尖锐物火源等</li><li><strong>保护头部</strong>在头下垫柔软物品衣物靠垫</li><li><strong>侧卧位</strong>发作停止后将患者转为侧卧恢复体位清理口腔分泌物</li></ol><h3> 绝对不要做的事</h3><ul><li><strong> 不要往嘴里塞任何东西</strong>包括手指毛巾筷子压舌板患者不会咬断舌头塞物反而可能导致牙齿断裂异物吸入窒息</li><li><strong> 不要强行按压肢体</strong>抽搐时骨骼脆弱强制约束可能导致骨折</li><li><strong> 不要掐人中灌药针刺</strong></li><li><strong> 不要CPR有正常呼吸脉搏时</strong></li></ul><h2>需要拨打120的情况任一满足</h2><ul><li>发作持续超过5分钟<strong>癫痫持续状态</strong></li><li>首次发作从未诊断过癫痫</li><li>在水中发作</li><li>发作停止后意识未恢复</li><li>发作后再次发作</li><li>患者怀孕</li><li>患者受伤</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1876, 267, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 创伤急救处理 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES008',
'外伤出血的快速止血技术',
'严重外伤出血是创伤致死的首要原因掌握直接压迫止血带填塞止血三种方法关键时刻能救命',
'IMAGE_TEXT',
'CAT003',
'切割伤撕裂伤车祸伤等导致的活动性出血',
'TAG008',
'<h2>出血严重程度判断</h2><table border="1"><tr><th>出血类型</th><th>特征</th><th>危险程度</th></tr><tr><td>毛细血管出血</td><td>缓慢渗出血色鲜红</td><td></td></tr><tr><td>静脉出血</td><td>持续流出血色暗红</td><td></td></tr><tr><td>动脉出血</td><td>喷射状随心跳搏动血色鲜红</td><td> 致命</td></tr></table><p>成人失血>500ml = 危险>1000ml = 休克>2000ml = 死亡</p><h2>直接压迫止血法首选</h2><ol><li>用干净纱布/毛巾/衣物直接按压伤口</li><li>持续用力按压至少10分钟<strong>不要中途掀开查看</strong></li><li>如纱布被血液浸透在上面<strong>再加一层</strong>不要更换会破坏已形成的血凝块</li><li>加压包扎固定</li></ol><h2>止血带止血法四肢大出血专用</h2><ul><li><strong>适应症</strong>直接加压无效的四肢大出血肢体离断</li><li><strong>位置</strong>伤口上方5-8厘米靠近心脏一侧避开关节</li><li><strong>力度</strong>以远端动脉搏动消失出血停止为准</li><li><strong>标记</strong>在患者额头或明显位置写上绑扎时间精确到分钟</li><li><strong>时间</strong>不超过2小时每30分钟松解检查1次但需评估再出血风险</li><li>不要用绳索电线铁丝代替</li></ul><h2>填塞止血法交界部位深部伤口</h2><p>适用于腹股沟腋窝颈部等不便使用止血带的部位</p><ol><li>将止血纱布/干净布条塞入伤口深部</li><li>从最深处向外逐层填塞保持压力</li><li>外部加压包扎固定</li></ol><h2>注意事项</h2><ul><li>穿戴手套如有避免直接接触血液</li><li>不要取出嵌入伤口的异物玻璃等固定异物包扎后送医</li><li>怀疑内出血腹胀休克无外伤但血压持续下降立即送医</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2134, 378, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES009',
'骨折与脊柱损伤的现场固定与搬运',
'错误的搬运可能让骨折变为截瘫本节讲解骨折的临时固定方法和脊柱损伤的安全搬运技术',
'IMAGE_TEXT',
'CAT003',
'高处坠落交通事故重物砸伤等可能导致骨折或脊柱损伤的场合',
'TAG009',
'<h2>骨折的识别</h2><ul><li>剧烈疼痛活动时加剧</li><li>局部肿胀瘀斑</li><li>畸形与对侧不对称异常弯曲</li><li>反常活动关节以外部位出现活动</li><li>骨擦音/骨擦感</li><li>功能丧失无法承重/活动</li></ul><h2>骨折固定原则</h2><ul><li><strong>固定范围</strong>骨折部位的上下两个关节</li><li>夹板长度应超过骨折上下关节</li><li>夹板与皮肤间用软物衬垫</li><li>固定松紧适度指尖可触及远端动脉搏动</li><li>开放性骨折<strong>不要将外露的骨端推回</strong>覆盖无菌敷料后固定</li></ul><h2>脊柱损伤的识别高度警惕</h2><ul><li>高处坠落>2米交通事故高速撞击跳水头部撞底</li><li>伤后颈部疼痛肢体麻木/无力大小便失禁</li></ul><h2>脊柱损伤安全搬运</h2><h3>🔴 黄金原则除非有立即生命危险火灾爆炸<strong>绝不要移动疑似脊柱损伤的患者</strong>等待专业急救人员</h3><h3>如必须移动</h3><ol><li>保持头--躯干<strong>呈一直线</strong>轴线翻身</li><li>至少3人协作一人固定头颈一人托躯干一人托下肢</li><li>使用硬质担架/门板/桌面</li><li>颈椎用颈托或沙袋/衣物卷固定头部两侧</li></ol><h2>各部位固定要点</h2><ul><li><strong>前臂骨折</strong>夹板固定后三角巾悬吊于胸前屈肘90°</li><li><strong>上臂骨折</strong>夹板固定后上臂紧贴胸壁前臂三角巾悬吊</li><li><strong>大腿骨折</strong>长夹板从腋下至足跟另用短夹板从大腿内侧至小腿</li><li><strong>小腿骨折</strong>夹板从大腿中部至足跟</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1678, 298, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES010',
'烧烫伤的急救五步法"冲脱泡盖送"',
'烧烫伤是生活中最常见的意外伤害之一记住"冲脱泡盖送"五个字正确处理创面避免二次伤害和留疤',
'IMAGE_TEXT',
'CAT003',
'热液烫伤火焰烧伤热固体烫伤化学烧伤等各类烧烫伤',
'TAG010',
'<h2>急救五步法</h2><h3>最关键的第一反应</h3><ul><li>立即用<strong>流动冷水</strong>冲洗伤处15-30分钟</li><li>水温15-25为宜不要用冰水可能导致血管过度收缩加重损伤</li><li>化学烧伤需冲洗更长时间至少30分钟</li></ul><h3></h3><ul><li>在冷水中<strong>小心去除</strong>覆盖伤处的衣物和饰物戒指手表手镯</li><li>如衣物与皮肤粘连<strong>不要强行撕扯</strong>用剪刀剪开周围衣物</li></ul><h3></h3><ul><li>将伤处浸泡在冷水中持续15-30分钟仅适用于小面积烧伤</li><li>大面积烧伤>10%体表面积不要长时间浸泡可能导致低体温</li></ul><h3></h3><ul><li><strong>清洁无菌的敷料或保鲜膜</strong>覆盖创面</li><li>保鲜膜覆盖可减少疼痛防止感染便于医生观察</li><li>不要用棉花纸巾纤维会粘连创面</li><li>不要涂抹牙膏酱油蛋清香油等偏方</li></ul><h3></h3><ul><li>及时送医小面积浅度烧伤社区医院大面积或深度烧伤烧伤专科</li></ul><h2>烧伤严重程度判断</h2><table border="1"><tr><th>分度</th><th>表现</th><th>处理</th></tr><tr><td>红斑性</td><td>皮肤发红疼痛无水肿和水疱</td><td>自行处理</td></tr><tr><td>水疱性</td><td>大水疱剧痛基底潮红</td><td>门诊处理</td></tr><tr><td></td><td>小水疱疼痛迟钝基底红白相间</td><td>需住院</td></tr><tr><td>焦痂性</td><td>皮肤蜡白或焦黑无痛觉</td><td>需住院手术</td></tr></table><h2>特殊类型烧伤</h2><ul><li><strong>电烧伤</strong>入口小出口大深部组织损伤严重必须就医</li><li><strong>吸入性烧伤</strong>面部烧伤+声音嘶哑+鼻腔有烟灰气道可能已受损立即就医</li><li><strong>化学烧伤</strong>干石灰先刷除再冲洗遇水产生高温</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 3567, 634, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 意外伤害急救 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES011',
'海姆立克急救法气道异物梗阻的救命技术',
'"吃东西噎住了"可能数分钟内致命海姆立克法是解除气道梗阻最有效的方法每个人都应该掌握含成人儿童婴儿及自救四种场景',
'IMAGE_TEXT',
'CAT004',
'进食时突然不能说话呼吸困难手抓喉咙V型手势面色发绀',
'TAG011',
'<h2>识别气道异物梗阻</h2><ul><li><strong>完全梗阻</strong>无法说话无法咳嗽无法呼吸面色发紫双手抓颈部典型的"窒息V手势" <strong>立即海姆立克</strong></li><li><strong>部分梗阻</strong>能说话能咳嗽鼓励患者用力咳嗽不要拍背</li></ul><h2>成人/儿童>1岁海姆立克法</h2><ol><li><strong>站位</strong>双脚前后分开稳定重心</li><li><strong>定位</strong>双手环绕患者腰部一手握拳拳心向内置于<strong>肚脐上方两横指</strong>剑突下</li><li><strong>冲击</strong>另一手包住拳头<strong>向内向上</strong>快速用力冲击</li><li><strong>重复</strong>直到异物排出或患者意识丧失</li></ol><h2>婴儿<1岁急救法</h2><ul><li><strong>不要用海姆立克法</strong>婴儿腹部器官脆弱易损伤</li><li>5次拍背 + 5次胸外按压交替</li></ul><ol><li><strong>拍背</strong>将婴儿面朝下放在前臂上头低于躯干用掌根在肩胛骨之间拍击5次</li><li><strong>胸外按压</strong>翻转婴儿面朝上头低于躯干用两指在胸骨中段按压5次</li><li><strong>检查口腔</strong>如异物可见用小指取出<strong>不要盲目掏挖</strong></li><li>重复5+5循环直到异物排出</li></ol><h2>自救方法</h2><ul><li><strong>腹部冲击法</strong>一手握拳置于肚脐上方另一手包住弯腰后向后上方冲击或利用椅背桌角栏杆等硬物顶压腹部</li><li>立即拨打120如能说话</li></ul><h2>患者意识丧失后怎么办</h2><ul><li>将患者平放地面立即开始CPR</li><li>做人工呼吸前检查口腔如有可见异物取出</li><li>CPR的胸外按压也可产生气道压力有助于排出异物</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 4123, 789, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES012',
'溺水急救水中解救与岸上复苏',
'溺水导致的死亡大多是可以避免的了解正确的水中救援方式和岸上心肺复苏要点不要让自己成为第二个溺水者',
'IMAGE_TEXT',
'CAT004',
'发现有人溺水或游泳过程中发生意外时',
'TAG012',
'<h2>水中解救首要原则保证自身安全</h2><ul><li><strong>非专业人员不要贸然下水</strong></li><li><strong></strong>用竹竿树枝衣物伸向溺水者</li><li><strong></strong>抛投救生圈绳索空塑料桶等漂浮物</li><li><strong></strong>划船靠近</li><li><strong></strong>仅受过专业水中救援训练者可游泳接近从后方接近避免被溺水者抱住</li></ul><h2>岸上急救</h2><h3>判断意识和呼吸</h3><p>将溺水者仰卧于硬质平面判断反应和呼吸</p><h3>如无反应+无正常呼吸 立即CPR</h3><ol><li><strong>先开放气道</strong>清理口腔内泥沙水草等异物</li><li><strong>先给5次人工呼吸</strong>溺水是缺氧性心脏骤停先通气</li><li>然后按<strong>30:2</strong>进行CPR</li><li>AED到达后使用擦干胸前皮肤</li></ol><h2>常见误区纠正</h2><ul><li><strong> 不要"控水"</strong>倒背跑按压腹部排水等溺水者胃里的水不会堵塞气道控水只会延误CPR时间增加呕吐误吸风险</li><li><strong> 不要因为溺水者能说话就掉以轻心</strong>可能发生"二次溺水"延迟性肺水肿</li><li><strong> 所有溺水者即使看起来没事都应送医观察</strong></li></ul><h2>冷水溺水特点</h2><ul><li>冷水<10溺水者可能经长时间CPR后存活</li><li>冷水有脑保护作用不要轻易放弃</li><li>切勿在未确认死亡前停止CPR</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2134, 345, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 环境相关急症 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES013',
'中暑的分级识别与急救处理',
'高温天气下中暑高发热痉挛热衰竭热射病是三个逐步升级的阶段热射病病死率高达50%早期识别和正确降温是关键',
'IMAGE_TEXT',
'CAT005',
'高温天气户外工作运动或密闭高温环境中出现头晕恶心等症状',
'TAG013',
'<h2>中暑的三个阶段</h2><h3>1. 热痉挛轻度</h3><ul><li><strong>表现</strong>大量出汗后出现四肢腹部肌肉痉挛抽筋体温正常或略高</li><li><strong>处理</strong>移至阴凉处补充含盐饮料/运动饮料拉伸痉挛肌肉休息</li></ul><h3>2. 热衰竭中度</h3><ul><li><strong>表现</strong>大汗淋淋面色苍白头晕头痛恶心呕吐心跳加快体温升高38-40血压下降</li><li><strong>处理</strong>移至阴凉通风处松开衣物补充含盐冷饮如能吞咽冷水擦拭扇风降温抬高下肢</li><li>如呕吐或意识改变立即就医</li></ul><h3>3. 热射病重度致命</h3><ul><li><strong>表现</strong>体温>40皮肤干热无汗典型意识障碍谵妄抽搐昏迷</li><li><strong>死亡率</strong>高达50%</li><li><strong>处理</strong><ul><li><strong>立即拨打120</strong></li><li><strong>立即开始降温</strong>争分夺秒不要等待救护车</li><li>冷水浸泡最有效将患者浸入冷水中水温<20不断搅动</li><li>或冰袋置于颈部腋下腹股沟大血管走行处</li><li>持续扇风+喷洒冷水</li><li>降温目标<strong>30分钟内将体温降至<39</strong></li></ul></li></ul><h2>预防要点</h2><ul><li>高温时段11:00-15:00减少户外活动</li><li>每小时饮水500-1000ml少量多次勿等口渴再喝</li><li>补充含盐饮料不要只喝纯水</li><li>穿透气浅色衣物</li><li>切勿将儿童或宠物留在密闭车内</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2345, 412, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES014',
'失温低体温症与冻伤的急救',
'低体温症不仅在极寒环境发生湿冷大风中10也能致命新疆冬季寒冷是失温和冻伤的高风险地区每位职工都应掌握',
'IMAGE_TEXT',
'CAT005',
'寒冷天气户外作业登山受困落水获救后长时间暴露在湿冷阴雨环境中',
'TAG014',
'<h2>低体温症的分级</h2><table border="1"><tr><th>分级</th><th>体温</th><th>表现</th></tr><tr><td>轻度</td><td>32-35</td><td>寒战手脚冰凉精细动作困难</td></tr><tr><td>中度</td><td>28-32</td><td>寒战停止危险信号意识模糊言语不清心跳呼吸减慢</td></tr><tr><td>重度</td><td><28</td><td>昏迷瞳孔散大心跳呼吸极微弱可能被误认为死亡</td></tr></table><h2>低体温症急救步骤</h2><ol><li><strong>脱离冷环境</strong>移到避风保暖处脱去湿衣物</li><li><strong>被动复温</strong>用保温毯睡袋干衣物包裹全身</li><li><strong>主动复温</strong>热水袋/暖宝宝置于<strong>胸部颈部腹股沟</strong>大血管区不要放四肢四肢加热会导致冷血回流心脏引发心律失常</li><li><strong>补充热量</strong>如患者清醒可吞咽给予温热含糖饮料<strong>不要给酒精和咖啡因</strong></li><li><strong>避免剧烈动作</strong>可能诱发室颤</li></ol><h2>急救注意事项</h2><ul><li>判断生命体征要<strong>测60秒</strong>严重低体温时脉搏和呼吸极其缓慢微弱</li><li>对重度低体温者<strong>不要轻易认定为死亡</strong>"Not dead until warm and dead"</li><li>CPR疑似心脏骤停者如无法在10秒内确认脉搏应立即开始CPR</li></ul><h2>冻伤的急救</h2><ul><li><strong>不要用雪搓患处</strong>加重组织损伤</li><li><strong>不要用明火/暖气直接烤</strong>冻伤部位感觉迟钝极易烫伤</li><li><strong>正确复温</strong>将冻伤部位浸泡在<strong>37-42温水</strong>持续15-30分钟水温用健康肢体测试</li><li>复温过程中会出现剧痛和颜色变化变红这是正常现象</li><li>如水疱形成不要刺破</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1456, 267, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 中毒急救 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES015',
'一氧化碳煤气中毒的识别与急救',
'一氧化碳中毒是新疆冬季供暖季最常见的急症之一CO无色无味被称为"沉默杀手"早期识别症状正确处置可避免悲剧',
'IMAGE_TEXT',
'CAT006',
'冬季使用煤炉取暖燃气热水器洗浴在密闭车库发动汽车时',
'TAG015',
'<h2>一氧化碳为什么危险</h2><ul><li>CO与血红蛋白亲和力比O<strong>240倍</strong>空气中极低浓度的CO即可导致严重缺氧</li><li>无色无味无刺激无法靠感官察觉</li><li>中毒早期症状类似"感冒"极易被忽视</li></ul><h2>中毒症状分级</h2><table border="1"><tr><th>程度</th><th>症状</th></tr><tr><td>轻度</td><td>头痛头晕恶心呕吐乏力像感冒</td></tr><tr><td>中度</td><td>剧烈头痛意识模糊嗜睡呼吸急促</td></tr><tr><td>重度</td><td>昏迷抽搐呼吸和循环衰竭大小便失禁</td></tr></table><p>特征性体征<strong>皮肤和黏膜呈樱桃红色</strong>出现时往往已重度中毒</p><h2>急救步骤</h2><ol><li><strong>第一时间开窗通风</strong>不要先做别的</li><li>迅速将患者移到<strong>空气新鲜处</strong>解开衣领和腰带</li><li>拨打120明确告知"一氧化碳中毒"</li><li>如患者无呼吸/心跳立即CPR</li><li><strong>高流量吸氧</strong>由急救人员操作这是最重要的治疗</li></ol><h2>常见误区</h2><ul><li><strong> "屋里放盆水能防煤气中毒"</strong>CO不溶于水完全无效</li><li><strong> "让中毒者喝醋、灌酸菜汤能解毒"</strong>无效且可能导致误吸</li><li><strong> "患者醒了就好了"</strong>CO中毒可能有<strong>迟发性脑病假愈期</strong>必须就医观察</li></ul><h2>预防措施</h2><ul><li>煤炉取暖必须安装排烟管道并定期检查</li><li>燃气热水器严禁安装在浴室或通风不良处</li><li>不在密闭车库发动汽车</li><li>安装<strong>一氧化碳报警器</strong></li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2876, 378, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES016',
'蛇咬伤犬咬伤与蜂蜇伤的应急处理',
'新疆地区野外作业可能遇到毒蛇野犬蜂群不同动物伤害的急救方法截然不同错误处理可能加重后果',
'IMAGE_TEXT',
'CAT006',
'野外作业户外活动时被蛇咬伤犬咬伤或蜂群攻击',
'TAG016',
'<h2>蛇咬伤急救</h2><h3>毒蛇与无毒蛇咬伤的鉴别</h3><ul><li><strong>毒蛇</strong>一对或单个较深牙痕局部肿胀迅速剧痛发黑</li><li><strong>无毒蛇</strong>两排细密牙痕轻微疼痛无全身症状</li><li><strong>不确定时</strong>一律按毒蛇咬伤处理</li></ul><h3>急救步骤</h3><ol><li><strong>保持镇静</strong>制动<strong>减少活动</strong>降低毒素扩散速度</li><li><strong>固定伤肢</strong>用夹板固定保持伤处低于心脏水平</li><li><strong>去除束缚物</strong>摘除戒指手镯手表等局部会迅速肿胀</li><li><strong>清洗伤口</strong>用清水冲洗如有条件</li><li><strong>立即就医</strong>采集/拍下蛇的照片有助于选择抗蛇毒血清</li></ol><h3>绝对禁忌传统做法大多有害</h3><ul><li><strong> 不要用嘴吸毒</strong>吸出量微乎其微反而导致口咽部吸收中毒</li><li><strong> 不要切口放血</strong>增加感染和损伤</li><li><strong> 不要扎止血带</strong>阻断动脉会导致肢体缺血坏死</li><li><strong> 不要冰敷</strong>加重组织损伤</li></ul><h2>犬咬伤急救</h2><ol><li><strong>冲洗</strong>大量流动清水+肥皂水交替冲洗伤口至少15分钟</li><li><strong>消毒</strong>碘伏或75%酒精消毒</li><li><strong>不缝合不包扎</strong>一般开放性伤口不予缝合包扎利于引流</li><li><strong>立即就医</strong>评估是否需要注射狂犬疫苗和破伤风抗毒素</li></ol><h2>蜂蜇伤的急救</h2><h3>局部反应大多数情况</h3><ol><li><strong>拔除毒刺</strong>用指甲或卡片背面沿皮肤<strong>逆刮</strong>不要用镊子夹可能将毒囊内毒液挤入体内</li><li><strong>清洗</strong>肥皂水冲洗</li><li><strong>冷敷</strong>减轻肿胀和疼痛</li></ol><h3>警惕严重过敏反应</h3><ul><li>被蜇后短时间出现全身荨麻疹呼吸困难面部肿胀 过敏性休克</li><li>使用肾上腺素笔如有+ 立即拨打120</li></ul><h3>多处蜇伤>10处</h3><ul><li>即使无过敏反应也需就医蜂毒累积毒性</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1567, 234, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 特殊人群急救 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES017',
'老年人跌倒后的应急处理',
'跌倒是65岁以上老年人伤害死亡的首位原因老人跌倒后盲目扶起可能造成二次伤害正确做法是"先评估、后处理"',
'IMAGE_TEXT',
'CAT007',
'老年人发生跌倒尤其在居家社区活动如厕洗澡时',
'TAG017',
'<h2>跌倒后第一反应不要急于扶起</h2><p>老人跌倒后<strong>保持原位</strong>先自我检查</p><ol><li>有没有剧烈疼痛尤其是髋部腰部</li><li>手脚能不能活动有没有麻木</li><li>有没有出血</li></ol><h2>目击者的处理步骤</h2><h3>1. 先评估不要立即扶起</h3><ul><li>询问疼痛部位能否活动</li><li>检查有无外伤出血</li><li>观察意识状态</li></ul><h3>2. 怀疑骨折/脊柱损伤 不要移动</h3><ul><li><strong>髋部骨折</strong>伤肢短缩外旋"脚不能动了"</li><li><strong>脊柱骨折</strong>腰背部剧痛下肢麻木或不能动</li><li>发现上述症状<strong>立即拨打120</strong>保持老人平卧保暖</li></ul><h3>3. 确认无骨折 安全扶起</h3><ol><li>协助老人翻身侧卧屈膝</li><li>用椅子做支撑手撑椅面缓慢起身</li><li>如老人头晕先坐一会再尝试站立</li></ol><h2>跌倒后最危险的两种情况</h2><h3>髋部骨折</h3><ul><li>被称为"人生最后一次骨折"骨折后长期卧床易引发肺炎压疮血栓一年内死亡率约20%-30%</li><li>必须有手术意识条件允许尽早手术</li></ul><h3>硬膜下血肿</h3><ul><li>跌倒撞头后<strong>数天至数周</strong>才出现症状"迟发性"</li><li>表现为逐渐加重的头痛嗜睡一侧肢体无力反应迟钝</li><li>头部外伤后如出现上述症状<strong>必须就医</strong>做头颅CT</li></ul><h2>预防胜于急救</h2><ul><li>家居防滑浴室铺防滑垫清除地面杂物和电线</li><li>改善照明卧室到卫生间的通道安装夜灯</li><li>适当锻炼太极拳平衡训练可显著降低跌倒风险</li><li>定期检查视力和药物副作用降压药安眠药等</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1987, 356, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES018',
'婴幼儿高热惊厥的正确处置',
'高热惊厥是6个月至5岁婴幼儿最常见的中枢神经系统急症看起来非常吓人但掌握正确处理后可以安全度过',
'IMAGE_TEXT',
'CAT007',
'婴幼儿6个月-5岁发热时突发全身或局部抽搐意识丧失',
'TAG018',
'<h2>什么是高热惊厥</h2><ul><li>通常在体温快速升高阶段发生>38</li><li>好发年龄6个月至5岁高峰18-24个月</li><li>家族遗传倾向明显</li><li><strong>绝大多数是良性的</strong>不会导致脑损伤或智力问题</li></ul><h2>发作时的表现</h2><ul><li>全身僵直或抽搐双眼上翻或凝视</li><li>口吐白沫意识丧失</li><li>约持续1-3分钟一般<strong>5分钟内自行停止</strong></li></ul><h2>家长急救步骤</h2><ol><li><strong>保持镇静</strong>记录发作开始时间</li><li><strong>侧卧位</strong>将孩子侧放防止呕吐物/口水呛入气道</li><li><strong>清除周围危险物</strong>硬物尖锐物</li><li><strong>松开衣物</strong>解开衣领和腰带</li><li><strong>不要往嘴里塞任何东西</strong>包括手指毛巾筷子压舌板</li><li><strong>不要强行按压四肢</strong></li><li>抽搐停止后用退热药对乙酰氨基酚/布洛芬按体重计算剂量</li></ol><h2>什么情况必须立即就医</h2><ul><li>抽搐持续<strong>超过5分钟</strong></li><li>抽搐停止后意识未恢复</li><li>反复抽搐</li><li>抽搐后一侧肢体无力Todd麻痹</li><li>首次发作</li><li>抽搐前有头部外伤史</li></ul><h2>是否会复发</h2><ul><li>约1/3患儿会复发</li><li>首次发作年龄越小发热程度越低<38.5就抽搐直系亲属有惊厥史者复发率更高</li><li>多数孩子5-6岁后不再发作</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2234, 445, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
-- ===================== 灾害事故急救 =====================
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES019',
'火灾现场逃生与烧伤急救',
'火灾死亡中大部分不是烧死而是吸入有毒烟雾窒息了解正确的逃生方法和初期火灾扑救关键时刻能救命',
'IMAGE_TEXT',
'CAT008',
'建筑火灾发生时的逃生自救和初期火灾处置',
'TAG019',
'<h2>火灾致死的主要原因</h2><ul><li>烟雾和有毒气体吸入COHCN等约占火灾死亡人数的<strong>75%</strong></li><li>高温气体吸入导致的气道烧伤</li><li>直接烧伤</li></ul><h2>逃生要诀</h2><h3>如果房间内起火且火势小初期</h3><ul><li>可用灭火器或湿棉被扑灭仅限火势小于垃圾桶大小且你有把握</li><li>电器着火先断电再灭火</li><li>油锅起火盖锅盖不要倒水</li></ul><h3>如果火势蔓延或烟雾较大 立即逃生</h3><ol><li><strong>低姿前进</strong>烟雾和热气向上聚集贴近地面空气相对清洁<strong>湿毛巾捂口鼻</strong>弯腰或匍匐前进</li><li><strong>触摸门把手</strong>开门前用手背试门把手温度烫手说明门外有火<strong>不要开门</strong></li><li><strong>走楼梯不坐电梯</strong></li><li><strong>身上着火</strong>Stop-Drop-Roll用厚重衣物包裹灭火</li></ol><h3>如果被困无法逃离</h3><ul><li>用湿布堵住门缝和通风口</li><li>拨打119报告自己的具体位置楼层房间</li><li>在窗边挥舞鲜艳衣物发出求救信号</li></ul><h2>灭火器使用"提拔我呀"---</h2><ol><li><strong></strong>提起灭火器</li><li><strong></strong>拔掉保险销</li><li><strong></strong>握住喷管前端对准<strong>火焰根部</strong></li><li><strong></strong>压下压把扫射</li></ol><p>站位站在<strong>上风方向</strong>距火源2-3米</p><h2>家庭火灾预防</h2><ul><li>安装烟雾报警器每个卧室+每层至少一个</li><li>制定家庭逃生计划确定集合点</li><li>不超负荷用电不私拉乱接电线</li><li>厨房烹饪不离人楼道勿堆放杂物</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 3234, 567, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES020',
'道路交通事故现场的检伤分类与急救',
'交通事故现场混乱伤者多资源有限了解检伤分类原则和基本急救技术在专业救援到达前做最有效的处置',
'IMAGE_TEXT',
'CAT008',
'道路交通事故尤其是多车相撞多人受伤的复杂事故现场',
'TAG020',
'<h2>现场安全第一</h2><ul><li>将车辆熄火拉手刹</li><li>在来车方向设置警示标志高速路白天150米外夜间250米外</li><li>注意燃油泄漏电线掉落等次生危险</li><li>不要吸烟</li></ul><h2>检伤分类国际通用四色法</h2><table border="1"><tr><th>颜色</th><th>优先级</th><th>标准</th></tr><tr><td>🔴 红色</td><td>第一优先</td><td>有生命危险但可救治气道梗阻张力性气胸大出血</td></tr><tr><td>🟡 黄色</td><td>第二优先</td><td>重伤但可等待骨折无大出血脊柱损伤稳定</td></tr><tr><td>🟢 绿色</td><td>第三优先</td><td>轻伤可步行轻微擦伤软组织挫伤</td></tr><tr><td> 黑色</td><td>最后</td><td>无生命体征或伤势不可救治</td></tr></table><h2>车祸伤员的急救要点</h2><h3>A - 气道管理</h3><ul><li>托下颌开放气道车祸伤员高度怀疑颈椎损伤<strong>不要仰头</strong></li><li>清除口腔血液和异物</li></ul><h3>B - 呼吸评估</h3><ul><li>观察呼吸频率对称性</li><li>张力性气胸呼吸困难+气管偏移+患侧呼吸音消失需紧急穿刺减压</li></ul><h3>C - 循环与止血</h3><ul><li>控制活动性出血</li><li>评估休克皮肤苍白湿冷烦躁不安脉搏细弱加快</li></ul><h3>D - 脊柱保护</h3><ul><li>除非有立即生命危险<strong>不要移动伤员</strong></li><li>如必须移动保持头--躯干轴线不变</li></ul><h2>不要做的事</h2><ul><li><strong> 不要给伤者喝水</strong>可能需紧急手术</li><li><strong> 不要取出插入体内的异物</strong>固定后送医</li><li><strong> 不要将头盔从摩托车手头上摘下</strong>可能加重颈椎损伤</li><li><strong> 不要一个人逞强</strong>分配任务A打120B设置警示C照护伤员</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 1876, 312, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES021',
'地震自救互救"十要点"',
'新疆地处地震带地震风险不容忽视掌握地震中"伏地、遮挡、手抓牢"的避险原则和震后自救互救技能',
'IMAGE_TEXT',
'CAT008',
'地震发生时和震后的自救互救',
'TAG019',
'<h2>地震发生时室内避险</h2><h3>"伏地、遮挡、手抓牢"Drop-Cover-Hold On</h3><ol><li><strong>伏地Drop</strong>立即蹲下降低重心</li><li><strong>遮挡Cover</strong>钻到牢固的桌子下或靠在内墙墙角用双臂/书包/靠垫护住头颈部</li><li><strong>手抓牢Hold On</strong>一只手抓住桌腿等固定物直到晃动停止</li></ol><h3>室内避震"三不"</h3><ul><li><strong>不要站在窗户边</strong>玻璃飞溅</li><li><strong>不要乘坐电梯</strong></li><li><strong>不要躲在吊灯吊扇高大家具下方</strong></li></ul><h2>地震发生时室外避险</h2><ul><li>远离建筑物电线杆广告牌跑到开阔地带</li><li>如果在开车靠边停车避开桥梁高架隧道留在车内</li></ul><h2>震后自救</h2><ul><li>保持镇静不要使用明火可能发生燃气泄漏</li><li>如被埋压<ul><li>清除口鼻灰尘保持呼吸通畅</li><li>用衣物捂住口鼻防止吸入粉尘</li><li><strong>不要大声呼喊</strong>消耗体力和氧气用敲击管道墙壁的方式发出信号</li><li>有规律敲击"三短三长三短"SOS</li></ul></li></ul><h2>震后互救</h2><ol><li>先救近处的容易救的"先易后难"</li><li>救人时注意被埋压者的头部暴露和呼吸道清理</li><li>不可用利器强力刨挖防止二次伤害</li><li>救出后评估伤势并给予基本急救</li></ol><h2>地震应急包建议备好</h2><ul><li>饮用水每人每天4L备3天干粮急救包</li><li>手电筒+备用电池口哨求救用</li><li>防尘口罩保温毯</li><li>重要证件复印件</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 2456, 423, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
INSERT INTO QH_EME_FIR_AID_RES (ID, TITLE, SUMMARY, CONTENT_TYPE, CATEGORY_ID, APPLICABLE_SCENARIO, TAGS, CONTENT, COVER_IMAGE, FILE_URL, EFFECTIVE_TIME, EXPIRY_TIME, AUDIT_STATUS, AUDIT_REJECT_REASON, AUDIT_BY, AUDIT_TIME, VIEW_COUNT, FAVORITE_COUNT, STATUS, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, DEL_FLAG)
VALUES (
'RES022',
'家庭急救箱的配置与使用指南',
'急救箱是每个家庭和车辆的必备品了解急救箱应该有哪些物品如何存放过期如何处理才能在紧急时刻真正发挥作用',
'IMAGE_TEXT',
'CAT004',
'家庭日常备置急救箱及发生轻微伤情时的处理',
'TAG011',
'<h2>基础急救箱清单</h2><h3>包扎材料</h3><ol><li>医用纱布片无菌各种尺寸x 10片</li><li>弹性绷带5cm8cm各2卷</li><li>三角巾 x 2条</li><li>创可贴各种尺寸x 20片</li><li>医用胶带透气型x 1卷</li></ol><h3>消毒用品</h3><ol><li>碘伏棉签/棉球 x 20支</li><li>75%酒精棉片 x 20片</li><li>一次性手套无粉x 5双</li></ol><h3>工具</h3><ol><li>圆头剪刀 x 1把</li><li>镊子 x 1把</li><li>体温计电子或红外x 1支</li><li>安全别针 x 5个</li><li>一次性冰袋捏破即冷型x 2袋</li></ol><h3>药品需根据家庭成员情况调整</h3><ol><li>退热镇痛药对乙酰氨基酚/布洛芬</li><li>抗过敏药氯雷他定/西替利嗪</li><li>止泻药蒙脱石散/口服补液盐</li><li>烫伤膏</li><li>抗生素软膏莫匹罗星</li><li>个人常备药硝酸甘油哮喘吸入剂等</li></ol><h2>急救箱管理要点</h2><ul><li>存放在<strong>阴凉干燥儿童拿不到但成人方便取用</strong>的地方</li><li><strong>3-6个月</strong>检查一次更换过期物品</li><li>车用急救箱与家用分开</li><li>箱外标注最近医院的地址和电话</li><li>在家人中指定一人负责维护</li></ul><h2>急救包的使用禁忌</h2><ul><li><strong>不要给不明原因腹痛者服用止痛药</strong>可能掩盖阑尾炎等急腹症</li><li><strong>不要使用过期消毒液和药品</strong></li><li><strong>不要共用眼药水</strong></li><li>任何开放性伤口确认破伤风疫苗接种在有效期<5年清洁伤<10年污染伤</li></ul>',
NULL, NULL,
DATE '2026-07-01', DATE '2027-12-31',
11, NULL, '系统管理员', SYSDATE, 5432, 1023, 1,
'admin', SYSDATE, 'admin', SYSDATE, 0
);
COMMIT;
-- =====================================================
-- 验证数据
-- =====================================================
-- 查看各分类及其下属标签
SELECT
c."NAME" AS 分类名称,
LISTAGG(t."NAME", '、') WITHIN GROUP (ORDER BY t."SORT_NO") AS 下属标签
FROM QH_EME_FIR_AID_RES_CATE c
LEFT JOIN QH_EME_FIR_AID_RES_CATE t ON t."PARENT_ID" = c."ID" AND t."TYPE" = 2 AND t."DEL_FLAG" = 0
WHERE c."TYPE" = 1 AND c."DEL_FLAG" = 0
GROUP BY c."ID", c."NAME", c."SORT_NO"
ORDER BY c."SORT_NO";
-- 统计
SELECT '分类' AS 统计项, COUNT(*) AS 数量 FROM QH_EME_FIR_AID_RES_CATE WHERE "TYPE" = 1 AND "DEL_FLAG" = 0
UNION ALL
SELECT '标签', COUNT(*) FROM QH_EME_FIR_AID_RES_CATE WHERE "TYPE" = 2 AND "DEL_FLAG" = 0
UNION ALL
SELECT '资源', COUNT(*) FROM QH_EME_FIR_AID_RES WHERE "DEL_FLAG" = 0;
@@ -3,8 +3,10 @@ package com.renkang.im.api;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.renkang.im.api.fallback.IMHelloFallback;
import com.renkang.im.dto.AddGroupMemberDTO;
import com.renkang.im.dto.DeleteGroupMemberDTO;
import com.renkang.im.entity.*;
import com.renkang.im.vo.AddGroupMemberVO;
import com.renkang.im.vo.GetGroupMemberInfoVO;
import com.renkang.im.vo.ImBaseResult;
import org.jeecg.common.api.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
@@ -46,6 +48,9 @@ public interface IMHelloApi {
@PostMapping(value = "/wnapp/sendGroupMsg")
IMGroupMsgResDO sendGroupMsg(@RequestBody @Validated ImGroupMsgTextReqDO imGroupMsgTextReqDO);
@PostMapping(value = "/wnapp/sendGroupMsgArticle")
IMGroupMsgResDO sendGroupMsgArticle(@RequestBody @Validated ImGroupMsgTextReqDO imGroupMsgTextReqDO);
@PostMapping(value = "/wnapp/sendGroupMsgTextNew")
IMGroupMsgResDO sendGroupMsgTextNew(@RequestBody @Validated ImGroupMsgTextReqDO imGroupMsgTextReqDO);
@@ -80,4 +85,20 @@ public interface IMHelloApi {
@GetMapping("/wnapp/destroyGroup")
Result<ImBaseResult> destroyGroup(@RequestParam("groupId") String groupId);
/**
* @description: 获取群成员详细资料
* @author PengJ
* @date 2026/7/7
*/
@GetMapping(value = "/wnapp/getGroupMemberInfo")
Result<GetGroupMemberInfoVO> getGroupMemberInfo(@RequestParam("groupId") String groupId);
/**
* @description: 删除群成员
* @author PengJ
* @date 2025/6/25
*/
@PostMapping(value = "/wnapp/deleteGroupMember")
Result<ImBaseResult> deleteGroupMember(@RequestBody @Validated DeleteGroupMemberDTO dto);
}
@@ -50,5 +50,15 @@ public class AddGroupMemberDTO {
@NotNull(message = "群成员ID不能为空")
@JsonProperty("Member_Account")
private String memberAccount;
/**
* 群成员名称可选仅用于IM账号导入不发送到腾讯IM
*/
private String memberName;
/**
* 群成员头像URL可选仅用于IM账号导入不发送到腾讯IM
*/
private String memberAvatar;
}
}
@@ -0,0 +1,41 @@
package com.renkang.im.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* @author PengJ
* @version 1.0
* @description: 删除群成员dto
* @date 2025/6/25
*/
@NoArgsConstructor
@Data
public class DeleteGroupMemberDTO {
/**
* 要操作的群组必填
*/
@NotNull(message = "群组ID不能为空")
@JsonProperty("GroupId")
private String groupId;
/**
* 是否静默删人
* 0非静默删人
* 1静默删人
* 不填该字段默认为0
*/
@JsonProperty("Silence")
private Integer silence;
/**
* 要删除的群成员ID列表
*/
@JsonProperty("MemberToDel_Account")
private List<String> memberToDelAccount;
}
@@ -0,0 +1,75 @@
package com.renkang.im.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author PengJ
* @version 1.0
* @description: 获取群成员详细资料响应VO
* @date 2026/7/7
*/
@NoArgsConstructor
@Data
public class GetGroupMemberInfoVO {
/**
* 请求处理的结果
* OK表示处理成功
* FAIL表示失败
*/
@JsonProperty("ActionStatus")
private String actionStatus;
/**
* 错误信息
*/
@JsonProperty("ErrorInfo")
private String errorInfo;
/**
* 错误码
* 0表示成功
* 非0表示失败
*/
@JsonProperty("ErrorCode")
private Integer errorCode;
/**
* 本群组的群成员总数
*/
@JsonProperty("MemberNum")
private Integer memberNum;
/**
* 群成员列表
*/
@JsonProperty("MemberList")
private List<MemberInfo> memberList;
@NoArgsConstructor
@Data
public static class MemberInfo {
/**
* 群成员 UserID
*/
@JsonProperty("Member_Account")
private String memberAccount;
/**
* 群内身份Owner/Admin/Member
*/
@JsonProperty("Role")
private String role;
/**
* 入群时间
*/
@JsonProperty("JoinTime")
private Integer joinTime;
}
}
@@ -6,12 +6,14 @@ import com.alibaba.fastjson.JSONObject;
import com.renkang.consultation.api.ConSessionHelloApi;
import com.renkang.consultation.entity.ConSessionDO;
import com.renkang.im.dto.AddGroupMemberDTO;
import com.renkang.im.dto.DeleteGroupMemberDTO;
import com.renkang.im.entity.*;
import com.renkang.im.service.IImMsgRecordAllService;
import com.renkang.im.util.IdWorker;
import com.renkang.im.util.TencentCloudImUtil;
import com.renkang.im.util.UniqId;
import com.renkang.im.vo.AddGroupMemberVO;
import com.renkang.im.vo.GetGroupMemberInfoVO;
import com.renkang.im.vo.ImBaseResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -245,6 +247,14 @@ public class TXController {
}
@Operation(summary = "文章推送群消息(宣教推送专用)", description = "发送方由调用方指定,不覆盖businessID")
@PostMapping("/sendGroupMsgArticle")
public IMGroupMsgResDO sendGroupMsgArticle(@RequestBody @Validated ImGroupMsgTextReqDO imGroupMsgTextReqDO) {
return tencentCloudImUtil.sendGroupMsgArticle(imGroupMsgTextReqDO);
}
@Operation(summary = "用户发送群消息", description = "用户发送群消息")
@PostMapping("/sendGroupMsgTextNew")
public IMGroupMsgResDO sendGroupMsgTextNew(@RequestBody @Validated ImGroupMsgTextReqDO imGroupMsgTextReqDO) {
@@ -329,7 +339,7 @@ public class TXController {
groupList.add(hashMap);
});
String result = tencentCloudImUtil.creatGroup(Owner_Account, Type, GroupId, conSessionRequestDO.getName() + "_" + DateUtils.getCurrentTimestamp(), groupList);
String result = tencentCloudImUtil.creatGroup(Owner_Account, Type, GroupId, "应急求助", groupList);
if (!JSONObject.parseObject(result).get("ErrorCode").equals(0)) {
return Result.error("创建群聊失败!");
}
@@ -380,7 +390,7 @@ public class TXController {
groupList.add(hashMap);
});
String result = tencentCloudImUtil.creatGroup(Owner_Account, Type, GroupId, conSessionRequestDO.getName() + "_" + DateUtils.getCurrentTimestamp(), groupList);
String result = tencentCloudImUtil.creatGroup(Owner_Account, Type, GroupId, "应急求助", groupList);
if (!JSONObject.parseObject(result).get("ErrorCode").equals(0)) {
return Result.error("创建群聊失败!");
}
@@ -398,12 +408,24 @@ public class TXController {
return Result.OK(tencentCloudImUtil.addGroupMember(dto));
}
@Operation(summary = "删除群成员", description = "删除群成员")
@PostMapping("/deleteGroupMember")
public Result<ImBaseResult> deleteGroupMember(@RequestBody @Validated DeleteGroupMemberDTO dto) {
return Result.OK(tencentCloudImUtil.deleteGroupMember(dto));
}
@Operation(summary = "解散群组", description = "解散群组")
@GetMapping("/destroyGroup")
public Result<ImBaseResult> destroyGroup(@RequestParam("groupId") String groupId) {
return Result.OK(tencentCloudImUtil.destroyGroup(groupId));
}
@Operation(summary = "获取群成员详细资料", description = "获取群成员详细资料")
@GetMapping("/getGroupMemberInfo")
public Result<GetGroupMemberInfoVO> getGroupMemberInfo(@RequestParam("groupId") String groupId) {
return Result.OK(tencentCloudImUtil.getGroupMemberInfo(groupId));
}
private String encodeChineseCharacters(String str) {
return str.codePoints()
.mapToObj(c -> {
@@ -11,8 +11,10 @@ import com.renkang.im.ImSendEnum;
import com.renkang.im.common.TencentCloudImApiConstant;
import com.renkang.im.common.TencentCloudImConstant;
import com.renkang.im.dto.AddGroupMemberDTO;
import com.renkang.im.dto.DeleteGroupMemberDTO;
import com.renkang.im.entity.*;
import com.renkang.im.vo.AddGroupMemberVO;
import com.renkang.im.vo.GetGroupMemberInfoVO;
import com.renkang.im.vo.ImBaseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
@@ -46,10 +48,12 @@ public class TencentCloudImUtil {
@Value("${IMConfig.requestUrl}")
private String HTTPS_URL_PREFIX;
private static final String APP_MANAGER = "administrator";
private static final String REDIS_IM_USER_SIG = "silence:im_user_sig:";
private static final String REDIS_IM_USER_SIG_PREFIX = "silence:im_user_sig:";
@Value("${IMConfig.sdkAppId}")
private long sdkAppId;
@Value("${IMConfig.secretKey}")
private String secretKey;
@Autowired
private TximUtil tximUtil;
@@ -71,23 +75,37 @@ public class TencentCloudImUtil {
return System.currentTimeMillis() + "" + StrUtil.subSuf(Math.random() + "", 2);
}
/**
* 获取IM配置指纹用于区分不同配置的缓存
* sdkAppId secretKey 变更时指纹变化旧缓存自动失效
*/
private String getConfigFingerprint() {
return Integer.toHexString(Objects.hash(sdkAppId, secretKey));
}
private String buildUserSigKey(String identifier) {
return REDIS_IM_USER_SIG_PREFIX + getConfigFingerprint() + ":" + identifier;
}
/**
* 获取腾讯云用户签名
*/
public String getTxCloudUserSig() {
String userSig = (String) redisServiceUtil.get(REDIS_IM_USER_SIG + APP_MANAGER);
String key = buildUserSigKey(APP_MANAGER);
String userSig = (String) redisServiceUtil.get(key);
if (StringUtils.isEmpty(userSig)) {
userSig = tximUtil.genUserSig(APP_MANAGER);
redisServiceUtil.set(REDIS_IM_USER_SIG + APP_MANAGER, userSig, 86400L);
redisServiceUtil.set(key, userSig, 86400L);
}
return userSig;
}
public String userSigAndroid(String userId) {
String userSig = (String) redisServiceUtil.get(REDIS_IM_USER_SIG + userId);
String key = buildUserSigKey(userId);
String userSig = (String) redisServiceUtil.get(key);
if (StringUtils.isEmpty(userSig)) {
userSig = tximUtil.genUserSig(userId);
redisServiceUtil.set(REDIS_IM_USER_SIG + userId, userSig, 86400L);
redisServiceUtil.set(key, userSig, 86400L);
}
return userSig;
}
@@ -156,6 +174,22 @@ public class TencentCloudImUtil {
return result;
}
/**
* 导入多个账号带昵称
*
* @param accounts 账号信息列表每个元素包含 UserID 和可选的 Nick
*/
public String importAccountsWithNames(List<JSONObject> accounts) {
Integer random = RandomUtils.nextInt(0, 999999999);
String httpsUrl = getHttpsUrl(TencentCloudImApiConstant.AccountManage.MULTI_ACCOUNT_IMPORT, random);
JSONObject jsonObject = new JSONObject();
jsonObject.put("AccountList", accounts);
log.info("腾讯云im导入多个账号,请求参数:{}", jsonObject);
String result = HttpUtil.doPost(httpsUrl, jsonObject);
log.info("腾讯云im导入多个账户,返回结果:{}", result);
return result;
}
/**
* 删除账号
*
@@ -270,6 +304,63 @@ public class TencentCloudImUtil {
}
/**
* 发送文章推送群消息宣教推送专用
* <p>
* sendGroupMsgText 的区别
* 1. From_Account 由调用方指定不写死为 administrator
* 2. 不覆盖 businessID保留调用方传入的业务标识
* 3. CloudCustomData.fromAdmin 根据实际发送人动态设置
*
* @param req 消息请求
* @return IMGroupMsgResDO
*/
public IMGroupMsgResDO sendGroupMsgArticle(ImGroupMsgTextReqDO req) {
IMGroupMsgResDO res = new IMGroupMsgResDO();
String url = HTTPS_URL_PREFIX + "v4/group_open_http_svc/send_group_msg";
String fromUserId = req.getFromUserId();
boolean isAdmin = "administrator".equals(fromUserId);
JSONObject param = new JSONObject();
param.put("GroupId", req.getGroupId());
param.put("MsgPriority", "High");
param.put("Random", this.getRandom());
param.put("From_Account", fromUserId);
param.put("MsgRandom", this.getRandomNum(8));
param.put("MsgTimeStamp", DateUtils.dateToUnixLong(new Date()));
param.put("CloudCustomData", new JSONObject().fluentPut("fromAdmin", isAdmin).toJSONString());
JSONObject jj = JSONObject.parseObject(req.getContent());
// 保持与 sendGroupMsgText 一致的协议字段客户端依赖 businessID 识别消息类型
jj.put("businessID", "article");
try {
JSONObject item = new JSONObject();
JSONObject msgContentData = new JSONObject();
msgContentData.put("Data", jj.toString());
item.put("MsgType", ImSendEnum.TIMCustomElem.getValue());
item.put("MsgContent", msgContentData);
JSONArray msgBody = new JSONArray();
msgBody.add(item);
param.put("MsgBody", msgBody);
log.info("文章推送请求参数: GroupId={}, From_Account={}, content={}, CloudCustomData={}",
req.getGroupId(), fromUserId, jj.toString(),
param.getString("CloudCustomData"));
String jsonStr = HttpUtil.doPost(buildRequestUrl(url), param.toString());
if (StringUtils.isNotEmpty(jsonStr)) {
JSONObject jsonObj = JSONObject.parseObject(jsonStr);
if (jsonObj.containsKey("ErrorCode") && jsonObj.getInteger("ErrorCode") == 0) {
log.info("=========={}==========", "文章推送发送成功!");
}
res = JsonUtil.toObject(jsonStr, IMGroupMsgResDO.class);
}
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
/**
* 默认单发发群文本消息
*
@@ -461,13 +552,36 @@ public class TencentCloudImUtil {
*/
public AddGroupMemberVO addGroupMember(AddGroupMemberDTO dto) {
try {
// 先导入账号及其名称确保成员在IM系统中已注册
if (dto.getMemberList() != null && !dto.getMemberList().isEmpty()) {
// 构建腾讯IM新版 AccountList 格式含昵称和头像
List<JSONObject> accountList = dto.getMemberList().stream().map(member -> {
JSONObject account = new JSONObject();
account.put("UserID", member.getMemberAccount());
if (StringUtils.isNotEmpty(member.getMemberName())) {
account.put("Nick", member.getMemberName());
}
if (StringUtils.isNotEmpty(member.getMemberAvatar())) {
account.put("FaceUrl", member.getMemberAvatar());
}
return account;
}).collect(Collectors.toList());
importAccountsWithNames(accountList);
}
Integer random = RandomUtils.nextInt(0, 999999999);
String httpsUrl = getHttpsUrl(TencentCloudImApiConstant.GroupManage.ADD_GROUP_MEMBER, random);
String jsonString = objectMapper.writeValueAsString(dto);
JSONObject jsonObject = JSONObject.parseObject(jsonString);
// JSONObject jsonObject = new JSONObject();
// jsonObject.put("GroupId", dto.getGroupId());
// jsonObject.put("MemberList", dto.getMemberList());
// 移除内部字段腾讯IM API 不接受
JSONArray memberList = jsonObject.getJSONArray("MemberList");
if (memberList != null) {
for (int i = 0; i < memberList.size(); i++) {
JSONObject member = memberList.getJSONObject(i);
member.remove("memberName");
member.remove("memberAvatar");
}
}
log.info("腾讯云im添加群成员,请求参数:{}", jsonObject);
String result = HttpUtil.doPost(httpsUrl, jsonObject);
log.info("腾讯云im添加群成员,返回结果:{}", result);
@@ -505,6 +619,54 @@ public class TencentCloudImUtil {
}
/**
* 获取群成员详细资料
*
* @param groupId 群组ID
* @return GetGroupMemberInfoVO
*/
public GetGroupMemberInfoVO getGroupMemberInfo(String groupId) {
try {
Integer random = RandomUtils.nextInt(0, 999999999);
String httpsUrl = getHttpsUrl(TencentCloudImApiConstant.GroupManage.GET_GROUP_MEMBER_INFO, random);
JSONObject jsonObject = new JSONObject();
jsonObject.put("GroupId", groupId);
log.info("腾讯云im【获取群成员详细资料】,请求参数:{}", jsonObject);
String result = HttpUtil.doPost(httpsUrl, jsonObject);
log.info("腾讯云im【获取群成员详细资料】,返回结果:{}", result);
return JSONObject.toJavaObject(JSONObject.parseObject(result), GetGroupMemberInfoVO.class);
} catch (Exception e) {
log.error("【获取群成员详细资料】异常:{}", e.getMessage());
GetGroupMemberInfoVO vo = new GetGroupMemberInfoVO();
vo.setActionStatus("FAIL");
return vo;
}
}
/**
* 删除群成员
*
* @param dto 删除群成员请求
* @return ImBaseResult
*/
public ImBaseResult deleteGroupMember(DeleteGroupMemberDTO dto) {
try {
Integer random = RandomUtils.nextInt(0, 999999999);
String httpsUrl = getHttpsUrl(TencentCloudImApiConstant.GroupManage.DELETE_GROUP_MEMBER, random);
String jsonString = objectMapper.writeValueAsString(dto);
JSONObject jsonObject = JSONObject.parseObject(jsonString);
log.info("腾讯云im【删除群成员】,请求参数:{}", jsonObject);
String result = HttpUtil.doPost(httpsUrl, jsonObject);
log.info("腾讯云im【删除群成员】,返回结果:{}", result);
return JSONObject.toJavaObject(JSONObject.parseObject(result), ImBaseResult.class);
} catch (Exception e) {
log.error("【删除群成员】异常:{}", e.getMessage());
ImBaseResult result = new ImBaseResult();
result.setActionStatus("FAIL");
return result;
}
}
/**
* 拉取群聊历史消息
*
@@ -2,6 +2,7 @@ package com.renkang.watch;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.renkang.watch.vo.WatchH5IndexItemVo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -48,6 +49,9 @@ public class WatchH5IndexItemNewVo {
@Schema(title = "最高阈值")
private String warnMax;
@JsonIgnore
private String shortDurationTotal;//睡眠数据乱七八糟的先加个字段处理app首页数据问题
public String getDataAvg() {
if(StrUtil.isNotEmpty(dataAvg)){
try {
@@ -52,4 +52,18 @@ public class AbnormalEvent extends BaseUser {
private Date bindDate;
@Schema(title = "重点指标用户(0:非重点指标用户 1:大病异常 2:慢病异常 3:指标异常)")
private List<WatchKeyUser> keyUserList;
/**
* 事件ID用于告警事件与GPS位置更新关联的唯一标识
*/
@Excel(name = "事件ID", width = 32, orderNum = "12")
@Schema(title = "事件ID")
private String eventId;
/**
* 定位时间手表定时上报的GPS定位时间
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "定位时间", format = "yyyy-MM-dd HH:mm:ss", width = 20, orderNum = "13")
@Schema(title = "定位时间")
private Date locateTime;
}
@@ -151,6 +151,20 @@ public class WatchMonitorData implements Serializable {
@Excel(name = "部门编码", width = 15)
@Schema(title = "部门编码")
private java.lang.String orgCode;
/**
* 事件ID用于告警事件与GPS位置更新关联的唯一标识
*/
@Excel(name = "事件ID", width = 32)
@Schema(title = "事件ID")
private java.lang.String eventId;
/**
* 定位时间手表上报的GPS定位时间定时上报场景使用
*/
@Excel(name = "定位时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(title = "定位时间")
private java.util.Date locateTime;
@Schema(title = "员工编号")
@TableField(exist = false)
@@ -45,4 +45,15 @@ public class WatchNotifyVo implements Serializable {
private Date warnTime;
private String gpsErrorMsg;
/**
* 事件ID用于告警事件与GPS位置更新关联的唯一标识
*/
private String eventId;
/**
* 定位时间手表上报的GPS定位时间定时上报场景使用
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date locateTime;
}
@@ -899,7 +899,7 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
// 构建数据类型 异步查询 Future映射消除 switch-case 冗余
Map<String, CompletableFuture<WatchH5IndexItemNewVo>> futureMap = new HashMap<>();
futureMap.put("sleep", CompletableFuture.supplyAsync(() -> latestSleepData(userId), getAsyncTaskExecutor()));
futureMap.put("sleep", CompletableFuture.supplyAsync(() -> latestIndexSleepData(userId), getAsyncTaskExecutor()));
futureMap.put("heart_rate", CompletableFuture.supplyAsync(() -> latestHeartRateData(userId), getAsyncTaskExecutor()));
futureMap.put("spo2", CompletableFuture.supplyAsync(() -> latestSpo2Data(userId), getAsyncTaskExecutor()));
futureMap.put("stress", CompletableFuture.supplyAsync(() -> latestStressData(userId), getAsyncTaskExecutor()));
@@ -963,7 +963,7 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
});
}
private WatchH5IndexItemNewVo latestSleepData(String userId) {
private WatchH5IndexItemNewVo latestIndexSleepData(String userId) {
MPJLambdaWrapper<WatchStatUserInfoDaySleep> wrapper = new MPJLambdaWrapper<>();
//因为睡眠详情页取的是长睡时长作为当天的睡眠时间 , 所以这里保持一致并没有取当天的睡眠总时长
wrapper.selectAs(WatchStatUserInfoDaySleep::getLongDurationTotal,WatchH5IndexItemNewVo::getDataValue);
@@ -971,10 +971,19 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
wrapper.selectAs(WatchStatUserInfoDaySleep::getLightSleepTimes,WatchH5IndexItemNewVo::getDataMin);
wrapper.selectAs(WatchStatUserInfoDaySleep::getSleepAvg,WatchH5IndexItemNewVo::getDataAvg);
wrapper.selectAs(WatchStatUserInfoDaySleep::getLastUploadTime,WatchH5IndexItemNewVo::getDataDateNew);
wrapper.selectAs(WatchStatUserInfoDaySleep::getShortDurationTotal,WatchH5IndexItemNewVo::getShortDurationTotal);
wrapper.eq(WatchStatUserInfoDaySleep::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDaySleep::getDataDate);
wrapper.last("FETCH FIRST 1 ROWS ONLY");
return watchStatUserInfoDaySleepMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
WatchH5IndexItemNewVo vo = watchStatUserInfoDaySleepMapper.selectJoinOne(WatchH5IndexItemNewVo.class, wrapper);
// 长睡(dataValue)为空或0 当天可能仅有午睡(短睡),用短睡总时长补齐 dataValue
if (vo != null
&& (StrUtil.isEmpty(vo.getDataValue()) || "0".equals(vo.getDataValue()))
&& StrUtil.isNotEmpty(vo.getShortDurationTotal())
&& !"0".equals(vo.getShortDurationTotal())) {
vo.setDataValue(vo.getShortDurationTotal());
}
return vo;
}
private WatchH5IndexItemNewVo latestHeartRateData(String userId) {
@@ -44,6 +44,21 @@ public class WatchApiController {
return watchMonitorDataService.notice(json);
}
/**
* 更新告警事件GPS位置信息接口
* 根据 watchNo + eventId 联合定位已有告警数据
* 仅更新GPS相关字段不推送大屏与短信
* 内部会调用高德接口进行GPS84GCJ02坐标转换与地址反查
*
* @param json 入参watchNoeventIdlonlataddresslocateTimegpsErrorMsg
* @return 处理结果
*/
@PostMapping("/updateMonitorGps")
@Operation(summary = "更新告警事件GPS位置信息")
public AjaxJson updateMonitorGps(@RequestBody JSONObject json) {
return watchMonitorDataService.updateMonitorGps(json);
}
@GetMapping("/setData")
public AjaxJson setData() {
AjaxJson ajaxJson = new AjaxJson();
@@ -38,6 +38,17 @@ public interface IWatchMonitorDataService extends IService<WatchMonitorData> {
*/
AjaxJson notice(JSONObject jsonObject);
/**
* 更新告警事件GPS位置信息
* 根据 watchNo + eventId 联合定位已有告警数据
* 缺失任一定位字段则忽略已存在GPS信息则忽略
* 仅更新数据不推送大屏与短信
*
* @param jsonObject 入参 watchNoeventIdlonlataddresslocateTimegpsErrorMsg
* @return 处理结果
*/
AjaxJson updateMonitorGps(JSONObject jsonObject);
/**
* 处理历史数据 orgCode为空的问题
* @return
@@ -265,6 +265,102 @@ public class WatchMonitorDataServiceImpl extends ServiceImpl<WatchMonitorDataMap
return ajaxJson;
}
/**
* 更新告警事件GPS位置信息
* 根据 watchNo + eventId 联合定位已有告警数据
* watchNo eventId 任一缺失则忽略
* 已存在 GPS 信息则忽略本次更新
* 参考 createMonitorData 进行 GPS84 GCJ02 坐标转换 + 高德地址反查
* 仅更新数据不推送大屏与短信
*
* @param json 入参 watchNoeventIdlonlataddresslocateTimegpsErrorMsg
* @return 处理结果
*/
@Override
public AjaxJson updateMonitorGps(JSONObject json) {
GlobalUtils.setHttpFeignToken();
AjaxJson ajaxJson = new AjaxJson();
logger.info("接收更新告警事件GPS信息=={}", json.toJSONString());
WatchNotifyVo data = json.toJavaObject(WatchNotifyVo.class);
// watchNo eventId 任一缺失则忽略
if (StrUtil.isBlank(data.getWatchNo()) || StrUtil.isBlank(data.getEventId())) {
logger.warn("更新GPS信息缺失关键定位字段,忽略本次请求 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
ajaxJson.setSuccess(true);
return ajaxJson;
}
// watchNo + eventId 联合定位已有告警数据
LambdaQueryWrapper<WatchMonitorData> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(WatchMonitorData::getWatchNo, data.getWatchNo())
.eq(WatchMonitorData::getEventId, data.getEventId());
List<WatchMonitorData> existList = this.list(queryWrapper);
if (CollUtil.isEmpty(existList)) {
logger.warn("更新GPS信息失败,未匹配到告警记录 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
ajaxJson.setSuccess(true);
return ajaxJson;
}
// 已存在GPS信息则忽略本次更新避免覆盖已有有效坐标
boolean hasGps = existList.stream().anyMatch(m -> m.getLon() != null && m.getLat() != null);
if (hasGps) {
logger.info("告警记录已有GPS信息,忽略本次更新 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
ajaxJson.setSuccess(true);
return ajaxJson;
}
// 计算高德坐标与高德地址参考 createMonitorData 逻辑
WatchMonitorData updateEntity = new WatchMonitorData();
updateEntity.setLon(data.getLon());
updateEntity.setLat(data.getLat());
updateEntity.setAddress(data.getAddress());
updateEntity.setGpsErrorMsg(data.getGpsErrorMsg());
updateEntity.setLocateTime(data.getLocateTime());
if (Stream.of(data.getLon(), data.getLat()).allMatch(value -> ObjUtil.isNotEmpty(value) && value != 0)) {
Gps gps = GPS84ToGCJ02(data.getLon(), data.getLat());
if (null != gps) {
// 写入高德坐标
updateEntity.setLonGd(BigDecimal.valueOf(gps.getLongitude()));
updateEntity.setLatGd(BigDecimal.valueOf(gps.getLatitude()));
// 调用高德接口获取地址信息
AmapResultVo amapResultVo = amapService.getFormattedAddress(gps.getLongitude(), gps.getLatitude(), 5000);
if (amapResultVo.isSuccess()) {
updateEntity.setAddressGd(amapResultVo.getAddress());
updateEntity.setAddress(amapResultVo.getAddress());
} else {
String originError = StrUtil.isBlank(updateEntity.getGpsErrorMsg()) ? "" : updateEntity.getGpsErrorMsg() + "";
updateEntity.setGpsErrorMsg(originError + amapResultVo.getErrorMsg());
}
}
}
// 仅更新 GPS 相关字段不推送大屏不发短信
boolean updated = this.lambdaUpdate()
.eq(WatchMonitorData::getWatchNo, data.getWatchNo())
.eq(WatchMonitorData::getEventId, data.getEventId())
.set(WatchMonitorData::getLon, updateEntity.getLon())
.set(WatchMonitorData::getLat, updateEntity.getLat())
.set(WatchMonitorData::getAddress, updateEntity.getAddress())
.set(WatchMonitorData::getLonGd, updateEntity.getLonGd())
.set(WatchMonitorData::getLatGd, updateEntity.getLatGd())
.set(WatchMonitorData::getAddressGd, updateEntity.getAddressGd())
.set(WatchMonitorData::getGpsErrorMsg, updateEntity.getGpsErrorMsg())
.set(WatchMonitorData::getLocateTime, updateEntity.getLocateTime())
.update();
if (!updated) {
logger.warn("更新GPS信息失败 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
} else {
logger.info("更新GPS信息成功 watchNo={}, eventId={}, 共更新{}条", data.getWatchNo(), data.getEventId(), existList.size());
}
ajaxJson.setSuccess(true);
return ajaxJson;
}
/**
* 构建异常数据
*
@@ -272,13 +368,16 @@ public class WatchMonitorDataServiceImpl extends ServiceImpl<WatchMonitorDataMap
* @return 异常数据
*/
private WatchMonitorData createMonitorData(WatchNotifyVo data) {
log.info("开始构建异常数据 => {}", JSONObject.toJSONString(data));
WatchMonitorData monitorDataEntity = new WatchMonitorData();
monitorDataEntity.setAddress(data.getAddress());
monitorDataEntity.setWatchNo(data.getWatchNo());
monitorDataEntity.setLon(data.getLon());
monitorDataEntity.setLat(data.getLat());
monitorDataEntity.setLocateTime(data.getLocateTime());
monitorDataEntity.setEventType(data.getEventType());
monitorDataEntity.setEventId(data.getEventId());
monitorDataEntity.setDataValue(data.getDataValue());
monitorDataEntity.setCreateDate(data.getCreateTime());
monitorDataEntity.setWarnTime(data.getWarnTime());
@@ -587,7 +587,13 @@ public class WatchServiceImpl implements IWatchService {
wrapper.eq(WatchStatUserInfoDaySleep::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDaySleep::getDataDate);
wrapper.last("FETCH FIRST 1 ROWS ONLY");
return daySleepMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
WatchH5IndexItemNewVo vo = daySleepMapper.selectJoinOne(WatchH5IndexItemNewVo.class, wrapper);
// 短睡(午睡)有值但长睡为空 当天仅有午睡数据,用短睡补齐 dataValue/dataMax
if (vo != null && StrUtil.isNotEmpty(vo.getDataMin()) && StrUtil.isEmpty(vo.getDataMax())) {
vo.setDataValue(vo.getDataMin());
vo.setDataMax(vo.getDataMin());
}
return vo;
}
private WatchH5IndexItemNewVo latestStepsData(String userId) {
@@ -42,6 +42,7 @@ import org.jeecgframework.poi.excel.entity.params.ExcelExportEntity;
import org.jeecgframework.poi.exception.excel.enums.ExcelExportEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@@ -208,7 +209,12 @@ public class WatchUserDataServiceImpl extends ServiceImpl<WatchUserDataMapper, W
watchUserData.setDataDate(bindDateSave);
watchUserData.setWatchNo(watchNo);
watchUserData.setCreateDate(new Date());
try {
this.save(watchUserData);
} catch (DuplicateKeyException e) {
//并发上报同一手表同一天数据时,先删后插存在竞态,另一线程已插入相同记录,忽略即可
log.warn("QH_WAT_USER_DATA并发写入冲突,已忽略: watchNo={}, dataDate={}", watchNo, s);
}
}
// @Override
+33
View File
@@ -43,6 +43,14 @@
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>yixiong-tech</id>
<name>yixiong-tech Repository</name>
<url>https://maven.ops.yixiong-tech.com/repository/public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
@@ -367,6 +375,31 @@
</exclusion>
</exclusions>
</dependency>
<!-- IOSP 日志 Kafka 上送:Boot2 starter(JDK8字节码),启动时把日志异步推送到对方Kafka app-log topic -->
<dependency>
<groupId>ptr.xjyt.iosp.starter</groupId>
<artifactId>iosp-starter-log</artifactId>
<version>2.0.9.RELEASE</version>
</dependency>
<!-- hongyousoft logback KafkaAppender 底层实现 -->
<dependency>
<groupId>com.hongyousoft</groupId>
<artifactId>kafka-appender</artifactId>
<version>0.0.4</version>
</dependency>
<!-- SkyWalking 日志 traceId 占位符 %tid 支持(无agent时显示N/A,不影响推送) -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>9.3.0</version>
</dependency>
<!-- Kafka 客户端:与 starter 设计基线一致(2.4.0),对方3.7.2 broker 向下兼容 -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
</project>
@@ -48,7 +48,8 @@ public enum RoleEnum {
MANAGER("manager"),
MEDICALUSER("medicaluser"),
MEDICALADMIN("medicaladmin"),
HOS_DOCTOR("hos_doctor");
HOS_DOCTOR("hos_doctor"),
NOTICE_ROLE("notice_role");
/**
* 角色名
@@ -2302,7 +2302,7 @@
},
"429005": {
"4290051": "竹根滩镇",
"4290054": "江汉石油管理局",
"4290054": "江汉SY管理局",
"42900545": "周矶管理区",
"429005001": "园林",
"429005002": "杨市",
+4
View File
@@ -84,6 +84,10 @@
<groupId>com.renkang</groupId>
<artifactId>xj-anyan-api</artifactId>
</dependency>
<dependency>
<groupId>com.renkang</groupId>
<artifactId>health-im-api</artifactId>
</dependency>
</dependencies>
@@ -41,13 +41,36 @@ public class SysMessageController extends JeecgController<SysMessage, ISysMessag
*/
@GetMapping(value = "/list")
public Result<?> queryPageList(SysMessage sysMessage, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) {
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(name = "employeePhone", required = false) String employeePhone,
HttpServletRequest req) {
QueryWrapper<SysMessage> queryWrapper = QueryGenerator.initQueryWrapper(sysMessage, req.getParameterMap());
// 员工电话: esContent中手机号以"3位~4位~4位"(全角波浪线)格式存储,
// 前端传入纯数字手机号,此处还原为存储格式后对 esContent 做模糊查询
if (employeePhone != null && !employeePhone.trim().isEmpty()) {
queryWrapper.like("ES_CONTENT", buildEmployeePhoneLikeValue(employeePhone));
}
Page<SysMessage> page = new Page<SysMessage>(pageNo, pageSize);
IPage<SysMessage> pageList = sysMessageService.page(page, queryWrapper);
return Result.ok(pageList);
}
/**
* 将前端传入的员工电话还原为 esContent 中存储的"3位~4位~4位"(全角波浪线)格式
* 11位纯数字 -> 还原为 ddddddddddd;其余情况用原始输入兜底(兼容用户直接粘贴带的原文)
* 等效SQL: ES_CONTENT LIKE '%18599205354%'
*
* @param employeePhone 前端传入的员工电话
* @return 用于模糊查询 esContent 的值
*/
private String buildEmployeePhoneLikeValue(String employeePhone) {
String digits = employeePhone.replaceAll("[^0-9]", "");
if (digits.length() == 11) {
return digits.substring(0, 3) + "" + digits.substring(3, 7) + "" + digits.substring(7, 11);
}
return employeePhone.trim();
}
/**
* 添加
*
@@ -85,7 +85,7 @@ public class ArchivesUserServiceImpl implements ArchivesUserService {
return null;
}
if (managedCodes.contains("A01")) {
return Collections.singletonList("长庆油田");
return Collections.singletonList("分公司");
}
List<String> result = new ArrayList<>();
managedCodes.forEach(orgCode -> {
@@ -1,6 +1,7 @@
package org.jeecg.modules.system.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.fastjson.JSON;
@@ -11,12 +12,15 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.jeecg.bean.request.ListUser;
import org.jeecg.bean.request.PeoplePsyAssessPlanVO;
import org.jeecg.bean.request.PeopleStatisticVO;
import org.jeecg.common.api.vo.Result;
@@ -25,6 +29,7 @@ import org.jeecg.common.aspect.annotation.PermissionData;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.bean.request.ResetPassword;
import org.jeecg.common.system.bean.request.SimpleUser;
import org.jeecg.common.system.bean.response.BaseUser;
@@ -32,6 +37,7 @@ import org.jeecg.common.system.bean.response.GroupUser;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.common.system.vo.SysUserModel;
import org.jeecg.common.util.*;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
@@ -130,6 +136,10 @@ public class SysUserController {
private ISysUserPostService sysUserPostService;
@Autowired
private UserCacheManager userCacheManager;
@Autowired
private IMHelloApi imHelloApi;
@Autowired
private ISysBaseAPI sysBaseAPI;
@GetMapping("userRoles")
public Result<?> userRoles(@RequestParam String userId) {
if (StrUtil.isBlank(userId)) {
@@ -524,6 +534,8 @@ public class SysUserController {
//获取租户ids
String relTenantIds = jsonObject.getString("relTenantIds");
sysUserService.editUser(user, roles, departs, relTenantIds);
// 同步IM个人资料
syncImProfile(user.getId());
result.success("修改成功!");
}
} catch (Exception e) {
@@ -1711,6 +1723,8 @@ public class SysUserController {
}
sysUser.setUpdateTime(new Date());
sysUserService.updateById(sysUser);
// 同步IM个人资料
syncImProfile(sysUser.getId());
}
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -2335,4 +2349,17 @@ public class SysUserController {
Page<ScopeUserVO> page = new Page<>(pageNo, pageSize);
return Result.ok(sysUserService.scopeUserList(query,page));
}
private void syncImProfile(String userId) {
try {
List<LoginUserNew> users = sysBaseAPI.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollUtil.isNotEmpty(users)) {
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
}
} catch (Exception e) {
log.error("同步IM个人资料失败, userId: {}", userId, e);
}
}
}
@@ -189,12 +189,12 @@ public class SysMessageNoticeServiceImpl extends ServiceImpl<SysMessageNoticeMap
LoginUser loginUser = GlobalUtils.getLoginUser();
/*
* 1.后台接口的消息列表需要做一定数据权限控制需考虑的角色有admin,system,second_manager,third_manager
* 2.admin,system可查看所有的活动
* 2.admin,system可查看所有的消息通知
* 3.second_manager,third_manager可查看管理单位下的消息列表
*/
boolean permissions = false;
List<String> roleCodes = Arrays.asList(Objects.toString(loginUser.getRoleCodes(), "").split(","));
if (roleCodes.contains(RoleEnum.ADMIN_MANAGER.roleName) || roleCodes.contains(RoleEnum.SYSTEM.roleName) || roleCodes.contains(RoleEnum.WEIGHT_PLAN_ADMIN.roleName)) {
if (roleCodes.contains(RoleEnum.ADMIN_MANAGER.roleName) || roleCodes.contains(RoleEnum.SYSTEM.roleName)) {
permissions = true;
} else if (roleCodes.contains(RoleEnum.SECOND_MANAGER.roleName)) {
if (StrUtil.isNotEmpty(loginUser.getDepartCodes())) {
@@ -203,8 +203,8 @@ public class SysMessageNoticeServiceImpl extends ServiceImpl<SysMessageNoticeMap
permissions = true;
}
}
// 没有权限返回空结果
wrapper.eq(!permissions, SysMessageNotice::getId, "-1");
// 只查看本人创建的消息
wrapper.eq(!permissions, SysMessageNotice::getCreateBy, loginUser.getUsername());
}
/**
@@ -16,6 +16,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.renkang.consultation.api.ConSessionHelloApi;
import com.renkang.consultation.vo.FamilyNum;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import com.renkang.watch.api.WatchApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
@@ -24,6 +26,7 @@ import org.jeecg.bean.entity.UserDataRecordBWH;
import org.jeecg.bean.entity.UserDataRecordBmi;
import org.jeecg.bean.enums.UserDataSourceEnum;
import org.jeecg.bean.request.HealthMealsWeightQueryVO;
import org.jeecg.bean.request.ListUser;
import org.jeecg.bean.request.QueryWeightSignUserParam;
import org.jeecg.bean.response.*;
import org.jeecg.common.api.vo.Result;
@@ -34,6 +37,7 @@ import org.jeecg.common.export.PoiExportHandler;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.DictUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.LoginUserNew;
import org.jeecg.common.system.vo.SysUserModel;
import org.jeecg.common.util.*;
import org.jeecg.config.mybatis.AnalyzeTableManager;
@@ -122,6 +126,8 @@ public class HealthUserEmployeeExServiceImpl extends ServiceImpl<HealthUserEmplo
@Autowired
private ConSessionHelloApi conSessionHelloApi;
@Autowired
private IMHelloApi imHelloApi;
@Autowired
private WatchApi watchApi;
@Autowired
private CommonExportsInfoController commonExportsInfoController;
@@ -993,6 +999,8 @@ public class HealthUserEmployeeExServiceImpl extends ServiceImpl<HealthUserEmplo
if (com.xkcoding.http.util.StringUtil.isNotEmpty(orgCode)) {
sendUpdateMsgToRedisStream(userEmployee.getId(), orgCode);
}
// 同步IM个人资料
syncImProfile(userEmployee.getId());
return Result.ok("编辑成功");
} else {
return Result.error("编辑失败");
@@ -1010,6 +1018,19 @@ public class HealthUserEmployeeExServiceImpl extends ServiceImpl<HealthUserEmplo
redisStreamUtil.add(RedisStreamKeyEnum.CHANGE_USER_DEPART, userModel);
}
private void syncImProfile(String userId) {
try {
List<LoginUserNew> users = sysBaseAPI.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollUtil.isNotEmpty(users)) {
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
}
} catch (Exception e) {
log.error("同步IM个人资料失败, userId: {}", userId, e);
}
}
public boolean updateByIdAndUser(UserEmployee userEmployee) {
// 添加账号 电话 身份证号校验
/* List<String> checkExistUser = sysBaseAPI.checkExistUser(userEmployee.getId(), userEmployee.getUsername(), userEmployee.getPhone(), userEmployee.getIdCard());
@@ -1127,8 +1127,13 @@ public class SysBaseApiImpl implements ISysBaseAPI {
if (map != null) {
for (Map.Entry<String, String> entry : map.entrySet()) {
String str = "${" + entry.getKey() + "}";
title = title.replace(str, entry.getValue());
content = content.replace(str, entry.getValue());
String value = entry.getValue() != null ? entry.getValue() : "";
if (title != null) {
title = title.replace(str, value);
}
if (content != null) {
content = content.replace(str, value);
}
}
}
SysMessage sysMessage = new SysMessage();
@@ -1161,7 +1166,10 @@ public class SysBaseApiImpl implements ISysBaseAPI {
if (map != null) {
for (Map.Entry<String, String> entry : map.entrySet()) {
String str = "${" + entry.getKey() + "}";
content = content.replace(str, entry.getValue());
String value = entry.getValue() != null ? entry.getValue() : "";
if (content != null) {
content = content.replace(str, value);
}
}
}
return content;
@@ -19,6 +19,8 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.renkang.im.api.IMHelloApi;
import com.renkang.im.entity.ConSessionRequestDO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
@@ -35,6 +37,7 @@ import org.jeecg.bean.entity.UserDataRecordBmi;
import org.jeecg.bean.enums.UserDataSourceEnum;
import org.jeecg.bean.request.PeoplePsyAssessPlanVO;
import org.jeecg.bean.request.PeopleStatisticVO;
import org.jeecg.bean.request.ListUser;
import org.jeecg.bean.response.BaseEmployeeInfo;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.bean.request.StatAllReq;
@@ -50,6 +53,7 @@ import org.jeecg.common.desensitization.annotation.SensitiveEncode;
import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.export.PoiExportHandler;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.bean.request.ResetPassword;
import org.jeecg.common.system.bean.request.SimpleUser;
import org.jeecg.common.system.bean.request.SysUserInfoDO;
@@ -95,6 +99,7 @@ import org.slf4j.Logger;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.context.annotation.Lazy;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Service;
@@ -197,6 +202,11 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
private UserDataRecordBWHMapper bwhMapper;
@Autowired
private UserDataRecordBmiMapper bmiMapper;
@Autowired
private IMHelloApi imHelloApi;
@Lazy
@Autowired
private ISysBaseAPI sysBaseAPI;
private final GroupUserExportHandler groupUserExportHandler = new GroupUserExportHandler();
@@ -1811,6 +1821,21 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
userModel.setId(loginUser.getId());
userModel.setAvatar(avatar);
redisStreamUtil.add(RedisStreamKeyEnum.UPDATE_USER_INFO, RedisConstant.REDIS_STREAM_KEY_FIELD, userModel);
// 同步IM个人资料
syncImProfile(loginUser.getId());
}
private void syncImProfile(String userId) {
try {
List<LoginUserNew> users = sysBaseAPI.listUserByIds(new ListUser(Collections.singletonList(userId)));
if (CollUtil.isNotEmpty(users)) {
ConSessionRequestDO req = new ConSessionRequestDO();
req.setAccountList(Collections.singletonList(users.get(0)));
imHelloApi.accountReg(req);
}
} catch (Exception e) {
log.error("同步IM个人资料失败, userId: {}", userId, e);
}
}
@Override
+2 -2
View File
@@ -143,7 +143,7 @@
<repository>
<id>dt-releases</id>
<name>jeecg Repository</name>
<url>https://maven.icdat.cn/repository/releases/</url>
<url>https://maven.ops.yixiong-tech.com/repository/releases/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
@@ -151,7 +151,7 @@
<repository>
<id>dt-snapshots</id>
<name>jeecg-snapshots Repository</name>
<url>https://maven.icdat.cn/repository/shapshots/</url>
<url>https://maven.ops.yixiong-tech.com/repository/shapshots/</url>
<releases>
<enabled>false</enabled>
</releases>