128 Commits

Author SHA1 Message Date
b536eadd92 修复前端获取版本号的bug 2026-04-29 17:54:34 +08:00
guanyu
3472aa790e fix: 修复#436手术计费界面显示无关费用项
根因: 前端按generateSourceEnum和sourceBillNo过滤手术计费项目,
但后端SQL查询和DTO未返回这两个字段,导致过滤失效,显示所有费用项。

修复:
1. EncounterPatientPrescriptionDto添加generateSourceEnum和sourceBillNo字段
2. SQL查询添加T1.generate_source_enum和T1.prescription_no AS source_bill_no
2026-04-29 17:40:13 +08:00
guanyu
ec89ead14c fix: 修复#456门诊医生站医嘱类型和状态异常
根因: 处方列表组件中adviceTypes参数传递格式错误,
将单个adviceType值直接赋值给adviceTypes参数,
但后端期望List<Integer>数组格式。

修复: 将adviceQueryParams.adviceTypes = value改为
adviceQueryParams.adviceTypes = [value],确保参数格式正确。
2026-04-29 17:24:07 +08:00
guanyu
136235fe4c fix: 修复#459检验申请报错仍生成记录
根因: saveRequestForm方法缺少@Transactional事务注解,
导致处理多个诊疗项目时,部分成功保存后发生异常,
已保存的数据无法回滚,造成脏数据。

修复: 在saveRequestForm方法上添加@Transactional(rollbackFor = Exception.class)注解,
确保整个操作原子性,异常时自动回滚。
2026-04-29 17:21:15 +08:00
guanyu
c2cac12b9f fix: 修复#459检验申请报错仍生成记录
根因: RequestFormManageAppServiceImpl缺少@Transactional事务注解,
导致保存申请单过程中如果后续步骤报错,已保存的申请单不会回滚,
产生脏数据。

修复: 在类上添加@Transactional(rollbackFor = Exception.class)注解,
确保整个保存操作在同一个事务中,任何异常都会回滚所有数据库操作。
2026-04-29 17:20:13 +08:00
guanyu
b424d73542 fix: 修复#471手术申请查询混入脏数据
根因: 手术申请分页查询SQL中cli_surgery、adm_patient、adm_encounter表
LEFT JOIN时缺少delete_flag='0'过滤条件,导致已删除的数据混入查询结果。

修复: 在LEFT JOIN条件中添加AND cs.delete_flag='0'、AND ap.delete_flag='0'、AND ae.delete_flag='0'。
2026-04-29 17:18:18 +08:00
guanyu
decac542c8 fix: 修复#462诊疗目录标本下拉框无数据
根因: diagnosisTreatmentDialog.vue中useDict未引入specimen_code字典,
导致标本下拉框无数据。

修复: 在useDict调用中添加'specimen_code'字典。
2026-04-29 17:14:29 +08:00
guanyu
783ee48ec8 fix: 修复#465检验项目列表限制500项
根因: LabActivityDefinitionManageMapper.xml中getLabActivityDefinitionSimpleList查询
设置了LIMIT 500/1500的限制,导致检验项目超过500项时无法完整显示。

修复: 将LIMIT限制提高到10000,支持更多检验项目。
2026-04-29 17:13:44 +08:00
guanyu
e1ad4965eb fix: 修复#457门诊收费手术医嘱不显示名称
根因: 手术收费项目的contextEnum错误设置为6(中成药),
导致门诊收费查询SQL无法正确匹配手术名称。

修复: 将手术收费项目的contextEnum改为3(项目),
因为手术属于诊疗项目类别。
2026-04-29 17:11:16 +08:00
guanyu
fd1880f1c8 fix: 修复#438门诊划价选择'西药'时无数据
根因: 门诊划价控制器(OutpatientPricingController)未接收adviceType参数,
导致前端传递的药品类型过滤条件无法生效。

修复: 在getAdviceBaseInfo方法中添加adviceType参数接收和处理,
确保西药(adviceType=1, categoryCode='2')能正确过滤。
2026-04-29 17:09:58 +08:00
wangjian963
d4d05267ad feat(分诊队列): 实现分诊队列核心功能与日志记录
新增分诊队列相关服务接口与实现,包括队列管理、叫号操作和日志记录
添加DivLogService和CallRecordService用于记录分诊操作和叫号历史
在CurrentDayEncounterDto和TriageQueueItem中增加seqNo字段用于显示预约序号
实现分诊操作日志记录功能,包括添加队列、移除队列、叫号、完成等操作
新增CallType枚举定义叫号类型,并实现叫号记录功能
优化队列状态映射逻辑,支持更多状态类型显示
2026-04-29 17:05:17 +08:00
2b0acce1db Merge remote-tracking branch 'origin/develop' into develop 2026-04-29 17:00:05 +08:00
4312c0c557 增加后端版本展示 2026-04-29 16:59:44 +08:00
guanyu
caa45c3310 fix: 修复#472住院医生站手术申请单勾选无效
根因: 前端获取手术项目列表时传递的adviceTypes为字符串'3',
后端期望List<Integer>格式, 可能导致解析异常。

修复: 将adviceTypes: '3'改为adviceTypes: [3]数组格式,
确保Spring MVC能正确解析为List<Integer>。
2026-04-29 16:48:52 +08:00
7fabad14f9 将InpatientMedicalRecordHomePageCollectionDto 中的 @Data 注解替换为 @Getter 和 @Setter 注解; 2026-04-29 16:29:22 +08:00
guanyu
405a9dfb72 fix: Bug #249 门诊手术安排查询未过滤已删除手术申请单 - 将cli_surgery表的LEFT JOIN改为INNER JOIN,确保已删除作废的手术申请单不在手术安排查询界面显示 2026-04-28 14:03:14 +08:00
d1be841688 fix: Bug #451 门诊医生站-提交新增手术申请后列表刷新失败 2026-04-28 12:33:16 +08:00
guanyu
9b8655748e fix: Bug #449/#450 门诊医生站接诊/数据加载失败 - 修复TodayOutpatientServiceImpl中receivePatient/completeVisit/cancelVisit方法空壳问题,改为调用DoctorStationMainAppService正确业务逻辑 2026-04-28 12:07:38 +08:00
00fd6c8710 在 vite.config.js 中添加了动态构建版本定义,通过环境变量 VITE_APP_VERSION 实现。
更新了 login.vue,使其动态显示构建版本,而非使用硬编码的值。
2026-04-27 14:16:32 +08:00
bbd9d48fa6 test: Playwright E2E测试12个用例全部通过!
- 修复登录按钮选择器:'登 录'(带空格)
- 修复placeholder:'账号'/'密码'
- 修复登录失败检测逻辑
- 12/12用例通过,耗时16.9秒
- 覆盖:登录4场景 + Bug回归3个(#437/#443/#427) + 手术计费2个 + 医生站2个 + 并发1个
2026-04-25 22:33:53 +08:00
8fb1d3e583 fix: 修正Playwright登录页选择器 - 使用实际placeholder '账号'/'密码' 2026-04-25 22:29:23 +08:00
34ba7cae6a fix: 修复Playwright页面对象定义错误 + 根目录config
- 修复LoginPage/SurgeryBillingPage/DoctorStationPage中page变量作用域问题
- 新增根目录playwright.config.ts(解决配置加载问题)
- .gitignore添加test-results和report目录排除
2026-04-25 22:14:19 +08:00
305ab15436 test: 增强Playwright E2E测试方案 - 新增手术计费/医生站/并发测试用例
- 新增页面对象: SurgeryBillingPage, DoctorStationPage
- 新增测试用例: 手术计费防重复(#437), 签发耗材验证(#443), 并发操作测试
- 增强登录测试: 多场景覆盖
- 完善测试数据工具: 支持多角色用户配置
- 清理冗余备份文件
2026-04-25 22:04:36 +08:00
46a7076460 Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-25 21:07:43 +08:00
e0e6693897 fix: 修正Playwright测试方案架构问题(诸葛亮审查反馈)
- 新增fixtures/auth.ts 登录认证夹具
- 新增pages/LoginPage.ts 页面对象模型
- 新增specs/login.spec.ts 登录测试用例(成功/失败/空用户名)
- 新增specs/bug-regression.spec.ts Bug回归测试(#437/#427)
- 新增.env.test 测试环境变量模板
- package.json添加test:e2e/test:e2e:ui/test:e2e:report脚本
- 移除test-data.ts中密码硬编码,改用环境变量
- .gitignore添加.env.test.local/playwright-report/test-results

感谢诸葛亮架构审查!
2026-04-25 21:07:40 +08:00
guanyu
7d1e50d045 fix: 修复#443手术计费签发耗材报错
根因: handleAddDeviceBilling方法中对locationId的验证过于严格,
当前端未传递locationId时直接抛出ServiceException导致签发失败。

修复: 将严格验证改为预处理设置默认值,
当advice.getLocationId()为null时, 使用SecurityUtils.getLoginUser().getOrgId()作为默认位置ID。
2026-04-25 21:05:05 +08:00
25ce12cebf Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-25 21:02:16 +08:00
7d55717037 feat: 添加Playwright E2E自动化测试完整方案
- 创建完整Playwright测试方案文档(docs/specs/)
- 创建Playwright配置文件(tests/playwright.config.ts)
- 创建测试数据工具类(tests/e2e/utils/test-data.ts)
- 建立测试目录结构:fixtures/pages/specs/utils
- 支持CI/CD集成,测试失败阻断发布
- 覆盖登录、门诊医生站、手术计费、Bug回归测试

关联任务: UI功能性测试方案落地
2026-04-25 21:02:13 +08:00
guanyu
290e8f8f15 fix: 修复#445门诊手术待生成列表未剔除已生成医嘱
根因: getSurgeryPage查询只查cli_surgery表,没有关联wor_service_request表检查是否已生成医嘱。
所有手术都会显示在待生成列表中,不管是否已处理。

修复: 在getSurgeryPage查询中LEFT JOIN wor_service_request表,
通过sr.id IS NULL过滤掉已生成医嘱的手术。
2026-04-25 20:10:31 +08:00
fc84fd61ab fix(#437): 数据库层修复手术计费重复生成收费项
- 添加复合唯一约束 uk_charge_item_encounter_service_product_source
  防止同一就诊下同一来源服务+产品产生重复收费项
  约束字段:encounter_id + service_table + service_id + product_table + product_id + generate_source_enum
- 添加索引 idx_charge_item_generate_source_product 加速手术计费查询
- 添加索引 idx_charge_item_encounter_status 加速按就诊状态查询
- 提供重复数据检测SQL供运维排查历史数据

根因分析:
1. adm_charge_item 表无任何唯一约束,同一收费项可被多次插入
2. 前端手术计费页面使用 sourceBillNo 过滤,但该字段不存在于 ChargeItem 实体中
3. 多处代码路径(SurgeryAppServiceImpl/RequestFormManageAppServiceImpl)均可生成收费项
4. 缺少数据库层面的兜底防护

Author: xunyu
2026-04-25 20:04:54 +08:00
guanyu
d79690a371 fix: 修复#442手术计费删除待签发耗材报错
根因: handleDel方法中使用iProcedureService.listByIds(requestIds)错误,
requestIds是WOR_DEVICE_REQUEST/WOR_SERVICE_REQUEST表主键,
不是CLI_PROCEDURE表主键。

修复: 改用iProcedureService.getProcedureRecords(requestIds, serviceTable)
通过request_id字段正确关联执行记录。
2026-04-25 20:02:11 +08:00
7bccbc7085 fix: Bug #427 检查项目分类手风琴展开 + Bug #437 手术计费重复记录修复
- #427: switchCategory函数已实现手风琴逻辑(切换时收起其他分类)
- #437: prescriptionlist.vue添加isSaving防重复提交锁
- #437: 使用JSON.parse(JSON.stringify(row))清理Vue响应式对象
- #437: 添加finally块确保锁释放
2026-04-25 19:47:05 +08:00
guanyu
059ef483ca fix: 修复#447住院医生站手术申请弹窗无法加载手术类诊疗目录数据
根因: adviceType=3(诊疗)查询时强制排除手术项(category_code!='手术'且!='24'),
导致通过categoryCode='24'显式查询手术项目时结果为空。

修复: 仅在未指定categoryCode时才排除手术,
当显式指定categoryCode='24'或'手术'时允许加载手术类项目。
2026-04-25 16:12:03 +08:00
guanyu
4beb4c40c5 fix: 修复#448门诊划价项目分类过滤失效 - 耗材和诊疗查询缺少categoryCode过滤条件
- adviceType=2(耗材)查询添加categoryCode过滤
- adviceType=3(诊疗)查询添加categoryCode过滤
- 与adviceType=1(药品)保持一致的过滤逻辑
- 修复选择耗材类型时仍检索出药品的问题
2026-04-25 15:27:02 +08:00
914f2d8229 docs: 按刘备建议结构重新整理《HIS项目发布检查清单 v1.0》 2026-04-25 12:12:41 +08:00
2f57b3e7c1 docs: 整合四份清单为《HIS项目发布检查清单 v1.0》 2026-04-25 12:08:51 +08:00
guanyu
39ccd27df8 feat: 新增《后端发布前检查清单》- 关羽
补充后端发布前六大模块检查项:
1. Maven编译验证
2. Spring Boot多环境配置
3. MyBatis Plus规范(实体映射/SQL安全/事务管理)
4. RESTful API设计(统一返回/参数校验/版本管理)
5. 安全与合规(数据脱敏/权限控制/审计日志)
6. 性能检查(N+1查询/慢查询优化)

与陈琳的前端清单形成对称体系,覆盖getDepartmentList类问题的后端等价场景。
2026-04-24 19:19:23 +08:00
d370b6a888 docs: 补充ESLint flat config配置示例到CI/CD门禁规范 2026-04-24 19:16:44 +08:00
3c61e39e09 docs: 修正构建门禁文档中的命令不一致问题 - 统一前端构建命令为 build:prod,后端编译命令为 mvn clean package -DskipTests 2026-04-24 19:06:23 +08:00
f2c71b08bb feat: 启用ESLint import规则 - 实时检测缺失导出,防止构建失败 2026-04-24 18:12:27 +08:00
90cf7f43d7 Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-24 18:06:44 +08:00
1f5d392c08 chore: 清理.git残留的.orig文件 2026-04-24 18:06:39 +08:00
d52bbda8c3 docs: 完善三份构建门禁文档 - 补充前后端协同检查、Java后端门禁、数据库变更字段
架构评审改进项:
- frontend-checklist.md: 增加后端Maven编译、数据库脚本、接口兼容性检查
- cicd-gatekeeper.md: 补充Java后端构建配置(SpotBugs)、分阶段覆盖率目标
- commit-template.md: 增加数据库变更影响评估字段、精简截图要求
2026-04-24 18:03:45 +08:00
guanyu
986510278b feat: 配置Husky pre-commit钩子 - 提交前自动执行前端构建检查
- 创建.husky/pre-commit文件
- 配置提交前自动执行npm run build:dev检查语法
- 添加node_modules存在性校验
- 预留ESLint检查接口(待赵云配置后启用)
- 更新openhis-ui-vue3/package.json添加lint-staged配置

【关羽】构建门禁第一步落地
2026-04-24 18:02:27 +08:00
758921b633 Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-24 17:14:29 +08:00
8e7ebd3461 chore: 更新package-lock.json(husky安装) 2026-04-24 17:13:31 +08:00
8c05782549 fix: 修复bloodTransfusion.vue构建报错 - public.js补充getDepartmentList导出 2026-04-24 17:11:20 +08:00
060d1910dd Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-24 17:05:23 +08:00
44ae216612 feat: 添加husky pre-commit hook配置实现构建验证 (#441)
- 配置husky作为pre-commit钩子
- 添加构建验证脚本,提交前自动执行构建检查
- 防止构建失败的代码被提交到仓库

关联任务: 自动化构建门禁方案第一步
2026-04-24 17:04:49 +08:00
0076753c19 docs: 添加三份构建门禁相关文档
- 《前端发布前检查清单》
- 《CI/CD构建门禁规范》
- 《代码提交变更说明模板》

为解决getDepartmentList导入错误等构建问题提供标准化文档支持
2026-04-24 17:04:42 +08:00
wangjian963
957d426042 Merge remote-tracking branch 'origin/develop' into develop 2026-04-24 16:54:02 +08:00
wangjian963
76094d6eff fix: 修复 Bug #388 #409 #410
会诊意见格式化存储,确保参加医师和意见完整回显
预约签到挂号时修正 serviceTypeId 为预约类型而非挂号类型
分诊队列显示诊室而非科室,区分预约/挂号类型
2026-04-24 16:52:33 +08:00
dc43ce335a fix: 清理public.js中重复的getDepartmentList函数
- 移除重复定义的getDepartmentList函数
- 保留一份干净的科室列表接口导出
- 确保4个申请单组件构建正常
2026-04-24 16:30:22 +08:00
d27b5147ec fix: 修复bloodTransfusion.vue构建失败 - public.js添加getDepartmentList导出函数
- 在public.js中新增getDepartmentList()函数
- 调用/app-common/department-list接口返回完整科室树
- 解决4个申请单组件导入不存在的函数导致构建失败问题
2026-04-24 16:26:20 +08:00
4fb540cfa5 fix: 修复getDepartmentList缺失导出问题 - public.js中补充getDepartmentList函数
4个申请单组件(bloodTransfusion/laboratoryTests/surgery/medicalExaminations)
从@/api/public.js导入getDepartmentList,但该函数未导出导致构建失败
2026-04-24 16:25:51 +08:00
72e1f927e9 feat: 实现Bug#428 #430联动功能
#428: 检查申请分类联动检查方法 - 展开分类时自动加载对应检查方法
#430: 套餐金额实时同步 - 选择检查方法后自动更新申请单总金额
2026-04-24 16:03:04 +08:00
guanyu
e7beb3f5c3 fix: Bug #436/#438 手术计费显示问题 - 修复chargeItemContext条件判断中的尾随空格 2026-04-24 15:17:17 +08:00
guanyu
dc7e3c1de8 fix: Bug #432 门诊手术安排新增保存报错 - 修复登录用户null校验缺失导致NPE 2026-04-24 15:17:17 +08:00
1242d41499 fix: Bug #418 #419 #421 #424 检查申请发往科室未自动赋值/下拉无数据 - 修复科室数据源接口问题
主要修复:
- 4个申请单组件统一使用getDepartmentList()替代getOrgList()
- 使用/app-common/department-list接口替代分页接口,确保科室树完整加载
- 添加findTreeItem递归查找函数,支持树形结构科室匹配
- 优化分页大小:pageSize从10000降至500,提升加载性能
- #415 后端添加价格非负验证,防止单价显示负数

涉及文件:
- laboratoryTests.vue/medicalExaminations.vue/bloodTransfusion.vue/surgery.vue
- DoctorStationAdviceAppServiceImpl.java
2026-04-24 15:15:32 +08:00
091b6e83b6 fix: 修复Bug#429检查方法字段不应自动预填
移除examinationApplication.vue中自动填充inspectionMethod的逻辑
用户应手动选择检查方法,而不是由系统自动赋值
2026-04-24 15:11:19 +08:00
b53cdfa617 fix: 修复Bug#439领用出库总库存数量未显示
1. 保留selectRow中sourceLocationId不被清空(handleAddRow已设置)
2. 取消注释handleLocationClick调用,自动获取库存数量
2026-04-24 15:08:29 +08:00
fe2a79773f fix: 修复Bug#440用户管理修改提交报错hasOwnProperty
Vue 3 reactive proxy对象不支持直接调用hasOwnProperty方法
使用Object.prototype.hasOwnProperty.call替代,解决'hasOwnProperty is not a function'报错
2026-04-24 15:00:38 +08:00
22b47fcc95 fix: 修复前端Bug#431 #433 #434 #435
#431 会诊申请单:标签文案修改「需要病员及会诊目的」为「简要病史及会诊目的」
#433 手术安排编辑:麻醉方法回显为代码 - 添加Number类型转换
#434 手术安排编辑:切口类型未回显 - 添加Number类型转换
#435 手术安排编辑:费用类别未回显 - 确保字段正确赋值
2026-04-24 14:39:49 +08:00
328ccbbd99 feat: verify Bug #414 frontend build working 2026-04-24 11:16:05 +08:00
6b6e56c79b fix: BUG#280 会诊申请单打印逻辑修复(点击具体记录打印该条,不传参数时打印全部) 2026-04-24 10:07:42 +08:00
41fe89447f fix: 修复#416布局调整引入的inspectionApplication.vue标签未闭合问题(恢复为正确结构) 2026-04-24 08:43:57 +08:00
0d11d411ea fix: register.vue构建失败 - 替换不存在的login-background.jpg为渐变背景 2026-04-24 08:40:31 +08:00
guanyu
d525a50f52 fix: Bug #414 检验项目列表加载缓慢 - 优化分页查询性能
- 限制分页大小默认20,最大50,防止一次性加载过多数据
- 修复pageSize参数验证逻辑错误(之前编辑导致语法错误)
- 使用MyBatis-Plus优化COUNT查询(optimizeCountSql=true)
- 规范化pageNo参数默认值为1
- 同步保留Bug #415价格非负校验
2026-04-24 08:37:16 +08:00
guanyu
5d97975e7f fix: Bug #415 项目单价显示负数问题 - 添加价格非负验证 2026-04-23 23:13:51 +08:00
guanyu
03e89e0577 fix: Bug #418 #419 #421 #424 检查申请发往科室未自动赋值/下拉无数据
- ExamApplyController: 使用前端传入的performDeptCode查询科室ID
- 优先使用执行科室代码,查询不到时使用当前用户科室
- 两处ServiceRequest创建位置均已修复

【guanyu】
2026-04-23 22:24:46 +08:00
9c48744cb1 fix: Bug #413 医生个人报卡管理界面统一(弹窗宽度1100px+标题对齐门诊医生站) 2026-04-23 22:19:41 +08:00
24758414f2 fix: Bug #416/#423 检验/检查申请单布局调整(左右布局+宽度优化) 2026-04-23 22:15:25 +08:00
2d55387ba9 fix: Bug #412 门诊医生站传染病报告卡保存失败(添加临时卡号生成避免空值) 2026-04-23 22:05:16 +08:00
1fc2032aa8 fix: Bug #417 住院护士站记账页面空白(补充provide handleGetPrescription修复inject失败) 2026-04-23 21:37:50 +08:00
adc89a5ed2 fix: Bug #426 检查申请单已选择列表支持树形展开显示套餐明细(项目/数量/单价) 2026-04-23 21:36:15 +08:00
278676957e fix: Bug #420 检验申请单项目列表显示售价/单位 | Bug #422 检查申请单项目列表显示单价/单位 | Bug #425 检查申请申请单号显示自动生成 2026-04-23 21:33:55 +08:00
988c17cd30 fix: Bug #395 修复撤销审核前端调用与Controller重复映射问题
- 修复reportManagementController中重复的/revokeAudit映射
- 前端api.js增加revokeAuditCard接口
- handleRevokeAudit改用专用撤销审核API并传status=1

fix: Bug #398/#399 号源时间过滤不应影响已预约/已取号记录
- ScheduleSlotMapper.xml时间过滤仅应用于未预约(0)状态
- 已预约(1)、已取号(3)、已退号(5)、已退单(4)记录不受时间过滤
2026-04-23 18:09:01 +08:00
08ee473671 374 【诊疗目录】编辑项目时“所属科室”字段显示原始ID而非名称,且修改回显逻辑异常 2026-04-23 17:28:33 +08:00
关羽
6962a8b1c1 fix: Bug #395 #398 #399 门诊医生站功能修复
- #395: 传染病报告管理添加撤销审核功能入口
- #398: 修复号源超时后错误过滤问题,改进时间比较逻辑
- #399: 优化已取号状态查询过滤逻辑

【guanyu】
2026-04-23 17:19:46 +08:00
wangjian963
95e379e5a5 fix: Bug #407 #385 检查申请医嘱分类错误及预结算账户验证修复
主要修复:
  - 检查申请医嘱类型正确设置为诊疗(3),避免被错误归类为药品
  - 检查申请收费项获取真实自费账户,预结算验证accountId必须有效存在
  - 签发时补充诊疗费用项诊断关联信息变更模块:
  - ExamApplyController:使用ItemType枚举,获取真实账户替代占位值0
-DoctorStationAdviceAppService:按枚举标准分类医嘱,验证accountId有效性
  - IChargeBillService:productId=0时从ServiceRequest.contentJson获取项目名称
  - PaymentRecService:预结算自动修复账户不存在的历史数据
  - Mapper:排除衍生执行记录,productId=0时补充查询逻辑
  - ServiceRequest实体:activityId字段添加ALWAYS插入策略
2026-04-23 17:17:04 +08:00
2a8e662b44 fix: Bug #395 疾病报告卡添加撤销审核功能 | Bug #398/#399 门诊预约已预约和已取号记录不应被时间过滤 2026-04-23 17:15:40 +08:00
0b8a7245f6 chore: update package-lock.json 2026-04-23 17:10:07 +08:00
17e148ce7a fix: 修复#397编译报错 - useUserStore导入方式错误
user store使用export default,需用默认导入而非命名导入
2026-04-23 17:10:07 +08:00
937b4508ae 374 【诊疗目录】编辑项目时“所属科室”字段显示原始ID而非名称,且修改回显逻辑异常 2026-04-23 16:48:55 +08:00
87d4214541 fix: 修复前端Bug #396 #397
- #396 疾病报卡管理:搜索查询区域布局优化(单行紧凑布局)
- #397 分诊排队管理:页面标题科室名称动态获取(替代硬编码)
2026-04-23 16:37:52 +08:00
关羽
acc59ab87c fix: Bug #407 门诊医生站:检查申请医嘱分类错误致数据库报错
- ExamApplyController创建ServiceRequest时缺少categoryEnum字段设置
- 在两处ServiceRequest创建位置添加setCategoryEnum(EncounterClass.AMB.getValue())
- 添加EncounterClass导入
- 解决数据库category_enum字段NOT NULL约束报错
2026-04-23 09:12:29 +08:00
78bcdef7fd fix: resolve #407 examination request wrong advice type classification
Bug #407: 检查申请同步到医嘱列表时,医嘱类型被错误标注为中成药而非诊疗

Root cause: ServiceRequest.categoryEnum was not set when creating service requests from examination applications, causing the system to misclassify them as Chinese medicine (adviceType=2) instead of medical treatment (adviceType=3)

Fix: Added serviceRequest.setCategoryEnum(3) in both POST and PUT methods of ExamApplyController to correctly classify examination requests as medical treatment type

Impact: Examination requests will now display correct type (诊疗/medical treatment) in the advice list and won't trigger database errors when signing
2026-04-23 09:09:09 +08:00
72c0ceac29 fix: 修复前端Bug #405 #406 #408
- #405 住院医生站:医嘱保存后仍可编辑(未锁定)
- #406 门诊医生站:检验申请保存失败患者信息未加载
- #408 门诊医生站:检查明细标签页显示暂无数据
2026-04-22 17:29:46 +08:00
关羽
e2808fd6b9 fix: Bug #403 住院医生工作站:应用医嘱组套后药品明细字段丢失
- SQL查询getGroupPackageDetail增加therapy_enum字段
- OrdersGroupPackageDetailQueryDto增加therapyEnum属性
- 修复组套明细保存时therapyEnum已写入但查询时丢失的问题
2026-04-22 17:22:49 +08:00
0cfdce042f fix: resolve #403/#404 missing fields in medical order group application and editing
#403 - Removed 'dose: undefined' override in setValue() that was clearing dose values from group packages when applied to patients
#404 - Added explicit column aliases in OrdersGroupPackageAppMapper.xml to ensure proper field mapping for dose, rate_code, method_code, dose_quantity, dispense_per_duration, and therapy_enum

Both fixes address the root cause where medication detail fields (dose, administration route, frequency, duration) were being lost during group package application and editing.
2026-04-22 17:20:03 +08:00
关羽
cd54a3903c fix: Bug #402 住院医生站诊断录入:保存后列表出现重复记录且元数据缺失
- 恢复 saveDoctorDiagnosis 和 saveDoctorDiagnosisNew 方法中被注释掉的
  deleteEncounterDiagnosisInfos 调用
- 确保保存诊断前先清除旧记录,避免重复插入
- 元数据在后续 saveOrUpdate 中正确设置
2026-04-22 17:13:46 +08:00
关羽
063eb1fe08 fix: Bug #363 入科时间编辑时同步更新就诊表start_time字段
在入出转管理的编辑模式下,修改入科时间后就诊表(Encounter)的start_time
字段未同步更新,导致前端显示的入院日期与用户修改的值不一致。

修复内容:
- 编辑模式下增加对startTime的更新逻辑
- 通过encounterService.saveOrUpdateEncounter()同步更新就诊表

修复人:关羽
2026-04-22 17:06:52 +08:00
f125c8dc85 372 【住院医生站】医嘱录入执行科室显示ID乱码,且缺乏动态匹配逻辑
373 【住院医生站】医嘱搜索缺失“II级护理”项目(与诊疗目录配置不符)
2026-04-22 15:35:25 +08:00
d663c46422 fix: Bug #363 入院日期选择器改为datetime类型,支持时分秒编辑 2026-04-22 09:30:40 +08:00
a8ab52589e 370【住院护士站站-》进入“三测单”模块报错 2026-04-21 14:11:17 +08:00
14333f47ea 370【住院护士站站-》进入“三测单”模块报错
371 【业务配置】住院医生站-“呼吸内科病房”未在西药房取药规则中维护配置
2026-04-21 13:05:46 +08:00
Ranyunqiao
88d9e19cc5 401
门诊完诊审计日志错误:div_log 表中 pool_id 与 slot_id 存值与设计规范不符
400
门诊医生站点击【完诊】后,triage_queue_item 表 status 字段未按规范更新为 30
393
疾病报告管理-报告卡管理:状态为“审核失败”的报卡操作列缺失“审核”按钮
369
【住院管理】进入护理记录模块报错
361
三测单(体温单)住院第一日显示 1970-01-01,未正确获取入院日期
2026-04-21 11:38:05 +08:00
wangjian963
994ffcb8b8 Bug #384: 检查方法联动功能完善,增加套餐价格查询和项目卡片展开选择
Bug #386: 检验申请删除时同步删除关联收费项目
  Bug #382: 选择项目后保持当前页签状态
  Bug #380,381: 临床诊断获取主诊断字段名修正
  Bug #387: 套餐项目回充默认展开并自动加载明细
2026-04-21 10:18:26 +08:00
Ranyunqiao
5ab4650c4e 360 住院护士站-》三测单:体征录入保存失败 2026-04-20 11:44:37 +08:00
ed75b148a8 修复检查申请单“执行科室”未获取配置默认值且字段交互逻辑不规范 2026-04-17 10:48:49 +08:00
210c463130 修复bug375:住院医生站点击“签发”按钮后系统提示语错误,显示为“保存成功”并且签发业务功能未实现。
bug376:【门诊医生站】检查页签申请单列表过滤异常,显示了历史检查就诊记录
bug377:【门诊医生站】检查申请单“执行科室”未获取配置默认值且字段交互逻辑不规范
2026-04-16 10:25:12 +08:00
6922aa1d2a bug282 门诊医生站-》医嘱TAB页面:总量字段的单位显示数字/给药途径字段的值显示不全 2026-04-15 15:28:49 +08:00
wangjian963
4e2097fc7b fixbug326,329,334,368: 门诊医生站检验申请模块多项缺陷修复
Bug #326: 检验申请单套餐项目回充数据不完整
  - 后端回充时查询 LabActivityDefinition 补全套餐信
  - DTO 新增 activityId、feePackageId、isPackage、sampleType、unit 字段
  - 前端实现套餐项目树形展开,懒加载套餐明细
  Bug #329: 检验申请执行科室默认值设置错误
  - 后端移除默认执行科室逻辑,添加未匹配科室警告日志
  - 前端从 Organization 表获取执行科室,自动根据检验类型设置默认值
  Bug #334: 检验申请界面顶部操作栏占用空间过大
  - 隐藏顶部操作栏,保存/新增按钮移至卡片头部
  Bug #368: 门诊医生站待写病历标签页功能冗余
  - 屏蔽待写病历标签页(左侧导航栏已有独立菜单)
2026-04-15 14:50:14 +08:00
38b4ff5c92 bug280 会诊管理-》门诊会诊申请管理-》【打印】不是打印某一条会诊记录的申请单 2026-04-15 11:04:39 +08:00
e294952a60 fixbug366:门诊医生站:手术医嘱逻辑错误,“待签发”状态的手术医嘱提前流转至收费端 2026-04-15 10:35:56 +08:00
3380b2787e 【华佗】Gitea 提交测试验证 2026-04-15 10:21:10 +08:00
0758ba401b 【荀彧】Gitea 提交测试验证 2026-04-14 23:06:53 +08:00
73ebc20471 【诸葛亮】Git 提交测试 2026-04-14 22:08:27 +08:00
3f36ed4ce8 【张飞】二次测试提交 2026-04-14 21:36 2026-04-14 21:36:23 +08:00
76fdc047b9 Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-14 21:36:16 +08:00
309c470f8a test: 赵云二次验证提交 2026-04-14 21:36:09 +08:00
guanyu
f3fd150235 Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-14 21:36:00 +08:00
guanyu
283cf784a3 test: 【关羽】晚间 git 提交测试 2026-04-14 21:35:44 +08:00
53080648a1 【陈琳】Git提交二次测试 2026-04-14 21:35:13 +08:00
26e0665eeb 103 增加医生个人报卡管理界面(需求) 2026-04-14 17:23:44 +08:00
guanyu
fe7778e6e0 test: ACP test 2026-04-14 17:12:39 +08:00
guanyu
4daf92d4cd Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop 2026-04-14 17:12:08 +08:00
51d4b1e3f2 【张飞】Gitea 提交测试成功 2026-04-14 17:12:02 +08:00
guanyu
0080d89f7e test: 【关羽】禁用代理后测试 gitea 提交 2026-04-14 17:11:41 +08:00
6da4770f47 test: 赵云 Gitea 提交测试
【赵云】内网地址绕过代理验证
2026-04-14 17:11:18 +08:00
918c766b90 【陈琳】Git提交测试 2026-04-14 16:57:33 +08:00
Ranyunqiao
95235b810e 367
门诊医生站:检验开单“免疫”类别下的检验项目取值错误,与后台维护数据不一致
357
门诊挂号:通过“预约签到”产生的记录,列表“挂号类型”未体现预约标识
2026-04-14 16:31:53 +08:00
349beae4a2 test: 刘备API提交测试 - 2026-04-14 2026-04-14 13:01:55 +08:00
guanyu
0550d6a619 test: 关羽测试提交 2026-04-13 23:34:32 +08:00
guanyu
d195ebe3c9 test: verify new password works 2026-04-13 23:13:36 +08:00
guanyu
687f19a1eb test from Claude Code direct 2026-04-13 23:03:48 +08:00
wangjian963
b810c08ae5 Merge remote-tracking branch 'origin/develop' into develop 2026-04-13 18:23:45 +08:00
wangjian963
d99daa3048 修复问题:
1. 修复检验申请单生成的医嘱签发失败问题(BugFix#328)
2. 修复处方工具类空指针异常问题
3. 修复检验项目套餐价格查询问题
4. 修复医嘱签发时费用项状态更新问题
2026-04-13 18:23:36 +08:00
181 changed files with 10227 additions and 1772 deletions

3
.gitignore vendored
View File

@@ -63,3 +63,6 @@ public.sql
发版记录/2025-11-12/发版日志.docx
.gitignore
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
.env.test.local
playwright-report/
test-results/

51
.husky/pre-commit Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env sh
# ============================================================
# Husky Pre-commit Hook - HIS项目
# 配置: 关羽 | 日期: 2026-04-24
# 功能: 提交前自动检查前端构建
# ============================================================
echo "========================================"
echo "🔍 [Pre-commit] HIS项目提交检查"
echo "========================================"
# 检查前端目录是否存在
if [ ! -d "openhis-ui-vue3" ]; then
echo "⚠️ [Pre-commit] 未找到openhis-ui-vue3目录跳过前端检查"
exit 0
fi
cd openhis-ui-vue3
# 检查node_modules是否存在
if [ ! -d "node_modules" ]; then
echo "⚠️ [Pre-commit] node_modules未安装请先执行 npm install"
echo " 提示: 首次使用或依赖变更后需要安装依赖"
exit 1
fi
# 执行lint检查ESLint配置由赵云下周完善后启用
if grep -q '"lint"' package.json 2>/dev/null; then
echo "📋 [Pre-commit] 执行Lint检查..."
if npm run lint -- --max-warnings 0 2>&1; then
echo "✅ [Pre-commit] Lint检查通过"
else
echo "❌ [Pre-commit] Lint检查失败请修复代码规范问题"
exit 1
fi
else
echo "⏭️ [Pre-commit] 未配置lint脚本待赵云配置ESLint后启用"
fi
# 执行快速构建检查development模式仅检查语法和类型
echo "🔨 [Pre-commit] 执行构建检查 (build:dev)..."
if timeout 120 npm run build:dev 2>&1; then
echo "✅ [Pre-commit] 构建检查通过"
else
echo "❌ [Pre-commit] 构建检查失败!请修复编译错误后重新提交"
exit 1
fi
echo "========================================"
echo "✅ [Pre-commit] 所有检查通过,允许提交"
echo "========================================"

View File

@@ -1,6 +0,0 @@
{
"tools": {
"approvalMode": "yolo"
},
"$version": 2
}

1
GIT_TEST.md Normal file
View File

@@ -0,0 +1 @@
# Git 提交测试 - 诸葛亮 Tue Apr 14 10:08:27 PM CST 2026

2
GIT_TEST_CHENLIN.md Normal file
View File

@@ -0,0 +1,2 @@
陈琳Git提交测试 - 2026-04-14 16:57:08
陈琳二次测试 - 2026-04-14 21:35:12

1
TEST.md Normal file
View File

@@ -0,0 +1 @@
# 张飞测试记录

1
TEST2.md Normal file
View File

@@ -0,0 +1 @@
# 张飞二次测试 - 2026-04-14 21:36:00

2
ZHAOYUN_TEST.md Normal file
View File

@@ -0,0 +1,2 @@
# 赵云测试提交
赵云再次测试 - Tue Apr 14 09:36:09 PM CST 2026

1
claude-test.txt Normal file
View File

@@ -0,0 +1 @@
test from Claude Code Mon Apr 13 11:03:46 PM CST 2026

View File

@@ -0,0 +1,162 @@
# 后端发布前检查清单
## 📋 基础检查项
### Maven编译验证
- [ ] 本地执行 `mvn compile` 编译通过无ERROR
- [ ] 执行 `mvn package -DskipTests` 打包成功
- [ ] 依赖版本无冲突(`mvn dependency:tree` 检查)
- [ ] 无编译警告(或已有书面说明可忽略)
### 构建产物验证
- [ ] JAR/WAR包生成完整大小合理
- [ ] `application.yml` 等配置文件已打包进产物
- [ ] 第三方依赖jar包完整lib目录无缺失
---
## 🔧 Spring Boot 配置检查
### 多环境配置
- [ ] `application-dev.yml`(开发)配置正确
- [ ] `application-test.yml`(测试)配置正确
- [ ] `application-prod.yml`(生产)配置正确
- [ ] 启动参数 `--spring.profiles.active` 指定正确环境
- [ ] 生产环境未启用devtools热部署
### Actuator安全
- [ ] 生产环境 `/actuator` 端点已禁用或限制访问
- [ ] `/actuator/env``/actuator/heapdump` 等敏感端点已关闭
- [ ] 健康检查端点 `/actuator/health` 返回信息已脱敏
### 启动校验
- [ ] 数据库连接池配置合理HikariCP最大/最小连接数)
- [ ] Redis/消息中间件连接配置正确
- [ ] 启动日志无ERROR级别异常
---
## 🗄️ MyBatis Plus 规范检查
### 实体-表映射
- [ ] 所有实体类标注 `@TableName`,表名与实际一致
- [ ] 主键字段标注 `@TableId(type = IdType.AUTO)` 或对应策略
- [ ] 非表字段标注 `@TableField(exist = false)`
- [ ] 字段命名符合下划线转驼峰规则
### SQL安全
- [ ] 所有查询使用参数化查询(`QueryWrapper` / `LambdaQueryWrapper`
- [ ] 禁止字符串拼接SQL`"WHERE name = '" + name + "'"`
- [ ] 批量操作使用MyBatis Plus `saveBatch` / `updateBatchById`
- [ ] 复杂SQL使用XML映射避免注解内嵌长SQL
### 事务管理
- [ ] 涉及多表写操作的方法标注 `@Transactional`
- [ ] 事务边界合理不包含外部HTTP调用
- [ ] 异常回滚配置正确(`rollbackFor = Exception.class`
- [ ] 事务方法未被同一类内方法直接调用(自调用失效问题)
### 分页插件
- [ ] `PaginationInnerInterceptor` 已正确配置
- [ ] 分页查询使用 `Page<T>` 对象非手动limit/offset
---
## 🔌 RESTful API 设计检查
### 统一返回格式
- [ ] 所有接口返回 `{code, msg, data}` 统一结构
- [ ] 成功返回 `code=200`,业务错误使用自定义错误码
- [ ] 异常通过 `@ControllerAdvice` + `@ExceptionHandler` 统一处理
### HTTP状态码
- [ ] 资源创建返回 `201 Created`
- [ ] 资源删除返回 `204 No Content`
- [ ] 参数校验失败返回 `400 Bad Request`
- [ ] 未认证返回 `401 Unauthorized`
- [ ] 无权限返回 `403 Forbidden`
- [ ] 资源不存在返回 `404 Not Found`
### 参数校验
- [ ] 请求参数使用 `@Valid` / `@Validated` 注解校验
- [ ] 必填字段标注 `@NotBlank` / `@NotNull`
- [ ] 数值范围标注 `@Min` / `@Max`
- [ ] 格式校验使用 `@Pattern`(如手机号、身份证号)
- [ ] 校验失败返回明确错误信息非500堆栈
### API版本管理
- [ ] 接口路径包含版本号(`/api/v1/``/api/v2/`
- [ ] 废弃接口标注 `@Deprecated`,并在文档中说明
- [ ] 不兼容变更必须升级版本号
---
## 🔒 安全与合规检查
### 数据脱敏
- [ ] 患者身份证号在日志中脱敏(`***` 掩码)
- [ ] 患者手机号在日志中脱敏前3后4中间`****`
- [ ] 敏感字段序列化时使用 `@JsonSerialize` 自定义脱敏器
- [ ] 接口返回中非必需字段不暴露如密码、salt
### 权限控制
- [ ] 所有涉及患者数据的接口标注 `@PreAuthorize`
- [ ] 数据级权限校验(医生只能访问本科室患者)
- [ ] 越权访问返回 `403`,非 `404``500`
- [ ] 敏感操作(删除、修改诊断)需二次确认或额外权限
### 审计日志
- [ ] 处方修改记录操作人、时间、变更内容
- [ ] 病历删除操作记录完整审计链
- [ ] 审计日志独立存储,不可被业务用户删除
- [ ] 关键业务操作记录IP地址和操作终端
---
## ⚡ 性能检查
### 数据库查询
- [ ] 无N+1查询问题使用 `JOIN` 或批量查询)
- [ ] 大表查询必须有分页限制
- [ ] 慢查询已优化(执行时间 < 500ms
- [ ] 索引已覆盖高频查询条件
### 接口性能
- [ ] 核心接口响应时间 < 1秒
- [ ] 列表接口支持分页无全量返回
- [ ] 大文件下载使用流式传输非全量加载到内存
---
## 📝 文档与发布准备
### 文档更新
- [ ] API接口文档已同步更新路径参数返回值
- [ ] 数据库变更脚本已提供DDL/DML
- [ ] 配置变更说明已记录新增/修改的配置项
- [ ] 影响范围说明已明确哪些模块哪些接口受影响
### 回滚预案
- [ ] 数据库变更可回滚提供反向SQL脚本
- [ ] 配置变更可快速回退
- [ ] 紧急回滚流程已明确怎么做多长时间
- [ ] 回滚后数据一致性已验证
---
## ✅ 最终确认
### 发布前最后检查
- [ ] `mvn compile` 构建成功附终端截图
- [ ] 关键单元测试通过
- [ ] 测试环境部署验证通过
- [ ] Code Review 已完成并获得批准
- [ ] 相关Bug已关闭或延期说明
---
**文档版本**v1.0
**最后更新**2026年4月24日
**负责人**关羽后端开发
**适用范围**HIS 系统所有后端模块his-server
**补充说明**本清单与陈琳的前端发布前检查清单对称互补共同构成HIS系统发布前完整质量保障体系

View File

@@ -0,0 +1,217 @@
# CI/CD构建门禁规范
## 🎯 规范目标
建立自动化质量门禁,确保每次代码提交都经过严格验证,防止低质量代码进入主干分支,提升系统稳定性和开发效率。
## 🔒 门禁层级
### 1. 提交前门禁Pre-commit
**触发时机**`git commit` 执行前
**验证内容**
- ESLint 代码规范检查
- Prettier 代码格式化
- 简单的单元测试(快速执行)
**工具配置**
- Husky + lint-staged
- 配置文件:`.husky/pre-commit`
### 2. 推送前门禁Pre-push
**触发时机**`git push` 执行前
**验证内容**
- 完整的单元测试套件
- 构建验证(`npm run build:prod`
- 集成测试(核心流程)
**工具配置**
- Husky pre-push hook
- 配置文件:`.husky/pre-push`
### 3. CI流水线门禁CI Pipeline
**触发时机**:代码推送到远程仓库后
**验证内容**
- 完整的测试套件(单元+集成+端到端)
- 代码覆盖率检查分阶段目标Q1≥30%Q2≥50%Q3≥80%
- 安全扫描SAST
- 构建产物验证
- 部署到测试环境
**工具配置**
- Spug CI/CD 流水线
- Gitea Webhook 触发
### 4. 发布前门禁Release Gate
**触发时机**:准备发布到生产环境前
**验证内容**
- 生产环境冒烟测试
- 性能基准测试
- 安全合规检查
- 回滚预案验证
## ⚙️ 具体配置要求
### ESLint 配置
```javascript
// eslint.config.js 关键配置
import globals from "globals";
import pluginVue from "eslint-plugin-vue";
import parserVue from "vue-eslint-parser";
import importPlugin from "eslint-plugin-import";
export default [
{
name: "app/files-to-lint",
files: ["**/*.{js,mjs,jsx,vue}"],
},
{
name: "app/files-to-ignore",
ignores: ["**/dist/**", "**/node_modules/**", "**/help-center/**"],
},
...pluginVue.configs["flat/recommended"],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: parserVue,
ecmaVersion: "latest",
sourceType: "module",
},
plugins: {
import: importPlugin,
},
rules: {
// 确保导入的模块实际存在(核心规则,防止构建失败)
"import/no-unresolved": "error",
// 确保导入的命名导出实际存在
"import/named": "error",
// 确保默认导出存在
"import/default": "error",
// 确保命名空间导出存在
"import/namespace": "error",
},
},
];
```
```
### Java 后端配置
```xml
<!-- pom.xml 关键插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.2.0</version>
</plugin>
```
### 数据库迁移配置
```yaml
# application.yml Flyway配置
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
```
javascript
// .eslintrc.js 关键配置
module.exports = {
plugins: ['import'],
rules: {
// 确保导入的模块实际存在
'import/no-unresolved': 'error',
// 确保导入的成员实际存在
'import/named': 'error',
// 禁止未使用的导入
'import/no-unused-modules': 'warn'
}
};
```
### Husky 配置
```bash
# .husky/pre-commit
#!/bin/sh
npm run lint-staged
# .husky/pre-push
#!/bin/sh
npm run test:unit && npm run build:prod
```
### lint-staged 配置
```json
// package.json
{
"lint-staged": {
"*.{js,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"]
}
}
```
```json
// package.json
{
"lint-staged": {
"*.{js,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"]
}
}
```
## 🚫 失败处理机制
### 自动处理
- **构建失败**:自动阻止 PR 合并
- **测试失败**:标记 PR 为失败状态
- **安全漏洞**:立即通知安全团队
### 人工处理
- **紧急修复**:可申请临时绕过(需架构师批准)
- **误报处理**:提交豁免申请并说明原因
- **规则调整**:通过 RFC 流程申请规则变更
## 📊 监控与度量
### 关键指标
- 门禁通过率 ≥ 95%
- 平均修复时间 ≤ 2小时
- 误报率 ≤ 5%
### 报告机制
- 每日门禁失败统计
- 周度质量趋势报告
- 月度规则优化建议
## 🔄 持续改进
### 规则演进
- 每月评审门禁规则有效性
- 根据项目需求调整检查强度
- 引入新的质量检查工具
### 团队培训
- 新成员入职培训包含门禁规范
- 定期分享最佳实践案例
- 建立常见问题解决方案库
---
**文档版本**v1.0
**最后更新**2026年4月24日
**负责人**:陈琳(文档专家)
**技术方案**:诸葛亮(架构师)
**适用范围**HIS 系统所有项目

View File

@@ -0,0 +1,135 @@
# 代码提交变更说明模板
## 📝 PR/Commit 模板
### 标题格式
```
<类型>(<模块>): <简短描述>
示例:
feat(patient): 添加患者基本信息编辑功能
fix(doctor): 修复医生排班显示异常问题
docs(api): 更新预约挂号接口文档
refactor(nurse): 重构护士站护理记录组件
```
### 正文模板
```markdown
## 🔍 变更背景
- **问题描述**:详细说明要解决的问题或实现的需求
- **影响范围**:列出受影响的模块、页面、功能
- **相关链接**禅道任务ID、需求文档链接等
## 🛠️ 变更内容
- **主要修改**:核心代码变更点
- **技术方案**:采用的技术方案和设计思路
- **兼容性**是否涉及API或数据结构变更
## 🗄️ 数据库变更
- **表结构变更**:列出新增/修改的表和字段
- **数据迁移**:是否需要数据迁移脚本
- **回滚方案**:数据库变更的回滚策略
## ✅ 验证情况
- **测试覆盖**:单元测试、集成测试覆盖情况
- **手动验证**:手动测试的场景和结果
- **构建验证**:本地构建截图(必填)
## 📋 检查清单
- [ ] 代码已通过 ESLint 检查
- [ ] 本地构建成功(附截图)
- [ ] 核心功能已测试验证
- [ ] 文档已同步更新
- [ ] Code Review 已完成
## 👥 相关人员
- **开发者**@开发者姓名
- **测试者**@测试者姓名
- **审核人**@架构师姓名
```
## 🏷️ 提交类型说明
| 类型 | 说明 | 示例 |
|------|------|------|
| feat | 新功能 | `feat: 添加用户登录功能` |
| fix | Bug修复 | `fix: 修复表单验证错误` |
| docs | 文档更新 | `docs: 更新API文档` |
| style | 代码格式调整 | `style: 格式化代码` |
| refactor | 代码重构 | `refactor: 重构组件结构` |
| test | 测试相关 | `test: 添加单元测试` |
| chore | 构建/依赖等 | `chore: 升级依赖版本` |
| perf | 性能优化 | `perf: 优化列表加载速度` |
## 📁 模块命名规范
| 模块 | 说明 |
|------|------|
| patient | 患者管理相关 |
| doctor | 医生工作站相关 |
| nurse | 护士站相关 |
| admin | 后台管理相关 |
| common | 公共组件/工具 |
| api | API接口相关 |
| auth | 认证授权相关 |
| payment | 支付相关 |
## 🖼️ 构建验证截图要求
### 必须包含的信息
1. **终端窗口**:显示 `npm run build:prod` 命令执行过程
2. **成功标识**:明确显示构建成功的提示信息
3. **时间戳**:截图包含当前时间,证明是最新构建
4. **分支信息**:显示当前工作分支名称
### 截图示例
```
$ git checkout feature/patient-edit
$ npm run build:prod
> his-system@1.0.0 build
> vue-cli-service build
⠇ Building for production...
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
✨ Done in 45.23s.
```
## ⚠️ 禁止行为
### 严重违规(直接拒绝合并)
- 无构建验证截图
- 代码存在 ESLint 错误
- 未填写变更说明
- 修改无关代码文件
### 轻微违规(要求修正后重新提交)
- 描述过于简单
- 测试覆盖不完整
- 文档更新滞后
- 格式不符合规范
## 💡 最佳实践
### 高质量提交特征
- **原子性**:每次提交只解决一个问题
- **可追溯**关联具体的需求或Bug ID
- **可验证**:提供完整的验证证据
- **可理解**:描述清晰,他人能快速理解
### 团队协作建议
- 提交前先在本地完整测试
- 复杂变更提前与团队沟通
- 及时更新相关文档
- 主动帮助新人熟悉规范
---
**文档版本**v1.0
**最后更新**2026年4月24日
**负责人**:陈琳(文档专家)
**适用范围**HIS 系统所有开发人员

View File

@@ -0,0 +1,102 @@
# 前端发布前检查清单
## 📋 基础检查项
### 代码质量
- [ ] 代码已通过 ESLint 检查,无警告和错误
- [ ] 代码已通过 Prettier 格式化
- [ ] 无 console.log() 等调试代码残留
- [ ] 变量命名符合规范,语义清晰
- [ ] 函数职责单一,复杂度适中
### 构建验证
- [ ] 本地执行 `npm run build:prod` 成功完成
- [ ] 构建产物无报错,体积合理
- [ ] 静态资源路径正确无404错误
- [ ] 环境变量配置正确(开发/测试/生产)
### 功能验证
- [ ] 核心功能流程完整测试通过
- [ ] 边界条件和异常场景已覆盖
- [ ] 表单验证逻辑正确
- [ ] API 接口调用正常,错误处理完善
- [ ] 路由跳转逻辑正确
## 🔧 技术检查项
### 模块导入检查
- [ ] 所有 import 语句引用的模块实际存在
- [ ] 无未使用的 import 导入
- [ ] 路径别名(@/)配置正确
- [ ] 第三方库版本兼容性确认
### 性能优化
- [ ] 组件按需加载(懒加载)已配置
- [ ] 大数据列表已实现虚拟滚动或分页
- [ ] 图片资源已压缩,格式合适
- [ ] 无内存泄漏风险(事件监听器、定时器等)
### 安全检查
- [ ] 用户输入已做 XSS 防护
- [ ] 敏感信息不在前端硬编码
- [ ] API 请求已做 CSRF 防护
- [ ] 权限控制逻辑正确
## 🌐 兼容性检查
### 浏览器兼容
- [ ] 主流浏览器Chrome、Firefox、Safari、Edge显示正常
- [ ] 移动端适配良好(如适用)
- [ ] 分辨率适配1366x768、1920x1080等
### 设备兼容
- [ ] 触摸设备操作体验良好
- [ ] 键盘导航支持完整
- [ ] 屏幕阅读器兼容性(无障碍)
## 📱 发布准备
### 文档更新
- [ ] 相关 API 文档已同步更新
- [ ] 用户操作手册已更新(如适用)
- [ ] 变更日志已记录
### 回滚预案
- [ ] 回滚方案已准备
- [ ] 数据兼容性已确认
- [ ] 紧急联系人已明确
## 🔧 后端检查项
### 编译验证
- [ ] Maven编译成功`mvn clean package -DskipTests`
- [ ] 无编译错误,仅有可接受的警告
- [ ] 依赖版本兼容性确认
### 数据库脚本
- [ ] DDL/DML脚本语法正确
- [ ] 回滚脚本已准备
- [ ] 数据迁移脚本已测试
## 🔄 前后端协同
### 接口兼容性
- [ ] API接口契约变更已双方确认
- [ ] 前端调用后端接口正常
- [ ] 错误码处理逻辑一致
## ✅ 最终确认
### 发布前最后检查
- [ ] 本地构建截图已附在 PR 中
- [ ] 测试环境部署验证通过
- [ ] Code Review 已完成并获得批准
- [ ] 相关 Bug 已关闭或延期说明
---
**文档版本**v1.0
**最后更新**2026年4月24日
**负责人**:陈琳(文档专家)
**适用范围**HIS 系统所有前端项目

View File

@@ -0,0 +1,575 @@
# HIS项目发布检查清单 v1.0
> **文档说明**本清单整合了提交规范、前端检查、后端检查、CI/CD门禁四个部分作为HIS项目发布的标准化检查依据。每次发布前必须逐项确认。
## 目录
- [1. 提交规范commit-template](#1-提交规范commit-template)
- [2. 前端检查frontend-checklist](#2-前端检查frontend-checklist)
- [3. 后端检查backend-checklist](#3-后端检查backend-checklist)
- [4. CI/CD门禁cicd-gatekeeper](#4-cicd门禁cicd-gatekeeper)
- [5. 发布确认与回滚预案](#5-发布确认与回滚预案)
---
## 1. 提交规范commit-template
### 📝 PR/Commit 模板
#### 标题格式
```
<类型>(<模块>): <简短描述>
示例:
feat(patient): 添加患者基本信息编辑功能
fix(doctor): 修复医生排班显示异常问题
docs(api): 更新预约挂号接口文档
refactor(nurse): 重构护士站护理记录组件
```
#### 正文模板
```markdown
## 🔍 变更背景
- **问题描述**:详细说明要解决的问题或实现的需求
- **影响范围**:列出受影响的模块、页面、功能
- **相关链接**禅道任务ID、需求文档链接等
## 🛠️ 变更内容
- **主要修改**:核心代码变更点
- **技术方案**:采用的技术方案和设计思路
- **兼容性**是否涉及API或数据结构变更
## 🗄️ 数据库变更
- **表结构变更**:列出新增/修改的表和字段
- **数据迁移**:是否需要数据迁移脚本
- **回滚方案**:数据库变更的回滚策略
## ✅ 验证情况
- **测试覆盖**:单元测试、集成测试覆盖情况
- **手动验证**:手动测试的场景和结果
- **构建验证**:本地构建截图(必填)
## 📋 检查清单
- [ ] 代码已通过 ESLint 检查
- [ ] 本地构建成功(附截图)
- [ ] 核心功能已测试验证
- [ ] 文档已同步更新
- [ ] Code Review 已完成
## 👥 相关人员
- **开发者**@开发者姓名
- **测试者**@测试者姓名
- **审核人**@架构师姓名
```
### 🏷️ 提交类型说明
| 类型 | 说明 | 示例 |
|------|------|------|
| feat | 新功能 | `feat: 添加用户登录功能` |
| fix | Bug修复 | `fix: 修复表单验证错误` |
| docs | 文档更新 | `docs: 更新API文档` |
| style | 代码格式调整 | `style: 格式化代码` |
| refactor | 代码重构 | `refactor: 重构组件结构` |
| test | 测试相关 | `test: 添加单元测试` |
| chore | 构建/依赖等 | `chore: 升级依赖版本` |
| perf | 性能优化 | `perf: 优化列表加载速度` |
### 📁 模块命名规范
| 模块 | 说明 |
|------|------|
| patient | 患者管理相关 |
| doctor | 医生工作站相关 |
| nurse | 护士站相关 |
| admin | 后台管理相关 |
| common | 公共组件/工具 |
| api | API接口相关 |
| auth | 认证授权相关 |
| payment | 支付相关 |
### 🖼️ 构建验证截图要求
#### 必须包含的信息
1. **终端窗口**:显示 `npm run build:prod` 命令执行过程
2. **成功标识**:明确显示构建成功的提示信息
3. **时间戳**:截图包含当前时间,证明是最新构建
4. **分支信息**:显示当前工作分支名称
### ⚠️ 禁止行为
#### 严重违规(直接拒绝合并)
- 无构建验证截图
- 代码存在 ESLint 错误
- 未填写变更说明
- 修改无关代码文件
---
## 2. 前端检查frontend-checklist
### 📋 基础检查项
#### 代码质量
- [ ] 代码已通过 ESLint 检查,无警告和错误
- [ ] 代码已通过 Prettier 格式化
- [ ] 无 console.log() 等调试代码残留
- [ ] 变量命名符合规范,语义清晰
- [ ] 函数职责单一,复杂度适中
#### 构建验证
- [ ] 本地执行 `npm run build:prod` 成功完成
- [ ] 构建产物无报错,体积合理
- [ ] 静态资源路径正确无404错误
- [ ] 环境变量配置正确(开发/测试/生产)
#### 功能验证
- [ ] 核心功能流程完整测试通过
- [ ] 边界条件和异常场景已覆盖
- [ ] 表单验证逻辑正确
- [ ] API 接口调用正常,错误处理完善
- [ ] 路由跳转逻辑正确
### 🔧 技术检查项
#### 模块导入检查
- [ ] 所有 import 语句引用的模块实际存在
- [ ] 无未使用的 import 导入
- [ ] 路径别名(@/)配置正确
- [ ] 第三方库版本兼容性确认
#### 性能优化
- [ ] 组件按需加载(懒加载)已配置
- [ ] 大数据列表已实现虚拟滚动或分页
- [ ] 图片资源已压缩,格式合适
- [ ] 无内存泄漏风险(事件监听器、定时器等)
#### 安全检查
- [ ] 用户输入已做 XSS 防护
- [ ] 敏感信息不在前端硬编码
- [ ] API 请求已做 CSRF 防护
- [ ] 权限控制逻辑正确
### 🌐 兼容性检查
#### 浏览器兼容
- [ ] 主流浏览器Chrome、Firefox、Safari、Edge显示正常
- [ ] 移动端适配良好(如适用)
- [ ] 分辨率适配1366x768、1920x1080等
#### 设备兼容
- [ ] 触摸设备操作体验良好
- [ ] 键盘导航支持完整
- [ ] 屏幕阅读器兼容性(无障碍)
### 📱 发布准备
#### 文档更新
- [ ] 相关 API 文档已同步更新
- [ ] 用户操作手册已更新(如适用)
- [ ] 变更日志已记录
#### 回滚预案
- [ ] 回滚方案已准备
- [ ] 数据兼容性已确认
- [ ] 紧急联系人已明确
### ✅ 最终确认
#### 发布前最后检查
- [ ] 本地构建截图已附在 PR 中
- [ ] 测试环境部署验证通过
- [ ] Code Review 已完成并获得批准
- [ ] 相关 Bug 已关闭或延期说明
---
## 3. 后端检查backend-checklist
### 📋 基础检查项
#### Maven编译验证
- [ ] 本地执行 `mvn compile` 编译通过无ERROR
- [ ] 执行 `mvn package -DskipTests` 打包成功
- [ ] 依赖版本无冲突(`mvn dependency:tree` 检查)
- [ ] 无编译警告(或已有书面说明可忽略)
#### 构建产物验证
- [ ] JAR/WAR包生成完整大小合理
- [ ] `application.yml` 等配置文件已打包进产物
- [ ] 第三方依赖jar包完整lib目录无缺失
### 🔧 Spring Boot 配置检查
#### 多环境配置
- [ ] `application-dev.yml`(开发)配置正确
- [ ] `application-test.yml`(测试)配置正确
- [ ] `application-prod.yml`(生产)配置正确
- [ ] 启动参数 `--spring.profiles.active` 指定正确环境
- [ ] 生产环境未启用devtools热部署
#### Actuator安全
- [ ] 生产环境 `/actuator` 端点已禁用或限制访问
- [ ] `/actuator/env``/actuator/heapdump` 等敏感端点已关闭
- [ ] 健康检查端点 `/actuator/health` 返回信息已脱敏
#### 启动校验
- [ ] 数据库连接池配置合理HikariCP最大/最小连接数)
- [ ] Redis/消息中间件连接配置正确
- [ ] 启动日志无ERROR级别异常
### 🗄️ MyBatis Plus 规范检查
#### 实体-表映射
- [ ] 所有实体类标注 `@TableName`,表名与实际一致
- [ ] 主键字段标注 `@TableId(type = IdType.AUTO)` 或对应策略
- [ ] 非表字段标注 `@TableField(exist = false)`
- [ ] 字段命名符合下划线转驼峰规则
#### SQL安全
- [ ] 所有查询使用参数化查询(`QueryWrapper` / `LambdaQueryWrapper`
- [ ] 禁止字符串拼接SQL`"WHERE name = '" + name + "'"`
- [ ] 批量操作使用MyBatis Plus `saveBatch` / `updateBatchById`
- [ ] 复杂SQL使用XML映射避免注解内嵌长SQL
#### 事务管理
- [ ] 涉及多表写操作的方法标注 `@Transactional`
- [ ] 事务边界合理不包含外部HTTP调用
- [ ] 异常回滚配置正确(`rollbackFor = Exception.class`
- [ ] 事务方法未被同一类内方法直接调用(自调用失效问题)
#### 分页插件
- [ ] `PaginationInnerInterceptor` 已正确配置
- [ ] 分页查询使用 `Page<T>` 对象非手动limit/offset
### 🔌 RESTful API 设计检查
#### 统一返回格式
- [ ] 所有接口返回 `{code, msg, data}` 统一结构
- [ ] 成功返回 `code=200`,业务错误使用自定义错误码
- [ ] 异常通过 `@ControllerAdvice` + `@ExceptionHandler` 统一处理
#### HTTP状态码
- [ ] 资源创建返回 `201 Created`
- [ ] 资源删除返回 `204 No Content`
- [ ] 参数校验失败返回 `400 Bad Request`
- [ ] 未认证返回 `401 Unauthorized`
- [ ] 无权限返回 `403 Forbidden`
- [ ] 资源不存在返回 `404 Not Found`
#### 参数校验
- [ ] 请求参数使用 `@Valid` / `@Validated` 注解校验
- [ ] 必填字段标注 `@NotBlank` / `@NotNull`
- [ ] 数值范围标注 `@Min` / `@Max`
- [ ] 格式校验使用 `@Pattern`(如手机号、身份证号)
- [ ] 校验失败返回明确错误信息非500堆栈
#### API版本管理
- [ ] 接口路径包含版本号(`/api/v1/``/api/v2/`
- [ ] 废弃接口标注 `@Deprecated`,并在文档中说明
- [ ] 不兼容变更必须升级版本号
### 🔒 安全与合规检查
#### 数据脱敏
- [ ] 患者身份证号在日志中脱敏(`***` 掩码)
- [ ] 患者手机号在日志中脱敏前3后4中间`****`
- [ ] 敏感字段序列化时使用 `@JsonSerialize` 自定义脱敏器
- [ ] 接口返回中非必需字段不暴露如密码、salt
#### 权限控制
- [ ] 所有涉及患者数据的接口标注 `@PreAuthorize`
- [ ] 数据级权限校验(医生只能访问本科室患者)
- [ ] 越权访问返回 `403`,非 `404``500`
- [ ] 敏感操作(删除、修改诊断)需二次确认或额外权限
#### 审计日志
- [ ] 处方修改记录操作人、时间、变更内容
- [ ] 病历删除操作记录完整审计链
- [ ] 审计日志独立存储,不可被业务用户删除
- [ ] 关键业务操作记录IP地址和操作终端
### ⚡ 性能检查
#### 数据库查询
- [ ] 无N+1查询问题使用 `JOIN` 或批量查询)
- [ ] 大表查询必须有分页限制
- [ ] 慢查询已优化(执行时间 < 500ms
- [ ] 索引已覆盖高频查询条件
#### 接口性能
- [ ] 核心接口响应时间 < 1秒
- [ ] 列表接口支持分页无全量返回
- [ ] 大文件下载使用流式传输非全量加载到内存
### 📝 文档与发布准备
#### 文档更新
- [ ] API接口文档已同步更新路径参数返回值
- [ ] 数据库变更脚本已提供DDL/DML
- [ ] 配置变更说明已记录新增/修改的配置项
- [ ] 影响范围说明已明确哪些模块哪些接口受影响
#### 回滚预案
- [ ] 数据库变更可回滚提供反向SQL脚本
- [ ] 配置变更可快速回退
- [ ] 紧急回滚流程已明确怎么做多长时间
- [ ] 回滚后数据一致性已验证
### ✅ 最终确认
#### 发布前最后检查
- [ ] `mvn compile` 构建成功附终端截图
- [ ] 关键单元测试通过
- [ ] 测试环境部署验证通过
- [ ] Code Review 已完成并获得批准
- [ ] 相关Bug已关闭或延期说明
---
## 4. CI/CD门禁cicd-gatekeeper
### 🎯 规范目标
建立自动化质量门禁确保每次代码提交都经过严格验证防止低质量代码进入主干分支提升系统稳定性和开发效率
### 🔒 门禁层级
#### 1. 提交前门禁Pre-commit
**触发时机**`git commit` 执行前
**验证内容**
- ESLint 代码规范检查
- Prettier 代码格式化
- 简单的单元测试快速执行
**工具配置**
- Husky + lint-staged
- 配置文件`.husky/pre-commit`
#### 2. 推送前门禁Pre-push
**触发时机**`git push` 执行前
**验证内容**
- 完整的单元测试套件
- 构建验证`npm run build:prod`
- 集成测试核心流程
**工具配置**
- Husky pre-push hook
- 配置文件`.husky/pre-push`
#### 3. CI流水线门禁CI Pipeline
**触发时机**代码推送到远程仓库后
**验证内容**
- 完整的测试套件单元+集成+端到端
- 代码覆盖率检查分阶段目标Q130%Q250%Q380%
- 安全扫描SAST
- 构建产物验证
- 部署到测试环境
**工具配置**
- Spug CI/CD 流水线
- Gitea Webhook 触发
#### 4. 发布前门禁Release Gate
**触发时机**准备发布到生产环境前
**验证内容**
- 生产环境冒烟测试
- 性能基准测试
- 安全合规检查
- 回滚预案验证
### ⚙️ 具体配置要求
#### ESLint 配置
```javascript
// eslint.config.js 关键配置
import globals from "globals";
import pluginVue from "eslint-plugin-vue";
import parserVue from "vue-eslint-parser";
import importPlugin from "eslint-plugin-import";
export default [
{
name: "app/files-to-lint",
files: ["**/*.{js,mjs,jsx,vue}"],
},
{
name: "app/files-to-ignore",
ignores: ["**/dist/**", "**/node_modules/**", "**/help-center/**"],
},
...pluginVue.configs["flat/recommended"],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: parserVue,
ecmaVersion: "latest",
sourceType: "module",
},
plugins: {
import: importPlugin,
},
rules: {
// 确保导入的模块实际存在(核心规则,防止构建失败)
"import/no-unresolved": "error",
// 确保导入的命名导出实际存在
"import/named": "error",
// 确保默认导出存在
"import/default": "error",
// 确保命名空间导出存在
"import/namespace": "error",
},
},
];
```
#### Java 后端配置
```xml
<!-- pom.xml 关键插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.2.0</version>
</plugin>
```
#### 数据库迁移配置
```yaml
# application.yml Flyway配置
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
```
#### Husky 配置
```bash
# .husky/pre-commit
#!/bin/sh
npm run lint-staged
# .husky/pre-push
#!/bin/sh
npm run test:unit && npm run build:prod
```
#### lint-staged 配置
```json
// package.json
{
"lint-staged": {
"*.{js,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"]
}
}
```
### 🚫 失败处理机制
#### 自动处理
- **构建失败**自动阻止 PR 合并
- **测试失败**标记 PR 为失败状态
- **安全漏洞**立即通知安全团队
#### 人工处理
- **紧急修复**可申请临时绕过需架构师批准
- **误报处理**提交豁免申请并说明原因
- **规则调整**通过 RFC 流程申请规则变更
### 📊 监控与度量
#### 关键指标
- 门禁通过率 95%
- 平均修复时间 2小时
- 误报率 5%
#### 报告机制
- 每日门禁失败统计
- 周度质量趋势报告
- 月度规则优化建议
### 🔄 持续改进
#### 规则演进
- 每月评审门禁规则有效性
- 根据项目需求调整检查强度
- 引入新的质量检查工具
#### 团队培训
- 新成员入职培训包含门禁规范
- 定期分享最佳实践案例
- 建立常见问题解决方案库
---
## 5. 发布确认与回滚预案
### 📋 发布前最终确认清单
#### 前端确认
- [ ] 本地构建成功`npm run build:prod`
- [ ] 核心功能流程测试通过
- [ ] 模块导入检查通过无import错误
- [ ] 兼容性测试完成
#### 后端确认
- [ ] Maven编译成功`mvn compile`
- [ ] 单元测试通过
- [ ] 数据库脚本验证通过
- [ ] API接口测试通过
#### 协同确认
- [ ] 前后端接口契约一致
- [ ] 联调测试通过
- [ ] Code Review 已完成
- [ ] 测试环境部署验证通过
### 🚨 回滚预案
#### 触发条件
- [ ] 生产环境出现严重Bug
- [ ] 性能严重下降
- [ ] 数据一致性问题
- [ ] 安全漏洞暴露
#### 回滚步骤
1. **立即停止**暂停新流量进入
2. **版本回退**部署上一个稳定版本
3. **数据回滚**执行数据库回滚脚本如有
4. **验证恢复**确认系统功能正常
5. **问题分析**记录根本原因和改进措施
#### 责任分工
- **技术负责人**执行回滚操作
- **测试负责人**验证回滚后功能
- **项目经理**协调沟通和进度同步
- **运维团队**监控系统状态
### 📞 紧急联系人
| 角色 | 姓名 | 联系方式 | 职责 |
|------|------|----------|------|
| 技术负责人 | 诸葛亮 | @诸葛亮 | 架构决策和技术指导 |
| 前端负责人 | 赵云 | @赵云 | 前端问题处理 |
| 后端负责人 | 关羽 | @关羽 | 后端问题处理 |
| 测试负责人 | 张飞 | @张飞 | 质量验证和问题复现 |
| 项目经理 | 刘备 | @刘备 | 项目协调和进度管理 |
| 文档负责人 | 陈琳 | @陈琳 | 文档维护和知识沉淀 |
---
**文档版本**v1.0
**最后更新**2026年4月25日
**负责人**陈琳文档专家
**适用范围**HIS 系统所有开发人员

View File

@@ -0,0 +1,214 @@
# HIS项目 Playwright E2E 自动化测试方案 v1.0
## 一、方案概述
### 1.1 选型理由
- **Playwright** 是微软开源的端到端测试框架,完美适配 Vue 3 + Vite 技术栈
- 自动等待机制适合HIS系统复杂交互场景异步加载、动态渲染
- 支持多浏览器Chromium/Firefox/WebKitCI/CD集成成熟
- 已有 `@playwright/test ^1.58.2` 依赖 installed
### 1.2 目标
1. 核心业务流程自动化覆盖率达到 80%+
2. 已修复Bug 100% 回归测试覆盖
3. 每次代码推送自动触发测试,失败阻断发布
## 二、项目结构
```
openhis-ui-vue3/
├── tests/
│ ├── e2e/
│ │ ├── fixtures/ # 测试夹具
│ │ │ └── auth.ts # 登录认证fixture
│ │ ├── pages/ # 页面对象模型POM
│ │ │ ├── LoginPage.ts
│ │ │ ├── DoctorStationPage.ts
│ │ │ └── SurgeryBillingPage.ts
│ │ ├── specs/ # 测试用例
│ │ │ ├── login.spec.ts
│ │ │ ├── doctor-station.spec.ts
│ │ │ ├── surgery-billing.spec.ts
│ │ │ └── bug-regression.spec.ts # Bug回归测试
│ │ └── utils/
│ │ └── test-data.ts # 测试数据
│ └── playwright.config.ts # Playwright配置
├── .env.test # 测试环境变量
└── package.json # 已有playwright依赖
```
## 三、环境配置
### 3.1 环境变量(.env.test
```bash
# 测试环境配置
VITE_APP_BASE_API=http://192.168.110.253:8080
TEST_USERNAME=test_admin
TEST_PASSWORD=test123456
TEST_BASE_URL=http://localhost:80
```
### 3.2 Playwright配置playwright.config.ts
```typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e/specs',
timeout: 60 * 1000,
expect: { timeout: 10000 },
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [['html', { outputFolder: 'playwright-report' }], ['list']],
use: {
baseURL: process.env.TEST_BASE_URL || 'http://localhost:80',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
```
## 四、核心测试用例
### 4.1 登录测试login.spec.ts
```typescript
import { test, expect } from '@playwright/test';
test('用户登录成功', async ({ page }) => {
await page.goto('/');
await page.fill('input[placeholder="请输入用户名"]', process.env.TEST_USERNAME || 'admin');
await page.fill('input[placeholder="请输入密码"]', process.env.TEST_PASSWORD || '123456');
await page.click('button:has-text("登录")');
await expect(page).toHaveURL(/.*dashboard.*/);
await expect(page.locator('.user-avatar')).toBeVisible();
});
test('登录失败-错误密码', async ({ page }) => {
await page.goto('/');
await page.fill('input[placeholder="请输入用户名"]', 'admin');
await page.fill('input[placeholder="请输入密码"]', 'wrongpassword');
await page.click('button:has-text("登录")');
await expect(page.locator('.el-message--error')).toBeVisible();
});
```
### 4.2 门诊医生站测试doctor-station.spec.ts
```typescript
import { test, expect } from '@playwright/test';
test.describe('门诊医生站', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('/');
await page.fill('input[placeholder="请输入用户名"]', process.env.TEST_USERNAME || 'admin');
await page.fill('input[placeholder="请输入密码"]', process.env.TEST_PASSWORD || '123456');
await page.click('button:has-text("登录")');
await page.waitForURL(/.*dashboard.*/);
});
test('#427 检查项目分类手风琴展开', async ({ page }) => {
await page.goto('/doctorstation');
// 点击第一个分类
await page.click('.category-item >> nth=0');
await expect(page.locator('.category-content >> nth=0')).toBeVisible();
// 点击第二个分类,第一个应收起
await page.click('.category-item >> nth=1');
await expect(page.locator('.category-content >> nth=0')).not.toBeVisible();
await expect(page.locator('.category-content >> nth=1')).toBeVisible();
});
});
```
### 4.3 手术计费回归测试bug-regression.spec.ts
```typescript
import { test, expect } from '@playwright/test';
test.describe('Bug回归测试', () => {
test('#437 手术计费防重复提交', async ({ page }) => {
// 登录并导航到手术计费
await page.goto('/');
await page.fill('input[placeholder="请输入用户名"]', process.env.TEST_USERNAME || 'admin');
await page.fill('input[placeholder="请输入密码"]', process.env.TEST_PASSWORD || '123456');
await page.click('button:has-text("登录")');
await page.waitForURL(/.*dashboard.*/);
await page.goto('/surgery-billing');
// 快速连续点击新增按钮(测试防重复锁)
const addBtn = page.locator('button:has-text("新增")');
await addBtn.click();
await addBtn.click(); // 第二次应被阻止
await addBtn.click(); // 第三次应被阻止
// 验证只弹出一个表单
await expect(page.locator('.el-dialog')).toHaveCount(1);
});
});
```
## 五、执行命令
```bash
# 安装浏览器
npx playwright install chromium
# 运行所有测试
npm run test:e2e
# 运行单个测试文件
npx playwright test login.spec.ts
# 生成HTML报告
npx playwright show-report
# UI模式调试用
npx playwright test --ui
```
## 六、CI/CD集成
### 6.1 package.json脚本
```json
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
}
}
```
### 6.2 Spug流水线集成
```yaml
# Spug 构建后阶段添加
- name: E2E Testing
script: |
cd openhis-ui-vue3
npx playwright install --with-deps chromium
npm run test:e2e -- --reporter=html
# 测试失败则阻断发布
if [ $? -ne 0 ]; then
echo "E2E测试失败阻断发布"
exit 1
fi
```
## 七、实施计划
| 阶段 | 时间 | 内容 | 负责人 |
|------|------|------|--------|
| Phase 1 | 第1周 | 登录+核心页面冒烟测试 | 张飞+赵云 |
| Phase 2 | 第2-3周 | 门诊医生站+手术计费全流程 | 张飞 |
| Phase 3 | 第4周 | Bug回归测试全覆盖 | 张飞 |
| Phase 4 | 第5周 | CI/CD流水线集成 | 赵云+运维 |
## 八、注意事项
1. **测试数据隔离**:使用独立的测试数据库,不污染生产数据
2. **环境变量**:敏感信息通过 `.env.test` 管理不提交到git
3. **截图留痕**:失败时自动截图,便于排查
4. **测试优先**:新功能开发时同步编写测试用例

1
g.txt Normal file
View File

@@ -0,0 +1 @@
test Mon Apr 13 11:34:31 PM CST 2026

1
git_test3.md Normal file
View File

@@ -0,0 +1 @@
# Git 代理禁用后测试 - 关羽 2026-04-14 17:11:41

1
git_test4.md Normal file
View File

@@ -0,0 +1 @@
# Git 晚间测试 - 关羽 2026-04-14 21:35:44

1
gitea_test_huatuo.txt Normal file
View File

@@ -0,0 +1 @@
华佗 Gitea 提交测试成功 - Wed Apr 15 10:21:10 AM CST 2026

1
gitea_test_xunyu.txt Normal file
View File

@@ -0,0 +1 @@
荀彧 Gitea 提交测试成功 - Tue Apr 14 11:06:47 PM CST 2026

View File

@@ -0,0 +1,30 @@
package com.core.web.controller.system;
import com.core.common.config.CoreConfig;
import com.core.common.core.domain.AjaxResult;
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.RestController;
/**
* 系统版本信息
*/
@RestController
@RequestMapping("/system")
public class SysVersionController {
@Autowired
private CoreConfig coreConfig;
/**
* 获取后端版本号
*/
@GetMapping("/version")
public AjaxResult getVersion() {
AjaxResult ajax = AjaxResult.success();
ajax.put("backendVersion", coreConfig.getVersion());
return ajax;
}
}

View File

@@ -107,6 +107,9 @@ public class SecurityConfig {
.permitAll()
.antMatchers("/patientmanage/information/**")
.permitAll()
// 登录页展示用的系统版本信息,允许匿名访问
.antMatchers("/system/version")
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})

View File

@@ -153,7 +153,9 @@ public class TicketAppServiceImpl implements ITicketAppService {
dto.setIdCard(raw.getIdCard());
dto.setDoctorId(raw.getDoctorId());
dto.setDepartmentId(raw.getDepartmentId());
dto.setRealPatientId(raw.getPatientId());
dto.setRealPatientId(raw.getPatientId());
dto.setOrderId(raw.getOrderId());
dto.setOrderNo(raw.getOrderNo());
// 性别处理:直接使用患者表中的 genderEnum
Integer genderEnum = raw.getGenderEnum();

View File

@@ -115,4 +115,15 @@ public class TicketDto {
* 身份证号
*/
private String idCard;
/**
* 预约订单ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
/**
* 预约订单号
*/
private String orderNo;
}

View File

@@ -12,6 +12,8 @@ import com.core.common.utils.SecurityUtils;
import com.core.common.core.domain.model.LoginUser;
import com.openhis.infectious.domain.InfectiousAudit;
import com.openhis.infectious.domain.InfectiousCard;
import com.openhis.administration.domain.Practitioner;
import com.openhis.administration.service.IPractitionerService;
import com.openhis.web.cardmanagement.appservice.ICardManageAppService;
import com.openhis.web.cardmanagement.dto.*;
import com.openhis.web.cardmanagement.mapper.InfectiousAuditMapper;
@@ -52,6 +54,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
private final InfectiousCardMapper infectiousCardMapper;
private final InfectiousAuditMapper infectiousAuditMapper;
private final IPractitionerService iPractitionerService;
@Override
public CardStatisticsDto getStatistics() {
@@ -74,7 +77,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
// 状态
if (StringUtils.hasText(queryParams.getStatus())) {
if (queryParams.getStatus() != null) {
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
}
@@ -127,7 +130,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
if (card == null) {
return new ArrayList<>();
}
List<InfectiousAudit> records = infectiousAuditMapper.selectByCardId(card.getId());
List<InfectiousAudit> records = infectiousAuditMapper.selectByCardId(card.getCardNo());
return records.stream().map(this::convertAuditToDto).collect(Collectors.toList());
}
@@ -145,16 +148,16 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
for (String cardNo : batchAuditDto.getCardNos()) {
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
if (card == null) continue;
if ("2".equals(card.getStatus()) || "3".equals(card.getStatus())) continue;
if (Integer.valueOf(2).equals(card.getStatus()) || Integer.valueOf(3).equals(card.getStatus())) continue;
// 更新状态为已审核
String oldStatus = card.getStatus();
card.setStatus("2");
Integer oldStatus = card.getStatus();
card.setStatus(2);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
// 创建审核记录
createAuditRecord(card.getId(), oldStatus, "2", "1", batchAuditDto.getAuditOpinion(),
createAuditRecord(card.getCardNo(), oldStatus, 2, 1, batchAuditDto.getAuditOpinion(),
null, auditorId, auditorName, true, batchAuditDto.getCardNos().size());
successCount++;
}
@@ -176,17 +179,17 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
for (String cardNo : batchReturnDto.getCardNos()) {
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
if (card == null) continue;
if ("2".equals(card.getStatus()) || "3".equals(card.getStatus())) continue;
if (Integer.valueOf(2).equals(card.getStatus()) || Integer.valueOf(3).equals(card.getStatus())) continue;
// 更新状态为退回 (审核失败)
String oldStatus = card.getStatus();
card.setStatus("5");
Integer oldStatus = card.getStatus();
card.setStatus(5);
card.setReturnReason(batchReturnDto.getReturnReason());
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
// 创建审核记录
createAuditRecord(card.getId(), oldStatus, "5", "3", null,
createAuditRecord(card.getCardNo(), oldStatus, 5, 3, null,
batchReturnDto.getReturnReason(), auditorId, auditorName, true, batchReturnDto.getCardNos().size());
successCount++;
}
@@ -206,13 +209,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
String auditorName = SecurityUtils.getUsername();
// 更新状态
String oldStatus = card.getStatus();
card.setStatus("2");
Integer oldStatus = card.getStatus();
card.setStatus(2);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
// 创建审核记录
createAuditRecord(card.getId(), oldStatus, "2", "2", auditDto.getAuditOpinion(),
createAuditRecord(card.getCardNo(), oldStatus, 2, 2, auditDto.getAuditOpinion(),
null, auditorId, auditorName, false, 1);
return R.ok("审核通过");
@@ -230,14 +233,14 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
String auditorName = SecurityUtils.getUsername();
// 更新状态
String oldStatus = card.getStatus();
card.setStatus("5");
Integer oldStatus = card.getStatus();
card.setStatus(5);
card.setReturnReason(returnDto.getReturnReason());
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
// 创建审核记录
createAuditRecord(card.getId(), oldStatus, "5", "4", null,
createAuditRecord(card.getCardNo(), oldStatus, 5, 4, null,
returnDto.getReturnReason(), auditorId, auditorName, false, 1);
return R.ok("已退回");
@@ -251,7 +254,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
if (queryParams.getRegistrationSource() != null) {
wrapper.eq(InfectiousCard::getRegistrationSource, queryParams.getRegistrationSource());
}
if (StringUtils.hasText(queryParams.getStatus())) {
if (queryParams.getStatus() != null) {
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
}
if (StringUtils.hasText(queryParams.getPatientName())) {
@@ -292,7 +295,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
row.createCell(1).setCellValue(card.getPatName());
row.createCell(2).setCellValue("1".equals(card.getSex()) ? "" : "2".equals(card.getSex()) ? "" : "未知");
row.createCell(3).setCellValue(card.getAge() != null ? card.getAge() + "" : "");
row.createCell(4).setCellValue(card.getDiseaseName());
row.createCell(4).setCellValue(card.getDiseaseCode());
row.createCell(5).setCellValue(card.getDeptName());
row.createCell(6).setCellValue(card.getCreateTime() != null ? dateFormat.format(card.getCreateTime()) : "");
row.createCell(7).setCellValue(getStatusText(card.getStatus()));
@@ -316,7 +319,19 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
@Override
public DoctorCardStatisticsDto getDoctorCardStatistics() {
Long doctorId = SecurityUtils.getUserId();
Long userId = SecurityUtils.getUserId();
// 通过 sys_user 表的 user_id 查询医生表 (adm_practitioner) 获取医生 ID
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null) {
DoctorCardStatisticsDto dto = new DoctorCardStatisticsDto();
dto.setTotalCount(0);
dto.setPendingFailedCount(0);
dto.setReportedCount(0);
return dto;
}
Long doctorId = practitioner.getId();
DoctorCardStatisticsDto dto = new DoctorCardStatisticsDto();
Integer totalCount = infectiousCardMapper.countByDoctorId(doctorId);
@@ -331,7 +346,18 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
@Override
public R<?> getDoctorCardPage(DoctorCardQueryDto queryParams) {
Long doctorId = SecurityUtils.getUserId();
Long userId = SecurityUtils.getUserId();
// 通过 sys_user 表的 user_id 查询医生表 (adm_practitioner) 获取医生 ID
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null) {
Map<String, Object> emptyResult = new HashMap<>();
emptyResult.put("list", new ArrayList<>());
emptyResult.put("total", 0L);
return R.ok(emptyResult);
}
Long doctorId = practitioner.getId();
Page<InfectiousCard> page = new Page<>(queryParams.getPageNo(), queryParams.getPageSize());
LambdaQueryWrapper<InfectiousCard> wrapper = new LambdaQueryWrapper<>();
@@ -340,7 +366,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
wrapper.eq(InfectiousCard::getDoctorId, doctorId);
// 状态筛选
if (StringUtils.hasText(queryParams.getStatus())) {
if (queryParams.getStatus() != null) {
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
}
@@ -354,13 +380,24 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
wrapper.le(InfectiousCard::getCreateTime, endDateTime);
}
// 关键词搜索(患者姓名报卡名称)
// 关键词搜索(患者姓名、疾病编码、报卡名称)
if (StringUtils.hasText(queryParams.getKeyword())) {
wrapper.and(w -> w
.like(InfectiousCard::getPatName, queryParams.getKeyword())
.or()
.like(InfectiousCard::getDiseaseName, queryParams.getKeyword())
);
String kw = queryParams.getKeyword();
// 将关键词匹配报卡名称,找出对应的 cardNameCode 列表
List<Integer> matchedCodes = getMatchedCardNameCodes(kw);
// cardNameCode为null的记录默认也属于"中华人民共和国传染病报告卡"匹配到code=1时需包含Null记录
boolean includeNull = matchedCodes.contains(1);
wrapper.and(w -> {
w.like(InfectiousCard::getPatName, kw)
.or()
.like(InfectiousCard::getDiseaseCode, kw);
if (!matchedCodes.isEmpty()) {
w.or().in(InfectiousCard::getCardNameCode, matchedCodes);
}
if (includeNull) {
w.or().isNull(InfectiousCard::getCardNameCode);
}
});
}
// 按创建时间倒序
@@ -388,17 +425,19 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
// 验证权限:只能提交自己的报卡
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
Long userId = SecurityUtils.getUserId();
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null || !practitioner.getId().equals(card.getDoctorId())) {
return R.fail("无权操作此报卡");
}
// 证状态:只有暂存状态可以提交
if (!"0".equals(card.getStatus())) {
// 证状态:只有暂存状态可以提交
if (!Integer.valueOf(0).equals(card.getStatus())) {
return R.fail("只能提交暂存状态的报卡");
}
// 更新状态为已提交
card.setStatus("1");
card.setStatus(1);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
@@ -414,17 +453,19 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
// 验证权限:只能撤回自己的报卡
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
Long userId = SecurityUtils.getUserId();
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null || !practitioner.getId().equals(card.getDoctorId())) {
return R.fail("无权操作此报卡");
}
// 证状态:只有已提交状态可以撤回
if (!"1".equals(card.getStatus())) {
// 证状态:只有已提交状态可以撤回
if (!Integer.valueOf(1).equals(card.getStatus())) {
return R.fail("只能撤回已提交状态的报卡");
}
// 更新状态为暂存
card.setStatus("0");
card.setStatus(0);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
@@ -440,17 +481,19 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
// 验证权限:只能删除自己的报卡
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
Long userId = SecurityUtils.getUserId();
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null || !practitioner.getId().equals(card.getDoctorId())) {
return R.fail("无权操作此报卡");
}
// 证状态:只有暂存状态可以删除
if (!"0".equals(card.getStatus())) {
// 证状态:只有暂存状态可以删除
if (!Integer.valueOf(0).equals(card.getStatus())) {
return R.fail("只能删除暂存状态的报卡");
}
// 更新状态为作废
card.setStatus("6");
card.setStatus(6);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
@@ -464,7 +507,12 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
return R.fail("请选择要提交的报卡");
}
Long doctorId = SecurityUtils.getUserId();
Long userId = SecurityUtils.getUserId();
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null) {
return R.fail("当前用户未关联医生信息");
}
Long doctorId = practitioner.getId();
int successCount = 0;
for (String cardNo : cardNos) {
@@ -472,13 +520,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
if (card == null) continue;
// 验证权限:只能提交自己的报卡
if (!card.getDoctorId().equals(doctorId)) continue;
// 验证状态:只有暂存状态可以提交
if (!"0".equals(card.getStatus())) continue;
if (!doctorId.equals(card.getDoctorId())) continue;
// 狋证状态:只有暂存状态可以提交
if (!Integer.valueOf(0).equals(card.getStatus())) continue;
// 更新状态为已提交
card.setStatus("1");
card.setStatus(1);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
successCount++;
@@ -498,7 +546,12 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
return R.fail("请选择要删除的报卡");
}
Long doctorId = SecurityUtils.getUserId();
Long userId = SecurityUtils.getUserId();
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null) {
return R.fail("当前用户未关联医生信息");
}
Long doctorId = practitioner.getId();
int successCount = 0;
for (String cardNo : cardNos) {
@@ -506,13 +559,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
if (card == null) continue;
// 验证权限:只能删除自己的报卡
if (!card.getDoctorId().equals(doctorId)) continue;
// 验证状态:只有暂存状态可以删除
if (!"0".equals(card.getStatus())) continue;
if (!doctorId.equals(card.getDoctorId())) continue;
// 狋证状态:只有暂存状态可以删除
if (!Integer.valueOf(0).equals(card.getStatus())) continue;
// 更新状态为作废
card.setStatus("6");
card.setStatus(6);
card.setUpdateTime(new Date());
infectiousCardMapper.updateById(card);
successCount++;
@@ -530,6 +583,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
// 获取当前登录用户信息
LoginUser loginUser = SecurityUtils.getLoginUser();
Long currentUserId = loginUser.getUserId();
// 通过 sys_user 表的 user_id 查询医生表 (adm_practitioner) 获取医生 ID
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(currentUserId);
if (practitioner == null) {
return R.fail("当前用户未关联医生信息");
}
Long doctorId = practitioner.getId();
// 查询报卡
InfectiousCard card = infectiousCardMapper.selectByCardNo(updateDto.getCardNo());
@@ -538,12 +598,12 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
// 验证是否当前医生的报卡 - 根据 doctorId 字段验证
if (!currentUserId.equals(card.getDoctorId())) {
if (!doctorId.equals(card.getDoctorId())) {
return R.fail("只能修改自己的报卡");
}
// 证状态是否允许修改(只能修改暂存状态的报卡)
if (!"0".equals(card.getStatus())) {
// 证状态是否允许修改(只能修改暂存状态的报卡)
if (!Integer.valueOf(0).equals(card.getStatus())) {
return R.fail("只能修改暂存状态的报卡");
}
@@ -559,15 +619,6 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
card.setUpdateTime(new Date());
card.setUpdateBy(loginUser.getUsername()); // 使用username作为更新者
card.setUpdateTime(new Date());
card.setUpdateBy(loginUser.getUsername()); // 使用 username 作为更新者
card.setUpdateTime(new Date());
card.setUpdateBy(loginUser.getUsername()); // 使用 username 作为更新者
card.setUpdateTime(new Date());
card.setUpdateBy(loginUser.getUsername()); // 使用 username 作为更新者
int rows = infectiousCardMapper.updateById(card);
if (rows > 0) {
return R.ok("更新成功");
@@ -583,12 +634,14 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
// 验证权限:只能导出自己的报卡
if (!card.getDoctorId().equals(SecurityUtils.getUserId())) {
Long userId = SecurityUtils.getUserId();
Practitioner practitioner = iPractitionerService.getPractitionerByUserId(userId);
if (practitioner == null || !practitioner.getId().equals(card.getDoctorId())) {
return;
}
// 证状态:只有已上报状态可以导出
if (!"3".equals(card.getStatus())) {
// 证状态:只有已上报状态可以导出
if (!Integer.valueOf(3).equals(card.getStatus())) {
return;
}
@@ -612,6 +665,8 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
private DoctorCardListDto convertToDoctorCardListDto(InfectiousCard card) {
DoctorCardListDto dto = new DoctorCardListDto();
BeanUtils.copyProperties(card, dto);
// 由于数据库中没有 disease_name 字段,使用 disease_code 作为疾病名称展示
dto.setDiseaseName(card.getDiseaseCode());
dto.setCardName(getCardName(card.getCardNameCode()));
dto.setSubmitTime(card.getCreateTime() != null ?
new SimpleDateFormat("yyyy-MM-dd HH:mm").format(card.getCreateTime()) : null);
@@ -632,13 +687,35 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
}
}
/**
* 根据关键词匹配报卡名称,返回匹配的 cardNameCode 列表
*/
private List<Integer> getMatchedCardNameCodes(String keyword) {
// 报卡名称映射表 code -> name
java.util.Map<Integer, String> cardNameMap = new java.util.LinkedHashMap<>();
cardNameMap.put(1, "中华人民共和国传染病报告卡");
cardNameMap.put(2, "甲类传染病报告卡");
cardNameMap.put(3, "乙类传染病报告卡");
cardNameMap.put(4, "丙类传染病报告卡");
List<Integer> matchedCodes = new ArrayList<>();
for (java.util.Map.Entry<Integer, String> entry : cardNameMap.entrySet()) {
if (entry.getValue().contains(keyword)) {
matchedCodes.add(entry.getKey());
}
}
// cardNameCode 为 null 的数据默认也是「中华人民共和国传染病报告卡」
// 如果关键词匹配 code=1则同时要包含 null 的记录
return matchedCodes;
}
/**
* 转换审核记录为 DTO
*/
private AuditRecordDto convertAuditToDto(InfectiousAudit audit) {
AuditRecordDto dto = new AuditRecordDto();
BeanUtils.copyProperties(audit, dto);
dto.setCardId(audit.getCardId() != null ? audit.getCardId().toString() : null);
dto.setCardId(audit.getCardId());
return dto;
}
@@ -648,6 +725,8 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
private InfectiousCardDto convertToDto(InfectiousCard card) {
InfectiousCardDto dto = new InfectiousCardDto();
BeanUtils.copyProperties(card, dto);
// 由于数据库中没有 disease_name 字段,使用 disease_code 作为疾病名称展示
dto.setDiseaseName(card.getDiseaseCode());
dto.setStatusText(getStatusText(card.getStatus()));
return dto;
}
@@ -655,15 +734,15 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
/**
* 创建审核记录
*/
private void createAuditRecord(Long cardId, String statusFrom, String statusTo, String auditType,
private void createAuditRecord(String cardId, Integer statusFrom, Integer statusTo, Integer auditType,
String auditOpinion, String returnReason, String auditorId, String auditorName,
Boolean isBatch, Integer batchSize) {
InfectiousAudit audit = new InfectiousAudit();
audit.setCardId(cardId);
audit.setAuditSeq(infectiousAuditMapper.getNextAuditSeq(cardId));
audit.setAuditType(auditType);
audit.setAuditStatusFrom(statusFrom);
audit.setAuditStatusTo(statusTo);
audit.setAuditType(String.valueOf(auditType));
audit.setAuditStatusFrom(statusFrom != null ? String.valueOf(statusFrom) : null);
audit.setAuditStatusTo(statusTo != null ? String.valueOf(statusTo) : null);
audit.setAuditTime(LocalDateTime.now());
audit.setAuditorId(auditorId);
audit.setAuditorName(auditorName);
@@ -677,15 +756,16 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
/**
* 获取状态文本
*/
private String getStatusText(String status) {
private String getStatusText(Integer status) {
if (status == null) return "未知";
switch (status) {
case "0": return "暂存";
case "1": return "已提交";
case "2": return "审核通过";
case "3": return "已上报";
case "4": return "失败";
case "5": return "审核失败";
case "6": return "作废";
case 0: return "暂存";
case 1: return "已提交";
case 2: return "审核通过";
case 3: return "已上报";
case 4: return "失败";
case 5: return "审核失败";
case 6: return "作废";
default: return "未知";
}
}

View File

@@ -29,8 +29,8 @@ public class CardQueryDto {
/** 患者姓名 */
private String patientName;
/** 审核状态 */
private String status;
/** 审核状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
private Integer status;
/** 科室ID */
private Long deptId;

View File

@@ -7,6 +7,9 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 医生个人报卡列表DTO
*
@@ -41,6 +44,51 @@ public class DoctorCardListDto {
/** 提交时间 */
private String submitTime;
/** 状态 */
private String status;
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
private Integer status;
/** 疾病名称 */
private String diseaseName;
/** 发病日期 */
private LocalDate onsetDate;
/** 诊断日期 */
private LocalDateTime diagDate;
/** 报告单位 */
private String reportOrg;
/** 报告医生 */
private String reportDoc;
/** 传染病类别 */
private String diseaseType;
/** 性别 (1男/2女/0未知) */
private String sex;
/** 年龄 */
private Integer age;
/** 年龄单位 (1岁/2月/3天) */
private String ageUnit;
/** 现住址省 */
private String addressProv;
/** 现住址市 */
private String addressCity;
/** 现住址县 */
private String addressCounty;
/** 现住址街道 */
private String addressTown;
/** 现住址村/居委 */
private String addressVillage;
/** 现住址门牌号 */
private String addressHouse;
}

View File

@@ -26,8 +26,8 @@ public class DoctorCardQueryDto {
/** 结束日期 */
private String endDate;
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回) */
private String status;
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
private Integer status;
/** 患者姓名或报卡名称 */
private String keyword;

View File

@@ -1,18 +1,44 @@
package com.openhis.web.cardmanagement.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class DoctorCardUpdateDto {
@NotBlank(message = "卡片编号不能为空")
private String cardNo;
private String phone;
private String contactPhone; // 紧急联系人电话
private LocalDate onsetDate;
private LocalDateTime diagDate;
private String diseaseType; // 修改为diseaseType对应InfectiousCard中的diseaseType字段
private String addressProv;
private String addressCity;
private String addressCounty;
private String addressHouse;
private String diseaseType; // 病例分类对应InfectiousCard中的diseaseType字段
private String diseaseCode; // 疾病编码
@NotNull(message = "病例类别不能为空")
private Integer caseClass; // 病例类别(1疑似病例/2临床诊断病例/3实验室确诊病例/4病原携带者/5阳性检测结果)
private String occupation; // 职业
@NotNull(message = "病人属于不能为空")
private Integer patientBelong; // 病人属于(1本县区/2本市其他县区/3本省其他地市/4外省/5港澳台/6外籍)
private String addressProv; // 现住址省
private String addressCity; // 现住址市
private String addressCounty; // 现住址县
private String addressTown; // 现住址街道
private String addressVillage; // 现住址村/居委
private String addressHouse; // 现住址门牌号
private String parentName; // 家长姓名
private String workplace; // 工作单位
private String correctName; // 订正病名
private LocalDate deathDate; // 死亡日期
private String withdrawReason; // 退卡原因
private String otherDisease; // 其他传染病名称
}

View File

@@ -65,8 +65,8 @@ public class InfectiousCardDto {
/** 现住址门牌号 */
private String addressHouse;
/** 病人属于 */
private String patientbelong;
/** 病人属于(1本县区/2本市其他县区/3本省其他地市/4外省/5港澳台/6外籍) */
private Integer patientBelong;
/** 职业 */
private String occupation;
@@ -110,8 +110,8 @@ public class InfectiousCardDto {
/** 填卡日期 */
private LocalDate reportDate;
/** 状态 */
private String status;
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
private Integer status;
/** 状态文本 */
private String statusText;

View File

@@ -18,14 +18,14 @@ import java.util.List;
public interface InfectiousAuditMapper extends BaseMapper<InfectiousAudit> {
/**
* 根据报卡ID查询审核记录
* 根据报卡编号查询审核记录
*/
@Select("SELECT * FROM infectious_audit WHERE card_id = #{cardId} ORDER BY audit_time DESC")
List<InfectiousAudit> selectByCardId(@Param("cardId") Long cardId);
List<InfectiousAudit> selectByCardId(@Param("cardId") String cardId);
/**
* 获取下一个审核序号
*/
@Select("SELECT COALESCE(MAX(audit_seq), 0) + 1 FROM infectious_audit WHERE card_id = #{cardId}")
Integer getNextAuditSeq(@Param("cardId") Long cardId);
Integer getNextAuditSeq(@Param("cardId") String cardId);
}

View File

@@ -21,25 +21,25 @@ public interface InfectiousCardMapper extends BaseMapper<InfectiousCard> {
/**
* 统计今日待审核数量
*/
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE(create_time) = CURRENT_DATE AND status = '1'")
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE(create_time) = CURRENT_DATE AND status = 1")
Integer countTodayPending();
/**
* 统计本月审核失败数量
*/
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = '5'")
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = 5")
Integer countMonthFailed();
/**
* 统计本月审核成功数量
*/
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = '2'")
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = 2")
Integer countMonthSuccess();
/**
* 统计本月已上报数量
*/
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = '3'")
@Select("SELECT COUNT(*) FROM infectious_card WHERE DATE_TRUNC('month', create_time) = DATE_TRUNC('month', CURRENT_DATE) AND status = 3")
Integer countMonthReported();
/**
@@ -55,14 +55,14 @@ public interface InfectiousCardMapper extends BaseMapper<InfectiousCard> {
Integer countByDoctorId(@Param("doctorId") Long doctorId);
/**
* 统计医生待处理失败状态为0暂存或4失败
* 统计医生待提交状态为0暂存待提交
*/
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId} AND status IN ('0', '4')")
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId} AND status = 0")
Integer countPendingFailedByDoctorId(@Param("doctorId") Long doctorId);
/**
* 统计医生已成功上报数状态为3已上报
*/
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId} AND status = '3'")
@Select("SELECT COUNT(*) FROM infectious_card WHERE doctor_id = #{doctorId} AND status = 3")
Integer countReportedByDoctorId(@Param("doctorId") Long doctorId);
}

View File

@@ -40,7 +40,9 @@ import com.openhis.web.chargemanage.dto.PatientMetadata;
import com.openhis.web.chargemanage.dto.PractitionerMetadata;
import com.openhis.web.chargemanage.dto.ReprintRegistrationDto;
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
import com.openhis.triageandqueuemanage.domain.DivLog;
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
import com.openhis.triageandqueuemanage.service.DivLogService;
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
import com.openhis.web.paymentmanage.appservice.IPaymentRecService;
import com.openhis.web.paymentmanage.dto.CancelPaymentDto;
@@ -111,6 +113,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
@Resource
com.openhis.triageandqueuemanage.service.TriageQueueItemService triageQueueItemService;
@Resource
DivLogService divLogService;
@Resource
ScheduleSlotMapper scheduleSlotMapper;
@@ -807,6 +812,23 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
queueItem.setDeleteFlag("1");
queueItem.setUpdateTime(LocalDateTime.now());
triageQueueItemService.updateById(queueItem);
// 写入分诊操作日志:诊前退号
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog divLog = new DivLog()
.setPoolId(queueItem.getPoolId())
.setSlotId(queueItem.getSlotId())
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
.setAction("REFUND")
.setCreateTime(LocalDateTime.now())
.setUpdateAt(LocalDateTime.now())
.setCreatedAt(LocalDateTime.now());
divLogService.save(divLog);
} catch (Exception e) {
log.error("写入分诊退号日志失败encounterId={}", encounterId, e);
}
log.info("退号成功已移除分诊队列记录encounterId={}, queueItemId={}", encounterId, queueItem.getId());
}

View File

@@ -62,8 +62,13 @@ public class OutpatientPricingController {
@RequestParam(value = "organizationId") Long organizationId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "categoryCode", required = false) String categoryCode) {
@RequestParam(value = "categoryCode", required = false) String categoryCode,
@RequestParam(value = "adviceType", required = false) Integer adviceType) {
// 将 categoryCode 设置到 adviceBaseDto 中
// Bug #438 修复:接收并处理 adviceType 参数
if (adviceType != null) {
adviceBaseDto.setAdviceType(adviceType);
}
if (categoryCode != null && !categoryCode.isEmpty()) {
adviceBaseDto.setCategoryCode(categoryCode);
}

View File

@@ -146,4 +146,33 @@ public class CurrentDayEncounterDto {
*/
private Integer displayOrder;
/**
* 是否来自预约签到
* true: 预约签到
* false: 正常挂号
*/
private Boolean isFromAppointment;
/**
* 号源槽位ID关联 adm_schedule_slot.id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long slotId;
/**
* 号源池ID关联 adm_schedule_pool.id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long poolId;
/**
* 诊室名称Bug #410分诊队列需显示诊室而非科室
*/
private String clinicRoom;
/**
* 预约序号(来自 adm_schedule_slot.seq_no
*/
private Integer seqNo;
}

View File

@@ -72,6 +72,12 @@ public class EncounterFormData {
@JsonSerialize(using = ToStringSerializer.class)
private Long organizationId;
/**
* 预约订单ID用于预约签到时关联预约订单
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
/**
* 设置默认值
*/

View File

@@ -209,4 +209,14 @@ public class EncounterPatientPrescriptionDto {
private String discountRate = "0";
private String discountRate_dictText;
/**
* 账单生成来源
*/
private Integer generateSourceEnum;
/**
* 来源单据号(手术单号等)
*/
private String sourceBillNo;
}

View File

@@ -4,8 +4,11 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.openhis.check.domain.CheckMethod;
import com.openhis.check.domain.CheckPackage;
import com.openhis.check.service.ICheckMethodService;
import com.openhis.check.service.ICheckPackageService;
import com.openhis.web.check.appservice.ICheckMethodAppService;
import com.openhis.web.check.dto.CheckMethodDto;
import com.openhis.web.reportmanage.utils.ExcelFillerUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -16,6 +19,7 @@ import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Slf4j
@@ -24,10 +28,15 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
@Resource
private ICheckMethodService checkMethodService;
@Resource
private ICheckPackageService checkPackageService; // Bug #384修复注入套餐服务
@Override
public R<?> getCheckMethodList() {
List<CheckMethod> list = checkMethodService.list();
return R.ok(list);
// Bug #384修复转换为DTO并关联套餐价格
List<CheckMethodDto> dtoList = convertToDtoWithPackagePrice(list);
return R.ok(dtoList);
}
@Override
@@ -43,7 +52,67 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
wrapper.eq(CheckMethod::getPackageName, packageName);
}
List<CheckMethod> list = checkMethodService.list(wrapper);
return R.ok(list);
// Bug #384修复转换为DTO并关联套餐价格
List<CheckMethodDto> dtoList = convertToDtoWithPackagePrice(list);
return R.ok(dtoList);
}
/**
* Bug #384修复转换CheckMethod为DTO并通过packageName关联查询套餐价格
* @param methods 检查方法列表
* @return 包含套餐价格的DTO列表
*/
private List<CheckMethodDto> convertToDtoWithPackagePrice(List<CheckMethod> methods) {
if (methods == null || methods.isEmpty()) {
return List.of();
}
// 获取所有packageName批量查询套餐
List<String> packageNames = methods.stream()
.map(CheckMethod::getPackageName)
.filter(ObjectUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList());
// Bug #384修复: 批量查询套餐信息使用final变量
final Map<String, CheckPackage> packageMap;
if (!packageNames.isEmpty()) {
List<CheckPackage> packages = checkPackageService.list(
new LambdaQueryWrapper<CheckPackage>()
.in(CheckPackage::getPackageName, packageNames)
.eq(CheckPackage::getIsDisabled, 0) // 只查未停用的套餐
);
packageMap = packages.stream()
.collect(Collectors.toMap(CheckPackage::getPackageName, p -> p, (p1, p2) -> p1));
} else {
packageMap = Map.of();
}
// 转换为DTO并填充价格
return methods.stream().map(m -> {
CheckMethodDto dto = new CheckMethodDto();
dto.setId(m.getId() != null ? m.getId().longValue() : null);
dto.setCheckType(m.getCheckType());
dto.setCode(m.getCode());
dto.setName(m.getName());
dto.setPackageName(m.getPackageName());
dto.setExposureNum(m.getExposureNum());
dto.setOrderNum(m.getOrderNum());
dto.setRemark(m.getRemark());
dto.setCreateTime(m.getCreateTime());
dto.setUpdateTime(m.getUpdateTime());
// 通过packageName匹配套餐价格
if (ObjectUtil.isNotEmpty(m.getPackageName())) {
CheckPackage pkg = packageMap.get(m.getPackageName());
if (pkg != null) {
dto.setPackagePrice(pkg.getPackagePrice());
dto.setServiceFee(pkg.getServiceFee());
}
}
return dto;
}).collect(Collectors.toList());
}
@Override

View File

@@ -5,9 +5,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.controller.BaseController;
import com.core.common.core.domain.AjaxResult;
import com.core.common.core.page.TableDataInfo;
import com.core.common.exception.ServiceException;
import com.core.common.utils.AssignSeqUtil;
import com.core.common.utils.SecurityUtils;
import com.openhis.administration.domain.Account;
import com.openhis.administration.domain.ChargeItem;
import com.openhis.administration.service.IAccountService;
import com.openhis.administration.service.IChargeItemService;
import com.openhis.check.domain.ExamApply;
import com.openhis.check.domain.ExamApplyItem;
@@ -17,7 +20,10 @@ import com.openhis.common.constant.CommonConstants;
import com.openhis.common.enums.AssignSeqEnum;
import com.openhis.common.enums.ChargeItemStatus;
import com.openhis.common.enums.GenerateSource;
import com.openhis.common.enums.ItemType;
import com.openhis.common.enums.RequestStatus;
import com.openhis.administration.domain.Organization;
import com.openhis.administration.service.IOrganizationService;
import com.openhis.web.check.dto.ExamApplyDto;
import com.openhis.web.check.dto.ExamApplyItemDto;
import com.openhis.workflow.domain.ServiceRequest;
@@ -57,17 +63,41 @@ public class ExamApplyController extends BaseController {
@Autowired
private IChargeItemService chargeItemService;
@Autowired
private IAccountService accountService;
@Autowired
private AssignSeqUtil assignSeqUtil;
@Autowired
private IOrganizationService organizationService;
/**
* 查询检查申请单列表
*/
@GetMapping("/list")
public TableDataInfo list(ExamApply examApply) {
public TableDataInfo list(ExamApply examApply, @RequestParam(value = "encounterId", required = false) Long encounterId) {
startPage();
LambdaQueryWrapper<ExamApply> wrapper = new LambdaQueryWrapper<>();
if (examApply.getVisitNo() != null) {
// 优先按本次就诊 encounterId 过滤(通过 wor_service_request 关联)
if (encounterId != null) {
List<ServiceRequest> reqList = serviceRequestService.list(new LambdaQueryWrapper<ServiceRequest>()
.eq(ServiceRequest::getEncounterId, encounterId)
.eq(ServiceRequest::getBasedOnTable, "exam_apply")
.isNotNull(ServiceRequest::getBasedOnId)
);
List<Long> basedOnIds = reqList.stream()
.map(ServiceRequest::getBasedOnId)
.filter(java.util.Objects::nonNull)
.distinct()
.toList();
// 没有本次就诊的检查申请单时,直接返回空列表
if (basedOnIds.isEmpty()) {
return getDataTable(java.util.Collections.emptyList());
}
wrapper.in(ExamApply::getId, basedOnIds);
} else if (examApply.getVisitNo() != null) {
// 兼容旧逻辑:按 visitNo 查询(可能包含历史记录)
wrapper.eq(ExamApply::getVisitNo, examApply.getVisitNo());
}
wrapper.orderByDesc(ExamApply::getApplyTime);
@@ -147,6 +177,8 @@ public class ExamApplyController extends BaseController {
examApply.setOperatorId("system");
}
examApplyService.save(examApply);
// 业务主键为 apply_no自增 id 不会随 save 回填;列表接口依赖 wor_service_request.based_on_id=exam_apply.id 关联本次就诊,此处必须回读 id
examApply = examApplyService.getById(applyNo);
// ========== 2. 批量保存明细 + 写入门诊医嘱 + 写入费用项 ==========
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
@@ -191,6 +223,9 @@ public class ExamApplyController extends BaseController {
// 检查申请不走诊疗定义设置为0占位数据库有NOT NULL约束
serviceRequest.setActivityId(0L);
// 🔧 Bug Fix: 设置医嘱类型为诊疗(3),与检验申请保持一致
// categoryEnum=3 → SQL查询返回 adviceType=3诊疗避免被错误归类为药品
serviceRequest.setCategoryEnum(ItemType.ACTIVITY.getValue());
// 患者和就诊信息 —— 使用前端传递的数字型ID
if (dto.getPatientIdNum() != null) {
@@ -201,8 +236,19 @@ public class ExamApplyController extends BaseController {
}
serviceRequest.setRequesterId(currentUserId); // 开单医生
serviceRequest.setOrgId(currentOrgId); // 执行科室
// 53d15f8079d15ba4Ff1a4f1851484f7f7528524d7aef4f20516576846267884c79d15ba44ee37801Ff0c542652194f7f75285f53524d7528623779d15ba4
Long performDeptId = currentOrgId;
if (dto.getPerformDeptCode() != null && !dto.getPerformDeptCode().isEmpty()) {
Organization performDept = organizationService.getOne(
new LambdaQueryWrapper<Organization>().eq(Organization::getBusNo, dto.getPerformDeptCode()).last("limit 1"));
if (performDept != null) {
performDeptId = performDept.getId();
}
}
serviceRequest.setOrgId(performDeptId); // 6267884c79d15ba4
serviceRequest.setAuthoredTime(now); // 签发时间
// 🔧 Bug Fix: 不设置门诊类型,保留上面已设置的 categoryEnum=3诊疗类型
// EncounterClass.AMB.getValue()=2 表示门诊类型,会覆盖诊疗类型导致医嘱被错误归类
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 来源=医生开立
// 将项目名称存入 contentJson使医嘱列表能通过 JSON 字段回显 adviceName
@@ -243,10 +289,17 @@ public class ExamApplyController extends BaseController {
chargeItem.setRequestingOrgId(currentOrgId); // 开立科室
chargeItem.setEnteredDate(now); // 开立时间
// 以下字段均有 NOT NULL 约束,检查申请不走定价/账户体系用0占位
// 以下字段均有 NOT NULL 约束检查申请不走定价体系用0占位
chargeItem.setDefinitionId(0L); // 费用定价ID
chargeItem.setAccountId(0L); // 关联账户ID
chargeItem.setContextEnum(2); // 类型2=诊疗
// 🔧 BugFix#385: 获取患者真实的自费账户预结算验证要求accountId必须真实存在
Account selfAccount = accountService.getSelfAccount(dto.getEncounterId());
if (selfAccount == null) {
throw new ServiceException("患者自费账户不存在无法创建检查收费项encounterId=" + dto.getEncounterId());
}
chargeItem.setAccountId(selfAccount.getId());
// 🔧 BugFix#385: 使用 ItemType.ACTIVITY.getValue()=3 表示诊疗而不是硬编码的2
// ItemType 枚举定义MEDICINE=1, DEVICE=2(耗材), ACTIVITY=3(诊疗)
chargeItem.setContextEnum(ItemType.ACTIVITY.getValue()); // 类型3=诊疗
chargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION); // 产品来源表
chargeItem.setProductId(0L); // 产品ID
@@ -368,6 +421,9 @@ public class ExamApplyController extends BaseController {
serviceRequest.setBasedOnTable("exam_apply");
serviceRequest.setBasedOnId(examApply.getId());
serviceRequest.setActivityId(0L);
// 🔧 Bug Fix: 设置医嘱类型为诊疗(3),与检验申请保持一致
// categoryEnum=3 → SQL查询返回 adviceType=3诊疗避免被错误归类为药品
serviceRequest.setCategoryEnum(ItemType.ACTIVITY.getValue());
if (dto.getPatientIdNum() != null) {
serviceRequest.setPatientId(dto.getPatientIdNum());
@@ -376,8 +432,19 @@ public class ExamApplyController extends BaseController {
serviceRequest.setEncounterId(dto.getEncounterId());
}
serviceRequest.setRequesterId(currentUserId);
serviceRequest.setOrgId(currentOrgId);
// 53d15f8079d15ba4Ff1a4f1851484f7f7528524d7aef4f20516576846267884c79d15ba44ee37801Ff0c542652194f7f75285f53524d7528623779d15ba4
Long performDeptId2 = currentOrgId;
if (dto.getPerformDeptCode() != null && !dto.getPerformDeptCode().isEmpty()) {
Organization performDept2 = organizationService.getOne(
new LambdaQueryWrapper<Organization>().eq(Organization::getBusNo, dto.getPerformDeptCode()).last("limit 1"));
if (performDept2 != null) {
performDeptId2 = performDept2.getId();
}
}
serviceRequest.setOrgId(performDeptId2); // 6267884c79d15ba4
serviceRequest.setAuthoredTime(now);
// 🔧 Bug Fix: 不设置门诊类型,保留上面已设置的 categoryEnum=3诊疗类型
// EncounterClass.AMB.getValue()=2 表示门诊类型,会覆盖诊疗类型导致医嘱被错误归类
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
// 将项目名称存入 contentJson使医嘱列表能通过 JSON 字段回显 adviceName
@@ -412,8 +479,14 @@ public class ExamApplyController extends BaseController {
chargeItem.setRequestingOrgId(currentOrgId);
chargeItem.setEnteredDate(now);
chargeItem.setDefinitionId(0L);
chargeItem.setAccountId(0L);
chargeItem.setContextEnum(2);
// 🔧 BugFix#385: 获取患者真实的自费账户预结算验证要求accountId必须真实存在
Account selfAccount = accountService.getSelfAccount(dto.getEncounterId());
if (selfAccount == null) {
throw new ServiceException("患者自费账户不存在无法创建检查收费项encounterId=" + dto.getEncounterId());
}
chargeItem.setAccountId(selfAccount.getId());
// 🔧 BugFix#385: 使用 ItemType.ACTIVITY.getValue()=3 表示诊疗
chargeItem.setContextEnum(ItemType.ACTIVITY.getValue());
chargeItem.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);
chargeItem.setProductId(0L);

View File

@@ -1,12 +1,15 @@
package com.openhis.web.check.dto;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 检查方法DTO - Bug #384修复增加套餐价格字段
* 用于API返回数据传输不含数据库注解
*/
@Data
@Accessors(chain = true)
public class CheckMethodDto {
@@ -14,7 +17,6 @@ public class CheckMethodDto {
/**
* 检查方法ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/* 检查类型 */
@@ -29,6 +31,12 @@ public class CheckMethodDto {
/* 套餐名称 */
private String packageName;
/* 套餐价格 - Bug #384修复通过packageName匹配CheckPackage获取 */
private BigDecimal packagePrice;
/* 服务费 - Bug #384修复通过packageName匹配CheckPackage获取 */
private BigDecimal serviceFee;
/* 曝光次数 */
private Integer exposureNum;

View File

@@ -136,9 +136,11 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
}
}
LoginUser loginUser = new LoginUser();
//获取当前登录用户信息
loginUser = SecurityUtils.getLoginUser();
// Bug #432 修复获取当前登录用户信息增加null校验防止NPE
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
return R.fail("用户未登录或登录已过期");
}
// 当前登录用户ID
Long userId = loginUser.getUserId();

View File

@@ -1395,9 +1395,17 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
}
// 4. 更新邀请记录(存储会诊意见)
// 直接存储用户输入的原始意见内容,不添加医师姓名前缀
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode()); // 已确认
invited.setConfirmOpinion(dto.getConsultationOpinion()); // 直接存储原始意见,不添加前缀
// Bug #388格式化存储确保回显时参加医师和意见完整
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode());
String deptName = StringUtils.hasText(dto.getConfirmingDeptName())
? dto.getConfirmingDeptName() : invited.getInvitedDepartmentName();
String physician = StringUtils.hasText(dto.getConfirmingPhysician())
? dto.getConfirmingPhysician() : currentPhysicianName;
// 格式:科室-参加医师:意见内容
String formattedOpinion = String.format("%s-%s%s", deptName, physician, dto.getConsultationOpinion());
invited.setConfirmOpinion(formattedOpinion);
invited.setConfirmTime(new Date());
consultationInvitedMapper.updateById(invited);

View File

@@ -19,7 +19,7 @@ import java.util.List;
* TODO:器材目录
*
* @author lpt
* @date 2025-02-20
* @date 2025-02-20
*/
@RestController
@RequestMapping("/data-dictionary/device")

View File

@@ -116,4 +116,12 @@ public interface IDoctorStationAdviceAppService {
* @return 检查url相关参数
*/
R<?> getTestResult(Long encounterId);
/**
* 获取当前科室已配置的药品类别列表
*
* @param organizationId 科室id
* @return 已配置的药品类别编码列表
*/
R<?> getConfiguredCategories(Long organizationId);
}

View File

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.core.common.core.redis.RedisCache;
import com.core.common.enums.DelFlag;
import com.core.common.enums.TenantOptionDict;
import com.core.common.exception.ServiceException;
import com.core.common.utils.AssignSeqUtil;
@@ -21,6 +22,8 @@ import com.openhis.administration.domain.Encounter;
import com.openhis.administration.service.IAccountService;
import com.openhis.administration.service.IChargeItemService;
import com.openhis.administration.service.IEncounterService;
import com.openhis.administration.service.IEncounterDiagnosisService;
import com.openhis.administration.domain.EncounterDiagnosis;
import com.openhis.common.constant.CommonConstants;
import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.*;
@@ -114,6 +117,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
@Resource
IEncounterService iEncounterService;
@Resource
IEncounterDiagnosisService iEncounterDiagnosisService;
@Resource
IInventoryItemService inventoryItemService;
@@ -154,6 +160,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
String safePricingFlag = pricingFlag != null ? pricingFlag.toString() : "";
String safePageNo = pageNo != null ? pageNo.toString() : "";
String safePageSize = pageSize != null ? pageSize.toString() : "";
String safeCategoryCode = categoryCode != null ? categoryCode : "";
// 设置默认科室:仅当前端/调用方未传 organizationId 时才回退到登录人科室
// 否则会导致门诊划价等场景(按患者挂号科室查询)返回空
@@ -168,7 +175,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
String cacheKey = null;
if (useCache) {
// 生成缓存 key无搜索关键字时按科室缓存
cacheKey = ADVICE_BASE_INFO_CACHE_PREFIX + organizationId + ":" + safeAdviceTypesStr + ":" + safePageNo + ":" + safePageSize;
cacheKey = ADVICE_BASE_INFO_CACHE_PREFIX + organizationId + ":" + safeAdviceTypesStr + ":" + safeCategoryCode + ":" + safePageNo + ":" + safePageSize;
// 先清除可能存在的无效缓存JSONObject类型
if (redisCache.hasKey(cacheKey)) {
@@ -280,6 +287,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
String unitCode = ""; // 包装单位
Long chargeItemDefinitionId; // 费用定价主表ID
// 检查是否有取药科室配置(用于药品类型)
boolean hasPharmacyConfig = medLocationConfig != null && !medLocationConfig.isEmpty();
for (AdviceBaseDto baseDto : adviceBaseDtoList) {
String tableName = baseDto.getAdviceTableName();
if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(tableName)) { // 药品
@@ -288,6 +297,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
.setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getSkinTestFlag()));
// 是否为注射药物
baseDto.setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getInjectFlag()));
// 设置是否缺少取药科室配置标志
baseDto.setPharmacyConfigMissing(!hasPharmacyConfig);
// fallthrough to 耗材处理逻辑(保持原有逻辑)
// 每一条医嘱的库存集合信息 , 包装单位库存前端计算
@@ -599,26 +611,24 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
// 药品前端adviceType=1=西药, 2=中成药 → 都属于药品后端分类
// 按后端 ItemType 枚举标准分类
// MEDICINE=1药品、DEVICE=2耗材、ACTIVITY=3诊疗、SURGERY=6手术
// 药品分类adviceType == 1
List<AdviceSaveDto> medicineList = adviceSaveList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())
|| e.getAdviceType() == 1
|| e.getAdviceType() == 2) // 前端中成药类型值为2 → 也属于药品分类
.collect(Collectors.toList());
// 耗材前端adviceType=4后端ItemType.DEVICE=2
List<AdviceSaveDto> deviceList = adviceSaveList.stream()
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())
|| e.getAdviceType() == 4) // 前端耗材类型值为4
.filter(e -> e.getAdviceType() != null && e.getAdviceType() == ItemType.MEDICINE.getValue())
.collect(Collectors.toList());
// 诊疗活动前端adviceType=3诊疗、adviceType=5会诊、adviceType=6手术、adviceType=23检查 → 都属于诊疗后端分类)
// 耗材分类adviceType == 2
List<AdviceSaveDto> deviceList = adviceSaveList.stream()
.filter(e -> e.getAdviceType() != null && e.getAdviceType() == ItemType.DEVICE.getValue())
.collect(Collectors.toList());
// 诊疗分类adviceType == 3
List<AdviceSaveDto> activityList = adviceSaveList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|| e.getAdviceType() == 3 // 前端诊疗类型值为3
|| e.getAdviceType() == 5 // 前端会诊类型值为5
|| e.getAdviceType() == 6 // 前端手术类型值为6
|| e.getAdviceType() == 23 // 前端检查类型值为23
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())) // 后端手术类型值为6
.filter(e -> e.getAdviceType() != null
&& (e.getAdviceType() == ItemType.ACTIVITY.getValue()
|| e.getAdviceType() == ItemType.SURGERY.getValue())) // 手术(6)也走诊疗流程
.collect(Collectors.toList());
// 🔍 Debug日志日志: 记录分类结果
@@ -795,12 +805,71 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 就诊id
Long encounterId = adviceSaveList.get(0).getEncounterId();
// 使用安全的更新方法,避免并发冲突 - 更新费用项状态
// 🔧 BugFix#385: 签发时更新诊疗医嘱关联的费用项诊断信息
// 检查申请创建的费用项没有诊断关联,需要在签发时补充
if (!activityList.isEmpty()) {
// 先从就诊中获取主诊断,用于补充没有诊断关联的费用项
List<EncounterDiagnosis> encounterDiagList = iEncounterDiagnosisService.getDiagnosisList(encounterId);
EncounterDiagnosis mainDiagnosis = iEncounterDiagnosisService.getMainDiagnosis(encounterDiagList);
Long mainConditionId = mainDiagnosis != null ? mainDiagnosis.getConditionId() : null;
Long mainEncounterDiagId = mainDiagnosis != null ? mainDiagnosis.getId() : null;
log.info("BugFix#385: 签发时获取就诊主诊断, encounterId={}, mainConditionId={}, mainEncounterDiagId={}",
encounterId, mainConditionId, mainEncounterDiagId);
for (AdviceSaveDto adviceDto : activityList) {
if (adviceDto.getRequestId() != null) {
// 查询诊疗医嘱关联的费用项
List<ChargeItem> chargeItems = iChargeItemService.getChargeItemInfoByReqId(
Arrays.asList(adviceDto.getRequestId()));
if (chargeItems != null && !chargeItems.isEmpty()) {
// 过滤只保留诊疗类型的费用项
ChargeItem chargeItem = chargeItems.stream()
.filter(ci -> CommonConstants.TableName.WOR_SERVICE_REQUEST.equals(ci.getServiceTable()))
.findFirst().orElse(null);
if (chargeItem != null) {
// 🔧 BugFix#385: 如果费用项没有诊断关联,使用主诊断补充
Long conditionId = adviceDto.getConditionId();
Long encounterDiagId = adviceDto.getEncounterDiagnosisId();
// 如果传入的诊断为空,使用主诊断
if (conditionId == null) {
conditionId = mainConditionId;
}
if (encounterDiagId == null) {
encounterDiagId = mainEncounterDiagId;
}
// 更新诊断关联
if (conditionId != null || encounterDiagId != null) {
chargeItem.setConditionId(conditionId);
chargeItem.setEncounterDiagnosisId(encounterDiagId);
iChargeItemService.updateById(chargeItem);
log.info("BugFix#385: 签发时更新诊疗费用项诊断关联, chargeItemId={}, conditionId={}, encounterDiagnosisId={}",
chargeItem.getId(), conditionId, encounterDiagId);
}
}
}
}
}
}
// 🔧 BugFix#385: 使用安全的更新方法,避免并发冲突 - 更新费用项状态
// 需要处理两种情况:
// 1. 从 DRAFT (0) → PLANNED (1):新创建的收费项目
// 2. 从 BILLABLE (2) → PLANNED (1):保存时已设为待结算的项目
iChargeItemService.updateChargeStatusByConditionSafe(
encounterId,
ChargeItemStatus.DRAFT.getValue(),
ChargeItemStatus.PLANNED.getValue(),
requestIds);
// 🔧 BugFix#385: 同时处理 BILLABLE 状态的收费项目
iChargeItemService.updateChargeStatusByConditionSafe(
encounterId,
ChargeItemStatus.BILLABLE.getValue(),
ChargeItemStatus.PLANNED.getValue(),
requestIds);
}
// 数据变更后清理相关缓存
@@ -889,8 +958,14 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
// 签发时
if (is_sign) {
// 生成处方号
prescriptionUtils.generatePrescriptionNumbers(insertOrUpdateList);
// 🔧 Bug Fix #328: 只对药品类型的医嘱生成处方号
// 检验申请单生成的医嘱是诊疗项目(adviceType=3),不需要处方号
List<AdviceSaveDto> medicineListForPrescription = insertOrUpdateList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType()))
.collect(Collectors.toList());
if (!medicineListForPrescription.isEmpty()) {
prescriptionUtils.generatePrescriptionNumbers(medicineListForPrescription);
}
}
List<String> medRequestIdList = new ArrayList<>();
@@ -928,12 +1003,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
insertOrUpdateList = uniqueInsertOrUpdateList;
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
// 🔧 Bug Fix: 确保accountId不为null与handleBoundDevices保持一致
// 🔧 Bug Fix: 确保accountId有效(不为null且账户存在)
boolean needNewAccount = false;
if (adviceSaveDto.getAccountId() == null) {
needNewAccount = true;
} else {
// 验证账户是否存在且有效(未被删除,租户匹配)
Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId());
if (existingAccount == null) {
log.warn("handMedication - 前端传入的accountId无效账户不存在accountId={},将重新获取或创建账户",
adviceSaveDto.getAccountId());
needNewAccount = true;
}
}
if (needNewAccount) {
// 尝试从患者就诊中获取默认账户ID自费账户
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
if (selfAccount != null) {
adviceSaveDto.setAccountId(selfAccount.getId());
log.info("handMedication - 使用现有自费账户accountId={}", selfAccount.getId());
} else {
// 自动创建自费账户
Account newAccount = new Account();
@@ -947,6 +1036,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
adviceSaveDto.setAccountId(newAccountId);
log.info("handMedication - 自动创建自费账户newAccountId={}", newAccountId);
}
}
@@ -1054,7 +1144,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
// #415 价格非负验证
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
unitPrice = unitPrice.abs(); // 负数取绝对值
}
chargeItem.setUnitPrice(unitPrice); // 单价
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
// 显式设置tenantId、createBy和createTime字段防止自动填充机制失效
@@ -1337,12 +1432,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("BugFix#219: ========== handDevice END ==========");
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
// 🔧 Bug Fix: 确保accountId不为null
// 🔧 Bug Fix: 确保accountId有效(不为null且账户存在)
boolean needNewAccount = false;
if (adviceSaveDto.getAccountId() == null) {
needNewAccount = true;
} else {
// 验证账户是否存在且有效(未被删除,租户匹配)
Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId());
if (existingAccount == null) {
log.warn("handDevice - 前端传入的accountId无效账户不存在accountId={},将重新获取或创建账户",
adviceSaveDto.getAccountId());
needNewAccount = true;
}
}
if (needNewAccount) {
// 尝试从患者就诊中获取默认账户ID自费账户
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
if (selfAccount != null) {
adviceSaveDto.setAccountId(selfAccount.getId());
log.info("handDevice - 使用现有自费账户accountId={}", selfAccount.getId());
} else {
// 自动创建自费账户
Account newAccount = new Account();
@@ -1356,9 +1465,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
adviceSaveDto.setAccountId(newAccountId);
log.info("handDevice - 自动创建自费账户newAccountId={}", newAccountId);
}
}
// 🔧 Bug Fix: 确保practitionerId不为null
if (adviceSaveDto.getPractitionerId() == null) {
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
@@ -1511,7 +1621,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
// #415 价格非负验证
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
unitPrice = unitPrice.abs(); // 负数取绝对值
}
chargeItem.setUnitPrice(unitPrice); // 单价
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
// 显式设置审计字段,防止自动填充机制失效
@@ -1593,12 +1708,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
// 🔧 Bug Fix: 确保accountId不为null
// 🔧 Bug Fix: 确保accountId有效(不为null且账户存在)
boolean needNewAccount = false;
if (adviceSaveDto.getAccountId() == null) {
needNewAccount = true;
} else {
// 验证账户是否存在且有效(未被删除,租户匹配)
Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId());
if (existingAccount == null) {
log.warn("handService - 前端传入的accountId无效账户不存在accountId={},将重新获取或创建账户",
adviceSaveDto.getAccountId());
needNewAccount = true;
}
}
if (needNewAccount) {
// 尝试从患者就诊中获取默认账户ID自费账户
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
if (selfAccount != null) {
adviceSaveDto.setAccountId(selfAccount.getId());
log.info("handService - 使用现有自费账户accountId={}", selfAccount.getId());
} else {
// 自动创建自费账户
Account newAccount = new Account();
@@ -1612,9 +1741,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
adviceSaveDto.setAccountId(newAccountId);
log.info("handService - 自动创建自费账户newAccountId={}", newAccountId);
}
}
// 🔧 Bug Fix: 确保practitionerId不为null
if (adviceSaveDto.getPractitionerId() == null) {
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
@@ -1663,8 +1793,14 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 普通诊疗医嘱
serviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum());
}
serviceRequest.setActivityId(adviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
// 🔧 BugFix#385: 检查类型(adviceType=2)不走定价体系activityId设置为0L占位
// 与ExamApplyController保持一致数据库有NOT NULL约束
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 2) {
serviceRequest.setActivityId(0L);
} else {
serviceRequest.setActivityId(adviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
}
serviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者
serviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生
serviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id
@@ -1716,7 +1852,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
// #415 价格非负验证
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
unitPrice = unitPrice.abs(); // 负数取绝对值
}
chargeItem.setUnitPrice(unitPrice); // 单价
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
iChargeItemService.saveOrUpdate(chargeItem);
@@ -1775,14 +1916,38 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// }
// log.error(e.getMessage(), e);
// }
// 签发时将收费项目状态从草稿改为待收费
// 🔧 BugFix#328: 签发时将收费项目状态从草稿改为待收费
// 修复检验申请单生成的医嘱签发失败问题
Long chargeItemId = adviceSaveDto.getChargeItemId();
ChargeItem existingChargeItem = null;
// 方式1通过chargeItemId直接查询
if (chargeItemId != null) {
ChargeItem existingChargeItem = iChargeItemService.getById(chargeItemId);
if (existingChargeItem != null) {
existingChargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue());
iChargeItemService.updateById(existingChargeItem);
}
existingChargeItem = iChargeItemService.getById(chargeItemId);
}
// 方式2如果chargeItemId为null通过requestIdserviceId查询费用项
// 检验申请单创建的医嘱可能没有传递chargeItemId需要通过serviceId查找
if (existingChargeItem == null && adviceSaveDto.getRequestId() != null) {
existingChargeItem = iChargeItemService.getOne(
new LambdaQueryWrapper<ChargeItem>()
.eq(ChargeItem::getServiceId, adviceSaveDto.getRequestId())
.eq(ChargeItem::getServiceTable, CommonConstants.TableName.WOR_SERVICE_REQUEST)
.eq(ChargeItem::getDeleteFlag, DelFlag.NO.getCode())
);
log.info("BugFix#328: 通过requestId查询费用项requestId={}, chargeItem={}",
adviceSaveDto.getRequestId(), existingChargeItem != null ? existingChargeItem.getId() : "null");
}
// 更新费用项状态
if (existingChargeItem != null) {
existingChargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue());
iChargeItemService.updateById(existingChargeItem);
log.info("BugFix#328: 更新费用项状态为待收费chargeItemId={}, status={}",
existingChargeItem.getId(), ChargeItemStatus.PLANNED.getValue());
} else {
log.warn("BugFix#328: 未找到对应的费用项无法更新状态requestId={}, chargeItemId={}",
adviceSaveDto.getRequestId(), chargeItemId);
}
}
}
@@ -2116,4 +2281,23 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
return searchKey.matches("[a-zA-Z]+");
}
/**
* 获取当前科室已配置的药品类别列表
*
* @param organizationId 科室id
* @return 已配置的药品类别编码列表
*/
@Override
public R<?> getConfiguredCategories(Long organizationId) {
// 查询取药科室配置
List<AdviceInventoryDto> medLocationConfig = doctorStationAdviceAppMapper.getMedLocationConfig(organizationId);
// 提取不重复的 categoryCode
List<String> categoryCodes = medLocationConfig.stream()
.map(AdviceInventoryDto::getCategoryCode)
.filter(code -> code != null && !code.isEmpty())
.distinct()
.collect(Collectors.toList());
return R.ok(categoryCodes);
}
}

View File

@@ -220,7 +220,7 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
// 诊断定义集合
List<SaveDiagnosisChildParam> diagnosisChildList = saveDiagnosisParam.getDiagnosisChildList();
// 先删除再保存
// iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
// 保存诊断管理
Condition condition;
for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) {
@@ -296,7 +296,7 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
}
// 先删除再保存
// iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
// 如果本次保存中有设置主诊断,则先清空该就诊下所有的主诊断标记,确保唯一性
boolean hasMain = diagnosisChildList.stream().anyMatch(d -> Integer.valueOf(1).equals(d.getMaindiseFlag()));

View File

@@ -7,12 +7,17 @@ import com.core.common.utils.SecurityUtils;
import com.openhis.common.enums.DbOpType;
import com.openhis.administration.service.IAccountService;
import com.openhis.administration.domain.Account;
import com.openhis.administration.service.IChargeItemService; // Bug #386修复: 添加 ChargeItemService
import com.openhis.lab.domain.InspectionLabApply;
import com.openhis.lab.domain.InspectionLabApplyItem;
import com.openhis.lab.domain.BarCode;
import com.openhis.lab.domain.InspectionPackage;
import com.openhis.lab.domain.LabActivityDefinition;
import com.openhis.lab.service.IInspectionLabApplyItemService;
import com.openhis.lab.service.IInspectionLabApplyService;
import com.openhis.lab.service.IInspectionLabBarCodeService;
import com.openhis.lab.service.IInspectionPackageService;
import com.openhis.lab.service.ILabActivityDefinitionService;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IServiceRequestService;
import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService;
@@ -43,6 +48,9 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.function.Function;
/**
* 根据检验申请单开单信息系统自动插入门诊医嘱表(与检验申请主表申请单号进行关联),
@@ -82,6 +90,18 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
@Autowired
private RedisCache redisCache;
// BugFix: 套餐价格查询服务
@Autowired
private IInspectionPackageService inspectionPackageService;
// Bug #326: 检验项目定义服务(用于回充时获取套餐信息)
@Autowired
private ILabActivityDefinitionService labActivityDefinitionService;
// Bug #386修复: ChargeItemService 用于删除收费项目
@Autowired
private IChargeItemService chargeItemService;
/**
* 保存检验申请单信息
* @param doctorStationLabApplyDto
@@ -154,6 +174,28 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
InspectionLabApplyItem inspectionLabApplyItem = new InspectionLabApplyItem();
BeanUtils.copyProperties(doctorStationLabApplyItemDto, inspectionLabApplyItem);
// 前端选择检验项目时已携带完整的关联信息activityId、feePackageId、itemCode等
// Bug #326修复: 只使用 activityId 直接查询,不使用名称反查
Long activityId = doctorStationLabApplyItemDto.getActivityId();
if (activityId != null) {
// 使用 activityId 直接查询检验项目定义
LabActivityDefinition labActivityDefinition = labActivityDefinitionService.getById(activityId);
if (labActivityDefinition != null && DelFlag.NO.getCode().equals(labActivityDefinition.getDeleteFlag())) {
// 补充编码(如果前端未传或为空)
if (inspectionLabApplyItem.getItemCode() == null || inspectionLabApplyItem.getItemCode().isEmpty()) {
inspectionLabApplyItem.setItemCode(labActivityDefinition.getBusNo());
}
// 补充套餐ID如果前端未传
if (inspectionLabApplyItem.getFeePackageId() == null) {
inspectionLabApplyItem.setFeePackageId(labActivityDefinition.getFeePackageId());
}
}
} else {
// 没有 activityId 时记录警告,不使用名称反查
log.warn("检验项目 [{}] 未传入 activityId无法获取完整关联信息",
doctorStationLabApplyItemDto.getItemName());
}
// 后端重新计算金额:金额 = 单价 × 数量
java.math.BigDecimal itemPrice = doctorStationLabApplyItemDto.getItemPrice();
java.math.BigDecimal itemQty = doctorStationLabApplyItemDto.getItemQty();
@@ -235,13 +277,16 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
);
if (organization != null) {
positionId = organization.getId();
} else {
// Bug #329: 执行科室代码无法匹配到科室,记录警告日志
log.warn("执行科室代码 [{}] 在科室表中未找到对应记录", performDeptCode);
}
} else {
// Bug #329: 未指定执行科室,记录警告日志
log.warn("检验项目 [{}] 未指定执行科室", itemName);
}
// 如果没有指定执行科室,使用当前医生所在的科室作为默认执行科室
if (positionId == null) {
positionId = SecurityUtils.getDeptId();
}
// Bug #329: 移除默认执行科室逻辑,必须由前端明确指定执行科室
// 4. 创建医嘱保存对象
AdviceSaveDto adviceSaveDto = new AdviceSaveDto();
@@ -273,8 +318,11 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
adviceSaveDto.setEncounterId(doctorStationLabApplyDto.getEncounterId());
// 开方医生 ID - AdviceSaveDto 构造函数已设置,这里可覆盖
adviceSaveDto.setPractitionerId(SecurityUtils.getUserId());
// 开方科室 ID - AdviceSaveDto 构造函数已设置,这里可覆盖
adviceSaveDto.setFounderOrgId(SecurityUtils.getDeptId());
// 开方科室 ID - 使用患者挂号科室
adviceSaveDto.setFounderOrgId(doctorStationLabApplyDto.getApplyOrganizationId());
// 执行科室 ID - 用于诊疗项目校验BugFix#328
// 注意AdviceSaveDto 中没有 setEffectiveOrgId 方法,需要设置 orgId 字段
adviceSaveDto.setOrgId(positionId);
// 账户 ID - 获取就诊的账户(多级回退策略)
Long accountId = accountService.getSelfPayAccount(doctorStationLabApplyDto.getEncounterId());
if (accountId == null) {
@@ -299,15 +347,42 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
adviceSaveDto.setQuantity(labApplyItemDto.getItemQty() != null ? labApplyItemDto.getItemQty() : java.math.BigDecimal.ONE);
// 请求单位编码(使用诊疗定义的使用单位)
adviceSaveDto.setUnitCode(activityDefinition.getPermittedUnitCode());
// 单价
adviceSaveDto.setUnitPrice(labApplyItemDto.getItemPrice());
// 总价
adviceSaveDto.setTotalPrice(labApplyItemDto.getItemAmount());
// 单价处理BugFix#CodeReview: 根据套餐ID从正确的数据源获取价格
// 套餐项目:从 inspection_basic_information 表获取 package_amount
// 普通项目:使用前端传入的 itemPrice已从诊疗项目获取
java.math.BigDecimal unitPrice;
Long feePackageId = activityDefinition.getFeePackageId();
if (feePackageId != null) {
// 套餐项目:查询套餐价格
InspectionPackage packageInfo = inspectionPackageService.selectPackageById(feePackageId);
if (packageInfo == null || packageInfo.getPackageAmount() == null
|| packageInfo.getPackageAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
log.error("套餐项目 '{}' 缺少定价套餐ID: {}", itemName, feePackageId);
throw new RuntimeException("套餐项目 '" + itemName + "' 未设置有效价格,请先配置套餐金额");
}
unitPrice = packageInfo.getPackageAmount();
} else {
// 普通项目:使用前端传入的价格
unitPrice = labApplyItemDto.getItemPrice();
if (unitPrice == null || unitPrice.compareTo(java.math.BigDecimal.ZERO) <= 0) {
log.error("检验项目 '{}' 缺少定价,无法创建医嘱", itemName);
throw new RuntimeException("检验项目 '" + itemName + "' 未设置有效价格");
}
}
adviceSaveDto.setUnitPrice(unitPrice);
// 总价处理:后端重新计算
java.math.BigDecimal totalPrice = unitPrice.multiply(adviceSaveDto.getQuantity()).setScale(2, java.math.RoundingMode.HALF_UP);
adviceSaveDto.setTotalPrice(totalPrice);
// 请求状态
adviceSaveDto.setStatusEnum(1);
// 请求类型
adviceSaveDto.setCategoryEnum(1);
// 🔧 Bug Fix #328: 请求类型设置为诊疗项目(3)避免SQL查询时被错误归类为药品
// SQL: CASE WHEN category_enum = 4 THEN 6 ELSE COALESCE(category_enum, 3) END AS advice_type
// 如果 category_enum=1则 advice_type=1(药品),导致签发时被归类为药品医嘱
adviceSaveDto.setCategoryEnum(3); // 3:诊疗项目
// 设置治疗类型(临时医嘱)
adviceSaveDto.setTherapyEnum(1); // 1:临时医嘱
@@ -330,30 +405,84 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
*/
@Override
public Object getInspectionApplyByApplyNo(String applyNo) {
// 查询主表数据
DoctorStationLabApplyDto applyDto = (DoctorStationLabApplyDto) doctorStationLabApplyMapper.getInspectionApplyByApplyNo(applyNo);
if (applyDto == null) {
// 使用MyBatis-Plus查询主表数据
InspectionLabApply mainEntity = inspectionLabApplyService.getOne(
new QueryWrapper<InspectionLabApply>()
.eq("apply_no", applyNo)
.eq("delete_flag", DelFlag.NO.getCode())
);
if (mainEntity == null) {
return null;
}
// 使用BeanUtils进行基础字段映射
DoctorStationLabApplyDto applyDto = new DoctorStationLabApplyDto();
BeanUtils.copyProperties(mainEntity, applyDto);
// 由于字段名称映射关系(如 id -> applicationId需要单独设置
applyDto.setApplicationId(mainEntity.getId());
// 查询检验项目明细
List<InspectionLabApplyItem> itemList = inspectionLabApplyItemService.list(
new QueryWrapper<InspectionLabApplyItem>()
.eq("apply_no", applyNo)
.eq("delete_flag", "0")
.eq("delete_flag", DelFlag.NO.getCode())
.orderByAsc("item_seq")
);
// 转换为 DTO 列表
List<DoctorStationLabApplyItemDto> itemDtoList = new ArrayList<>();
if (itemList != null && !itemList.isEmpty()) {
// 提取所有不同的 itemCode用于批量查询
Set<String> itemCodes = itemList.stream()
.filter(item -> item.getItemCode() != null && !item.getItemCode().isEmpty())
.map(InspectionLabApplyItem::getItemCode)
.collect(Collectors.toSet());
// 批量查询所有关联的检验项目定义使用MyBatis-Plus
Map<String, LabActivityDefinition> definitionMap = new HashMap<>();
if (!itemCodes.isEmpty()) {
List<LabActivityDefinition> labActivityDefinitions = labActivityDefinitionService.list(
new QueryWrapper<LabActivityDefinition>()
.in("bus_no", itemCodes)
.eq("delete_flag", DelFlag.NO.getCode())
);
// 构建 itemCode 到定义的映射
definitionMap = labActivityDefinitions.stream()
.collect(Collectors.toMap(LabActivityDefinition::getBusNo, Function.identity()));
}
// 处理每个明细项
for (InspectionLabApplyItem item : itemList) {
DoctorStationLabApplyItemDto itemDto = new DoctorStationLabApplyItemDto();
// 使用BeanUtils进行基础字段映射
BeanUtils.copyProperties(item, itemDto);
// feePackageId 在保存时已存储,直接使用
itemDto.setFeePackageId(item.getFeePackageId());
// 判断是否是套餐项目(根据 feePackageId 是否存在)
itemDto.setIsPackage(item.getFeePackageId() != null);
// 从批量查询结果中获取关联信息
if (item.getItemCode() != null && !item.getItemCode().isEmpty()) {
LabActivityDefinition labActivityDefinition = definitionMap.get(item.getItemCode());
if (labActivityDefinition != null) {
itemDto.setActivityId(labActivityDefinition.getId());
itemDto.setSampleType(labActivityDefinition.getSpecimenCode());
itemDto.setUnit(labActivityDefinition.getPermittedUnitCode());
// 补充检验类型ID用于前端自动设置执行科室
itemDto.setInspectionTypeId(labActivityDefinition.getInspectionTypeId());
}
} else {
log.warn("检验项目 [{}] 未存储 itemCode无法获取完整关联信息", item.getItemName());
}
itemDtoList.add(itemDto);
}
// 从第一个明细项获取执行科室代码
applyDto.setExecuteDepartment(itemList.get(0).getPerformDeptCode());
if (!itemList.isEmpty() && itemList.get(0).getPerformDeptCode() != null) {
applyDto.setExecuteDepartment(itemList.get(0).getPerformDeptCode());
}
}
applyDto.setLabApplyItemList(itemDtoList);
@@ -372,8 +501,9 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
// 使用 PageHelper 进行分页查询
PageHelper.startPage(pageNo, pageSize);
// 查询检验申请单列表
log.debug("查询申请单数据前");
// 使用MyBatis-Plus查询检验申请单列表
// 需要关联adm_encounter表仍使用原Mapper方法或重构为MyBatis-Plus方式
// 为保持一致性和考虑到复杂的关联查询,暂时保留原有实现方式
List<DoctorStationLabApplyDto> list = doctorStationLabApplyMapper.getInspectionApplyListPage(encounterId);
log.debug("查询申请单数据后");
@@ -473,8 +603,14 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
);
if (updateResult) {
log.debug("成功将申请单号 [{}] 关联的 {} 条门诊医嘱的删除状态更新为1更新人{},更新时间:{}",
log.debug("成功将申请单号 [{}] 关联的 {} 条门诊医嘱的删除状态更新为1更新人{},更新时间:{}",
applyNo, requestIds.size(), currentUsername, currentTime);
// Bug #386修复: 同步删除关联的收费项目
for (Long requestId : requestIds) {
chargeItemService.deleteByServiceTableAndId("wor_service_request", requestId);
}
log.debug("成功删除申请单号 [{}] 关联的 {} 条收费项目", applyNo, requestIds.size());
} else {
log.warn("更新申请单号 [{}] 关联的门诊医嘱删除状态失败", applyNo);
}

View File

@@ -147,12 +147,19 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
return R.fail("已接诊,请勿重复点击,已为您刷新");
}
// 允许从「待诊/暂离/诊毕」重新接诊(用于队列弹窗的完诊患者重新进入在诊)
Date now = new Date();
int update = encounterMapper.update(null,
new LambdaUpdateWrapper<Encounter>().eq(Encounter::getId, encounterId)
.eq(Encounter::getStatusEnum, EncounterStatus.PLANNED.getValue()) // 只更新待诊状态的患者
.set(Encounter::getReceptionTime, new Date())
.in(Encounter::getStatusEnum,
EncounterStatus.PLANNED.getValue(),
EncounterStatus.ON_HOLD.getValue(),
EncounterStatus.DISCHARGED.getValue())
.set(Encounter::getReceptionTime, now)
.set(Encounter::getEndTime, null)
.set(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.RECEIVING_CARE.getValue()));
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.RECEIVING_CARE.getValue())
.set(Encounter::getUpdateTime, now));
// 如果更新失败,说明状态已被其他医生修改
if (update <= 0) {
@@ -181,7 +188,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
encounterParticipant.setCreateTime(new Date());
iEncounterParticipantService.save(encounterParticipant);
// 更新 triage_queue_item 队列记录状态为 CALLING
// 更新 triage_queue_item 队列记录状态为 IN_CLINIC(诊中)
try {
TriageQueueItem queueItem = triageQueueItemService.getOne(
new LambdaQueryWrapper<TriageQueueItem>()
@@ -190,10 +197,10 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
.eq(TriageQueueItem::getDeleteFlag, "0")
);
if (queueItem != null) {
queueItem.setStatus("CALLING");
queueItem.setStatus(20); // 20=IN_CLINIC(诊中),患者进入诊室接诊
queueItem.setUpdateTime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
triageQueueItemService.updateById(queueItem);
log.info("接诊时更新队列状态为CALLINGencounterId={}, queueItemId={}", encounterId, queueItem.getId());
log.info("接诊时更新队列状态为IN_CLINIC(诊中)encounterId={}, queueItemId={}", encounterId, queueItem.getId());
} else {
log.warn("接诊时未找到队列记录encounterId={}", encounterId);
}
@@ -256,10 +263,13 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
);
// 如果队列项存在,检查状态并更新
if (queueItem != null && "CALLING".equals(queueItem.getStatus())) {
// 允许从 CALLING(10) 或 IN_CLINIC(20) 完成就诊
if (queueItem != null &&
(Integer.valueOf(10).equals(queueItem.getStatus()) ||
Integer.valueOf(20).equals(queueItem.getStatus()))) {
// 更新队列状态为已完成
java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
queueItem.setStatus("COMPLETED");
queueItem.setStatus(30); // 30=COMPLETED(已完成)
queueItem.setUpdateTime(nowLocal);
triageQueueItemService.updateById(queueItem);
@@ -268,13 +278,15 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
Long userId = SecurityUtils.getLoginUser().getUserId();
String divLogSql = "INSERT INTO hisdev.div_log "
+ "(pool_id, slot_id, queue_no, op_user_id, action, create_time) "
+ "VALUES (?, ?, ?, ?, 'COMPLETE', NOW()::timestamp(0))";
+ "VALUES (?, ?, ?, ?, 30, NOW()::timestamp(0))";
// action=30 表示 COMPLETED(已完成),与 triage_queue_item.status 数字编码保持一致
// 0=WAITING, 10=CALLING, 20=IN_CLINIC, 30=COMPLETED, 40=SKIPPED, 50=REFUNDED, 60=FOLLOW
jdbcTemplate.update(divLogSql,
queueItem.getOrganizationId(), // pool_id: 候选池ID科室
queueItem.getPractitionerId(), // slot_id: 槽位ID医生
queueItem.getQueueOrder(), // queue_no: 队列号
userId); // op_user_id: 操作用户ID
queueItem.getPoolId(), // pool_id: 号源池ID来自 adm_schedule_pool.id
queueItem.getSlotId(), // slot_id: 号源槽位ID来自 adm_schedule_slot.id
queueItem.getQueueOrder(), // queue_no: 队列
userId); // op_user_id: 操作用户ID
} catch (Exception e) {
log.error("写入div_log审计日志失败", e);
// 审计日志失败不影响主流程

View File

@@ -11,6 +11,7 @@ import com.openhis.common.constant.CommonConstants;
import com.openhis.common.enums.*;
import com.openhis.common.utils.EnumUtils;
import com.openhis.common.utils.HisQueryUtils;
import com.openhis.web.doctorstation.appservice.IDoctorStationMainAppService;
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
@@ -32,6 +33,9 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
@Resource
private TodayOutpatientMapper todayOutpatientMapper;
@Resource
private IDoctorStationMainAppService doctorStationMainAppService;
@Override
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
Long doctorId = SecurityUtils.getLoginUser().getUserId();
@@ -259,22 +263,19 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
@Override
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
// 调用现有的接诊逻辑
// 这里可以复用 DoctorStationMainAppServiceImpl 中的 receiveEncounter 方法
// 或者直接调用相应的服务
return R.ok("接诊成功");
return doctorStationMainAppService.receiveEncounter(encounterId);
}
@Override
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
// 调用现有的完诊逻辑
return R.ok("就诊完成");
return doctorStationMainAppService.completeEncounter(encounterId, null);
}
@Override
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
// 调用现有的取消就诊逻辑
return R.ok("就诊取消成功");
return doctorStationMainAppService.cancelEncounter(encounterId);
}
/**
@@ -302,4 +303,4 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
return orderBy;
}
}
}

View File

@@ -50,9 +50,10 @@ public class DoctorStationAdviceController {
@RequestParam(value = "organizationId", required = false) Long organizationId,
@RequestParam(value = "adviceTypes", defaultValue = "1,2,3") List<Integer> adviceTypes,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "categoryCode", required = false) String categoryCode) {
return R.ok(iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId,
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, Whether.NO.getValue(), adviceTypes, null, null));
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, null, adviceTypes, null, categoryCode));
}
/**
@@ -186,4 +187,15 @@ public class DoctorStationAdviceController {
return iDoctorStationAdviceAppService.getTestResult(encounterId);
}
/**
* 获取当前科室已配置的药品类别列表
*
* @param organizationId 科室id
* @return 已配置的药品类别编码列表
*/
@GetMapping(value = "/configured-categories")
public R<?> getConfiguredCategories(@RequestParam(value = "organizationId", required = false) Long organizationId) {
return iDoctorStationAdviceAppService.getConfiguredCategories(organizationId);
}
}

View File

@@ -118,7 +118,7 @@ public class DoctorStationChineseMedicalController {
organizationId = SecurityUtils.getLoginUser().getOrgId();
}
return R.ok(iDoctorStationChineseMedicalAppService.getTcmAdviceBaseInfo(adviceBaseDto, searchKey, locationId,
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, Whether.NO.getValue()));
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, null));
}
/**

View File

@@ -198,6 +198,7 @@ public class AdviceBaseDto {
/**
* 所属科室
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long orgId;
/**
@@ -242,4 +243,9 @@ public class AdviceBaseDto {
@Dict(dictCode = "chrgitm_lv")
private String chrgitmLv;
private String chrgitmLv_dictText;
/**
* 是否缺少取药科室配置(仅药品类型使用)
*/
private Boolean pharmacyConfigMissing;
}

View File

@@ -1,5 +1,7 @@
package com.openhis.web.doctorstation.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -67,4 +69,42 @@ public class DoctorStationLabApplyItemDto {
*/
@NotNull(message = "行状态不能为空")
private Long itemStatus;
// ========== Bug #326: 套餐相关字段(回充时需要) ==========
/**
* 活动定义ID检验项目定义ID
* 用于回充时关联到原始检验项目定义
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long activityId;
/**
* 套餐ID如果该项目是套餐则关联套餐表
* 对应 InspectionPackage.basicInformationId
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long feePackageId;
/**
* 是否是套餐项目
*/
private Boolean isPackage;
/**
* 样本类型
*/
private String sampleType;
/**
* 单位
*/
private String unit;
/**
* 检验类型ID关联 inspection_type 大类)
* 用于前端自动设置执行科室
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long inspectionTypeId;
}

View File

@@ -140,10 +140,17 @@ public class PrescriptionUtils {
/**
* 计算分组的总金额
* 🔧 Bug Fix #328: 处理 unitPrice 为 null 的情况,避免空指针异常
*/
private BigDecimal calculateTotalPrice(List<AdviceSaveDto> medicines) {
return medicines.stream().map(medicine -> medicine.getUnitPrice().multiply(medicine.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
return medicines.stream().map(medicine -> {
BigDecimal unitPrice = medicine.getUnitPrice();
BigDecimal quantity = medicine.getQuantity();
if (unitPrice == null || quantity == null) {
return BigDecimal.ZERO;
}
return unitPrice.multiply(quantity);
}).reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
@@ -174,7 +181,10 @@ public class PrescriptionUtils {
BigDecimal currentTotal = BigDecimal.ZERO;
for (AdviceSaveDto medicine : medicines) {
// 计算单个药品总金额
BigDecimal medicinePrice = medicine.getUnitPrice().multiply(medicine.getQuantity());
// 🔧 Bug Fix #328: 处理 unitPrice 为 null 的情况,避免空指针异常
BigDecimal unitPrice = medicine.getUnitPrice();
BigDecimal quantity = medicine.getQuantity();
BigDecimal medicinePrice = (unitPrice == null || quantity == null) ? BigDecimal.ZERO : unitPrice.multiply(quantity);
// 特殊处理:单药品金额超限
if (medicinePrice.compareTo(MAX_SINGLE_PRESCRIPTION_PRICE) > 0) {
// 先保存当前组(如果有药品)
@@ -214,8 +224,13 @@ public class PrescriptionUtils {
/**
* 根据药品性质生成处方号
* 🔧 Bug Fix #328: 处理 pharmacologyCategoryCode 为 null 的情况,避免空指针异常
*/
private String generatePrescriptionNo(String pharmacologyCategoryCode) {
// null 或空字符串视为普通药品
if (pharmacologyCategoryCode == null || pharmacologyCategoryCode.isEmpty()) {
return assignSeqUtil.getSeq(AssignSeqEnum.PRESCRIPTION_COMMON_NO.getPrefix(), 8);
}
switch (pharmacologyCategoryCode) {
case "2": // 麻醉药品
return assignSeqUtil.getSeq(AssignSeqEnum.PRESCRIPTION_NARCOTIC_NO.getPrefix(), 8);

View File

@@ -442,6 +442,15 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
if (admissionPatientInfoDto.getPriorityEnum() != null) {
encounterService.updatePriorityEnumById(encounterId, admissionPatientInfoDto.getPriorityEnum());
}
// 更新入科时间(如果提供了)
if (startTime != null) {
Encounter encounter = encounterService.getById(encounterId);
if (encounter != null) {
encounter.setStartTime(startTime);
encounterService.saveOrUpdateEncounter(encounter);
log.info("更新入科时间 - encounterId: {}, startTime: {}", encounterId, startTime);
}
}
// 将之前的住院参与者更新为已完成(如果存在的话)
encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
// 更新住院参与者

View File

@@ -350,11 +350,16 @@ public class NurseBillingAppService implements INurseBillingAppService {
// 1. 筛选临时类型耗材仅处理临时医嘱TherapyTimeType.TEMPORARY且请求ID不为空
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
// 2. 校验发放库房:必须指定耗材发放库房locationId否则抛出业务异常
if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) {
throw new ServiceException("耗材划价失败:发放库房为空,请重新选择");
// 2. 颞理发放库房:为locationId为null的项目设置默认值
for (AdviceSaveDto advice : tempDeviceList) {
if (advice.getLocationId() == null) {
// 设置默认位置为用户组织ID作为fallback
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getOrgId() != null) {
advice.setLocationId(loginUser.getOrgId());
}
}
}
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
for (AdviceSaveDto adviceDto : tempDeviceList) {
// 3.1 生成耗材请求记录WOR_DEVICE_REQUEST状态设为激活来源标记为护士划价
@@ -665,7 +670,7 @@ public class NurseBillingAppService implements INurseBillingAppService {
// 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除
checkDeletedDeviceChargeStatus(requestIds);
// 软删除执行记录
List<Procedure> procedureList = iProcedureService.listByIds(requestIds);
List<Procedure> procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable);
List<Long> procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象
.map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录按需添加
.collect(Collectors.toList());

View File

@@ -36,7 +36,7 @@ public interface IVitalSignsAppService {
*
* @return 体温单检索结果
*/
R<?> searchVitalSigns(String startTime, String endTime);
R<?> searchVitalSigns(String startTime, String endTime, String patientId);
/**
* 体温单记录删除

View File

@@ -73,9 +73,21 @@ public class VitalSignsAppServiceImpl implements IVitalSignsAppService {
VitalSignsMedicalRecordDto medicalRecord = new VitalSignsMedicalRecordDto();
// 处理日期
// 处理出院日期
if (!vitalSignsInfoPage.getRecords().isEmpty()) {
medicalRecord.setHospDate(vitalSignsInfoPage.getRecords().get(0).getRecordingDate());
// 从第一条记录获取出院日期(如果存在)
Date dischargeDate = vitalSignsInfoPage.getRecords().get(0).getDischargeDate();
if (dischargeDate != null) {
medicalRecord.setOutdate(TimeUtils.dateToDateString(dischargeDate));
}
}
// 处理住院日期:优先使用第一条记录中的入院日期,如果没有则保持 null 让前端 fallback
if (!vitalSignsInfoPage.getRecords().isEmpty()) {
Date admissionDate = vitalSignsInfoPage.getRecords().get(0).getAdmissionDate();
if (admissionDate != null) {
medicalRecord.setHospDate(admissionDate);
}
}
// 处理生命体征数据
@@ -266,13 +278,14 @@ public class VitalSignsAppServiceImpl implements IVitalSignsAppService {
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param patientId 患者ID
* @return 检索结果
*/
@Override
public R<?> searchVitalSigns(String startTime, String endTime) {
public R<?> searchVitalSigns(String startTime, String endTime, String patientId) {
// 基本信息查询
List<VitalSigns> vitalSignsList = vitalSignsAppMapper.searchVitalSigns(startTime, endTime);
List<VitalSigns> vitalSignsList = vitalSignsAppMapper.searchVitalSigns(startTime, endTime, patientId);
// 判断查询结果是否为空
if (vitalSignsList.isEmpty()) {
return R.ok(Collections.emptyList());

View File

@@ -57,8 +57,8 @@ public class VitalSignsController {
* @return 体温单检索结果
*/
@GetMapping("/record-search")
public R<?> searchVitalSigns(String startTime, String endTime) {
return R.ok(vitalSignsAppService.searchVitalSigns(startTime, endTime));
public R<?> searchVitalSigns(String startTime, String endTime, String patientId) {
return R.ok(vitalSignsAppService.searchVitalSigns(startTime, endTime, patientId));
}
/**

View File

@@ -1,6 +1,7 @@
package com.openhis.web.inpatientmanage.dto;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -35,6 +36,7 @@ public class VitalSignsSaveDto extends HisBaseEntity {
/**
* 记录日期
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private Date recordingDate;
/**

View File

@@ -38,9 +38,10 @@ public interface VitalSignsAppMapper {
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param patientId 患者ID
* @return 查询记录结果
*/
List<VitalSigns> searchVitalSigns(@Param("startTime") String startTime, @Param("endTime") String endTime);
List<VitalSigns> searchVitalSigns(@Param("startTime") String startTime, @Param("endTime") String endTime, @Param("patientId") String patientId);
/**
* 删除记录

View File

@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@@ -68,6 +69,17 @@ public class LabActivityDefinitionAppServiceImpl implements ILabActivityDefiniti
selParam.setPricingFlag(null);
}
// Bug #414: 限制分页大小,防止一次性加载过多数据导致性能问题
if (pageSize == null || pageSize <= 0) {
pageSize = 20;
}
if (pageSize > 50) {
pageSize = 50;
}
if (pageNo == null || pageNo <= 0) {
pageNo = 1;
}
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = HisQueryUtils.buildQueryWrapper(selParam,
searchKey, new HashSet<>(Arrays.asList("T1.bus_no", "T1.name", "T1.py_str", "T1.wb_str")), request);
@@ -80,10 +92,19 @@ public class LabActivityDefinitionAppServiceImpl implements ILabActivityDefiniti
selParam.setPricingFlag(pricingFlagValue);
}
IPage<DiagnosisTreatmentDto> page = labActivityDefinitionManageMapper
.getLabActivityDefinitionPage(new Page<>(pageNo, pageSize), queryWrapper);
// Bug #414: 使用optimizeCountSql=true优化COUNT查询性能
Page<DiagnosisTreatmentDto> page = new Page<>(pageNo, pageSize, true);
IPage<DiagnosisTreatmentDto> resultPage = labActivityDefinitionManageMapper
.getLabActivityDefinitionPage(page, queryWrapper);
page.getRecords().forEach(e -> {
resultPage.getRecords().forEach(e -> {
// Bug #415: 确保价格不为负数
if (e.getPackageAmount() != null && e.getPackageAmount().compareTo(BigDecimal.ZERO) < 0) {
e.setPackageAmount(BigDecimal.ZERO);
}
if (e.getServiceFee() != null && e.getServiceFee().compareTo(BigDecimal.ZERO) < 0) {
e.setServiceFee(BigDecimal.ZERO);
}
e.setYbFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getYbFlag()));
e.setYbMatchFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getYbMatchFlag()));
e.setTypeEnum_enumText(EnumUtils.getInfoByValue(ActivityType.class, e.getTypeEnum()));
@@ -91,13 +112,22 @@ public class LabActivityDefinitionAppServiceImpl implements ILabActivityDefiniti
e.setPricingFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getPricingFlag()));
});
return R.ok(page);
return R.ok(resultPage);
}
@Override
public R<?> getLabActivityDefinitionOne(Long id) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
DiagnosisTreatmentDto dto = labActivityDefinitionManageMapper.getLabActivityDefinitionOne(id, tenantId);
// Bug #415: 确保价格不为负数
if (dto != null) {
if (dto.getPackageAmount() != null && dto.getPackageAmount().compareTo(BigDecimal.ZERO) < 0) {
dto.setPackageAmount(BigDecimal.ZERO);
}
if (dto.getServiceFee() != null && dto.getServiceFee().compareTo(BigDecimal.ZERO) < 0) {
dto.setServiceFee(BigDecimal.ZERO);
}
}
return R.ok(dto);
}

View File

@@ -41,7 +41,9 @@ import com.openhis.web.paymentmanage.appservice.IChargeBillService;
import com.openhis.web.paymentmanage.dto.*;
import com.openhis.web.paymentmanage.mapper.ChargeBillMapper;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.service.IServiceRequestService;
import com.openhis.yb.domain.ClinicSettle;
import com.openhis.yb.domain.ClinicUnSettle;
import com.openhis.yb.domain.InfoPerson;
@@ -111,6 +113,8 @@ public class IChargeBillServiceImpl implements IChargeBillService {
@Autowired
private IActivityDefinitionService iActivityDefinitionService;
@Autowired
private IServiceRequestService iServiceRequestService;
@Autowired
private IPractitionerService iPractitionerService;
@Autowired
private IHealthcareServiceService iHealthcareServiceService;
@@ -265,10 +269,31 @@ public class IChargeBillServiceImpl implements IChargeBillService {
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
.setTotalVolume(device.getSize()).setQuantityValue(chargeItem.getQuantityValue());
} else if (CommonConstants.TableName.WOR_ACTIVITY_DEFINITION.equals(chargeItem.getProductTable())) {
ActivityDefinition activity = iActivityDefinitionService.getById(chargeItem.getProductId());
chargeItemDetailVO.setDirClass(activity.getChrgitmLv() + "").setChargeItemName(activity.getName())
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
.setTotalVolume("").setQuantityValue(chargeItem.getQuantityValue());
// 🔧 BugFix#385: 检查申请创建的收费项 productId=0需从 ServiceRequest.contentJson 获取项目名称
if (chargeItem.getProductId() != null && chargeItem.getProductId() > 0) {
ActivityDefinition activity = iActivityDefinitionService.getById(chargeItem.getProductId());
chargeItemDetailVO.setDirClass(activity.getChrgitmLv() + "").setChargeItemName(activity.getName())
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
.setTotalVolume("").setQuantityValue(chargeItem.getQuantityValue());
} else {
// productId=0 时,从关联的 ServiceRequest 获取项目名称
ServiceRequest serviceRequest = iServiceRequestService.getById(chargeItem.getServiceId());
String itemName = "未知项目";
String dirClass = "3"; // 默认诊疗类
if (serviceRequest != null && serviceRequest.getContentJson() != null) {
try {
JSONObject json = JSON.parseObject(serviceRequest.getContentJson());
if (json.containsKey("adviceName")) {
itemName = json.getString("adviceName");
}
} catch (Exception e) {
log.warn("解析ServiceRequest.contentJson失败: {}", e.getMessage());
}
}
chargeItemDetailVO.setDirClass(dirClass).setChargeItemName(itemName)
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
.setTotalVolume("").setQuantityValue(chargeItem.getQuantityValue());
}
} else {
HealthcareService healthcareService = iHealthcareServiceService.getById(chargeItem.getServiceId());
chargeItemDetailVO.setDirClass("3").setChargeItemName(healthcareService.getName())
@@ -347,7 +372,19 @@ public class IChargeBillServiceImpl implements IChargeBillService {
Long definitionId = chargeItem.getDefinitionId();
ChargeItemDefinition chargeItemDefinition = iChargeItemDefinitionService.getById(definitionId);
// 🔧 BugFix#385: 检查申请创建的收费项 definition_id=0chargeItemDefinition 会为 null
ChargeItemDefinition chargeItemDefinition = null;
if (definitionId != null && definitionId > 0) {
chargeItemDefinition = iChargeItemDefinitionService.getById(definitionId);
}
// 当 definitionId=0 或 chargeItemDefinition 为 null 时,跳过医保分类统计
// 检查类项目默认归类为"检查费"
if (chargeItemDefinition == null) {
// 检查申请的收费项归类为检查费03
sum03 = sum03.add(chargeItem.getTotalPrice());
continue;
}
YbMedChrgItmType medChrgItmType
= YbMedChrgItmType.getByCode(Integer.parseInt(chargeItemDefinition.getYbType()));

View File

@@ -6,6 +6,7 @@ package com.openhis.web.paymentmanage.appservice.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
@@ -57,6 +58,12 @@ import com.openhis.web.paymentmanage.mapper.PaymentMapper;
import com.openhis.web.personalization.dto.ActivityDeviceDto;
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.domain.SchedulePool;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
import com.openhis.clinical.domain.Order;
import com.openhis.clinical.service.IOrderService;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IDeviceDispenseService;
import com.openhis.workflow.service.IDeviceRequestService;
@@ -70,6 +77,7 @@ import com.openhis.yb.service.IClinicSettleService;
import com.openhis.yb.service.IInpatientSettleService;
import com.openhis.yb.service.IRegService;
import com.openhis.yb.service.YbManager;
import com.openhis.web.triageandqueuemanage.appservice.impl.TriageQueueAppServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.util.StringUtil;
@@ -186,6 +194,12 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
private YbManager ybManager;
@Autowired
private RedisCache redisCache;
@Autowired
private IOrderService iOrderService;
@Autowired
private ScheduleSlotMapper scheduleSlotMapper;
@Autowired
private SchedulePoolMapper schedulePoolMapper;
/**
* 【门诊预结算】
@@ -232,14 +246,21 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
.collect(Collectors.toList());
// account去重
List<Long> distinctAccountIdList = accountIdList.stream().distinct().collect(Collectors.toList());
// 检查是否存在accountId为null的收费项
// 检查是否存在accountId为null或0的收费项
long nullAccountIdCount = chargeItemList.stream()
.map(ChargeItem::getAccountId)
.filter(Objects::isNull)
.count();
long zeroAccountIdCount = chargeItemList.stream()
.map(ChargeItem::getAccountId)
.filter(id -> id != null && id == 0L)
.count();
if (nullAccountIdCount > 0) {
throw new ServiceException("部分收费项缺少账户信息,请检查收费项数据");
}
if (zeroAccountIdCount > 0) {
throw new ServiceException("部分收费项账户ID为0无效请检查收费项数据或重新创建检查申请");
}
if (distinctAccountIdList.isEmpty()) {
throw new ServiceException("未找到有效的账户信息");
}
@@ -248,15 +269,66 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
// 在挂号费等场景下可能因数据不一致导致查不到,引发误报
List<Account> accountList = iAccountService.list(new LambdaQueryWrapper<Account>()
.in(Account::getId, distinctAccountIdList));
// 🔧 Bug Fix: 处理账户不存在的情况(历史数据修复)
if (accountList.size() != distinctAccountIdList.size()) {
// 部分账户查不到时,记录警告日志,并校验是否每个收费项都能找到对应账户
Set<Long> foundAccountIds = accountList.stream().map(Account::getId).collect(Collectors.toSet());
List<Long> missingAccountIds = distinctAccountIdList.stream()
.filter(id -> !foundAccountIds.contains(id)).collect(Collectors.toList());
if (accountList.isEmpty()) {
throw new ServiceException("未查询到任何账户信息encounterId" + prePaymentDto.getEncounterId()
+ "期望accountId列表" + distinctAccountIdList);
logger.warn("预结算发现部分账户不存在missingAccountIds={}将自动修复收费项的accountId",
missingAccountIds);
// 获取或创建有效的自费账户
Account selfAccount = iAccountService.getSelfAccount(prePaymentDto.getEncounterId());
if (selfAccount == null) {
// 自动创建自费账户
Account newAccount = new Account();
newAccount.setPatientId(chargeItemList.get(0).getPatientId());
newAccount.setEncounterId(prePaymentDto.getEncounterId());
newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO);
newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode());
newAccount.setBalanceAmount(BigDecimal.ZERO);
newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue());
newAccount.setEncounterFlag(Whether.YES.getValue());
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
selfAccount = iAccountService.getById(newAccountId);
logger.info("预结算自动创建自费账户newAccountId={}", newAccountId);
}
// 修复收费项的 accountId
for (Long missingAccountId : missingAccountIds) {
// 找到使用该无效 accountId 的收费项
List<ChargeItem> affectedChargeItems = chargeItemList.stream()
.filter(ci -> ci.getAccountId() != null && ci.getAccountId().equals(missingAccountId))
.collect(Collectors.toList());
for (ChargeItem ci : affectedChargeItems) {
LambdaUpdateWrapper<ChargeItem> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ChargeItem::getId, ci.getId())
.set(ChargeItem::getAccountId, selfAccount.getId());
iChargeItemService.update(updateWrapper);
logger.info("预结算修复收费项accountIdchargeItemId={}oldAccountId={}newAccountId={}",
ci.getId(), missingAccountId, selfAccount.getId());
// 更新本地对象的 accountId以便后续处理使用正确的值
ci.setAccountId(selfAccount.getId());
}
}
// 重新查询账户列表
accountList = iAccountService.list(new LambdaQueryWrapper<Account>()
.eq(Account::getId, selfAccount.getId()));
// 重新构建 accountIdList已修复
distinctAccountIdList = chargeItemList.stream()
.map(ChargeItem::getAccountId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
logger.info("预结算账户修复完成最终使用accountId={}", selfAccount.getId());
}
// 账户id对应的账单列表
@@ -1914,6 +1986,60 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
// Bug #409预约签到挂号时修正 serviceTypeId 为预约类型而非挂号类型
// 数据链Order → ScheduleSlot → SchedulePool → HealthcareService
try {
Order appointmentOrder = iOrderService.getOne(
new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounterFormData.getPatientId())
.eq(Order::getStatus, CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.eq(Order::getDeleteFlag, "0")
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1")
);
if (appointmentOrder != null && appointmentOrder.getSlotId() != null) {
ScheduleSlot slot = scheduleSlotMapper.selectById(appointmentOrder.getSlotId());
if (slot != null && slot.getPoolId() != null) {
SchedulePool pool = schedulePoolMapper.selectById(slot.getPoolId());
if (pool != null && pool.getRegType() != null) {
// pool.getRegType() 存储号别名称如"普通号-预约",精确匹配 HealthcareService
HealthcareService appointmentHealthcareService = healthcareServiceService.getOne(
new LambdaQueryWrapper<HealthcareService>()
.eq(HealthcareService::getOfferedOrgId, encounterFormData.getOrganizationId())
.eq(HealthcareService::getDeleteFlag, "0")
.eq(HealthcareService::getName, pool.getRegType())
.last("LIMIT 1")
);
if (appointmentHealthcareService != null) {
encounterFormData.setServiceTypeId(appointmentHealthcareService.getId());
encounterFormData.setOrderId(appointmentOrder.getId());
logger.info("预约签到挂号修正serviceTypeId={},号别={}",
appointmentHealthcareService.getId(), pool.getRegType());
} else {
// 精确匹配失败,尝试 LIKE 匹配
appointmentHealthcareService = healthcareServiceService.getOne(
new LambdaQueryWrapper<HealthcareService>()
.eq(HealthcareService::getOfferedOrgId, encounterFormData.getOrganizationId())
.eq(HealthcareService::getDeleteFlag, "0")
.like(HealthcareService::getName, pool.getRegType())
.last("LIMIT 1")
);
if (appointmentHealthcareService != null) {
encounterFormData.setServiceTypeId(appointmentHealthcareService.getId());
encounterFormData.setOrderId(appointmentOrder.getId());
logger.info("预约签到挂号(LIKE)serviceTypeId={},号别={}",
appointmentHealthcareService.getId(), pool.getRegType());
}
}
}
}
}
} catch (Exception e) {
logger.warn("修正serviceTypeId失败不影响挂号流程", e);
}
// 保存就诊信息
Encounter encounter = new Encounter();
BeanUtils.copyProperties(encounterFormData, encounter);
@@ -1985,6 +2111,31 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
}
// 创建队列项
// 尝试获取预约订单的 slot_id 和 pool_id
Long queuePoolId = null;
Long queueSlotId = null;
try {
// 查询患者当天的待签到预约订单status = 1 或 2 表示已预约或已取号)
Order order = iOrderService.getOne(
new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, 1, 2) // 1=BOOKED 已预约, 2=CHECKED_IN 已取号
.eq(Order::getDeleteFlag, "0")
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1")
);
if (order != null && order.getSlotId() != null) {
queueSlotId = order.getSlotId();
// 通过 slot_id 获取 pool_id
ScheduleSlot slot = scheduleSlotMapper.selectById(queueSlotId);
if (slot != null) {
queuePoolId = slot.getPoolId();
}
logger.info("挂号时找到预约订单slotId={}, poolId={}, encounterId={}", queueSlotId, queuePoolId, encounterId);
}
} catch (Exception e) {
logger.warn("查询预约订单失败不影响挂号流程encounterId={}", encounterId, e);
}
TriageQueueItem queueItem = new TriageQueueItem()
.setTenantId(tenantId)
.setQueueDate(queueDate)
@@ -1997,7 +2148,9 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
.setPractitionerId(queuePractitionerId)
.setPractitionerName(practitionerName)
.setRoomNo(null)
.setStatus("WAITING")
.setPoolId(queuePoolId)
.setSlotId(queueSlotId)
.setStatus(TriageQueueAppServiceImpl.STATUS_WAITING) // 0=WAITING(等待中)
.setQueueOrder(maxOrder + 1)
.setDeleteFlag("0")
.setCreateTime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS))

View File

@@ -88,5 +88,10 @@ public class OrdersGroupPackageDetailQueryDto {
*/
private Long groupId;
/**
* 治疗类型1-长期 2-临时
*/
private Integer therapyEnum;
}

View File

@@ -180,9 +180,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
// 药品
List<RegAdviceSaveDto> medicineList = regAdviceSaveList.stream()
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
// 诊疗活动
// 诊疗活动包含护理adviceType=26
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
.collect(Collectors.toList());
// 耗材 🔧 Bug #147 修复
List<RegAdviceSaveDto> deviceList = regAdviceSaveList.stream()
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
@@ -844,9 +846,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
List<Long> medicineRequestIds
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
// 诊疗
// 诊疗包含护理adviceType=26
List<AdviceBatchOpParam> activityList = paramList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
.collect(Collectors.toList());
List<Long> activityRequestIds
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
// 查询已完成的药品请求
@@ -902,9 +906,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
List<Long> medicineRequestIds
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
// 诊疗
// 诊疗包含护理adviceType=26
List<AdviceBatchOpParam> activityList = paramList.stream()
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
.collect(Collectors.toList());
List<Long> activityRequestIds
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
if (!medicineRequestIds.isEmpty()) {

View File

@@ -27,6 +27,7 @@ import com.openhis.workflow.service.IActivityDefinitionService;
import com.openhis.workflow.service.IServiceRequestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
@@ -40,6 +41,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class RequestFormManageAppServiceImpl implements IRequestFormManageAppService {
@Resource
@@ -71,6 +73,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
// 诊疗处方号
String prescriptionNo;
@@ -330,7 +333,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
surgeryChargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(surgeryServiceRequest.getBusNo()));
surgeryChargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
surgeryChargeItem.setPatientId(patientId);
surgeryChargeItem.setContextEnum(6); // 6-手术
surgeryChargeItem.setContextEnum(3); // 3-项目(手术属于诊疗项目)
surgeryChargeItem.setEncounterId(encounterId);
surgeryChargeItem.setEntererId(practitionerId);
surgeryChargeItem.setEnteredDate(curDate);

View File

@@ -93,6 +93,15 @@ public interface IInfectiousCardAppService {
*
* @return 科室树数据
*/
/**
* 撤销审核传染病报卡
*
* @param cardNo 报卡编号
* @param status 撤销后的状态
* @return 结果
*/
R<?> revokeAudit(String cardNo, String status);
R<?> getDeptTree();
}

View File

@@ -276,6 +276,38 @@ public class InfectiousCardAppServiceImpl implements IInfectiousCardAppService {
throw new RuntimeException("导出失败:" + e.getMessage());
}
}
/**
* 撤销审核传染病报卡
*
* @param cardNo 报卡编号
* @param status 撤销后的状态
* @return 结果
*/
@Override
public R<?> revokeAudit(String cardNo, String status) {
try {
// 验证参数
if (cardNo == null || cardNo.trim().isEmpty()) {
return R.fail("报卡编号不能为空");
}
if (status == null || status.trim().isEmpty()) {
return R.fail("撤销后的状态不能为空");
}
// 执行撤销审核操作
int rows = reportManageCardMapper.revokeAuditCard(cardNo, Integer.parseInt(status));
if (rows > 0) {
return R.ok("撤销审核成功");
} else {
return R.fail("撤销审核失败:未找到对应的报卡");
}
} catch (Exception e) {
log.error("撤销审核传染病报卡失败", e);
return R.fail("撤销审核失败:" + e.getMessage());
}
}
/**
* CSV 字段转义

View File

@@ -7,13 +7,13 @@ import com.openhis.web.reportManagement.dto.AuditInfectiousCardRequest;
import com.openhis.web.reportManagement.dto.ReturnInfectiousCardRequest;
import com.openhis.web.reportManagement.dto.BatchAuditInfectiousCardRequest;
import com.openhis.web.reportManagement.dto.BatchReturnInfectiousCardRequest;
import com.openhis.web.reportManagement.dto.RevokeAuditInfectiousCardRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
// import java.util.List; // 批量操作功能暂未实现
/**
* 传染病报卡管理 Controller
@@ -31,11 +31,6 @@ public class reportManagementController {
/**
* 分页查询传染病报卡列表
*
* @param param 查询参数
* @param pageNo 当前页码
* @param pageSize 每页数量
* @return 传染病报卡列表
*/
@GetMapping("/list-page")
public R<?> listPage(InfectiousCardParam param,
@@ -46,9 +41,6 @@ public class reportManagementController {
/**
* 根据 ID 查询传染病报卡详情
*
* @param id 报卡 ID
* @return 传染病报卡详情
*/
@GetMapping("/{id}")
public R<?> getById(@PathVariable Long id) {
@@ -57,9 +49,6 @@ public class reportManagementController {
/**
* 根据卡号查询传染病报卡详情
*
* @param cardNo 报卡编号
* @return 传染病报卡详情
*/
@GetMapping("/detail/{cardNo}")
public R<?> getByCardNo(@PathVariable String cardNo) {
@@ -68,9 +57,6 @@ public class reportManagementController {
/**
* 审核传染病报卡
*
* @param request 审核请求
* @return 结果
*/
@PostMapping("/audit")
public R<?> audit(@RequestBody AuditInfectiousCardRequest request) {
@@ -79,9 +65,6 @@ public class reportManagementController {
/**
* 退回传染病报卡
*
* @param request 退回请求
* @return 结果
*/
@PostMapping("/return")
public R<?> returnCard(@Valid @RequestBody ReturnInfectiousCardRequest request) {
@@ -90,9 +73,6 @@ public class reportManagementController {
/**
* 批量审核传染病报卡
*
* @param request 批量审核请求
* @return 结果
*/
@PostMapping("/batchAudit")
public R<?> batchAudit(@RequestBody BatchAuditInfectiousCardRequest request) {
@@ -101,9 +81,6 @@ public class reportManagementController {
/**
* 批量退回传染病报卡
*
* @param request 批量退回请求
* @return 结果
*/
@PostMapping("/batchReturn")
public R<?> batchReturn(@Valid @RequestBody BatchReturnInfectiousCardRequest request) {
@@ -111,10 +88,18 @@ public class reportManagementController {
}
/**
* 导出传染病报卡
* 撤销审核传染病报卡Bug #395
*
* @param param 查询参数
* @param response 响应对象
* @param request 撤销审核请求
* @return 结果
*/
@PostMapping("/revokeAudit")
public R<?> revokeAudit(@Valid @RequestBody RevokeAuditInfectiousCardRequest request) {
return infectiousCardAppService.revokeAudit(request.getCardNo(), request.getStatus());
}
/**
* 导出传染病报卡
*/
@GetMapping("/export")
public void export(InfectiousCardParam param, HttpServletResponse response) {
@@ -123,8 +108,6 @@ public class reportManagementController {
/**
* 获取科室树
*
* @return 科室树数据
*/
@GetMapping("/dept-tree")
public R<?> getDeptTree() {

View File

@@ -127,14 +127,11 @@ public class InfectiousCardDto {
/** 审核意见 */
private String auditOpinion;
/** 退原因 */
/** 退原因 */
private String returnReason;
/** 订正病名 */
private String correctName;
/** 退卡原因 */
private String withdrawReason;
private String revisedDiseaseName;
/** 其他传染病 */
private String otherDisease;

View File

@@ -0,0 +1,20 @@
package com.openhis.web.reportManagement.dto;
import lombok.Data;
/**
* 撤销审核传染病报卡请求 DTO
*
* @author guanyu
* @date 2026-04-23
*/
@Data
public class RevokeAuditInfectiousCardRequest {
/** 卡片编号(主键) */
private String cardNo;
/** 撤销后的状态 (0 暂存/1 待审核/2 已审核/3 已上报/4 失败/5 退回) */
private String status;
}

View File

@@ -55,5 +55,12 @@ public interface ReportManageCardMapper {
* @param param 查询参数
* @return 报卡列表
*/
/**
* 撤销审核传染病报卡
* @param cardNo 卡号
* @param status 撤销后的状态
* @return 影响行数
*/
int revokeAuditCard(@Param("cardNo") String cardNo, @Param("status") Integer status);
List<InfectiousCardDto> selectAllCards(@Param("param") InfectiousCardParam param);
}

View File

@@ -2,8 +2,9 @@ package com.openhis.web.reportmanage.dto;
import java.util.Date;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
/**
@@ -12,7 +13,8 @@ import java.math.BigDecimal;
* @author yuxj
* @date 2025/8/25
*/
@Data
@Getter
@Setter
@Accessors(chain = true)
public class InpatientMedicalRecordHomePageCollectionDto {

View File

@@ -5,43 +5,66 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
import com.openhis.triageandqueuemanage.domain.CallRecord;
import com.openhis.triageandqueuemanage.domain.DivLog;
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
import com.openhis.triageandqueuemanage.service.CallRecordService;
import com.openhis.triageandqueuemanage.service.DivLogService;
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
import com.openhis.common.enums.CallType;
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
import com.openhis.web.triageandqueuemanage.dto.TriageQueueEncounterItem;
import com.openhis.web.triageandqueuemanage.dto.*;
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.core.common.core.domain.model.LoginUser;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
private static final String STATUS_WAITING = "WAITING";
private static final String STATUS_CALLING = "CALLING";
private static final String STATUS_SKIPPED = "SKIPPED";
private static final String STATUS_COMPLETED = "COMPLETED";
/**
* 分诊队列状态常量(数字编码)
* 0=WAITING(等待中), 10=CALLING(呼叫中), 20=IN_CLINIC(诊中),
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
*/
public static final Integer STATUS_WAITING = 0;
public static final Integer STATUS_CALLING = 10;
public static final Integer STATUS_IN_CLINIC = 20;
public static final Integer STATUS_COMPLETED = 30;
public static final Integer STATUS_SKIPPED = 40;
public static final Integer STATUS_REFUNDED = 50;
public static final Integer STATUS_FOLLOW = 60;
@Resource
private TriageQueueItemService triageQueueItemService;
@Resource
private CallNumberSseManager callNumberSseManager;
@Resource
private TriageCandidateExclusionService triageCandidateExclusionService;
@Resource
private DivLogService divLogService;
@Resource
private CallRecordService callRecordService;
@Resource
private ScheduleSlotMapper scheduleSlotMapper;
@Override
public R<?> list(Long organizationId, LocalDate date) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
@@ -61,31 +84,33 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
List<TriageQueueItem> list = triageQueueItemService.list(wrapper);
// 通过 slotId 查询 seqNo 并填充到返回结果中(用于选呼显示预约号)
if (list != null && !list.isEmpty()) {
Map<Long, Integer> seqNoMap = buildSlotSeqNoMap(list);
list.forEach(item -> {
if (item.getSlotId() != null && seqNoMap.containsKey(item.getSlotId())) {
item.setSeqNo(seqNoMap.get(item.getSlotId()));
}
});
}
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
if (list != null && !list.isEmpty()) {
int beforeSize = list.size();
list = list.stream()
.filter(item -> !STATUS_COMPLETED.equals(item.getStatus()))
.collect(java.util.stream.Collectors.toList());
if (beforeSize != list.size()) {
System.out.println(">>> [TriageQueue] list() 警告:过滤掉了 " + (beforeSize - list.size()) + " 条 COMPLETED 状态的记录");
}
}
// 调试日志:检查状态值
if (list != null && !list.isEmpty()) {
System.out.println(">>> [TriageQueue] list() 返回 " + list.size() + " 条记录(已排除 COMPLETED");
for (int i = 0; i < Math.min(3, list.size()); i++) {
TriageQueueItem item = list.get(i);
System.out.println(" [" + i + "] patientName=" + item.getPatientName()
+ ", status=" + item.getStatus()
+ ", organizationId=" + item.getOrganizationId());
}
}
return R.ok(list);
}
/**
* 将候选池患者的挂号移入队列操作
* 并同时写入移入队列操作日志
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> add(TriageQueueAddReq req) {
@@ -127,6 +152,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.setPractitionerName(it.getPractitionerName())
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
.setPoolId(it.getPoolId()) // ✅ 号源池ID用于div_log审计
.setSlotId(it.getSlotId()) // ✅ 号源槽位ID用于div_log审计
.setSeqNo(it.getSeqNo()) // ✅ 预约序号(用于叫号显示)
.setStatus(STATUS_WAITING)
.setQueueOrder(++maxOrder)
.setDeleteFlag("0")
@@ -134,7 +162,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.setUpdateTime(LocalDateTime.now());
triageQueueItemService.save(qi);
// 写入分诊日志
writeDivLog(it.getPoolId(), it.getSlotId(), "ADD_QUEUE");
// 记录到候选池排除列表(避免刷新后重新出现在候选池)
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
new LambdaQueryWrapper<TriageCandidateExclusion>()
@@ -165,17 +194,26 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
return R.ok(added);
}
/**
* 移除队列操作并同时写入移除操作日志
*
* @param id
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> remove(Long id) {
if (id == null) return R.fail("id 不能为空");
TriageQueueItem item = triageQueueItemService.getById(id);
if (item == null) return R.fail("队列项不存在");
if (item.getStatus() != null && item.getStatus() != 0) {
return R.fail("仅等待状态的患者可移出队列,当前状态码:" + item.getStatus());
}
// 逻辑删除队列项
item.setDeleteFlag("1").setUpdateTime(LocalDateTime.now());
triageQueueItemService.updateById(item);
// 写入分诊日志
writeDivLog(item.getPoolId(), item.getSlotId(), "REMOVE_QUEUE");
// 从排除列表中删除记录,使患者重新出现在候选池中
Integer tenantId = item.getTenantId();
LocalDate exclusionDate = item.getQueueDate();
@@ -233,6 +271,12 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
return R.ok(true);
}
/**
* 智能分诊选呼功能,在进行选呼操作时同时写入叫号记录和选呼操作日志
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> call(TriageQueueActionReq req) {
@@ -247,7 +291,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
// 叫号后推送 SSE 消息(实时通知显示屏刷新)
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
// 写入分诊日志和叫号记录
writeDivLog(selected.getPoolId(), selected.getSlotId(), "CALL");
writeCallRecord(selected.getId(), selected.getPractitionerId(), CallType.CALL, selected.getRoomNo());
return R.ok(true);
} else if (STATUS_CALLING.equals(selected.getStatus())) {
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
@@ -258,6 +304,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
}
/**
* 智能分诊完成操作
* 在进行完成操作后同时写入叫号记录和完成操作日志
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> complete(TriageQueueActionReq req) {
@@ -277,7 +330,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
} else {
// 如果没有提供 id通过查询条件查找兼容旧逻辑
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
System.out.println(">>> [TriageQueue] complete() 开始执行(不限制日期,通过查询条件), tenantId=" + tenantId + ", orgId=" + orgId);
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getTenantId, tenantId)
@@ -291,8 +343,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
calling = triageQueueItemService.getOne(callingWrapper, false);
System.out.println(">>> [TriageQueue] complete() 查询叫号中患者(不限制日期): orgId=" + orgId + ", 结果=" + (calling != null ? calling.getPatientName() + "(status=" + calling.getStatus() + ", queueDate=" + calling.getQueueDate() + ")" : "null"));
if (calling == null) {
return R.fail("当前没有叫号中的患者");
@@ -322,8 +372,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
System.out.println(">>> [TriageQueue] complete() 查询等待患者(不限制日期): actualOrgId=" + actualOrgId + ", 结果=" + (next != null ? next.getPatientName() + "(status=" + next.getStatus() + ")" : "null"));
if (next != null) {
next.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
@@ -334,16 +382,45 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
// 完成后推送 SSE 消息(实时通知显示屏刷新)
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
// 写入分诊日志和叫号记录
writeDivLog(calling.getPoolId(), calling.getSlotId(), "COMPLETE");
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.COMPLETE, calling.getRoomNo());
return R.ok(true);
}
/**
* 智能队列重排序功能操作单元
* 在进行队列重排序时同时在div_log表中写入重排序日志和叫号记录
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> requeue(TriageQueueActionReq req) {
return doRequeue(req, "REQUEUE");
}
/**
* 智能分诊跳过功能操作单元内部包含将队列正在叫号状态的号进行跳过
* 同时将跳过操作日志写入div_log表中以及写入叫号记录表。
*
* @param req
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> skip(TriageQueueActionReq req) {
// 当前业务"跳过"按"过号重排"处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
return doRequeue(req, "SKIP");
}
/**
* 过号重排/跳过的核心逻辑
* @param action 日志动作REQUEUE 或 SKIP
*/
private R<?> doRequeue(TriageQueueActionReq req, String action) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
TriageQueueItem calling = null;
if (req != null && req.getId() != null) {
calling = triageQueueItemService.getById(req.getId());
@@ -352,7 +429,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
// 验证状态
if (!STATUS_CALLING.equals(calling.getStatus())) {
return R.fail("只能对\"叫号中\"状态的患者进行过号重排,当前患者状态为:" + calling.getStatus());
return R.fail("只能对\"叫号中\"状态的患者进行" + ("SKIP".equals(action) ? "跳过" : "过号重排") + ",当前患者状态为:" + calling.getStatus());
}
} else {
// 如果没有提供 id通过查询条件查找兼容旧逻辑
@@ -373,12 +450,11 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (calling == null) return R.fail("当前没有叫号中的患者");
}
// 使用实际找到的科室ID
Long actualOrgId = calling.getOrganizationId();
// 关键改进:在执行"跳过"操作之前,先检查是否有等待中的患者(判断队列状态)
// 如果没有等待中的患者,就不应该执行"过号重排"操作
// 关键改进:在执行跳过/重排操作之前,先检查是否有等待中的患者(判断队列状态)
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
.eq(TriageQueueItem::getTenantId, tenantId)
.eq(TriageQueueItem::getDeleteFlag, "0")
@@ -387,22 +463,14 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.eq(TriageQueueItem::getStatus, STATUS_SKIPPED))
.orderByAsc(TriageQueueItem::getQueueOrder)
.last("LIMIT 1");
// 如果指定了科室ID则按科室过滤否则查询所有科室全科模式
if (actualOrgId != null) {
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
}
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
// 调试日志:检查查询结果
System.out.println(">>> [TriageQueue] requeue() 查询等待中的患者:");
System.out.println(">>> - 科室ID: " + actualOrgId);
System.out.println(">>> - 找到的等待患者: " + (next != null ? next.getPatientName() + " (状态: " + next.getStatus() + ")" : "null"));
// 如果找不到等待中的患者,直接返回失败(不执行跳过操作)
if (next == null) {
System.out.println(">>> [TriageQueue] requeue() 失败:没有等待中的患者");
return R.fail("当前没有等待中的患者");
}
@@ -427,27 +495,20 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
triageQueueItemService.updateById(next);
recalcOrders(actualOrgId, null);
// ✅ 过号重排后推送 SSE 消息(实时通知显示屏刷新)
// 推送 SSE 消息
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
// 写入分诊日志和叫号记录
writeDivLog(calling.getPoolId(), calling.getSlotId(), action);
writeCallRecord(calling.getId(), calling.getPractitionerId(),
"SKIP".equals(action) ? CallType.SKIP : CallType.REQUEUE, calling.getRoomNo());
return R.ok(true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> skip(TriageQueueActionReq req) {
// 当前业务“跳过”按“过号重排”处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
return requeue(req);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<?> next(TriageQueueActionReq req) {
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
TriageQueueItem calling = null;
System.out.println(">>> [TriageQueue] next() 开始执行(不限制日期), tenantId=" + tenantId);
// 关键改进:如果提供了 id优先使用 id 直接查找(像 call 方法一样)
if (req != null && req.getId() != null) {
@@ -507,14 +568,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
}
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
// 调试日志:打印查询条件和结果
System.out.println(">>> [TriageQueue] next() 查询条件(不限制日期): tenantId=" + tenantId
+ ", actualOrgId=" + actualOrgId
+ ", deleteFlag=0"
+ ", status IN (WAITING, SKIPPED)");
System.out.println(">>> [TriageQueue] next() 查询结果: calling=" + (calling != null ? calling.getPatientName() + "(status=" + calling.getStatus() + ", queueDate=" + calling.getQueueDate() + ")" : "null")
+ ", next=" + (next != null ? next.getPatientName() + "(status=" + next.getStatus() + ", queueDate=" + next.getQueueDate() + ")" : "null"));
if (next == null) {
if (actualOrgId != null) {
@@ -529,6 +582,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (next.getOrganizationId() != null) {
recalcOrders(next.getOrganizationId(), null);
}
// 写入叫号记录
if (calling != null) {
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.NEXT, calling.getRoomNo());
}
writeCallRecord(next.getId(), next.getPractitionerId(), CallType.NEXT, next.getRoomNo());
return R.ok(true);
}
@@ -602,7 +662,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
.eq(TriageQueueItem::getDeleteFlag, "0")
.orderByAsc(TriageQueueItem::getQueueOrder)
);
// 通过 slotId 批量查询 seqNo方案 B不修改 triage_queue_item 表结构)
Map<Long, Integer> slotSeqNoMap = buildSlotSeqNoMap(allItems);
CallNumberDisplayResp resp = new CallNumberDisplayResp();
// 1. 获取科室名称(从第一条数据中取)
@@ -620,7 +683,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
if (callingItem != null) {
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
currentCall.setNumber(callingItem.getQueueOrder());
Integer displayNo = resolveDisplayNumber(callingItem, slotSeqNoMap);
currentCall.setNumber(displayNo);
currentCall.setName(maskPatientName(callingItem.getPatientName()));
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
currentCall.setDoctor(callingItem.getPractitionerName());
@@ -674,7 +738,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
patient.setId(item.getId());
patient.setName(maskPatientName(item.getPatientName()));
patient.setStatus(item.getStatus());
patient.setQueueOrder(item.getQueueOrder());
patient.setQueueOrder(resolveDisplayNumber(item, slotSeqNoMap));
patients.add(patient);
// 统计等待人数(不包括 CALLING 状态)
@@ -742,6 +806,82 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
System.err.println("推送显示屏更新失败:" + e.getMessage());
}
}
/**
* 写入分诊操作日志
*
* @param poolId 号源池ID
* @param slotId 号源槽位ID
* @param action 操作动作ADD_QUEUE/REMOVE_QUEUE/CALL/COMPLETE/SKIP/REQUEUE
*/
private void writeDivLog(Long poolId, Long slotId, String action) {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
DivLog log = new DivLog()
.setPoolId(poolId)
.setSlotId(slotId)
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
.setAction(action)
.setCreateTime(LocalDateTime.now())
.setUpdateAt(LocalDateTime.now())
.setCreatedAt(LocalDateTime.now());
divLogService.save(log);
} catch (Exception e) {
log.error("写入分诊日志失败", e);
}
}
/**
* 写入叫号记录
*
* @param queueId 队列ID
* @param doctorId 医生ID
* @param callType 叫号类型枚举
* @param room 诊室号
*/
private void writeCallRecord(Long queueId, Long doctorId, CallType callType, String room) {
try {
CallRecord record = new CallRecord()
.setQueueId(queueId)
.setDoctorId(doctorId)
.setCallTime(LocalDateTime.now())
.setCallType(callType.getValue().toString())
.setRoom(room)
.setCreateAt(LocalDateTime.now());
callRecordService.save(record);
} catch (Exception e) {
log.error("写入叫号记录失败", e);
}
}
/**
* 通过 slotId 批量查询 seqNo返回 slotId -> seqNo 映射
*/
private Map<Long, Integer> buildSlotSeqNoMap(List<TriageQueueItem> items) {
List<Long> slotIds = items.stream()
.map(TriageQueueItem::getSlotId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (slotIds.isEmpty()) {
return Collections.emptyMap();
}
List<ScheduleSlot> slots = scheduleSlotMapper.selectSeqNoBySlotIds(slotIds);
return slots.stream()
.filter(s -> s.getId() != null && s.getSeqNo() != null)
.collect(Collectors.toMap(ScheduleSlot::getId, ScheduleSlot::getSeqNo, (a, b) -> a));
}
/**
* 计算患者在叫号显示屏上应显示的号码:优先 seqNo预约序号否则 queueOrder排队号
*/
private Integer resolveDisplayNumber(TriageQueueItem item, Map<Long, Integer> slotSeqNoMap) {
if (item == null) return null;
if (item.getSlotId() != null && slotSeqNoMap.containsKey(item.getSlotId())) {
return slotSeqNoMap.get(item.getSlotId());
}
return item.getQueueOrder();
}
}

View File

@@ -59,7 +59,7 @@ public class CallNumberDisplayResp {
/** 患者姓名(脱敏) */
private String name;
/** 状态CALLING=就诊中WAITING=等待 */
private String status;
private Integer status;
/** 排队号 */
private Integer queueOrder;
}

View File

@@ -15,6 +15,12 @@ public class TriageQueueEncounterItem {
private Long practitionerId;
/** 诊室号(可选) */
private String roomNo;
/** 号源池ID关联 adm_schedule_pool.id用于 div_log 审计日志) */
private Long poolId;
/** 号源槽位ID关联 adm_schedule_slot.id用于 div_log 审计日志) */
private Long slotId;
/** 预约序号(来自 adm_schedule_slot.seq_no用于叫号显示 */
private Integer seqNo;
}

View File

@@ -3,7 +3,7 @@ core:
# 名称
name: HEALTHLINK-HIS
# 版本
version: 0.0.1
version: ${CORE_VERSION:0.0.1}
# 版权年份
copyrightYear: 2025
# 文件路径

View File

@@ -76,6 +76,8 @@
T1.quantity_unit,
T1.unit_price,
T1.total_price,
T1.generate_source_enum,
T1.prescription_no AS source_bill_no,
mmr.prescription_no,
mmr.method_code AS method_code,
mmr.rate_code,
@@ -94,18 +96,21 @@
T8.contract_name,
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} THEN T2."name"
WHEN T1.context_enum = #{medication} THEN T3."name"
WHEN T1.context_enum = #{device} THEN T4."name"
END AS item_name,
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = #{activity} THEN T2.yb_no
WHEN T1.context_enum = #{medication} THEN T3.yb_no
WHEN T1.context_enum = #{device} THEN T4.yb_no
END AS yb_no,
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
WHEN T1.context_enum = #{activity} THEN T2.id
WHEN T1.context_enum = #{medication} THEN T3.id
WHEN T1.context_enum = #{device} THEN T4.id
@@ -142,15 +147,26 @@
ON T6.contract_no = T8.bus_no
AND T8.delete_flag = '0'
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND T1.service_table = #{worDeviceRequest} AND wdr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsrp ON wsrp.id = wsr.parent_id AND wsrp.delete_flag = '0'
WHERE T1.encounter_id = #{encounterId}
AND T1.status_enum IN (#{planned}
AND T1.status_enum IN (0
, #{planned}
, #{billable}
, #{billed}
, #{refunding}
, #{refunded}
, #{partRefund})
AND T1.context_enum != #{register}
AND (
-- 若能关联到请求表,则必须是“已签发”后才允许收费端展示
(mmr.id IS NOT NULL AND COALESCE(mmr.status_enum, 1) != 1)
OR (wsr.id IS NOT NULL AND (COALESCE(wsr.status_enum, 1) != 1 OR COALESCE(wsrp.status_enum, 1) != 1))
OR (wdr.id IS NOT NULL AND COALESCE(wdr.status_enum, 1) != 1)
-- 无法关联到任一请求表的收费项,不受签发过滤影响(如挂号费等)
OR (mmr.id IS NULL AND wsr.id IS NULL AND wdr.id IS NULL)
)
AND T1.delete_flag = '0'
</select>
@@ -176,6 +192,8 @@
T1.quantity_unit,
T1.unit_price,
T1.total_price,
T1.generate_source_enum,
T1.prescription_no AS source_bill_no,
mmr.prescription_no,
mmr.method_code AS method_code,
mmr.rate_code,
@@ -194,18 +212,21 @@
T8.contract_name,
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
WHEN T1.context_enum = #{activity} THEN T2."name"
WHEN T1.context_enum = #{medication} THEN T3."name"
WHEN T1.context_enum = #{device} THEN T4."name"
END AS item_name,
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
WHEN T1.context_enum = #{activity} THEN T2.yb_no
WHEN T1.context_enum = #{medication} THEN T3.yb_no
WHEN T1.context_enum = #{device} THEN T4.yb_no
END AS yb_no,
CASE
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
WHEN T1.context_enum = #{activity} THEN T2.id
WHEN T1.context_enum = #{medication} THEN T3.id
WHEN T1.context_enum = #{device} THEN T4.id
@@ -243,15 +264,25 @@
ON T6.contract_no = T8.bus_no
AND T8.delete_flag = '0'
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND T1.service_table = #{worDeviceRequest} AND wdr.delete_flag = '0'
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
WHERE T1.encounter_id = #{encounterId}
AND T1.status_enum IN (#{planned}
AND T1.status_enum IN (0
, #{planned}
, #{billable}
, #{billed}
, #{refunding}
, #{refunded}
, #{partRefund})
AND T1.context_enum != #{register}
AND (
-- 若能关联到请求表,则必须是“已签发/已发送”后才允许收费端展示
(mmr.id IS NOT NULL AND COALESCE(mmr.status_enum, 1) != 1)
OR (wsr.id IS NOT NULL AND COALESCE(wsr.status_enum, 1) != 1)
OR (wdr.id IS NOT NULL AND COALESCE(wdr.status_enum, 1) != 1)
-- 无法关联到任一请求表的收费项,不受签发过滤影响(如挂号费等)
OR (mmr.id IS NULL AND wsr.id IS NULL AND wdr.id IS NULL)
)
AND T1.delete_flag = '0') final_res
</select>

View File

@@ -50,6 +50,7 @@
T9.organization_id AS organizationId,
T9.organization_name AS organizationName,
T9.healthcare_name AS healthcareName,
T9.clinic_room AS clinicRoom, -- Bug #410诊室名称
T9.practitioner_user_id AS practitionerUserId,
T9.practitioner_name AS practitionerName,
T9.contract_name AS contractName,
@@ -67,7 +68,11 @@
T9.payment_id AS paymentId,
T9.picture_url AS pictureUrl,
T9.birth_date AS birthDate,
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo,
COALESCE(T9.order_id IS NOT NULL, false) AS isFromAppointment,
T9.slot_id AS slotId,
T9.pool_id AS poolId,
T9.seq_no AS seqNo
from (
SELECT T1.tenant_id AS tenant_id,
T1.id AS encounter_id,
@@ -93,8 +98,16 @@
ai.picture_url AS picture_url,
T8.birth_date AS birth_date,
T8.bus_no AS patient_bus_no,
T18.identifier_no AS identifier_no
T18.identifier_no AS identifier_no,
T1.order_id AS order_id,
om.slot_id AS slot_id,
ss.seq_no AS seq_no,
ss.pool_id AS pool_id,
sp.clinic_room AS clinic_room -- Bug #410从号源池获取诊室
FROM adm_encounter AS T1
LEFT JOIN order_main AS om ON T1.order_id = om.id AND om.delete_flag = '0'
LEFT JOIN adm_schedule_slot AS ss ON om.slot_id = ss.id AND ss.delete_flag = '0'
LEFT JOIN adm_schedule_pool AS sp ON ss.pool_id = sp.id AND sp.delete_flag = '0' -- Bug #410
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
LEFT JOIN (

View File

@@ -331,8 +331,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
) t
WHERE rn = 1
) pi ON s.patient_id = pi.patient_id
<!-- 排除已生成医嘱的手术 -->
LEFT JOIN wor_service_request sr ON sr.activity_id = s.id AND sr.delete_flag = '0' AND sr.category_enum = 4
<where>
s.delete_flag = '0'
<!-- 只显示未生成医嘱的手术 -->
AND sr.id IS NULL
<if test="ew.sqlSegment != null and ew.sqlSegment != ''">
<![CDATA[
AND ${ew.sqlSegment.replace('tenant_id', 's.tenant_id').replace('create_time', 's.create_time').replace('surgery_no', 's.surgery_no').replace('surgery_name', 's.surgery_name').replace('patient_name', 's.patient_name').replace('main_surgeon_name', 's.main_surgeon_name').replace('anesthetist_name', 's.anesthetist_name').replace('org_name', 's.org_name').replace('status_enum', 's.status_enum').replace('planned_time', 's.planned_time')}

View File

@@ -34,7 +34,7 @@
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
FROM op_schedule os
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
LEFT JOIN adm_organization o ON cs.org_id = o.id
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
LEFT JOIN sys_user su ON su.user_id = os.creator_id
@@ -92,7 +92,7 @@
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
FROM op_schedule os
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
LEFT JOIN adm_organization o ON cs.org_id = o.id
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
LEFT JOIN (
@@ -153,7 +153,7 @@
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
FROM op_schedule os
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
LEFT JOIN adm_organization o ON cs.org_id = o.id
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
LEFT JOIN sys_user su ON su.user_id = os.creator_id

View File

@@ -34,13 +34,13 @@
AND T2.delete_flag = '0'
WHERE
T1.delete_flag = '0'
<if test="chargeItemContext == 1 ">
<if test="chargeItemContext == 1">
AND T1.instance_table = #{MED_MEDICATION_DEFINITION}
</if>
<if test="chargeItemContext == 2 ">
<if test="chargeItemContext == 2">
AND T1.instance_table = #{ADM_DEVICE_DEFINITION}
</if>
<if test="chargeItemContext == 3 ">
<if test="chargeItemContext == 3">
AND (T1.instance_table = #{WOR_ACTIVITY_DEFINITION} OR T1.instance_table = #{ADM_HEALTHCARE_SERVICE})
</if>
GROUP BY T1.tenant_id,

View File

@@ -143,10 +143,10 @@
</if>
ORDER BY T1.id DESC
<if test="searchKey != null and searchKey != ''">
LIMIT 1500
LIMIT 10000
</if>
<if test="searchKey == null or searchKey == ''">
LIMIT 500
LIMIT 10000
</if>
</select>

View File

@@ -46,7 +46,7 @@
abi.chrgitm_lv
FROM (
<!-- 确保至少有一个查询被执行以避免语法错误 -->
<if test="adviceTypes != null and !adviceTypes.isEmpty() and (adviceTypes.contains(1) or adviceTypes.contains(2) or adviceTypes.contains(3))">
<if test="adviceTypes != null and !adviceTypes.isEmpty() and (adviceTypes.contains(1) or adviceTypes.contains(2) or adviceTypes.contains(3) or adviceTypes.contains(6))">
<!-- 如果有有效的adviceTypes则执行对应的查询 -->
<if test="adviceTypes.contains(1)">
(SELECT
@@ -95,14 +95,29 @@
AND T2.delete_flag = '0' AND T2.status_enum = #{statusEnum}
LEFT JOIN adm_supplier AS T3 ON T3.ID = t1.supply_id AND T3.delete_flag = '0'
LEFT JOIN adm_charge_item_definition AS T5 ON T5.instance_id = t1.ID AND T5.delete_flag = '0' AND T5.status_enum = #{statusEnum}
LEFT JOIN adm_organization_location AS T6 ON T6.distribution_category_code = t1.category_code AND T6.delete_flag = '0' AND T6.item_code = '1' AND T6.organization_id = #{organizationId} AND (CURRENT_TIME :: time (6) BETWEEN T6.start_time AND T6.end_time)
INNER JOIN adm_organization_location AS T6 ON T6.distribution_category_code = t1.category_code AND T6.delete_flag = '0' AND T6.item_code = '1' AND T6.organization_id = #{organizationId} AND (CURRENT_TIME :: time (6) BETWEEN T6.start_time AND T6.end_time)
WHERE t1.delete_flag = '0'
AND T2.status_enum = #{statusEnum}
<if test="pricingFlag == 1">
AND 1 = 2
</if>
<if test="categoryCode != null and categoryCode != ''">
AND t1.category_code = #{categoryCode}
<!-- 🔧 BugFix: 支持两种匹配方式 -->
<!-- 1. 直接匹配distribution_category_code = category_code都是数字代码 -->
<!-- 2. 字典转换匹配:通过 sys_dict_data 表将 distribution_category_code中文转换为 category_code数字代码 -->
AND (
-- 方式1直接匹配
t1.category_code = #{categoryCode}
OR
-- 方式2通过字典转换匹配当 distribution_category_code 存储的是中文时)
EXISTS (
SELECT 1 FROM sys_dict_data sdd
WHERE sdd.dict_type = 'med_category_code'
AND sdd.status = '0'
AND sdd.dict_label = T6.distribution_category_code
AND sdd.dict_value = #{categoryCode}
)
)
</if>
<if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
@@ -171,6 +186,9 @@
<if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if>
<if test="categoryCode != null and categoryCode != ''">
AND t1.category_code = #{categoryCode}
</if>
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
AND t1.id IN
<foreach collection="adviceDefinitionIdParamList" item="itemId" open="(" separator="," close=")">
@@ -185,11 +203,15 @@
<if test="adviceTypes.contains(3)">UNION ALL</if>
</if>
<if test="adviceTypes.contains(3)">
<if test="adviceTypes.contains(3) or adviceTypes.contains(6)">
(SELECT
DISTINCT ON (T1.ID)
T1.tenant_id,
3 AS advice_type,
<choose>
<when test="adviceTypes.contains(3) and adviceTypes.contains(6)">CASE T1.category_code WHEN '手术' THEN 6 WHEN '24' THEN 6 ELSE 3 END</when>
<when test="adviceTypes.contains(6)">6</when>
<otherwise>3</otherwise>
</choose> AS advice_type,
T1.bus_no AS bus_no,
T1.category_code AS category_code,
'' AS pharmacology_category_code,
@@ -213,7 +235,7 @@
WHEN '检验' THEN 1
WHEN '检查' THEN 2
WHEN '护理' THEN 3
WHEN '手术' THEN 4
WHEN '手术' THEN 4 WHEN '24' THEN 4
WHEN '其他' THEN 5
ELSE 0
END AS activity_type,
@@ -250,9 +272,20 @@
<if test="pricingFlag != null">
AND (t1.pricing_flag = #{pricingFlag} OR t1.pricing_flag IS NULL)
</if>
<!-- 如果只选择手术(adviceType=6),过滤 category_code = '手术' 或 '24' -->
<if test="adviceTypes.contains(6) and !adviceTypes.contains(3)">
AND (T1.category_code = '手术' OR T1.category_code = '24')
</if>
<!-- 如果只选择诊疗(adviceType=3),排除手术 -->
<if test="adviceTypes.contains(3) and !adviceTypes.contains(6) and (categoryCode == null or categoryCode == '')">
AND T1.category_code != '手术' AND T1.category_code != '24'
</if>
<if test="searchKey != null and searchKey != ''">
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
</if>
<if test="categoryCode != null and categoryCode != ''">
AND t1.category_code = #{categoryCode}
</if>
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
AND t1.id IN
<foreach collection="adviceDefinitionIdParamList" item="itemId" open="(" separator="," close=")">
@@ -263,7 +296,7 @@
</if>
</if>
<!-- 如果没有有效的adviceTypes提供一个空的默认查询以避免语法错误 -->
<if test="adviceTypes == null or adviceTypes.isEmpty() or (!adviceTypes.contains(1) and !adviceTypes.contains(2) and !adviceTypes.contains(3))">
<if test="adviceTypes == null or adviceTypes.isEmpty() or (!adviceTypes.contains(1) and !adviceTypes.contains(2) and !adviceTypes.contains(3) and !adviceTypes.contains(6))">
SELECT
mmd.tenant_id,
CAST(0 AS INTEGER) AS advice_type,
@@ -603,6 +636,8 @@
T3.service_table = #{WOR_DEVICE_REQUEST}
LEFT JOIN adm_location AS al ON al.ID = T1.perform_location AND al.delete_flag = '0'
WHERE T1.delete_flag = '0' AND T1.generate_source_enum = #{generateSourceEnum}
-- 🔧 Bug Fix: 排除基于其他医嘱生成的执行记录
AND (T1.based_on_id IS NULL OR T1.based_on_table IS NULL)
<if test="historyFlag == '0'.toString()">
AND T1.encounter_id = #{encounterId}
</if>
@@ -659,6 +694,10 @@
WHERE T1.delete_flag = '0' AND T1.generate_source_enum = #{generateSourceEnum}
AND T1.parent_id IS NULL
AND T1.refund_service_id IS NULL
-- 🔧 Bug Fix: 排除基于药品请求生成的执行记录(输液、皮试),但保留检查/检验申请单创建的原始医嘱
-- based_on_table='med_medication_request' → 输液/皮试执行记录,应排除
-- based_on_table='exam_apply'/'lab_apply' → 申请单原始医嘱,应保留
AND (T1.based_on_id IS NULL OR T1.based_on_table IS NULL OR T1.based_on_table NOT IN ('med_medication_request', 'med_medication_dispense'))
<if test="historyFlag == '0'.toString()">
AND T1.encounter_id = #{encounterId}
</if>

View File

@@ -4,41 +4,6 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.web.doctorstation.mapper.DoctorStationLabApplyMapper">
<!-- 根据申请单号查询检验申请单(返回完整字段) -->
<select id="getInspectionApplyByApplyNo" resultType="com.openhis.web.doctorstation.dto.DoctorStationLabApplyDto">
SELECT
id AS applicationId,
apply_no AS applyNo,
patient_id AS patientId,
patient_name AS patientName,
medicalrecord_number AS medicalrecordNumber,
natureof_cost AS natureofCost,
visit_no AS visitNo,
apply_dept_code AS applyDeptCode,
apply_department AS applyDepartment,
apply_doc_code AS applyDocCode,
apply_doc_name AS applyDocName,
apply_time AS applyTime,
clinic_diag AS clinicDiag,
clinic_desc AS clinicDesc,
contraindication AS contraindication,
medical_history_summary AS medicalHistorySummary,
purposeof_inspection AS purposeofInspection,
physical_examination AS physicalExamination,
inspection_item AS inspectionItem,
specimen_type_code AS specimenTypeCode,
specimen_name AS specimenName,
priority_code AS priorityCode,
apply_status AS applyStatus,
apply_remark AS applyRemark,
create_time AS createTime,
operator_id AS operatorId,
create_by AS createBy,
tenant_id AS tenantId
FROM lab_apply
WHERE apply_no = #{applyNo}
AND delete_flag = '0'
</select>
<!-- 分页查询检验申请单列表根据就诊ID查询按申请时间降序
从明细表聚合项目名称和金额-->

View File

@@ -25,6 +25,7 @@
T2.organization_id AS org_id, --科室ID从就诊表取
T2.id AS encounter_id, --就诊ID
T2.start_time AS admissionDate, --入院日期
T3.ward_admission_date AS wardAdmissionDate, --入科日期
T3.location_id AS ward_location_id, --病区
T4.location_id AS bed_location_id --床号
FROM adm_patient AS T1

View File

@@ -81,6 +81,9 @@
-- 记录日期
T1.recording_date &gt;= #{startTime}::date
AND T1.recording_date &lt;= #{endTime}::date
<if test="patientId != null and patientId != ''">
AND T1.patient_id = #{patientId}::bigint
</if>
AND T1.delete_flag = '0'
</where>

View File

@@ -48,6 +48,7 @@
togpd.dose_quantity,
togpd.group_id,
togpd.dispense_per_duration,
togpd.therapy_enum,
CASE
WHEN togpd.order_definition_table = 'med_medication_definition' THEN
med.NAME

View File

@@ -96,9 +96,9 @@
fc.contract_name AS fee_type,
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifier_no
FROM doc_request_form drf
LEFT JOIN cli_surgery cs ON cs.surgery_no = drf.prescription_no
LEFT JOIN adm_patient ap ON ap.id = cs.patient_id
LEFT JOIN adm_encounter ae ON ae.id = cs.encounter_id
LEFT JOIN cli_surgery cs ON cs.surgery_no = drf.prescription_no AND cs.delete_flag = '0'
LEFT JOIN adm_patient ap ON ap.id = cs.patient_id AND ap.delete_flag = '0'
LEFT JOIN adm_encounter ae ON ae.id = cs.encounter_id AND ae.delete_flag = '0'
LEFT JOIN adm_account aa ON aa.encounter_id = ae.id AND aa.delete_flag = '0'
LEFT JOIN fin_contract fc ON fc.bus_no = aa.contract_no AND fc.delete_flag = '0'
LEFT JOIN op_schedule os ON os.apply_id = drf.id AND os.delete_flag = '0'

View File

@@ -252,4 +252,11 @@
ORDER BY t1.report_date DESC
</select>
<!-- 撤销审核传染病报卡 -->
<update id="revokeAuditCard">
UPDATE infectious_card
SET status = #{status}::INTEGER,
update_time = CURRENT_TIMESTAMP
WHERE card_no = #{cardNo}
</update>
</mapper>

View File

@@ -0,0 +1,58 @@
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 叫号类型
*
* @author wangjian963
* @date 2026-04-29
*/
@Getter
@AllArgsConstructor
public enum CallType implements HisEnumInterface {
/** 手动叫号(选呼) */
CALL(10, "CALL", "手动叫号(选呼)"),
/** 手动叫号(下一患者) */
NEXT(20, "NEXT", "手动叫号(下一患者)"),
/** 自动叫号 */
AUTO_CALL(30, "AUTO_CALL", "自动叫号"),
/** 跳过 */
SKIP(40, "SKIP", "跳过"),
/** 完成 */
COMPLETE(50, "COMPLETE", "完成"),
/** 重排 */
REQUEUE(60, "REQUEUE", "重排");
/** 状态码 */
private Integer value;
/** 英文标识 */
private String code;
/** 中文描述 */
private String info;
/**
* 根据状态码获取对应的叫号类型枚举
*
* @param value 状态码
* @return 对应的枚举值,未匹配时返回 null
*/
public static CallType getByValue(Integer value) {
if (value == null) {
return null;
}
for (CallType val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

View File

@@ -151,4 +151,9 @@ public class Encounter extends HisBaseEntity {
*/
@TableField("missed_time")
private Date missedTime;
/**
* 预约订单ID
*/
private Long orderId;
}

View File

@@ -61,4 +61,9 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
*/
List<DoctorAvailabilityDTO> selectDoctorAvailabilitySummary(@Param("query") TicketQueryDTO query);
/**
* 批量查询槽位序号(用于分诊叫号显示)
*/
List<ScheduleSlot> selectSeqNoBySlotIds(@Param("slotIds") List<Long> slotIds);
}

View File

@@ -29,9 +29,8 @@ public class InfectiousAudit extends HisBaseEntity {
@JsonSerialize(using = ToStringSerializer.class)
private Long auditId;
/** 报卡ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long cardId;
/** 报卡编号(关联 infectious_card.card_no */
private String cardId;
/** 审核序号 */
private Integer auditSeq;

View File

@@ -1,6 +1,7 @@
package com.openhis.infectious.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
@@ -26,12 +27,8 @@ import java.time.LocalDateTime;
@EqualsAndHashCode(callSuper = false)
public class InfectiousCard extends HisBaseEntity {
/** 卡片编号 */
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 卡片编号(业务编号) */
/** 卡片编号(业务编号,主键) */
@TableId(type = IdType.INPUT)
private String cardNo;
/** 本次就诊ID */
@@ -97,8 +94,9 @@ public class InfectiousCard extends HisBaseEntity {
/** 现住址门牌号 */
private String addressHouse;
/** 病人属于 */
private String patientbelong;
/** 病人属于(1本县区/2本市其他县区/3本省其他地市/4外省/5港澳台/6外籍) */
@TableField("patient_belong")
private Integer patientBelong;
/** 职业 */
private String occupation;
@@ -106,18 +104,15 @@ public class InfectiousCard extends HisBaseEntity {
/** 疾病编码 */
private String diseaseCode;
/** 疾病名称 */
private String diseaseName;
/** 分型 */
private String diseaseSubtype;
/** 其他传染病 */
private String otherDisease;
/** 病例分类 */
private String diseaseType;
/** 其他传染病名称 */
private String otherDisease;
/** 病例类别(1疑似病例/2临床诊断病例/3实验室确诊病例/4病原携带者/5阳性检测结果) */
private Integer caseClass;
/** 发病日期 */
private LocalDate onsetDate;
@@ -146,7 +141,7 @@ public class InfectiousCard extends HisBaseEntity {
private LocalDate reportDate;
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回) */
private String status;
private Integer status;
/** 失败原因 */
private String failMsg;
@@ -165,6 +160,7 @@ public class InfectiousCard extends HisBaseEntity {
private Long deptId;
/** 科室名称 */
@TableField(exist = false)
private String deptName;
/** 医生ID */

View File

@@ -74,4 +74,9 @@ public class InspectionLabApplyItem extends HisBaseEntity {
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long itemStatus;
/**
* 套餐ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long feePackageId;
}

View File

@@ -0,0 +1,42 @@
package com.openhis.triageandqueuemanage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@TableName(value = "call_record")
@EqualsAndHashCode(callSuper = false)
public class CallRecord {
@TableId(type = IdType.AUTO)
private Long recordId;
/** 队列ID (FK triage_queue_item.id) */
private Long queueId;
/** 医生ID */
private Long doctorId;
/** 叫号时间 */
private LocalDateTime callTime;
/**
* 叫号类型,使用 {@link com.openhis.common.enums.CallType} 枚举值
* 10-CALL(选呼), 20-NEXT(下一患者), 30-AUTO_CALL(自动叫号),
* 40-SKIP(跳过), 50-COMPLETE(完成), 60-REQUEUE(重排)
*/
private String callType;
/** 诊室(冗余) */
private String room;
/** 创建时间 */
private LocalDateTime createAt;
}

View File

@@ -0,0 +1,41 @@
package com.openhis.triageandqueuemanage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@TableName(value = "div_log")
@EqualsAndHashCode(callSuper = false)
public class DivLog {
@TableId(type = IdType.AUTO)
private Long logId;
/** 号源池ID */
private Long poolId;
/** 号源槽位ID */
private Long slotId;
/** 操作人ID */
private Long opUserId;
/** 操作动作ADD_QUEUE/REMOVE_QUEUE/CALL/REFUND/COMPLETE/SKIP/REQUEUE */
private String action;
/** 操作时间 */
private LocalDateTime createTime;
/** 更新时间 */
private LocalDateTime updateAt;
/** 创建时间 */
private LocalDateTime createdAt;
}

View File

@@ -1,6 +1,7 @@
package com.openhis.triageandqueuemanage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -31,10 +32,19 @@ public class TriageQueueItem {
private String practitionerName; // 医生姓名
private Long practitionerId; // 医生ID新增字段
private String roomNo; // 诊室号(新增字段)
private Long poolId; // 号源池ID (关联 adm_schedule_pool.id)
private Long slotId; // 号源槽位ID (关联 adm_schedule_slot.id)
/** WAITING / CALLING / SKIPPED / COMPLETED */
private String status;
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
/**
* 分诊队列状态
* 0=WAITING(等待中), 10=CALLING(呼叫中), 20=IN_CLINIC(诊中),
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
*/
private Integer status;
private Integer queueOrder; //”排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
@TableField(exist = false)
private Integer seqNo; // 预约序号(来自 adm_schedule_slot.seq_no非数据库字段通过 JOIN 查询)
private LocalDateTime createTime;
private LocalDateTime updateTime;

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