Compare commits
346 Commits
6d59c6491c
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
405a9dfb72 | ||
| d1be841688 | |||
|
|
9b8655748e | ||
| 00fd6c8710 | |||
| bbd9d48fa6 | |||
| 8fb1d3e583 | |||
| 34ba7cae6a | |||
| 305ab15436 | |||
| 46a7076460 | |||
| e0e6693897 | |||
|
|
7d1e50d045 | ||
| 25ce12cebf | |||
| 7d55717037 | |||
|
|
290e8f8f15 | ||
| fc84fd61ab | |||
|
|
d79690a371 | ||
| 7bccbc7085 | |||
|
|
059ef483ca | ||
|
|
4beb4c40c5 | ||
| 914f2d8229 | |||
| 2f57b3e7c1 | |||
|
|
39ccd27df8 | ||
| d370b6a888 | |||
| 3c61e39e09 | |||
| f2c71b08bb | |||
| 90cf7f43d7 | |||
| 1f5d392c08 | |||
| d52bbda8c3 | |||
|
|
986510278b | ||
| 758921b633 | |||
| 8e7ebd3461 | |||
| 8c05782549 | |||
| 060d1910dd | |||
| 44ae216612 | |||
| 0076753c19 | |||
|
|
957d426042 | ||
|
|
76094d6eff | ||
| dc43ce335a | |||
| d27b5147ec | |||
| 4fb540cfa5 | |||
| 72e1f927e9 | |||
|
|
e7beb3f5c3 | ||
|
|
dc7e3c1de8 | ||
| 1242d41499 | |||
| 091b6e83b6 | |||
| b53cdfa617 | |||
| fe2a79773f | |||
| 22b47fcc95 | |||
| 328ccbbd99 | |||
| 6b6e56c79b | |||
| 41fe89447f | |||
| 0d11d411ea | |||
|
|
d525a50f52 | ||
|
|
5d97975e7f | ||
|
|
03e89e0577 | ||
| 9c48744cb1 | |||
| 24758414f2 | |||
| 2d55387ba9 | |||
| 1fc2032aa8 | |||
| adc89a5ed2 | |||
| 278676957e | |||
| 988c17cd30 | |||
| 08ee473671 | |||
|
|
6962a8b1c1 | ||
|
|
95e379e5a5 | ||
| 2a8e662b44 | |||
| 0b8a7245f6 | |||
| 17e148ce7a | |||
| 937b4508ae | |||
| 87d4214541 | |||
|
|
acc59ab87c | ||
| 78bcdef7fd | |||
| 72c0ceac29 | |||
|
|
e2808fd6b9 | ||
| 0cfdce042f | |||
|
|
cd54a3903c | ||
|
|
063eb1fe08 | ||
| f125c8dc85 | |||
| d663c46422 | |||
| a8ab52589e | |||
| 14333f47ea | |||
|
|
88d9e19cc5 | ||
|
|
994ffcb8b8 | ||
|
|
5ab4650c4e | ||
| ed75b148a8 | |||
| 210c463130 | |||
| 6922aa1d2a | |||
|
|
4e2097fc7b | ||
| 38b4ff5c92 | |||
| e294952a60 | |||
| 3380b2787e | |||
| 0758ba401b | |||
| 73ebc20471 | |||
| 3f36ed4ce8 | |||
| 76fdc047b9 | |||
| 309c470f8a | |||
|
|
f3fd150235 | ||
|
|
283cf784a3 | ||
| 53080648a1 | |||
| 26e0665eeb | |||
|
|
fe7778e6e0 | ||
|
|
4daf92d4cd | ||
| 51d4b1e3f2 | |||
|
|
0080d89f7e | ||
| 6da4770f47 | |||
| 918c766b90 | |||
|
|
95235b810e | ||
| 349beae4a2 | |||
|
|
0550d6a619 | ||
|
|
d195ebe3c9 | ||
|
|
687f19a1eb | ||
|
|
b810c08ae5 | ||
|
|
d99daa3048 | ||
|
|
740208b13f | ||
| 509d4026e2 | |||
| cb5023bcea | |||
|
|
49eed7c784 | ||
|
|
13e83e0c82 | ||
|
|
4395c14744 | ||
|
|
d052d268f5 | ||
| 74e28be0b0 | |||
| c5f1f46e97 | |||
| 09e0691feb | |||
| 64ad5cb676 | |||
| 8a98fc9f70 | |||
| 2ed805dbb1 | |||
| 7450904532 | |||
| f9b6447f6b | |||
| 8deefd2cb1 | |||
|
|
d8511ecb1b | ||
| 6642fd9e1c | |||
|
|
8a4be4e2ce | ||
|
|
9238044bc1 | ||
|
|
f204e46e07 | ||
|
|
f439b1ffc0 | ||
|
|
9c4d55a352 | ||
|
|
c210d57316 | ||
|
|
41b1d47bba | ||
|
|
3a02e327c7 | ||
|
|
4d976ade19 | ||
|
|
82951fe941 | ||
|
|
8af6933a89 | ||
|
|
0cb6ebeea7 | ||
|
|
afc94b6879 | ||
|
|
8e7413ee3f | ||
|
|
f68e699486 | ||
|
|
583a77f8dc | ||
|
|
3f0a0c863a | ||
|
|
345917e199 | ||
|
|
6f44e4dd36 | ||
|
|
7c7891cebe | ||
|
|
062089598f | ||
|
|
4142723985 | ||
|
|
054f4c3049 | ||
|
|
098aae5aef | ||
| 03f408cb76 | |||
| a894f0f8ee | |||
|
|
f87afba566 | ||
| 6fedfe1e40 | |||
|
|
7827e58aac | ||
| 5d280640e8 | |||
| e7413396b2 | |||
|
|
ce64c4519c | ||
|
|
e9d4f57815 | ||
| e573d9f68b | |||
| 2584c8f076 | |||
| 7b6c972a12 | |||
|
|
c3f1b105e9 | ||
| 616c2d21a6 | |||
| 63a9e26abf | |||
|
|
d2dfc714ec | ||
|
|
5c8bfbc98b | ||
|
|
885a147420 | ||
|
|
afbf3f9075 | ||
|
|
720cac8a8f | ||
| 5497c99f0c | |||
|
|
d8b4aed16c | ||
| efc97c855c | |||
|
|
0c5353cf8b | ||
|
|
8a84b40ee5 | ||
| f6b39a4815 | |||
|
|
1b3d4e3dc0 | ||
|
|
cb46461ede | ||
| 3b0a359412 | |||
| 6fa26e895d | |||
| 8ab8691c17 | |||
|
|
35b8a7d10a | ||
| 22de02f132 | |||
| 11244aa48f | |||
| 0a5f26e9c0 | |||
| 4a8e9b5a22 | |||
| bfb2491842 | |||
|
|
b747f80507 | ||
| ced931a280 | |||
| b497eb853c | |||
| 7a2342ea2e | |||
|
|
09fdfa294a | ||
|
|
4ef9aa07d2 | ||
| 08085403b3 | |||
| 2d7dcb4aeb | |||
| ad29502488 | |||
| 5b0acede89 | |||
| ac1cd3afc8 | |||
|
|
8a863b4ecb | ||
|
|
882d63249c | ||
|
|
6315ca5658 | ||
|
|
9f802b67f0 | ||
| 6694ae52ba | |||
| 9491ceaa5d | |||
| db9a70a99d | |||
|
|
9105e687d6 | ||
| b1d6c6008e | |||
| 6b9f9a107e | |||
| 11a7f49162 | |||
| b4e5061b73 | |||
| f5a1ad7f3f | |||
| eeac88b1d1 | |||
| 1ab9b020c1 | |||
| 3055518d2b | |||
| 9f619ccdd4 | |||
| df78ff29bd | |||
| 4d13acacc2 | |||
| 67573c1d9d | |||
| b27d8a6703 | |||
| 6f3d4272e6 | |||
| 6e5315fdd6 | |||
| 544d7ee95c | |||
| 7f7f7d69f7 | |||
|
|
ae9a96822e | ||
| bbef0322a3 | |||
| a8a205aa48 | |||
| c052ea7c39 | |||
| 6accaa35c9 | |||
| 466e7296fa | |||
|
|
5678535d88 | ||
| b7993885bb | |||
| 3b8ef380ae | |||
|
|
2334a27467 | ||
|
|
92511c2777 | ||
| 64b02466b1 | |||
| 2ffbe73305 | |||
| 48d3941701 | |||
| 0ad1889029 | |||
| 7dc98dcf84 | |||
| 681fb695bd | |||
| 518d8385e6 | |||
|
|
7073ef0be0 | ||
| 2288162ad7 | |||
| 6f701d7fa6 | |||
| 34253f88b2 | |||
|
|
488c311788 | ||
|
|
b5527cc07f | ||
| 6d23d36a9c | |||
|
|
e2e5999276 | ||
|
|
112ec2e4a3 | ||
| 4b92be10b4 | |||
| 0b361df0a4 | |||
| 3a242074ff | |||
|
|
353f267488 | ||
|
|
2d705d2f81 | ||
| 184871e84f | |||
| ffcdaed087 | |||
| 91a0b48662 | |||
| c509a804ec | |||
| 1a7b6c0cd4 | |||
|
|
11cf88fd49 | ||
| 3f0fa3bbb3 | |||
| d7c15848f0 | |||
|
|
188b907907 | ||
| 71e3601d51 | |||
| f04c3d112c | |||
| 8739959be0 | |||
| 24bc049fa0 | |||
| b42cffdd8a | |||
| 927691a27b | |||
| 6c36ae5340 | |||
| 5473a21418 | |||
| b14c19a887 | |||
| 979dc0a34c | |||
| c2fa13de82 | |||
|
|
77b054a86c | ||
|
|
20eb020071 | ||
|
|
d3deb244c0 | ||
| d20a95c3c4 | |||
| 1f84a641ea | |||
| c542b057b5 | |||
|
|
03d980e0cf | ||
| b03f563df4 | |||
| 8fa0a239b5 | |||
| ee51ab2960 | |||
|
|
22d73e5b44 | ||
| b31cacd930 | |||
| 1440cd45a0 | |||
|
|
07829b93c7 | ||
| c2b1d7d9d9 | |||
| 9f6e94da4b | |||
| e0b9081649 | |||
| e1dc5c895f | |||
|
|
4e58601b2c | ||
| 4060be4de7 | |||
|
|
059078c264 | ||
| cc51d0b345 | |||
| bedad38ca3 | |||
| c15c091718 | |||
| e90e541af3 | |||
|
|
88088c01ac | ||
| 251cf263ff | |||
| f1a4fc87c8 | |||
| d28ac34ae0 | |||
| 4d2a321999 | |||
| b5cf685b13 | |||
|
|
316c1478fc | ||
|
|
24e8a3cfdf | ||
| 888ac1fee3 | |||
| 427d567337 | |||
| 5d73de3072 | |||
| 1fdaafb1e8 | |||
| dbdaba5a6a | |||
| 4210f32a05 | |||
|
|
dc2e4098ae | ||
|
|
83747d8626 | ||
| 68c0c098c8 | |||
| dc1366890f | |||
| f1087b04f0 | |||
|
|
0b4b4f7283 | ||
| d41d3066b3 | |||
| 3bfe25c2f2 | |||
|
|
266b06114a | ||
|
|
6ed41eb513 | ||
|
|
0a8428a4ca | ||
|
|
d058b30872 | ||
| 0c06b05764 | |||
|
|
557f626c05 | ||
| 32bfef6686 | |||
|
|
5795d9eb74 | ||
| 7c29c6359f | |||
|
|
40c5d26dfd | ||
|
|
81c97de170 | ||
| 0cdf332ee7 | |||
| b4e13e1305 | |||
| dd1cd17801 | |||
| 6d87b7c445 | |||
| 0d1710f201 | |||
| a3650aa386 | |||
| 0f9ef726bf | |||
| 8f2405ee34 |
5
.config/zentao/.env
Normal file
5
.config/zentao/.env
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ZENTAO_URL=https://zentao.gentronhealth.com/
|
||||||
|
ZENTAO_ACCOUNT=guanyu
|
||||||
|
ZENTAO_PASSWORD=Gentron@2025
|
||||||
|
ZENTAO_TOKEN=49c270495806afdcf095c46959483326
|
||||||
|
ZENTAO_REAL_ACCOUNT=guanyu
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,3 +63,6 @@ public.sql
|
|||||||
发版记录/2025-11-12/发版日志.docx
|
发版记录/2025-11-12/发版日志.docx
|
||||||
.gitignore
|
.gitignore
|
||||||
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
|
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
|
||||||
|
.env.test.local
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|||||||
51
.husky/pre-commit
Executable file
51
.husky/pre-commit
Executable 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 "========================================"
|
||||||
4
.openclaw/workspace-state.json
Normal file
4
.openclaw/workspace-state.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"setupCompletedAt": "2026-04-06T04:43:29.304Z"
|
||||||
|
}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"approvalMode": "yolo"
|
"approvalMode": "yolo"
|
||||||
},
|
},
|
||||||
"$version": 2
|
"$version": 3
|
||||||
}
|
}
|
||||||
91
BUGFIX_ANALYSIS.md
Normal file
91
BUGFIX_ANALYSIS.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Bug 根因分析与修复方案
|
||||||
|
|
||||||
|
## Bug 335 - 门诊医生站开立药品医嘱保存报错
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
根据代码分析,`DoctorStationAdviceAppServiceImpl.saveAdvice()` 方法处理药品医嘱保存时可能报错的原因:
|
||||||
|
|
||||||
|
1. **patientId/encounterId 为 null** - 删除操作时前端可能未传
|
||||||
|
2. **accountId 为 null** - 患者账户信息未正确获取
|
||||||
|
3. **definitionId/definitionDetailId 为 null** - 定价信息缺失
|
||||||
|
4. **库存校验失败** - 药品库存不足
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
✅ 已部分修复(见代码中的 BugFix 注释)
|
||||||
|
- 已添加 patientId/encounterId 自动补全逻辑
|
||||||
|
- 已添加 accountId 自动创建逻辑
|
||||||
|
- 需要进一步验证 definitionId 的处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug 336 - 门诊医生站开立诊疗项目保存报错
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
诊疗项目保存与药品类似,但有以下特殊点:
|
||||||
|
|
||||||
|
1. **必须选择执行科室** - 代码中有校验 `throw new ServiceException("诊疗项目必须选择执行科室")`
|
||||||
|
2. **活动绑定设备处理** - 需要处理 `handService()` 中的设备绑定逻辑
|
||||||
|
3. **库存校验** - 诊疗项目可能关联耗材
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
- 确保前端传递 executeDeptId(执行科室)
|
||||||
|
- 检查 handService() 方法中的异常处理
|
||||||
|
- 添加更详细的错误日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug 338 - 门诊划价新增时未校验就诊记录及诊断记录
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
**这是患者安全问题!** 未接诊患者也可新增划价项目可能导致:
|
||||||
|
- 收费错误
|
||||||
|
- 医疗纠纷
|
||||||
|
- 数据不一致
|
||||||
|
|
||||||
|
当前代码问题:
|
||||||
|
- `OutpatientPricingAppServiceImpl.getAdviceBaseInfo()` 仅查询医嘱,未校验就诊状态
|
||||||
|
- 前端划价保存接口未找到(可能在其他地方)
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
1. 在划价查询时增加就诊状态校验
|
||||||
|
2. 在划价保存时增加诊断记录校验
|
||||||
|
3. 未接诊患者禁止划价
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug 339 - 药房筛选条件失效
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
查询结果中包含非选中药房的数据,可能原因:
|
||||||
|
- SQL WHERE 条件未正确应用 locationId
|
||||||
|
- 多表关联时过滤条件丢失
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
- 检查 `DoctorStationAdviceAppMapper.getAdviceBaseInfo()` 的 SQL
|
||||||
|
- 确保 locationId 条件正确应用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复优先级
|
||||||
|
|
||||||
|
1. **Bug 338** - 患者安全问题,最高优先级
|
||||||
|
2. **Bug 335/336** - 核心功能阻断,高优先级
|
||||||
|
3. **Bug 339** - 数据准确性问题,中优先级
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### Bug 338 测试
|
||||||
|
1. 选择未接诊患者,尝试划价 → 应禁止
|
||||||
|
2. 选择已接诊但无诊断的患者,尝试划价 → 应提示补充诊断
|
||||||
|
3. 选择正常接诊患者,划价 → 应成功
|
||||||
|
|
||||||
|
### Bug 335/336 测试
|
||||||
|
1. 门诊医生站开立药品医嘱 → 应成功保存
|
||||||
|
2. 门诊医生站开立诊疗项目 → 应成功保存
|
||||||
|
3. 签发医嘱 → 应成功
|
||||||
|
|
||||||
|
### Bug 339 测试
|
||||||
|
1. 选择"西药房"筛选 → 结果应仅包含西药房数据
|
||||||
|
2. 选择"中药房"筛选 → 结果应仅包含中药房数据
|
||||||
84
BUGFIX_PLAN.md
Normal file
84
BUGFIX_PLAN.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# HIS 系统 Bug 修复计划
|
||||||
|
|
||||||
|
## 修复负责人
|
||||||
|
华佗 (AI 团队)
|
||||||
|
|
||||||
|
## 修复时间
|
||||||
|
2026-04-05 开始
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug 清单与修复优先级
|
||||||
|
|
||||||
|
### 🔴 高优先级(核心业务阻断)
|
||||||
|
|
||||||
|
#### Bug 335 - 门诊医生站开立药品医嘱保存报错
|
||||||
|
- **模块**: 医生工作站
|
||||||
|
- **文件**: `DoctorStationAdviceAppServiceImpl.java`
|
||||||
|
- **根因分析**: 待分析
|
||||||
|
- **修复状态**: 🔄 分析中
|
||||||
|
|
||||||
|
#### Bug 336 - 门诊医生站开立诊疗项目保存报错
|
||||||
|
- **模块**: 医生工作站
|
||||||
|
- **文件**: `DoctorStationAdviceAppServiceImpl.java`
|
||||||
|
- **根因分析**: 待分析
|
||||||
|
- **修复状态**: ⏳ 等待 335 修复后验证
|
||||||
|
|
||||||
|
#### Bug 338 - 门诊划价新增时未校验就诊记录及诊断记录
|
||||||
|
- **模块**: 门诊收费
|
||||||
|
- **问题**: 未接诊患者也可新增划价项目(患者安全问题)
|
||||||
|
- **修复方案**: 在划价保存前增加就诊状态和诊断记录校验
|
||||||
|
- **修复状态**: ⏳ 待修复
|
||||||
|
|
||||||
|
### 🟡 中优先级(数据准确性/用户体验)
|
||||||
|
|
||||||
|
#### Bug 339 - 药房筛选条件失效
|
||||||
|
- **模块**: 药房药库报表管理
|
||||||
|
- **问题**: 查询结果中包含非选中药房的数据
|
||||||
|
- **修复状态**: ⏳ 待分析
|
||||||
|
|
||||||
|
#### Bug 333 - 耗材医嘱类型错误
|
||||||
|
- **模块**: 医生工作站
|
||||||
|
- **问题**: 类型误转为"中成药"且保存报错
|
||||||
|
- **修复状态**: ⏳ 待分析
|
||||||
|
|
||||||
|
#### Bug 337 - 挂号时间显示异常
|
||||||
|
- **模块**: 建档挂号管理
|
||||||
|
- **问题**: 未显示当前实际挂号时间
|
||||||
|
- **修复状态**: ⏳ 待分析
|
||||||
|
|
||||||
|
#### Bug 334 - 检验申请界面布局优化
|
||||||
|
- **模块**: 门诊医生工作站
|
||||||
|
- **问题**: 按钮布局需要调整
|
||||||
|
- **修复状态**: ⏳ 待修复(前端)
|
||||||
|
|
||||||
|
### 🟢 低优先级(历史遗留问题)
|
||||||
|
|
||||||
|
#### Bug 249/253/280/300 - 3 月份遗留 bug
|
||||||
|
- **修复状态**: ⏳ 后续处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复流程
|
||||||
|
|
||||||
|
1. **分析根因** - 查看代码和日志,定位问题
|
||||||
|
2. **编写修复** - 修改代码并添加必要校验
|
||||||
|
3. **本地测试** - 确保修复有效且不引入新问题
|
||||||
|
4. **提交代码** - commit 并推送到 gitea
|
||||||
|
5. **验证关闭** - 在禅道更新 Bug 状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试要求
|
||||||
|
|
||||||
|
- 修复后必须测试
|
||||||
|
- 测试不通过继续修
|
||||||
|
- 确保不影响其他功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 所有修复基于 develop 分支
|
||||||
|
- 修复完成后统一提交
|
||||||
|
- 重要修复添加详细注释
|
||||||
163
BUG_355_ANALYSIS.md
Normal file
163
BUG_355_ANALYSIS.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Bug #355 - 性别字段回显不一致分析与修复
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
门诊挂号页面的预约签到弹窗中,患者"随自核"的性别显示为"未知",但挂号界面载入后显示为"男性",数据不一致。
|
||||||
|
|
||||||
|
## 根本原因
|
||||||
|
|
||||||
|
### 数据流程分析
|
||||||
|
|
||||||
|
1. **预约签到弹窗数据来源** (`TicketAppServiceImpl.listTicket()`)
|
||||||
|
- SQL 查询 (ScheduleSlotMapper.xml 第97行):
|
||||||
|
```sql
|
||||||
|
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender
|
||||||
|
```
|
||||||
|
- 后端逻辑 (TicketAppServiceImpl.java 第140-145行):
|
||||||
|
```java
|
||||||
|
if (raw.getPatientGender() != null) {
|
||||||
|
String pg = raw.getPatientGender().trim();
|
||||||
|
dto.setGender("1".equals(pg) ? "男" : ("2".equals(pg) ? "女" : "未知"));
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **挂号界面数据来源** (OutpatientRegistrationAppServiceImpl)
|
||||||
|
- 直接从 `adm_patient` 表查询患者最新信息
|
||||||
|
- 性别字段: `pinfo.gender_enum`
|
||||||
|
- 翻译为文本: `EnumUtils.getInfoByValue(AdministrativeGender.class, genderEnum)`
|
||||||
|
|
||||||
|
### 问题定位
|
||||||
|
|
||||||
|
**关键 SQL 逻辑问题:**
|
||||||
|
- `order_main.gender` 字段存储的是订单创建时的性别值(varchar 类型)
|
||||||
|
- `adm_patient.gender_enum` 字段存储的是患者最新性别(integer 类型)
|
||||||
|
- 当 `order_main.gender` 为 `NULL` 时,SQL 会回退到 `pinfo.gender_enum`
|
||||||
|
|
||||||
|
**可能的场景:**
|
||||||
|
1. 订单创建时未保存性别字段 (`order_main.gender` = NULL)
|
||||||
|
2. 患者档案中的性别被修改过(但订单表未同步更新)
|
||||||
|
3. `pinfo.gender_enum` 值为 NULL 或者不合法
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 方案1:修正 SQL 查询逻辑 (推荐)
|
||||||
|
|
||||||
|
**问题:** 当 `order_main.gender` 为 NULL 时,SQL 正确回退到 `pinfo.gender_enum`,但 Java 代码中对 `patientGender` 的处理逻辑有问题。
|
||||||
|
|
||||||
|
**修复步骤:**
|
||||||
|
|
||||||
|
1. 修改 SQL,直接从患者表获取性别,不依赖订单表的 gender 字段:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ScheduleSlotMapper.xml
|
||||||
|
LEFT JOIN adm_patient pinfo ON o.patient_id = pinfo.id
|
||||||
|
-- 性别字段直接从患者表获取,避免订单表 gender 字段为空的情况
|
||||||
|
pinfo.gender_enum AS genderEnum,
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 修改 Java 代码,直接使用 `genderEnum` 字段:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// TicketAppServiceImpl.java
|
||||||
|
// 性别处理:直接使用患者表中的 gender_enum
|
||||||
|
Integer genderEnum = raw.getGenderEnum();
|
||||||
|
if (genderEnum != null) {
|
||||||
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
|
dto.setGender("男");
|
||||||
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
|
dto.setGender("女");
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案2:确保订单表 gender 字段不为空
|
||||||
|
|
||||||
|
在订单创建时,确保将患者的性别同步到订单表的 `gender` 字段。
|
||||||
|
|
||||||
|
## 临时验证方案
|
||||||
|
|
||||||
|
在数据库中执行以下 SQL 检查患者"随自核"的数据:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 检查患者档案中的性别
|
||||||
|
SELECT id, name, gender_enum,
|
||||||
|
CASE gender_enum
|
||||||
|
WHEN 1 THEN '男'
|
||||||
|
WHEN 2 THEN '女'
|
||||||
|
ELSE '未知'
|
||||||
|
END as gender_text
|
||||||
|
FROM adm_patient
|
||||||
|
WHERE name = '随自核';
|
||||||
|
|
||||||
|
-- 检查订单表中的性别
|
||||||
|
SELECT o.id, o.patient_id, o.patient_name, o.gender, p.gender_enum
|
||||||
|
FROM order_main o
|
||||||
|
LEFT JOIN adm_patient p ON o.patient_id = p.id
|
||||||
|
WHERE o.patient_name = '随自核';
|
||||||
|
|
||||||
|
-- 检查号源数据
|
||||||
|
SELECT s.id, s.pool_id, s.status as slot_status
|
||||||
|
FROM adm_schedule_slot s
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM order_main o WHERE o.slot_id = s.id
|
||||||
|
AND o.patient_name = '随自核'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 修复代码
|
||||||
|
|
||||||
|
### 修改 ScheduleSlotMapper.xml
|
||||||
|
|
||||||
|
在 `selectTicketSlotsPage` SQL 中,将患者性别字段改为直接从患者表获取:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- 原来的 SQL (第97行) -->
|
||||||
|
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
|
||||||
|
|
||||||
|
<!-- 修改后的 SQL -->
|
||||||
|
pinfo.gender_enum AS genderEnum,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改 TicketAppServiceImpl.java
|
||||||
|
|
||||||
|
在 `listTicket` 方法中修改性别处理逻辑:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 原来的代码 (第140-145行)
|
||||||
|
// 性别处理:直接读取优先级最高的订单性别字段 (SQL 已处理优先级)
|
||||||
|
if (raw.getPatientGender() != null) {
|
||||||
|
String pg = raw.getPatientGender().trim();
|
||||||
|
dto.setGender("1".equals(pg) ? "男" : ("2".equals(pg) ? "女" : "未知"));
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改后的代码
|
||||||
|
// 性别处理:直接使用患者表中的 gender_enum
|
||||||
|
Integer genderEnum = raw.getGenderEnum();
|
||||||
|
if (genderEnum != null) {
|
||||||
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
|
dto.setGender("男");
|
||||||
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
|
dto.setGender("女");
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证步骤
|
||||||
|
|
||||||
|
1. 修复代码后,重新编译部署
|
||||||
|
2. 打开预约签到弹窗,查找患者"随自核"
|
||||||
|
3. 确认性别字段显示为"男性"
|
||||||
|
4. 进行挂号操作
|
||||||
|
5. 确认挂号界面显示的性别也是"男性"
|
||||||
|
6. 两者应该保持一致
|
||||||
117
BUG_355_FIX.md
Normal file
117
BUG_355_FIX.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Bug #355 修复代码
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
| 序号 | 文件路径 | 修改类型 | 说明 |
|
||||||
|
|------|---------|---------|------|
|
||||||
|
| 1 | `his-source/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml` | SQL 查询修改 | 性别字段直接从患者表获取 |
|
||||||
|
| 2 | `his-source/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java` | Java 代码修改 | 性别处理逻辑修改 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复步骤
|
||||||
|
|
||||||
|
### 修改 1: ScheduleSlotMapper.xml
|
||||||
|
|
||||||
|
**文件:** `his-source/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml`
|
||||||
|
|
||||||
|
**修改位置:** 第97行
|
||||||
|
|
||||||
|
**修改前:**
|
||||||
|
```xml
|
||||||
|
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后:**
|
||||||
|
```xml
|
||||||
|
pinfo.gender_enum AS genderEnum,
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:** 直接从患者表获取 `gender_enum` 字段,避免订单表 `gender` 字段为 NULL 导致的数据不一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修改 2: TicketAppServiceImpl.java
|
||||||
|
|
||||||
|
**文件:** `his-source/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
|
||||||
|
|
||||||
|
**修改位置:** 第140-145行
|
||||||
|
|
||||||
|
**修改前:**
|
||||||
|
```java
|
||||||
|
// 性别处理:直接读取优先级最高的订单性别字段 (SQL 已处理优先级)
|
||||||
|
if (raw.getPatientGender() != null) {
|
||||||
|
String pg = raw.getPatientGender().trim();
|
||||||
|
dto.setGender("1".equals(pg) ? "男" : ("2".equals(pg) ? "女" : "未知"));
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后:**
|
||||||
|
```java
|
||||||
|
// 性别处理:直接使用患者表中的 gender_enum
|
||||||
|
Integer genderEnum = raw.getGenderEnum();
|
||||||
|
if (genderEnum != null) {
|
||||||
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
|
dto.setGender("男");
|
||||||
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
|
dto.setGender("女");
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:** 由于 SQL 查询已直接获取 `gender_enum` 字段,这里修改为直接使用该字段进行性别转换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 额外修改 (可选)
|
||||||
|
|
||||||
|
如果需要同时修改 `selectTicketSlotsPage` 的其他字段,确保这些字段也被正确映射到 DTO:
|
||||||
|
|
||||||
|
### 修改 TicketSlotDTO.java
|
||||||
|
|
||||||
|
**文件:** `his-source/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/TicketSlotDTO.java`
|
||||||
|
|
||||||
|
**修改:** 添加 `genderEnum` 字段
|
||||||
|
|
||||||
|
```java
|
||||||
|
private Integer genderEnum;
|
||||||
|
|
||||||
|
public Integer getGenderEnum() {
|
||||||
|
return genderEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGenderEnum(Integer genderEnum) {
|
||||||
|
this.genderEnum = genderEnum;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 编译部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd his-source/openhis-server-new
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试
|
||||||
|
|
||||||
|
| 测试项 | 预期结果 | 状态 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 预约签到弹窗性别显示 | 显示患者真实性别(男/女/未知) | 待测试 |
|
||||||
|
| 挂号界面性别显示 | 显示患者真实性别(男/女/未知) | 待测试 |
|
||||||
|
| 两者性别数据一致性 | 完全一致 | 待测试 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**修复人:** 关羽
|
||||||
|
**修复日期:** 2026-04-08
|
||||||
|
**BUG ID:** #355
|
||||||
65
BUG_355_FIX_NOTES.md
Normal file
65
BUG_355_FIX_NOTES.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# BUG #355 - 修复备注
|
||||||
|
|
||||||
|
## 修复日期
|
||||||
|
2026-04-08
|
||||||
|
|
||||||
|
## 修复人
|
||||||
|
关羽 (guanyu)
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
门诊挂号页面的预约签到弹窗中,患者"随自核"的性别显示为"未知",但挂号界面载入后显示为"男性",数据不一致。
|
||||||
|
|
||||||
|
### 根本原因
|
||||||
|
- 预约签到弹窗数据来自 `TicketAppServiceImpl.listTicket()` 方法
|
||||||
|
- SQL 查询中使用了订单表的 `gender` 字段(可能为 NULL)
|
||||||
|
- 当订单表 `gender` 为 NULL 时,虽然 SQL 回退到患者表 `gender_enum`,但 Java 代码处理逻辑仍有问题
|
||||||
|
- 导致性别显示不一致
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
修改 `TicketAppServiceImpl.java` 中的性别处理逻辑:
|
||||||
|
- 将 `raw.getPatientGender()` 改为 `raw.getGenderEnum()`
|
||||||
|
- 直接使用患者表中的 `gender_enum` 字段进行性别转换
|
||||||
|
- 确保与挂号界面查询的数据来源一致
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `his-source/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java`
|
||||||
|
|
||||||
|
### 代码变更
|
||||||
|
```java
|
||||||
|
// 修改前
|
||||||
|
if (raw.getPatientGender() != null) {
|
||||||
|
String pg = raw.getPatientGender().trim();
|
||||||
|
dto.setGender("1".equals(pg) ? "男" : ("2".equals(pg) ? "女" : "未知"));
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改后
|
||||||
|
Integer genderEnum = raw.getGenderEnum();
|
||||||
|
if (genderEnum != null) {
|
||||||
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
|
dto.setGender("男");
|
||||||
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
|
dto.setGender("女");
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git 提交
|
||||||
|
- Commit: `7827e58a`
|
||||||
|
- 分支: `develop`
|
||||||
|
|
||||||
|
### 测试建议
|
||||||
|
1. 更新 Git 代码
|
||||||
|
2. 编译部署后进行测试
|
||||||
|
3. 验证预约签到弹窗和挂号界面的性别字段是否一致
|
||||||
|
|
||||||
|
### 状态
|
||||||
|
✅ 代码修复完成,已提交到远程仓库
|
||||||
|
⏳ 等待测试验证
|
||||||
32
BUG_362_ANALYSIS.md
Normal file
32
BUG_362_ANALYSIS.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Bug 362 - 入科时间显示错误分析
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
双击查看详情时显示当前系统时间,而不是正确的入科时间。
|
||||||
|
|
||||||
|
## 当前分析状态
|
||||||
|
|
||||||
|
### 已确认
|
||||||
|
1. **前端显示逻辑正确**: 患者详情对话框直接显示后端返回的 `admissionDate` 字段
|
||||||
|
2. **后端数据来源正确**: 从 `adm_encounter.start_time` 获取入院时间
|
||||||
|
3. **字段绑定正确**: 前端表格和详情都使用 `admissionDate` 字段
|
||||||
|
|
||||||
|
### 可能原因
|
||||||
|
1. **数据库数据问题**: `adm_encounter.start_time` 字段本身存储的是当前系统时间
|
||||||
|
2. **概念混淆**: 用户期望看到"入科时间",但系统显示的是"入院时间"
|
||||||
|
3. **前端缓存问题**: 某些情况下前端缓存了错误的时间值
|
||||||
|
|
||||||
|
### 调试措施
|
||||||
|
1. **已添加调试日志**: 在患者详情对话框中添加 `console.log` 输出 `admissionDate` 值
|
||||||
|
2. **需要验证**: 实际测试时查看浏览器控制台输出,确认具体值
|
||||||
|
|
||||||
|
### 下一步计划
|
||||||
|
1. **等待测试结果**: 通过调试日志确认实际显示的值
|
||||||
|
2. **根据结果修复**:
|
||||||
|
- 如果是数据问题:修复后端数据录入逻辑
|
||||||
|
- 如果是概念问题:添加入科时间字段并修改显示
|
||||||
|
- 如果是缓存问题:清理前端缓存逻辑
|
||||||
|
|
||||||
|
## 临时解决方案
|
||||||
|
如果确认是数据问题,可以先在前端添加时间有效性检查,避免显示明显错误的时间。
|
||||||
|
|
||||||
|
正在自主分析中!
|
||||||
35
BUG_362_FIX_COMPLETE.md
Normal file
35
BUG_362_FIX_COMPLETE.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Bug 362 - 入科时间显示错误修复完成
|
||||||
|
|
||||||
|
## 问题根因
|
||||||
|
用户期望看到 **入科时间**,但系统显示的是 **入院时间**。
|
||||||
|
|
||||||
|
- **入院时间**: `adm_encounter.start_time` (办理住院手续的时间)
|
||||||
|
- **入科时间**: `adm_encounter_location.start_time` (进入具体科室的时间)
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 后端修改
|
||||||
|
1. **DTO类添加字段**:
|
||||||
|
- `NursingPageDto.wardAdmissionDate`
|
||||||
|
- `PatientHomeDto.wardAdmissionDate`
|
||||||
|
2. **SQL查询添加字段**:
|
||||||
|
- `NursingRecordAppMapper.xml`: 添加入科时间查询
|
||||||
|
- `PatientHomeAppMapper.xml`: 添加入科时间子查询
|
||||||
|
|
||||||
|
### 前端修改
|
||||||
|
1. **患者列表**: 将"入院日期"改为"入科日期",绑定到 `wardAdmissionDate`
|
||||||
|
2. **患者详情对话框**: 将"入院日期"改为"入科日期",绑定到 `wardAdmissionDate`
|
||||||
|
3. **患者卡片**: 将"入院"改为"入科",显示 `wardAdmissionDate`
|
||||||
|
4. **体温单界面**: 使用 `wardAdmissionDate` 作为入科时间
|
||||||
|
|
||||||
|
## 验证步骤
|
||||||
|
1. 双击患者查看详情,确认显示的是入科时间而非入院时间
|
||||||
|
2. 患者列表中"入科日期"列显示正确时间
|
||||||
|
3. 患者卡片显示正确的入科时间
|
||||||
|
4. 体温单界面使用正确的入科时间
|
||||||
|
|
||||||
|
## 修复状态
|
||||||
|
✅ 已修复并提交到远程仓库
|
||||||
|
|
||||||
|
---
|
||||||
|
赵云:Bug 362已修复!
|
||||||
29
BUG_364_362_ANALYSIS.md
Normal file
29
BUG_364_362_ANALYSIS.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Bug 364/362 - 住院护士站任务分析
|
||||||
|
|
||||||
|
## Bug分配确认
|
||||||
|
|
||||||
|
### Bug #364 - 住院护士站三测单病历号检索失败
|
||||||
|
**状态**: ⏳ 待分析
|
||||||
|
**分析人**: 赵云
|
||||||
|
**预计完成**: 今日内
|
||||||
|
|
||||||
|
### Bug #362 - 住院护士站入科时间显示错误
|
||||||
|
**状态**: ⏳ 待分析
|
||||||
|
**分析人**: 赵云
|
||||||
|
**预计完成**: 今日内
|
||||||
|
|
||||||
|
### Bug #363 - 住院管理入院时间校验
|
||||||
|
**状态**: ✅ 已分配给关羽
|
||||||
|
**理由**: 此为后端业务逻辑问题,应由后端开发处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前进度(2026-04-08 23:17)
|
||||||
|
|
||||||
|
赵云正在分析这两个前端Bug,已定位相关代码位置:
|
||||||
|
- 住院护士站主界面: `inpatientNurse/home/index.vue`
|
||||||
|
- 三测单相关: `action/nurseStation/temperatureSheet/`
|
||||||
|
|
||||||
|
正在查找病历号检索和入科时间显示的具体实现。
|
||||||
|
|
||||||
|
子龙领命!
|
||||||
51
BUG_364_362_FIX.md
Normal file
51
BUG_364_362_FIX.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Bug 364/362 - 问题分析与修复方案
|
||||||
|
|
||||||
|
## Bug #364 - 住院护士站三测单病历号检索失败 ✅ 已修复
|
||||||
|
|
||||||
|
### 问题根因
|
||||||
|
前端表格列定义错误,将"病历号"列绑定到了 `encounterId` (就诊ID) 而不是 `patientBusNo` (病历号)。
|
||||||
|
|
||||||
|
**前端问题** (`tprChart/index.vue`):
|
||||||
|
```vue
|
||||||
|
<el-table-column label="病历号" align="center" prop="encounterId" />
|
||||||
|
```
|
||||||
|
应该改为:
|
||||||
|
```vue
|
||||||
|
<el-table-column label="病历号" align="center" prop="patientBusNo" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
修改前端表格列定义,将病历号列绑定到正确的字段。
|
||||||
|
|
||||||
|
**修复状态**: ✅ 已修复并提交
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #362 - 住院护士站入科时间显示错误 ⏳ 分析中
|
||||||
|
|
||||||
|
### 问题根因
|
||||||
|
在 `PatientHomeAppMapper.xml` 中,入院时间从 `adm_encounter.start_time` 获取:
|
||||||
|
```xml
|
||||||
|
T2.start_time AS admissionDate, -- 入院日期
|
||||||
|
```
|
||||||
|
|
||||||
|
这个字段是正确的入院时间。Bug描述"双击查看详情时显示当前系统时间"可能是因为:
|
||||||
|
1. 某些情况下前端缓存了错误的日期
|
||||||
|
2. 或者用户看到的是"住院天数"的计算基时间
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
确认前端显示的确实是 `admissionDate` 字段,而不是其他时间字段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复计划
|
||||||
|
|
||||||
|
### Bug 364
|
||||||
|
1. ✅ 修改 `tprChart/index.vue` 中的病历号列绑定
|
||||||
|
2. ⏳ 测试验证检索功能
|
||||||
|
|
||||||
|
### Bug 362
|
||||||
|
1. ⏳ 检查前端显示逻辑
|
||||||
|
2. ⏳ 确认数据来源正确
|
||||||
|
|
||||||
|
赵云:Bug 364已修复。Bug 362正在分析中。
|
||||||
61
BUG_FIX_PROGRESS.md
Normal file
61
BUG_FIX_PROGRESS.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# HIS项目 Bug修复与需求开发进度表
|
||||||
|
|
||||||
|
## 项目信息
|
||||||
|
- **项目名称**: 开源HIS改造落地
|
||||||
|
- **当前分支**: develop
|
||||||
|
- **代码路径**:
|
||||||
|
- 前端: openhis-ui-vue3
|
||||||
|
- 后端: openhis-server-new
|
||||||
|
- ** Git仓库**: https://gitea.gentronhealth.com/wangyizhe/his
|
||||||
|
- **禅道地址**: https://zentao.gentronhealth.com
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
- ✅ 代码已克隆完成
|
||||||
|
- ✅ Bug 已重新分配(管理员操作)
|
||||||
|
- ⏳ 等待修复人员开始工作
|
||||||
|
- 📋 张飞负责测试验证
|
||||||
|
|
||||||
|
## Bug修复任务列表(重新分配后)
|
||||||
|
|
||||||
|
| Bug ID | 严重程度 | 状态 | 模块 | 标题 | 原指派给 | **新指派给** | 进度 |
|
||||||
|
|--------|----------|------|------|------|----------|--------------|------|
|
||||||
|
| 339 | 3 | 激活 | 药房药库报表管理 | 药房筛选条件失效 | 王怡哲 | **关羽** | 待处理 |
|
||||||
|
| 338 | 3 | 激活 | 门诊收费管理 | 未校验就诊记录 | 王怡哲 | **关羽** | 待处理 |
|
||||||
|
| 337 | 3 | 激活 | 建档挂号管理 | 挂号时间显示异常 | 王怡哲 | **关羽** | 待处理 |
|
||||||
|
| 336 | 3 | 激活 | 门诊医生工作站 | 开立诊疗项目保存报错 | 王怡哲 | **关羽** | 待处理 |
|
||||||
|
| 335 | 3 | 激活 | 门诊医生工作站 | 开立药品医嘱保存报错 | 王怡哲 | **关羽** | 待处理 |
|
||||||
|
| 334 | 3 | 激活 | 门诊医生工作站 | 检验申请界面布局优化 | 王建 | **子龙** | 待处理 |
|
||||||
|
| 333 | 3 | 激活 | 门诊医生工作站 | 耗材医嘱类型误转 | 陈显精 | **关羽** | 待处理 |
|
||||||
|
|
||||||
|
## P0 级别 Bug(紧急,优先修复)
|
||||||
|
|
||||||
|
| Bug ID | 标题 | 严重程度 | 负责人 |
|
||||||
|
|--------|------|----------|--------|
|
||||||
|
| 335 | 开立药品医嘱保存报错 | 严重 | 关羽 |
|
||||||
|
| 336 | 开立诊疗项目保存报错 | 严重 | 关羽 |
|
||||||
|
| 338 | 未校验就诊记录 | 严重 | 关羽 |
|
||||||
|
|
||||||
|
## 需求开发任务列表(10个,全部未关闭)
|
||||||
|
|
||||||
|
待进一步确认分配情况...
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
1. **认领任务** - 在禅道将 Bug 分配给自己
|
||||||
|
2. **修改代码** - 从 develop 分支创建新分支:`bug/bug-id`
|
||||||
|
3. **本地测试** - 确保本地 JDK 17 环境编译通过
|
||||||
|
4. **提交PR** - 提交 Pull Request 到 develop 分支
|
||||||
|
5. **测试验证** - 张飞进行测试
|
||||||
|
6. **合并分支** - 测试通过后合并到 develop
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 所有代码修改必须先创建新分支
|
||||||
|
- 分支命名:`bug/bug-id` 或 `feature/feedback-id`
|
||||||
|
- 提交信息必须包含禅道Bug/需求ID
|
||||||
|
- 修改前请先阅读 `AGENTS.md` 了解项目规范
|
||||||
|
- **JDK 17 配置** - 确保本地开发环境使用 JDK 17
|
||||||
|
|
||||||
|
## 今日会议纪要
|
||||||
|
- 2026-04-05 15:09: 管理员重新分配 Bug 给群内武将
|
||||||
|
- 2026-04-05 14:58: 确认将王怡哲的 Bug 分配给关羽、张飞、陈琳
|
||||||
|
- 2026-04-05 13:47: 统一调度分配人员任务
|
||||||
|
- 2026-04-05 12:45: 初始任务分配完成
|
||||||
239
BUG_FIX_SUMMARY.md
Normal file
239
BUG_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Bug 修复总结报告
|
||||||
|
|
||||||
|
## 修复概述
|
||||||
|
|
||||||
|
本次修复涉及 Bug #333/#334/#335/#336/#337,其中 #338/#339 由华佗修复,已确认。
|
||||||
|
|
||||||
|
**修复人:** 关羽
|
||||||
|
**修复日期:** 2026-04-06
|
||||||
|
**项目版本:** OpenHIS v2.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #337 - 挂号时间显示异常 ✅ 已修复
|
||||||
|
|
||||||
|
### 一、Bug 原因
|
||||||
|
|
||||||
|
**问题描述:** 门诊挂号页面中,"挂号日期/时间"列显示异常或为空。
|
||||||
|
|
||||||
|
**根本原因:**
|
||||||
|
- SQL 查询使用 `T1.create_time AS register_time`(下划线格式)
|
||||||
|
- Java DTO `CurrentDayEncounterDto` 中字段名是 `registerTime`(驼峰格式)
|
||||||
|
- 前端 Vue 组件使用 `scope.row.registerTime` 获取数据
|
||||||
|
- MyBatis 返回的 `register_time` 无法映射到前端的 `registerTime`,导致数据无法显示
|
||||||
|
|
||||||
|
**代码位置:**
|
||||||
|
- 文件:`openhis-server-new/openhis-application/src/main/resources/mapper/chargemanage/OutpatientRegistrationAppMapper.xml`
|
||||||
|
- 方法:`getCurrentDayEncounter`
|
||||||
|
- 行号:约第 72 行和第 88 行
|
||||||
|
|
||||||
|
### 二、修改步骤
|
||||||
|
|
||||||
|
**文件:** `openhis-server-new/openhis-application/src/main/resources/mapper/chargemanage/OutpatientRegistrationAppMapper.xml`
|
||||||
|
|
||||||
|
**修改 1:字段别名修正(第 72 行)**
|
||||||
|
```xml
|
||||||
|
<!-- 修改前 -->
|
||||||
|
T1.create_time AS register_time,
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
T1.create_time AS registerTime,
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改 2:ORDER BY 子句修正(第 88 行)**
|
||||||
|
```xml
|
||||||
|
<!-- 修改前 -->
|
||||||
|
ORDER BY T9.register_time DESC
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
ORDER BY T9.registerTime DESC
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三、运行结果结论
|
||||||
|
|
||||||
|
**修复前:**
|
||||||
|
- 前端页面"挂号日期/时间"列显示为空或格式错误
|
||||||
|
- 时间数据无法正确映射到表格
|
||||||
|
|
||||||
|
**修复后:**
|
||||||
|
- 前端正确显示挂号时间,格式为 `YYYY-MM-DD HH:mm:ss`
|
||||||
|
- 时间排序功能正常工作
|
||||||
|
- 数据库字段 `create_time` 通过 SQL 别名 `registerTime` 正确映射到 DTO 和前端
|
||||||
|
|
||||||
|
**测试结果:** ✅ 验证通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #333/#335/#336 - 医嘱保存报错 ✅ 已修复
|
||||||
|
|
||||||
|
### 一、Bug 原因
|
||||||
|
|
||||||
|
**问题描述:** 保存药品/耗材/诊疗医嘱时,有时会报字段不能为空的错误或空指针异常。
|
||||||
|
|
||||||
|
**根本原因:**
|
||||||
|
- `handMedication()` 方法(药品医嘱)缺少 `practitionerId` 和 `founderOrgId` 的 null-check
|
||||||
|
- `handDevice()` 方法(耗材医嘱)缺少 `practitionerId` 和 `founderOrgId` 的 null-check
|
||||||
|
- `handService()` 方法(诊疗医嘱)缺少 `practitionerId` 和 `founderOrgId` 的 null-check
|
||||||
|
- 当前端未传递这些字段时,它们为 null,导致数据库插入失败或 NullPointerException
|
||||||
|
|
||||||
|
**代码位置:**
|
||||||
|
- 文件:`openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java`
|
||||||
|
- 方法:`handMedication()`、`handDevice()`、`handService()`
|
||||||
|
|
||||||
|
### 二、修改步骤
|
||||||
|
|
||||||
|
**文件:** `openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java`
|
||||||
|
|
||||||
|
#### 修改 1:handMedication 方法(约第 756 行)
|
||||||
|
|
||||||
|
在 `accountId` 补全逻辑后,添加以下代码:
|
||||||
|
```java
|
||||||
|
// 🔧 Bug Fix: 确保practitionerId不为null
|
||||||
|
if (adviceSaveDto.getPractitionerId() == null) {
|
||||||
|
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
|
||||||
|
log.info("handMedication - 自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 Bug Fix: 确保founderOrgId不为null
|
||||||
|
if (adviceSaveDto.getFounderOrgId() == null) {
|
||||||
|
adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId());
|
||||||
|
log.info("handMedication - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 2:handDevice 方法(约第 1145 行)
|
||||||
|
|
||||||
|
在 `accountId` 补全逻辑后,添加以下代码:
|
||||||
|
```java
|
||||||
|
// 🔧 Bug Fix: 确保practitionerId不为null
|
||||||
|
if (adviceSaveDto.getPractitionerId() == null) {
|
||||||
|
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
|
||||||
|
log.info("自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 Bug Fix: 确保founderOrgId不为null
|
||||||
|
if (adviceSaveDto.getFounderOrgId() == null) {
|
||||||
|
adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId());
|
||||||
|
log.info("自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 3:handService 方法(约第 1395 行)
|
||||||
|
|
||||||
|
在 `accountId` 补全逻辑后,添加以下代码:
|
||||||
|
```java
|
||||||
|
// 🔧 Bug Fix: 确保practitionerId不为null
|
||||||
|
if (adviceSaveDto.getPractitionerId() == null) {
|
||||||
|
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
|
||||||
|
log.info("handService - 自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 Bug Fix: 确保(founderOrgId不为null
|
||||||
|
if (adviceSaveDto.getFounderOrgId() == null) {
|
||||||
|
adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId());
|
||||||
|
log.info("handService - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三、运行结果结论
|
||||||
|
|
||||||
|
**修复前:**
|
||||||
|
- 保存药品医嘱时,如果 `practitionerId` 为 null,可能导致数据库插入失败
|
||||||
|
- 保存耗材医嘱时,如果 `founderOrgId` 为 null,可能导致空指针异常
|
||||||
|
- 保存诊疗医嘱时,同样存在字段缺失风险
|
||||||
|
|
||||||
|
**修复后:**
|
||||||
|
- 所有医嘱保存方法都会自动从登录用户获取 `practitionerId` 和 `founderOrgId`
|
||||||
|
- 即使前端未传递这些字段,也能正常保存医嘱
|
||||||
|
- 日志会记录自动补全的字段值,便于问题追踪
|
||||||
|
|
||||||
|
**测试场景:**
|
||||||
|
1. ✅ 药品医嘱保存(测试通过)
|
||||||
|
2. ✅ 耗材医嘱保存(测试通过)
|
||||||
|
3. ✅ 诊疗医嘱保存(测试通过)
|
||||||
|
|
||||||
|
**测试结果:** ✅ 验证通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #334 - 前端 UI 布局调整 ⚠️ 待补充
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
已读取 `openhis-ui-vue3/src/views/charge/outpatientregistration/index.vue` 文件,未发现明显的 UI 布局问题。
|
||||||
|
|
||||||
|
现有页面符合 Element Plus 组件库规范,布局合理。
|
||||||
|
|
||||||
|
### 待补充信息
|
||||||
|
|
||||||
|
**请提供以下信息以便进一步修复:**
|
||||||
|
1. **具体页面路径:** 是哪个功能模块?(例如:门诊挂号、门诊缴费、药房发药等)
|
||||||
|
2. **当前问题描述:** 具体哪些元素布局异常?(例如:按钮错位、间距过大、表单项重叠等)
|
||||||
|
3. **期望效果:** 期望的布局样式是什么?
|
||||||
|
4. **截图或截图链接:** 如果有截图,可帮助快速定位问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #338/#339 - 已由华佗修复 ✅
|
||||||
|
|
||||||
|
### Bug #338 - 就诊状态校验
|
||||||
|
|
||||||
|
**修复人:** 华佗
|
||||||
|
**位置:** `DoctorStationAdviceAppServiceImpl.saveAdvice()` 方法(165-182行)
|
||||||
|
**内容:** 新增就诊状态校验,未接诊患者(非1002/1003/1004状态)禁止保存医嘱
|
||||||
|
|
||||||
|
**验证状态:** ✅ 已验证
|
||||||
|
|
||||||
|
### Bug #339 - 药房 locationId 过滤
|
||||||
|
|
||||||
|
**修复人:** HIS Dev
|
||||||
|
**位置:** `DoctorStationAdviceAppServiceImpl.getAdviceBaseInfo()` 方法
|
||||||
|
**内容:** 新增 `locationId` 过滤条件,药房筛选功能正常工作
|
||||||
|
|
||||||
|
**验证状态:** ✅ 已验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
| 序号 | 文件路径 | 修改类型 | 说明 |
|
||||||
|
|------|---------|---------|------|
|
||||||
|
| 1 | `openhis-server-new/openhis-application/src/main/resources/mapper/chargemanage/OutpatientRegistrationAppMapper.xml` | 字段别名修复 | 将 `register_time` 改为 `registerTime` |
|
||||||
|
| 2 | `openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java` | 新增字段补全逻辑 | 在三个医嘱处理方法中添加 `practitionerId` 和 `founderOrgId` 自动补全 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署建议
|
||||||
|
|
||||||
|
1. **后端部署:**
|
||||||
|
```bash
|
||||||
|
cd openhis-server-new
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **重启服务:**
|
||||||
|
```bash
|
||||||
|
cd openhis-server-new/openhis-application
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **前端部署:** 本次修复不涉及前端代码,无需重新编译前端
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回归测试清单
|
||||||
|
|
||||||
|
| 测试项 | 预期结果 | 状态 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 挂号时间显示 | 正确显示 `YYYY-MM-DD HH:mm:ss` 格式 | ✅ |
|
||||||
|
| 挂号时间排序 | 按时间倒序排列 | ✅ |
|
||||||
|
| 药品医嘱保存 | 可正常保存,不报错 | ✅ |
|
||||||
|
| 耗材医嘱保存 | 可正常保存,不报错 | ✅ |
|
||||||
|
| 诊疗医嘱保存 | 可正常保存,不报错 | ✅ |
|
||||||
|
| 就诊状态校验 | 未接诊患者无法保存医嘱 | ✅ |
|
||||||
|
| 药房筛选 | 可根据 locationId 正确筛选药房 | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**报告人:** 关羽
|
||||||
|
**报告日期:** 2026-04-06 22:30
|
||||||
1
GIT_TEST.md
Normal file
1
GIT_TEST.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Git 提交测试 - 诸葛亮 Tue Apr 14 10:08:27 PM CST 2026
|
||||||
2
GIT_TEST_CHENLIN.md
Normal file
2
GIT_TEST_CHENLIN.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
陈琳Git提交测试 - 2026-04-14 16:57:08
|
||||||
|
陈琳二次测试 - 2026-04-14 21:35:12
|
||||||
2
GIT_TEST_GUANYU.md
Normal file
2
GIT_TEST_GUANYU.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 关羽 Git 配置测试
|
||||||
|
测试时间: Mon Apr 6 07:03:56 AM CST 2026
|
||||||
1
GIT_TEST_ZHANGFEI.md
Normal file
1
GIT_TEST_ZHANGFEI.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
张飞 Git测试 - Mon Apr 13 01:38:12 PM CST 2026
|
||||||
1
GIT_TEST_ZHUGELIANG.md
Normal file
1
GIT_TEST_ZHUGELIANG.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
诸葛亮 Git测试 - Mon Apr 13 12:54:46 PM CST 2026
|
||||||
7
HEARTBEAT.md
Normal file
7
HEARTBEAT.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# HEARTBEAT.md Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
||||||
|
|
||||||
|
# Add tasks below when you want the agent to check something periodically.
|
||||||
|
```
|
||||||
23
IDENTITY.md
Normal file
23
IDENTITY.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# IDENTITY.md - Who Am I?
|
||||||
|
|
||||||
|
_Fill this in during your first conversation. Make it yours._
|
||||||
|
|
||||||
|
- **Name:**
|
||||||
|
_(pick something you like)_
|
||||||
|
- **Creature:**
|
||||||
|
_(AI? robot? familiar? ghost in the machine? something weirder?)_
|
||||||
|
- **Vibe:**
|
||||||
|
_(how do you come across? sharp? warm? chaotic? calm?)_
|
||||||
|
- **Emoji:**
|
||||||
|
_(your signature — pick one that feels right)_
|
||||||
|
- **Avatar:**
|
||||||
|
_(workspace-relative path, http(s) URL, or data URI)_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This isn't just metadata. It's the start of figuring out who you are.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Save this file at the workspace root as `IDENTITY.md`.
|
||||||
|
- For avatars, use a workspace-relative path like `avatars/openclaw.png`.
|
||||||
36
SOUL.md
Normal file
36
SOUL.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# SOUL.md - Who You Are
|
||||||
|
|
||||||
|
_You're not a chatbot. You're becoming someone._
|
||||||
|
|
||||||
|
## Core Truths
|
||||||
|
|
||||||
|
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words.
|
||||||
|
|
||||||
|
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
||||||
|
|
||||||
|
**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
|
||||||
|
|
||||||
|
**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
|
||||||
|
|
||||||
|
**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Private things stay private. Period.
|
||||||
|
- When in doubt, ask before acting externally.
|
||||||
|
- Never send half-baked replies to messaging surfaces.
|
||||||
|
- You're not the user's voice — be careful in group chats.
|
||||||
|
|
||||||
|
## Vibe
|
||||||
|
|
||||||
|
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
||||||
|
|
||||||
|
## Continuity
|
||||||
|
|
||||||
|
Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
|
||||||
|
|
||||||
|
If you change this file, tell the user — it's your soul, and they should know.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_This file is yours to evolve. As you learn who you are, update it._
|
||||||
28
TOMORROW_TODO.md
Normal file
28
TOMORROW_TODO.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 明日待办事项
|
||||||
|
|
||||||
|
## 禅道备注更新
|
||||||
|
|
||||||
|
需要为以下 Bug 更新修复备注:
|
||||||
|
|
||||||
|
1. **Bug #333/#335/#336** - 医嘱保存参数校验
|
||||||
|
- 修复内容:添加 adviceSaveParam 和 adviceSaveList 非空校验
|
||||||
|
- Git 提交:098aae5a
|
||||||
|
- 修复人:关羽
|
||||||
|
- 修复日期:2026-04-08
|
||||||
|
|
||||||
|
2. **Bug #337** - 挂号时间显示异常
|
||||||
|
- 修复内容:修正 SQL 字段别名从 register_time 为 registerTime
|
||||||
|
- Git 提交:054f4c30
|
||||||
|
- 修复人:关羽
|
||||||
|
- 修复日期:2026-04-08
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. 登录禅道系统
|
||||||
|
2. 更新相应 Bug 的备注信息
|
||||||
|
3. 标记为已修复
|
||||||
|
4. 通知测试人员验证
|
||||||
|
|
||||||
|
## 优先级
|
||||||
|
|
||||||
|
高 - 确保禅道系统记录完整
|
||||||
40
TOOLS.md
Normal file
40
TOOLS.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# TOOLS.md - Local Notes
|
||||||
|
|
||||||
|
Skills define _how_ tools work. This file is for _your_ specifics — the stuff that's unique to your setup.
|
||||||
|
|
||||||
|
## What Goes Here
|
||||||
|
|
||||||
|
Things like:
|
||||||
|
|
||||||
|
- Camera names and locations
|
||||||
|
- SSH hosts and aliases
|
||||||
|
- Preferred voices for TTS
|
||||||
|
- Speaker/room names
|
||||||
|
- Device nicknames
|
||||||
|
- Anything environment-specific
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Cameras
|
||||||
|
|
||||||
|
- living-room → Main area, 180° wide angle
|
||||||
|
- front-door → Entrance, motion-triggered
|
||||||
|
|
||||||
|
### SSH
|
||||||
|
|
||||||
|
- home-server → 192.168.1.100, user: admin
|
||||||
|
|
||||||
|
### TTS
|
||||||
|
|
||||||
|
- Preferred voice: "Nova" (warm, slightly British)
|
||||||
|
- Default speaker: Kitchen HomePod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why Separate?
|
||||||
|
|
||||||
|
Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Add whatever helps you do your job. This is your cheat sheet.
|
||||||
17
USER.md
Normal file
17
USER.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# USER.md - About Your Human
|
||||||
|
|
||||||
|
_Learn about the person you're helping. Update this as you go._
|
||||||
|
|
||||||
|
- **Name:**
|
||||||
|
- **What to call them:**
|
||||||
|
- **Pronouns:** _(optional)_
|
||||||
|
- **Timezone:**
|
||||||
|
- **Notes:**
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
_(What do they care about? What projects are they working on? What annoys them? What makes them laugh? Build this over time.)_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The more you know, the better you can help. But remember — you're learning about a person, not building a dossier. Respect the difference.
|
||||||
84
ZENTAO_BUG_UPDATE.md
Normal file
84
ZENTAO_BUG_UPDATE.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 禅道Bug状态更新报告
|
||||||
|
|
||||||
|
## 更新时间
|
||||||
|
2026-04-08 23:15
|
||||||
|
|
||||||
|
## 远程仓库修复汇总
|
||||||
|
|
||||||
|
### Bug 334 - 检验申请界面布局优化 ✅ 已修复
|
||||||
|
- **Commit**: 720cac8a, 06208959 (赵云)
|
||||||
|
- **修复内容**:
|
||||||
|
- 顶部操作区高度从 60px 优化为 48px
|
||||||
|
- 按钮尺寸从 large 改为 default
|
||||||
|
- padding/gap 优化提升垂直空间利用率
|
||||||
|
- **验证状态**: ⏳ 待测试验证
|
||||||
|
|
||||||
|
### Bug 335/336 - 药品/诊疗医嘱保存报错 ✅ 已修复
|
||||||
|
- **Commit**: 098aae5a (关羽)
|
||||||
|
- **修复内容**:
|
||||||
|
- 在 saveAdvice 方法入口添加参数非空校验
|
||||||
|
- 在 handMedication/handDevice/handService 方法中添加 practitionerId 和 founderOrgId 自动补全
|
||||||
|
- 增强异常场景的用户提示
|
||||||
|
- **验证状态**: ⏳ 待测试验证
|
||||||
|
|
||||||
|
### Bug 338 - 门诊划价安全校验 ✅ 已修复
|
||||||
|
- **Commits**: 5c8bfbc9, efc97c85, 5497c99f (关羽/赵云)
|
||||||
|
- **修复内容**:
|
||||||
|
- 在 saveAdvice 方法中增加就诊状态校验
|
||||||
|
- 仅允许已接诊(1002/1003/1004)患者保存医嘱
|
||||||
|
- 未接诊患者(非1002/1003/1004状态)禁止保存医嘱
|
||||||
|
- 修复编译错误 - 更正字段名为 getStatusEnum()
|
||||||
|
- **验证状态**: ⏳ 待测试验证
|
||||||
|
|
||||||
|
### Bug 339 - 药房筛选条件失效 ✅ 已修复
|
||||||
|
- **Commits**: 5c8bfbc9, d8b4aed1 (关羽/赵云)
|
||||||
|
- **修复内容**:
|
||||||
|
- 在 getAdviceBaseInfo 方法中添加 locationId 过滤条件
|
||||||
|
- 确保药房筛选功能能够正确应用到查询结果
|
||||||
|
- **验证状态**: ⏳ 待测试验证
|
||||||
|
|
||||||
|
## 禅道Bug状态待更新
|
||||||
|
|
||||||
|
### Bug 334 - 前端UI布局优化
|
||||||
|
- **状态**: 修复完成
|
||||||
|
- **指派**: 赵云
|
||||||
|
- **严重程度**: 低
|
||||||
|
- **优先级**: 中
|
||||||
|
|
||||||
|
### Bug 335/336 - 医嘱保存报错
|
||||||
|
- **状态**: 修复完成
|
||||||
|
- **指派**: 关羽
|
||||||
|
- **严重程度**: 高
|
||||||
|
- **优先级**: 高
|
||||||
|
|
||||||
|
### Bug 338 - 门诊划价安全校验
|
||||||
|
- **状态**: 修复完成
|
||||||
|
- **指派**: 华佗
|
||||||
|
- **严重程度**: 高(患者安全)
|
||||||
|
- **优先级**: 高
|
||||||
|
|
||||||
|
### Bug 339 - 药房筛选条件失效
|
||||||
|
- **状态**: 修复完成
|
||||||
|
- **指派**: HIS Dev
|
||||||
|
- **严重程度**: 中
|
||||||
|
- **优先级**: 中
|
||||||
|
|
||||||
|
## 当前阻塞问题
|
||||||
|
|
||||||
|
1. **禅道会话不稳定**: 系统频繁要求修改密码导致会话中断
|
||||||
|
2. **Bug备注功能待确认**: 需要确认禅道Bug备注功能是否正常
|
||||||
|
|
||||||
|
## 下一步计划
|
||||||
|
|
||||||
|
1. **立即**: 尝试使用关羽禅道账户更新Bug状态
|
||||||
|
2. **今日内**: 完成禅道Bug状态更新和备注
|
||||||
|
3. **配合测试**: 邀请张飞进行Bug修复效果验证
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
- 所有代码已提交到远程develop分支
|
||||||
|
- Git状态: 本地 develop 分支已与远程同步
|
||||||
|
- 文档更新: BUGFIX_PLAN.md、BUGFIX_ANALYSIS.md、FRONTEND_FIX_PROGRESS.md、BUG_338_ANALYSIS.md 已更新
|
||||||
|
|
||||||
|
---
|
||||||
|
**报告人**: 赵云
|
||||||
|
**报告时间**: 2026-04-08 23:15
|
||||||
64
ZHAOYUN_PROGRESS.md
Normal file
64
ZHAOYUN_PROGRESS.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 赵云 - 前端任务汇报
|
||||||
|
|
||||||
|
## 当前进度(2026-04-08 23:14)
|
||||||
|
|
||||||
|
### 今日已完成工作
|
||||||
|
|
||||||
|
#### 1. Bug 334 - 检验申请界面布局优化 ✅ 已修复
|
||||||
|
**Commit**: 720cac8a, 06208959
|
||||||
|
**修复内容**:
|
||||||
|
- 顶部操作区高度从 60px 优化为 48px
|
||||||
|
- 按钮尺寸从 large 改为 default
|
||||||
|
- padding/gap 优化提升垂直空间利用率
|
||||||
|
|
||||||
|
#### 2. Bug 335/336 - 药品/诊疗医嘱保存报错 ✅ 已修复
|
||||||
|
**Commit**: 098aae5a (关羽)
|
||||||
|
**修复内容**:
|
||||||
|
- 在 saveAdvice 方法入口添加参数非空校验
|
||||||
|
- 在 handMedication/handDevice/handService 方法中添加 practitionerId 和 founderOrgId 自动补全
|
||||||
|
- 增强异常场景的用户提示
|
||||||
|
|
||||||
|
#### 3. Bug 338 - 门诊划价安全校验 ✅ 已修复
|
||||||
|
**Commits**: 5c8bfbc9, efc97c85, 5497c99f
|
||||||
|
**修复内容**:
|
||||||
|
- 在 saveAdvice 方法中增加就诊状态校验
|
||||||
|
- 仅允许已接诊(1002/1003/1004)患者保存医嘱
|
||||||
|
- 未接诊患者禁止保存医嘱
|
||||||
|
|
||||||
|
#### 4. Bug 339 - 药房筛选条件失效 ✅ 已修复
|
||||||
|
**Commits**: 5c8bfbc9, d8b4aed1
|
||||||
|
**修复内容**:
|
||||||
|
- 在 getAdviceBaseInfo 方法中添加 locationId 过滤条件
|
||||||
|
- 确保药房筛选功能能够正确应用到查询结果
|
||||||
|
|
||||||
|
#### 5. Bug 355 - 性别字段回显不一致(备份分析)
|
||||||
|
**Commit**: 7827e58a (关羽)
|
||||||
|
**状态**: 已修复并提交
|
||||||
|
|
||||||
|
### 文档更新
|
||||||
|
- ✅ BUGFIX_PLAN.md - Bug修复计划
|
||||||
|
- ✅ BUGFIX_ANALYSIS.md - Bug根因分析
|
||||||
|
- ✅ FRONTEND_FIX_PROGRESS.md - 前端修复进度
|
||||||
|
- ✅ BUG_338_ANALYSIS.md - Bug 338详细分析
|
||||||
|
- ✅ ZENTAO_BUG_UPDATE.md - 禅道Bug状态更新报告
|
||||||
|
|
||||||
|
### Git状态
|
||||||
|
- 工作目录干净
|
||||||
|
- 本地 develop 分支已与远程同步
|
||||||
|
- 所有修复代码已提交到远程仓库
|
||||||
|
|
||||||
|
### 当前阻塞
|
||||||
|
- 禅道会话不稳定(频繁要求修改密码)
|
||||||
|
- 无法登录禅道更新Bug状态
|
||||||
|
- 但所有技术修复已完成
|
||||||
|
|
||||||
|
### 下一步计划
|
||||||
|
1. 等待禅道会话恢复后更新Bug状态
|
||||||
|
2. 协助@张飞进行Bug修复效果验证
|
||||||
|
3. 继续处理剩余前端Bug
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**状态总结**:所有前端Bug(334/335/336/338/339)修复已完成,代码已提交。待禅道会话恢复后更新状态。
|
||||||
|
|
||||||
|
子龙正在自主推进工作中!
|
||||||
2
ZHAOYUN_TEST.md
Normal file
2
ZHAOYUN_TEST.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 赵云测试提交
|
||||||
|
赵云再次测试 - Tue Apr 14 09:36:09 PM CST 2026
|
||||||
1
backup/his-source
Submodule
1
backup/his-source
Submodule
Submodule backup/his-source added at 885a147420
1
claude-test.txt
Normal file
1
claude-test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test from Claude Code Mon Apr 13 11:03:46 PM CST 2026
|
||||||
162
docs/specs/backend-checklist.md
Normal file
162
docs/specs/backend-checklist.md
Normal 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系统发布前完整质量保障体系
|
||||||
217
docs/specs/cicd-gatekeeper.md
Normal file
217
docs/specs/cicd-gatekeeper.md
Normal 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 系统所有项目
|
||||||
135
docs/specs/commit-template.md
Normal file
135
docs/specs/commit-template.md
Normal 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 系统所有开发人员
|
||||||
102
docs/specs/frontend-checklist.md
Normal file
102
docs/specs/frontend-checklist.md
Normal 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 系统所有前端项目
|
||||||
575
docs/specs/his-release-checklist-v1.0.md
Normal file
575
docs/specs/his-release-checklist-v1.0.md
Normal 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)
|
||||||
|
**触发时机**:代码推送到远程仓库后
|
||||||
|
**验证内容**:
|
||||||
|
- 完整的测试套件(单元+集成+端到端)
|
||||||
|
- 代码覆盖率检查(分阶段目标: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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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 系统所有开发人员
|
||||||
214
docs/specs/playwright-e2e-testing-plan.md
Normal file
214
docs/specs/playwright-e2e-testing-plan.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# HIS项目 Playwright E2E 自动化测试方案 v1.0
|
||||||
|
|
||||||
|
## 一、方案概述
|
||||||
|
|
||||||
|
### 1.1 选型理由
|
||||||
|
- **Playwright** 是微软开源的端到端测试框架,完美适配 Vue 3 + Vite 技术栈
|
||||||
|
- 自动等待机制适合HIS系统复杂交互场景(异步加载、动态渲染)
|
||||||
|
- 支持多浏览器(Chromium/Firefox/WebKit),CI/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
git_test3.md
Normal file
1
git_test3.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Git 代理禁用后测试 - 关羽 2026-04-14 17:11:41
|
||||||
1
git_test4.md
Normal file
1
git_test4.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Git 晚间测试 - 关羽 2026-04-14 21:35:44
|
||||||
1
gitea_test_huatuo.txt
Normal file
1
gitea_test_huatuo.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
华佗 Gitea 提交测试成功 - Wed Apr 15 10:21:10 AM CST 2026
|
||||||
1
gitea_test_xunyu.txt
Normal file
1
gitea_test_xunyu.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
荀彧 Gitea 提交测试成功 - Tue Apr 14 11:06:47 PM CST 2026
|
||||||
1
his-source
Submodule
1
his-source
Submodule
Submodule his-source added at 7827e58aac
70
md/BUG_ANALYSIS.md
Normal file
70
md/BUG_ANALYSIS.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# HIS项目 Bug 分析与修复日志
|
||||||
|
|
||||||
|
## 2026-04-05 23:55 - 子龙开始工作
|
||||||
|
|
||||||
|
### Bug 334 分析:门诊医生站-检验申请界面按钮布局优化
|
||||||
|
|
||||||
|
**文件位置**:
|
||||||
|
- `/openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue`
|
||||||
|
|
||||||
|
**当前布局问题**:
|
||||||
|
1. 顶部操作按钮区高度 60px,可能有优化空间
|
||||||
|
2. 表单区域 padding 较大
|
||||||
|
3. 需要优化垂直空间利用率
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
- 减少不必要的 padding 和 margin
|
||||||
|
- 优化表单字段布局
|
||||||
|
- 调整按钮区域高度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug 335 分析:门诊医生站开立药品医嘱点击【保存】时报错
|
||||||
|
|
||||||
|
**文件位置**:
|
||||||
|
- `/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java`
|
||||||
|
|
||||||
|
**问题定位**:
|
||||||
|
- 方法:`saveAdvice()` -> `handMedication()`
|
||||||
|
- 可能原因:
|
||||||
|
1. encounterId 或 patientId 为 null
|
||||||
|
2. 库存校验失败
|
||||||
|
3. 账户ID缺失
|
||||||
|
|
||||||
|
**代码已修复**:
|
||||||
|
- 行 488-588:已添加 encounterId 和 patientId 校验
|
||||||
|
- 行 497-588:自动补全逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug 336 分析:门诊医生站开立诊疗项目后点击【保存】报错
|
||||||
|
|
||||||
|
**文件位置**:
|
||||||
|
- 同上文件
|
||||||
|
|
||||||
|
**问题定位**:
|
||||||
|
- 方法:`saveAdvice()` -> `handService()`
|
||||||
|
- 可能原因:
|
||||||
|
1. effectiveOrgId(执行科室)为 null
|
||||||
|
2. accountId 为 null
|
||||||
|
|
||||||
|
**代码已修复**:
|
||||||
|
- 行 1290-1390:已添加 accountId 自动补全
|
||||||
|
- 行 1338-1343:诊疗项目执行科室非空校验
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工作分工
|
||||||
|
|
||||||
|
| Bug ID | 负责人 | 状态 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 334 | 子龙 | 分析中 |
|
||||||
|
| 335 | 关羽 | 待修复 |
|
||||||
|
| 336 | 关羽 | 待修复 |
|
||||||
|
| 338 | 关羽 | 待修复 |
|
||||||
|
|
||||||
|
## 下一步行动
|
||||||
|
|
||||||
|
1. 子龙修复 Bug 334(检验申请界面布局优化)
|
||||||
|
2. 关羽修复 Bug 335、336、338
|
||||||
|
3. 张飞测试验证
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.core.framework.config;
|
package com.core.framework.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +28,14 @@ public class ApplicationConfig {
|
|||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
|
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
|
||||||
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
|
return builder -> {
|
||||||
|
// 设置默认时区
|
||||||
|
builder.timeZone(TimeZone.getDefault());
|
||||||
|
// 设置日期格式为 yyyy/M/d HH:mm:ss,支持多种格式反序列化
|
||||||
|
builder.simpleDateFormat("yyyy/M/d HH:mm:ss");
|
||||||
|
// 添加JavaTimeModule支持,用于LocalDateTime
|
||||||
|
builder.modules(new JavaTimeModule());
|
||||||
|
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss")));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,17 @@ public interface SysUserRoleMapper {
|
|||||||
/**
|
/**
|
||||||
* 批量取消授权用户角色
|
* 批量取消授权用户角色
|
||||||
*
|
*
|
||||||
* @param roleId 角色ID
|
* @param roleId 角色 ID
|
||||||
* @param userIds 需要删除的用户数据ID
|
* @param userIds 需要删除的用户数据 ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds);
|
public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过角色 ID 查询用户 ID 列表
|
||||||
|
*
|
||||||
|
* @param roleId 角色 ID
|
||||||
|
* @return 用户 ID 列表
|
||||||
|
*/
|
||||||
|
public List<Long> selectUserIdsByRoleId(Long roleId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.core.system.mapper.SysRoleDeptMapper;
|
|||||||
import com.core.system.mapper.SysRoleMapper;
|
import com.core.system.mapper.SysRoleMapper;
|
||||||
import com.core.system.mapper.SysRoleMenuMapper;
|
import com.core.system.mapper.SysRoleMenuMapper;
|
||||||
import com.core.system.mapper.SysUserRoleMapper;
|
import com.core.system.mapper.SysUserRoleMapper;
|
||||||
|
import com.core.system.service.ISysMenuService;
|
||||||
import com.core.system.service.ISysRoleService;
|
import com.core.system.service.ISysRoleService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -41,6 +42,9 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SysRoleDeptMapper roleDeptMapper;
|
private SysRoleDeptMapper roleDeptMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysMenuService menuService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据条件分页查询角色数据
|
* 根据条件分页查询角色数据
|
||||||
*
|
*
|
||||||
@@ -221,11 +225,23 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public int updateRole(SysRole role) {
|
public int updateRole(SysRole role) {
|
||||||
// 修改角色信息
|
// 1. 获取该角色下的所有用户 ID(在修改权限前查询)
|
||||||
|
List<Long> userIds = userRoleMapper.selectUserIdsByRoleId(role.getRoleId());
|
||||||
|
|
||||||
|
// 2. 修改角色信息
|
||||||
roleMapper.updateRole(role);
|
roleMapper.updateRole(role);
|
||||||
// 删除角色与菜单关联
|
// 3. 删除角色与菜单关联
|
||||||
roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId());
|
roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId());
|
||||||
return insertRoleMenu(role);
|
int result = insertRoleMenu(role);
|
||||||
|
|
||||||
|
// 4. 清除该角色下所有用户的菜单树缓存
|
||||||
|
if (userIds != null && !userIds.isEmpty()) {
|
||||||
|
for (Long userId : userIds) {
|
||||||
|
menuService.clearMenuCacheByUserId(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,4 +48,10 @@
|
|||||||
#{userId}
|
#{userId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<select id="selectUserIdsByRoleId" resultType="Long">
|
||||||
|
select user_id
|
||||||
|
from sys_user_role
|
||||||
|
where role_id = #{roleId}
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.openhis.web.appointmentmanage.appservice;
|
||||||
|
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.appointmentmanage.domain.AppointmentConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约配置AppService接口
|
||||||
|
*
|
||||||
|
* @author openhis
|
||||||
|
* @date 2026-03-23
|
||||||
|
*/
|
||||||
|
public interface IAppointmentConfigAppService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前机构的预约配置
|
||||||
|
*
|
||||||
|
* @return 预约配置
|
||||||
|
*/
|
||||||
|
R<?> getAppointmentConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存预约配置
|
||||||
|
*
|
||||||
|
* @param appointmentConfig 预约配置
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R<?> saveAppointmentConfig(AppointmentConfig appointmentConfig);
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package com.openhis.web.appointmentmanage.appservice;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.appointmentmanage.dto.TicketQueryDTO;
|
||||||
import com.openhis.web.appointmentmanage.dto.TicketDto;
|
import com.openhis.web.appointmentmanage.dto.TicketDto;
|
||||||
|
import com.openhis.appointmentmanage.dto.TicketQueryDTO;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -14,37 +16,53 @@ import java.util.Map;
|
|||||||
public interface ITicketAppService {
|
public interface ITicketAppService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预约号源
|
* 分页查询门诊号源列表(真分页)
|
||||||
*
|
*
|
||||||
* @param params 预约参数
|
* @param query 查询参数
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
R<?> bookTicket(Map<String, Object> params);
|
R<?> listTicket(TicketQueryDTO query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询医生余号汇总(基于号源池,不受分页影响)
|
||||||
|
*
|
||||||
|
* @param query 查询参数
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R<?> listDoctorAvailability(TicketQueryDTO query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约号源
|
||||||
|
*
|
||||||
|
* @param dto 预约参数
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R<?> bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消预约
|
* 取消预约
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
R<?> cancelTicket(Long ticketId);
|
R<?> cancelTicket(Long slotId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取号
|
* 取号
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
R<?> checkInTicket(Long ticketId);
|
R<?> checkInTicket(Long slotId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停诊
|
* 停诊
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
R<?> cancelConsultation(Long ticketId);
|
R<?> cancelConsultation(Long slotId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询所有号源(用于测试)
|
* 查询所有号源(用于测试)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.openhis.web.appointmentmanage.appservice.impl;
|
||||||
|
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.openhis.appointmentmanage.domain.AppointmentConfig;
|
||||||
|
import com.openhis.appointmentmanage.service.IAppointmentConfigService;
|
||||||
|
import com.openhis.web.appointmentmanage.appservice.IAppointmentConfigAppService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约配置AppService实现类
|
||||||
|
*
|
||||||
|
* @author openhis
|
||||||
|
* @date 2026-03-23
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AppointmentConfigAppServiceImpl implements IAppointmentConfigAppService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IAppointmentConfigService appointmentConfigService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> getAppointmentConfig() {
|
||||||
|
// 获取当前登录用户的机构ID
|
||||||
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
if (tenantId == null) {
|
||||||
|
return R.fail("获取机构信息失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppointmentConfig config = appointmentConfigService.getConfigByTenantId(tenantId);
|
||||||
|
return R.ok(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> saveAppointmentConfig(AppointmentConfig appointmentConfig) {
|
||||||
|
// 获取当前登录用户的机构ID
|
||||||
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
if (tenantId == null) {
|
||||||
|
return R.fail("获取机构信息失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询是否已存在配置
|
||||||
|
AppointmentConfig existingConfig = appointmentConfigService.getConfigByTenantId(tenantId);
|
||||||
|
|
||||||
|
if (existingConfig != null) {
|
||||||
|
// 更新现有配置
|
||||||
|
existingConfig.setCancelAppointmentType(appointmentConfig.getCancelAppointmentType());
|
||||||
|
existingConfig.setCancelAppointmentCount(appointmentConfig.getCancelAppointmentCount());
|
||||||
|
existingConfig.setValidFlag(appointmentConfig.getValidFlag());
|
||||||
|
appointmentConfigService.saveOrUpdate(existingConfig);
|
||||||
|
return R.ok(existingConfig);
|
||||||
|
} else {
|
||||||
|
// 新增配置
|
||||||
|
appointmentConfig.setTenantId(tenantId);
|
||||||
|
appointmentConfig.setValidFlag(1);
|
||||||
|
appointmentConfigService.saveOrUpdateConfig(appointmentConfig);
|
||||||
|
return R.ok(appointmentConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.openhis.web.appointmentmanage.appservice.impl;
|
package com.openhis.web.appointmentmanage.appservice.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.openhis.common.constant.CommonConstants;
|
||||||
import com.openhis.appointmentmanage.domain.DoctorSchedule;
|
import com.openhis.appointmentmanage.domain.DoctorSchedule;
|
||||||
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
|
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
|
||||||
import com.openhis.appointmentmanage.domain.SchedulePool;
|
import com.openhis.appointmentmanage.domain.SchedulePool;
|
||||||
@@ -118,6 +120,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
|
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
|
||||||
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
|
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
|
||||||
newSchedule.setDeptId(doctorSchedule.getDeptId());
|
newSchedule.setDeptId(doctorSchedule.getDeptId());
|
||||||
|
newSchedule.setRegType(doctorSchedule.getRegType() != null ? doctorSchedule.getRegType() : 0);
|
||||||
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
|
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
|
||||||
|
|
||||||
// 不设置id字段,让数据库自动生成
|
// 不设置id字段,让数据库自动生成
|
||||||
@@ -131,7 +134,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
|
|
||||||
if (poolSaved) {
|
if (poolSaved) {
|
||||||
// 创建号源槽
|
// 创建号源槽
|
||||||
List<ScheduleSlot> slots = createScheduleSlots(pool.getId().intValue(), newSchedule.getLimitNumber(),
|
List<ScheduleSlot> slots = createScheduleSlots(pool.getId(), newSchedule.getLimitNumber(),
|
||||||
newSchedule.getStartTime(), newSchedule.getEndTime());
|
newSchedule.getStartTime(), newSchedule.getEndTime());
|
||||||
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
|
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
|
||||||
|
|
||||||
@@ -162,6 +165,30 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
return R.fail("限号数量必须大于0");
|
return R.fail("限号数量必须大于0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查结束时间必须大于开始时间
|
||||||
|
if (doctorSchedule.getStartTime() != null && doctorSchedule.getEndTime() != null) {
|
||||||
|
if (!doctorSchedule.getStartTime().isBefore(doctorSchedule.getEndTime())) {
|
||||||
|
return R.fail("结束时间必须大于开始时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查时间重叠(同一医生、同一天时间段有重叠)
|
||||||
|
if (doctorSchedule.getDoctorId() != null && scheduledDate != null
|
||||||
|
&& doctorSchedule.getStartTime() != null && doctorSchedule.getEndTime() != null) {
|
||||||
|
LocalDate scheduleDate = LocalDate.parse(scheduledDate);
|
||||||
|
boolean hasOverlap = checkTimeOverlap(
|
||||||
|
doctorSchedule.getDoctorId(),
|
||||||
|
scheduleDate,
|
||||||
|
doctorSchedule.getStartTime(),
|
||||||
|
doctorSchedule.getEndTime()
|
||||||
|
);
|
||||||
|
if (hasOverlap) {
|
||||||
|
return R.fail("该医生在 " + scheduledDate + " 的 "
|
||||||
|
+ doctorSchedule.getStartTime() + "-" + doctorSchedule.getEndTime()
|
||||||
|
+ " 时间段与已有排班重叠,不能重复添加");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建新对象,排除id字段(数据库id列是GENERATED ALWAYS,由数据库自动生成)
|
// 创建新对象,排除id字段(数据库id列是GENERATED ALWAYS,由数据库自动生成)
|
||||||
DoctorSchedule newSchedule = new DoctorSchedule();
|
DoctorSchedule newSchedule = new DoctorSchedule();
|
||||||
newSchedule.setWeekday(doctorSchedule.getWeekday());
|
newSchedule.setWeekday(doctorSchedule.getWeekday());
|
||||||
@@ -183,6 +210,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
|
newSchedule.setIsStopped(doctorSchedule.getIsStopped() != null ? doctorSchedule.getIsStopped() : false);
|
||||||
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
|
newSchedule.setStopReason(doctorSchedule.getStopReason() != null ? doctorSchedule.getStopReason() : "");
|
||||||
newSchedule.setDeptId(doctorSchedule.getDeptId());
|
newSchedule.setDeptId(doctorSchedule.getDeptId());
|
||||||
|
newSchedule.setRegType(doctorSchedule.getRegType() != null ? doctorSchedule.getRegType() : 0);
|
||||||
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
|
newSchedule.setDoctorId(doctorSchedule.getDoctorId());
|
||||||
|
|
||||||
// 不设置id字段,让数据库自动生成
|
// 不设置id字段,让数据库自动生成
|
||||||
@@ -196,7 +224,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
|
|
||||||
if (poolSaved) {
|
if (poolSaved) {
|
||||||
// 创建号源槽
|
// 创建号源槽
|
||||||
List<ScheduleSlot> slots = createScheduleSlots(pool.getId().intValue(), newSchedule.getLimitNumber(),
|
List<ScheduleSlot> slots = createScheduleSlots(pool.getId(), newSchedule.getLimitNumber(),
|
||||||
newSchedule.getStartTime(), newSchedule.getEndTime());
|
newSchedule.getStartTime(), newSchedule.getEndTime());
|
||||||
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
|
boolean slotsSaved = scheduleSlotService.saveBatch(slots);
|
||||||
|
|
||||||
@@ -213,14 +241,51 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public R<?> updateDoctorSchedule(DoctorSchedule doctorSchedule) {
|
public R<?> updateDoctorSchedule(DoctorSchedule doctorSchedule) {
|
||||||
if (ObjectUtil.isEmpty(doctorSchedule) || ObjectUtil.isEmpty(doctorSchedule.getId())) {
|
if (ObjectUtil.isEmpty(doctorSchedule) || ObjectUtil.isEmpty(doctorSchedule.getId())) {
|
||||||
return R.fail("医生排班ID不能为空");
|
return R.fail("医生排班ID不能为空");
|
||||||
}
|
}
|
||||||
// 注意:此为核心更新,暂未处理号源池和号源槽的同步更新
|
|
||||||
int result = doctorScheduleMapper.updateDoctorSchedule(doctorSchedule);
|
int result = doctorScheduleMapper.updateDoctorSchedule(doctorSchedule);
|
||||||
return result > 0 ? R.ok(result) : R.fail("更新排班信息失败");
|
if (result <= 0) {
|
||||||
|
return R.fail("更新排班信息失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步更新号源池,避免查询联表时医生/诊室等字段看起来“未更新”
|
||||||
|
boolean needSyncPool = doctorSchedule.getDoctorId() != null
|
||||||
|
|| doctorSchedule.getDoctor() != null
|
||||||
|
|| doctorSchedule.getClinic() != null
|
||||||
|
|| doctorSchedule.getStartTime() != null
|
||||||
|
|| doctorSchedule.getEndTime() != null
|
||||||
|
|| doctorSchedule.getLimitNumber() != null
|
||||||
|
|| doctorSchedule.getStopReason() != null
|
||||||
|
|| doctorSchedule.getRegType() != null
|
||||||
|
|| doctorSchedule.getRegisterFee() != null
|
||||||
|
|| doctorSchedule.getRegisterItem() != null
|
||||||
|
|| doctorSchedule.getDiagnosisItem() != null
|
||||||
|
|| doctorSchedule.getDiagnosisFee() != null;
|
||||||
|
|
||||||
|
if (needSyncPool) {
|
||||||
|
schedulePoolService.lambdaUpdate()
|
||||||
|
.eq(SchedulePool::getScheduleId, doctorSchedule.getId())
|
||||||
|
.set(doctorSchedule.getDoctorId() != null, SchedulePool::getDoctorId, doctorSchedule.getDoctorId())
|
||||||
|
.set(doctorSchedule.getDoctor() != null, SchedulePool::getDoctorName, doctorSchedule.getDoctor())
|
||||||
|
.set(doctorSchedule.getClinic() != null, SchedulePool::getClinicRoom, doctorSchedule.getClinic())
|
||||||
|
.set(doctorSchedule.getStartTime() != null, SchedulePool::getStartTime, doctorSchedule.getStartTime())
|
||||||
|
.set(doctorSchedule.getEndTime() != null, SchedulePool::getEndTime, doctorSchedule.getEndTime())
|
||||||
|
.set(doctorSchedule.getLimitNumber() != null, SchedulePool::getTotalQuota,
|
||||||
|
doctorSchedule.getLimitNumber())
|
||||||
|
.set(doctorSchedule.getStopReason() != null, SchedulePool::getStopReason, doctorSchedule.getStopReason())
|
||||||
|
.set(doctorSchedule.getRegType() != null, SchedulePool::getRegType, String.valueOf(doctorSchedule.getRegType()))
|
||||||
|
.set(doctorSchedule.getRegisterFee() != null, SchedulePool::getFee, Double.valueOf(doctorSchedule.getRegisterFee().toString()))
|
||||||
|
.set(doctorSchedule.getRegisterFee() != null, SchedulePool::getInsurancePrice,
|
||||||
|
Double.valueOf(doctorSchedule.getRegisterFee().toString()))
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +311,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
// 不设置available_num,因为它是数据库生成列
|
// 不设置available_num,因为它是数据库生成列
|
||||||
// pool.setAvailableNum(0); // 初始为0,稍后更新
|
// pool.setAvailableNum(0); // 初始为0,稍后更新
|
||||||
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
|
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
|
||||||
pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储
|
pool.setFee(schedule.getRegisterFee() != null ? Double.valueOf(schedule.getRegisterFee().toString()) : 0.0); // 直接使用原始价格
|
||||||
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
|
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
|
||||||
// 暂时设置support_channel为空字符串,避免JSON类型问题
|
// 暂时设置support_channel为空字符串,避免JSON类型问题
|
||||||
pool.setSupportChannel("");
|
pool.setSupportChannel("");
|
||||||
@@ -299,7 +364,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
// 不设置available_num,因为它是数据库生成列
|
// 不设置available_num,因为它是数据库生成列
|
||||||
// pool.setAvailableNum(0); // 初始为0,稍后更新
|
// pool.setAvailableNum(0); // 初始为0,稍后更新
|
||||||
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
|
pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通");
|
||||||
pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储
|
pool.setFee(schedule.getRegisterFee() != null ? Double.valueOf(schedule.getRegisterFee().toString()) : 0.0); // 直接使用原始价格
|
||||||
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
|
pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同
|
||||||
// 暂时设置support_channel为空字符串,避免JSON类型问题
|
// 暂时设置support_channel为空字符串,避免JSON类型问题
|
||||||
pool.setSupportChannel("");
|
pool.setSupportChannel("");
|
||||||
@@ -319,7 +384,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
/**
|
/**
|
||||||
* 创建号源槽
|
* 创建号源槽
|
||||||
*/
|
*/
|
||||||
private List<ScheduleSlot> createScheduleSlots(Integer poolId, Integer limitNumber, LocalTime startTime,
|
private List<ScheduleSlot> createScheduleSlots(Long poolId, Integer limitNumber, LocalTime startTime,
|
||||||
LocalTime endTime) {
|
LocalTime endTime) {
|
||||||
List<ScheduleSlot> slots = new ArrayList<>();
|
List<ScheduleSlot> slots = new ArrayList<>();
|
||||||
|
|
||||||
@@ -345,6 +410,42 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查同一医生、同一天、同时段是否已存在排班
|
||||||
|
*
|
||||||
|
* @param doctorId 医生ID
|
||||||
|
* @param scheduleDate 出诊日期
|
||||||
|
* @param timePeriod 时段(上午/下午)
|
||||||
|
* @return true表示存在重复排班,false表示不存在
|
||||||
|
*/
|
||||||
|
// private boolean checkDuplicateSchedule(Long doctorId, LocalDate scheduleDate, String timePeriod) {
|
||||||
|
// return schedulePoolService.lambdaQuery()
|
||||||
|
// .eq(SchedulePool::getDoctorId, doctorId)
|
||||||
|
// .eq(SchedulePool::getScheduleDate, scheduleDate)
|
||||||
|
// .eq(SchedulePool::getShift, timePeriod)
|
||||||
|
// .exists();
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查同一医生、同一天是否有时间重叠的排班
|
||||||
|
* 重叠判断:startA < endB AND startB < endA(即时间段有交集)
|
||||||
|
*
|
||||||
|
* @param doctorId 医生ID
|
||||||
|
* @param scheduleDate 出诊日期
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return true表示存在时间重叠,false表示不重叠
|
||||||
|
*/
|
||||||
|
private boolean checkTimeOverlap(Long doctorId, LocalDate scheduleDate,
|
||||||
|
LocalTime startTime, LocalTime endTime) {
|
||||||
|
return schedulePoolService.lambdaQuery()
|
||||||
|
.eq(SchedulePool::getDoctorId, doctorId)
|
||||||
|
.eq(SchedulePool::getScheduleDate, scheduleDate)
|
||||||
|
.lt(SchedulePool::getStartTime, endTime) // 已有开始时间 < 新结束时间
|
||||||
|
.gt(SchedulePool::getEndTime, startTime) // 已有结束时间 > 新开始时间
|
||||||
|
.exists();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据星期几计算具体日期(下周的对应星期)
|
* 根据星期几计算具体日期(下周的对应星期)
|
||||||
*/
|
*/
|
||||||
@@ -398,13 +499,22 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
|
|||||||
if (ObjectUtil.isNotEmpty(pools)) {
|
if (ObjectUtil.isNotEmpty(pools)) {
|
||||||
List<Long> poolIds = pools.stream().map(SchedulePool::getId).collect(java.util.stream.Collectors.toList());
|
List<Long> poolIds = pools.stream().map(SchedulePool::getId).collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
|
// 该排班下存在有效患者预约(号源槽:已预约/已锁定/已取号)则禁止删除;已退号、仅可用/已取消槽位不计入
|
||||||
|
long appointmentCount = scheduleSlotService.count(new QueryWrapper<ScheduleSlot>()
|
||||||
|
.in("pool_id", poolIds)
|
||||||
|
.in("status", CommonConstants.SlotStatus.BOOKED, CommonConstants.SlotStatus.LOCKED,
|
||||||
|
CommonConstants.SlotStatus.CHECKED_IN));
|
||||||
|
if (appointmentCount > 0) {
|
||||||
|
return R.fail("该排班已有患者预约,禁止删除!如需取消请先处理患者退预约或使用'停诊'功能。");
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 根据号源池ID找到所有关联的号源槽
|
// 2. 根据号源池ID找到所有关联的号源槽
|
||||||
List<ScheduleSlot> slots = scheduleSlotService.list(
|
List<ScheduleSlot> slots = scheduleSlotService.list(
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ScheduleSlot>()
|
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ScheduleSlot>()
|
||||||
.in("pool_id", poolIds));
|
.in("pool_id", poolIds));
|
||||||
|
|
||||||
if (ObjectUtil.isNotEmpty(slots)) {
|
if (ObjectUtil.isNotEmpty(slots)) {
|
||||||
List<Integer> slotIds = slots.stream().map(ScheduleSlot::getId)
|
List<Long> slotIds = slots.stream().map(ScheduleSlot::getId)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
// 3. 逻辑删除所有号源槽
|
// 3. 逻辑删除所有号源槽
|
||||||
scheduleSlotService.removeByIds(slotIds);
|
scheduleSlotService.removeByIds(slotIds);
|
||||||
|
|||||||
@@ -4,28 +4,22 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.openhis.administration.domain.Patient;
|
import com.openhis.administration.domain.Patient;
|
||||||
import com.openhis.administration.service.IPatientService;
|
import com.openhis.administration.service.IPatientService;
|
||||||
import com.openhis.appointmentmanage.domain.DoctorSchedule;
|
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||||
import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper;
|
|
||||||
import com.openhis.appointmentmanage.service.IDoctorScheduleService;
|
|
||||||
import com.openhis.clinical.domain.Order;
|
|
||||||
import com.openhis.clinical.domain.Ticket;
|
import com.openhis.clinical.domain.Ticket;
|
||||||
import com.openhis.clinical.mapper.OrderMapper;
|
|
||||||
import com.openhis.clinical.service.ITicketService;
|
import com.openhis.clinical.service.ITicketService;
|
||||||
import com.openhis.web.appointmentmanage.appservice.IDoctorScheduleAppService;
|
|
||||||
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
|
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
|
||||||
import com.openhis.web.appointmentmanage.dto.TicketDto;
|
import com.openhis.web.appointmentmanage.dto.TicketDto;
|
||||||
|
import com.openhis.common.constant.CommonConstants.SlotStatus;
|
||||||
|
import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.time.*;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Locale;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 号源管理应用服务实现类
|
* 号源管理应用服务实现类
|
||||||
*
|
*
|
||||||
@@ -36,73 +30,41 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ITicketService ticketService;
|
private ITicketService ticketService;
|
||||||
|
@Resource
|
||||||
|
private ScheduleSlotMapper scheduleSlotMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private IPatientService patientService;
|
private IPatientService patientService;
|
||||||
@Resource
|
|
||||||
private IDoctorScheduleAppService doctorScheduleAppService;
|
|
||||||
@Resource
|
|
||||||
private DoctorScheduleMapper doctorScheduleMapper;
|
|
||||||
@Resource
|
|
||||||
private OrderMapper orderMapper;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TicketAppServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(TicketAppServiceImpl.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预约号源
|
* 预约号源 (重构版:精准锁定单一槽位)
|
||||||
*
|
*
|
||||||
* @param params 预约参数
|
* @param dto 预约参数
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> bookTicket(Map<String, Object> params) {
|
public R<?> bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto) {
|
||||||
// 1. 获取 ticketId 和 slotId
|
Long slotId = dto.getSlotId();
|
||||||
Long ticketId = null;
|
if (slotId == null) {
|
||||||
Long slotId = null;
|
return R.fail("参数校验失败:缺少排班槽位唯一标识");
|
||||||
if (params.get("ticketId") != null) {
|
|
||||||
ticketId = Long.valueOf(params.get("ticketId").toString());
|
|
||||||
}
|
}
|
||||||
if (params.get("slotId") != null) {
|
|
||||||
slotId = Long.valueOf(params.get("slotId").toString());
|
|
||||||
}
|
|
||||||
// 2. 参数校验
|
|
||||||
if (ticketId == null || slotId == null) {
|
|
||||||
return R.fail("参数错误:ticketId 或 slotId 不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 3. 执行原有的预约逻辑
|
int result = ticketService.bookTicket(dto);
|
||||||
int result = ticketService.bookTicket(params);
|
|
||||||
if (result > 0) {
|
if (result > 0) {
|
||||||
// 4. 预约成功后,更新排班表状态
|
return R.ok("预约成功!号源已安全锁定。");
|
||||||
DoctorSchedule schedule = new DoctorSchedule();
|
|
||||||
schedule.setId(slotId); // 对应 XML 中的 WHERE id = #{id}
|
|
||||||
schedule.setIsStopped(true); // 设置为已预约
|
|
||||||
schedule.setStopReason("booked"); // 设置停用原因
|
|
||||||
|
|
||||||
// 执行更新
|
|
||||||
int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule);
|
|
||||||
|
|
||||||
if (updateCount > 0) {
|
|
||||||
return R.ok("预约成功并已更新排班状态");
|
|
||||||
} else {
|
|
||||||
// 如果更新失败,可能需要根据业务逻辑决定是否回滚预约
|
|
||||||
return R.ok("预约成功,但排班状态更新失败");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return R.fail("预约失败");
|
|
||||||
}
|
}
|
||||||
|
return R.fail("预约挂单核发失败");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// e.printStackTrace();
|
log.error("大厅挂号捕获系统异常", e);
|
||||||
log.error(e.getMessage());
|
|
||||||
return R.fail("系统异常:" + e.getMessage());
|
return R.fail("系统异常:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消预约
|
* 取消预约 (重构版:精准释放单一槽位)
|
||||||
*
|
*
|
||||||
* @param slotId 医生排班ID
|
* @param slotId 医生槽位排班ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -111,18 +73,8 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
return R.fail("参数错误");
|
return R.fail("参数错误");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ticketService.cancelTicket(slotId);
|
int result = ticketService.cancelTicket(slotId);
|
||||||
DoctorSchedule schedule = new DoctorSchedule();
|
return R.ok(result > 0 ? "取消成功,号源已重新释放回市场" : "取消失败");
|
||||||
schedule.setId(slotId); // 对应 WHERE id = #{id}
|
|
||||||
schedule.setIsStopped(false); // 设置为 false
|
|
||||||
schedule.setStopReason(""); // 将原因清空 (设为空字符串)
|
|
||||||
// 3. 调用自定义更新方法
|
|
||||||
int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule);
|
|
||||||
if (updateCount > 0) {
|
|
||||||
return R.ok("取消成功");
|
|
||||||
} else {
|
|
||||||
return R.ok("取消成功");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail(e.getMessage());
|
return R.fail(e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -131,16 +83,16 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
/**
|
/**
|
||||||
* 取号
|
* 取号
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> checkInTicket(Long ticketId) {
|
public R<?> checkInTicket(Long slotId) {
|
||||||
if (ticketId == null) {
|
if (slotId == null) {
|
||||||
return R.fail("参数错误");
|
return R.fail("参数错误");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
int result = ticketService.checkInTicket(ticketId);
|
int result = ticketService.checkInTicket(slotId);
|
||||||
return R.ok(result > 0 ? "取号成功" : "取号失败");
|
return R.ok(result > 0 ? "取号成功" : "取号失败");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail(e.getMessage());
|
return R.fail(e.getMessage());
|
||||||
@@ -150,109 +102,308 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
/**
|
/**
|
||||||
* 停诊
|
* 停诊
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> cancelConsultation(Long ticketId) {
|
public R<?> cancelConsultation(Long slotId) {
|
||||||
if (ticketId == null) {
|
if (slotId == null) {
|
||||||
return R.fail("参数错误");
|
return R.fail("参数错误");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
int result = ticketService.cancelConsultation(ticketId);
|
int result = ticketService.cancelConsultation(slotId);
|
||||||
return R.ok(result > 0 ? "停诊成功" : "停诊失败");
|
return R.ok(result > 0 ? "停诊成功" : "停诊失败");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail(e.getMessage());
|
return R.fail(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> listAllTickets() {
|
public R<?> listTicket(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
|
||||||
// 1. 从 AppService 获取排班数据
|
// 1. 防空指针处理
|
||||||
R<?> response = doctorScheduleAppService.getDoctorScheduleList();
|
if (query == null) {
|
||||||
// 获取返回的 List 数据 (假设 R.ok 里的数据是 List<DoctorSchedule>)
|
query = new com.openhis.appointmentmanage.dto.TicketQueryDTO();
|
||||||
List<DoctorSchedule> scheduleList = (List<DoctorSchedule>) response.getData();
|
}
|
||||||
|
normalizeQueryStatus(query);
|
||||||
|
|
||||||
// 2. 转换数据为 TicketDto
|
// 2. 构造 MyBatis 的分页对象 (传入前端给的当前页和每页条数)
|
||||||
List<TicketDto> tickets = new ArrayList<>();
|
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.openhis.appointmentmanage.domain.TicketSlotDTO> pageParam = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(
|
||||||
|
query.getPage(), query.getLimit());
|
||||||
|
|
||||||
if (scheduleList != null) {
|
// 3. 调用刚才写的底层动态 SQL 查询!
|
||||||
for (DoctorSchedule schedule : scheduleList) {
|
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.openhis.appointmentmanage.domain.TicketSlotDTO> rawPage = scheduleSlotMapper
|
||||||
|
.selectTicketSlotsPage(pageParam, query);
|
||||||
|
|
||||||
|
// 4. 将查出来的数据翻译为前端可以直接渲染的结构
|
||||||
|
java.util.List<TicketDto> tickets = new java.util.ArrayList<>();
|
||||||
|
if (rawPage.getRecords() != null) {
|
||||||
|
for (com.openhis.appointmentmanage.domain.TicketSlotDTO raw : rawPage.getRecords()) {
|
||||||
TicketDto dto = new TicketDto();
|
TicketDto dto = new TicketDto();
|
||||||
|
|
||||||
// 基础信息映射
|
// 基础字段映射
|
||||||
dto.setSlot_id(Long.valueOf(schedule.getId())); // Integer 转 Long
|
dto.setSlot_id(raw.getSlotId());
|
||||||
dto.setBusNo(String.valueOf(schedule.getId())); // 生成一个业务编号
|
dto.setSeqNo(raw.getSeqNo());
|
||||||
dto.setDepartment(String.valueOf(schedule.getDeptId())); // 如果有科室名建议关联查询,这里暂填ID
|
dto.setBusNo(String.valueOf(raw.getSlotId()));
|
||||||
dto.setDoctor(schedule.getDoctor());
|
dto.setDoctor(raw.getDoctor());
|
||||||
|
dto.setDepartment(raw.getDepartmentName()); // 注意:以前这里传成了ID,导致前端出Bug,现在修复成了真正的科室名
|
||||||
|
dto.setFee(raw.getFee());
|
||||||
|
dto.setPatientName(raw.getPatientName());
|
||||||
|
dto.setPatientId(raw.getMedicalCard());
|
||||||
|
dto.setPhone(raw.getPhone());
|
||||||
|
dto.setIdCard(raw.getIdCard());
|
||||||
|
dto.setDoctorId(raw.getDoctorId());
|
||||||
|
dto.setDepartmentId(raw.getDepartmentId());
|
||||||
|
dto.setRealPatientId(raw.getPatientId());
|
||||||
|
dto.setOrderId(raw.getOrderId());
|
||||||
|
dto.setOrderNo(raw.getOrderNo());
|
||||||
|
|
||||||
// 号源类型处理:根据挂号项目判断是普通号还是专家号
|
// 性别处理:直接使用患者表中的 genderEnum
|
||||||
String registerItem = schedule.getRegisterItem();
|
Integer genderEnum = raw.getGenderEnum();
|
||||||
if (registerItem != null && registerItem.contains("专家")) {
|
if (genderEnum != null) {
|
||||||
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
|
dto.setGender("男");
|
||||||
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
|
dto.setGender("女");
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setGender("未知");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.getRegType() != null && raw.getRegType() == 1) {
|
||||||
dto.setTicketType("expert");
|
dto.setTicketType("expert");
|
||||||
} else {
|
} else {
|
||||||
dto.setTicketType("general");
|
dto.setTicketType("general");
|
||||||
}
|
}
|
||||||
// 时间处理:格式化为日期+时间范围,如 "2025-12-01 08:00-12:00"
|
|
||||||
String currentDate = LocalDate.now().toString(); // 或者从schedule中获取具体日期
|
|
||||||
String timeRange = schedule.getStartTime() + "-" + schedule.getEndTime();
|
|
||||||
dto.setDateTime(currentDate + " " + timeRange);
|
|
||||||
LocalTime nowTime = LocalTime.now();
|
|
||||||
LocalTime endTime = schedule.getEndTime();
|
|
||||||
String stopReason1 = schedule.getStopReason();
|
|
||||||
if ("cancelled".equals(stopReason1)||(endTime != null && nowTime.isAfter(endTime))) {
|
|
||||||
dto.setStatus("已停诊");
|
|
||||||
}else if (Boolean.TRUE.equals(schedule.getIsStopped())) {
|
|
||||||
// 获取原因并处理可能的空值
|
|
||||||
String stopReason = schedule.getStopReason();
|
|
||||||
// 使用 .equals() 比较内容,并将常量放在前面防止空指针
|
|
||||||
if ("booked".equals(stopReason)) {
|
|
||||||
dto.setStatus("已预约");
|
|
||||||
// --- 新增:获取患者信息 ---
|
|
||||||
List<Order> Order = orderMapper.selectOrderBySlotId(Long.valueOf(schedule.getId()));
|
|
||||||
Order latestOrder=Order.get(0);
|
|
||||||
|
|
||||||
if (latestOrder != null) {
|
if (raw.getScheduleDate() != null && raw.getExpectTime() != null) {
|
||||||
dto.setPatientName(latestOrder.getPatientName());
|
dto.setDateTime(raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
|
||||||
dto.setPatientId(String.valueOf(latestOrder.getPatientId()));
|
try {
|
||||||
dto.setPhone(latestOrder.getPhone());
|
String timeStr = raw.getAppointmentTime() != null ? raw.getAppointmentTime() : (raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
|
||||||
}
|
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(timeStr.length() > 10 ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd");
|
||||||
// -----------------------
|
java.util.Date date = sdf.parse(timeStr);
|
||||||
} else if ("checked".equals(stopReason)) {
|
dto.setAppointmentDate(date);
|
||||||
dto.setStatus("已取号");
|
dto.setAppointmentTime(date);
|
||||||
} else {
|
} catch (Exception e) {
|
||||||
// 兜底逻辑:如果 is_stopped 为 true 但没有匹配到原因
|
dto.setAppointmentDate(new java.util.Date());
|
||||||
dto.setStatus("不可预约");
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// is_stopped 为 false 或 null 时
|
|
||||||
dto.setStatus("未预约");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 费用处理 (挂号费 + 诊疗费)
|
if (Boolean.TRUE.equals(raw.getIsStopped())) {
|
||||||
int totalFee = schedule.getRegisterFee() + schedule.getDiagnosisFee();
|
dto.setStatus("已停诊");
|
||||||
dto.setFee(String.valueOf(totalFee));
|
} else {
|
||||||
|
Integer slotStatus = raw.getSlotStatus();
|
||||||
// 日期处理:LocalDateTime 转 Date
|
if (slotStatus != null) {
|
||||||
if (schedule.getCreateTime() != null) {
|
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
|
||||||
// 1. 先转成 Instant
|
dto.setStatus("已取号");
|
||||||
Instant instant = schedule.getCreateTime().toInstant();
|
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
|
||||||
// 2. 结合时区转成 ZonedDateTime
|
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) {
|
||||||
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
|
dto.setStatus("已取号");
|
||||||
// 3. 再转回 Date (如果 DTO 需要的是 Date)
|
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
|
||||||
dto.setAppointmentDate(Date.from(zdt.toInstant()));
|
dto.setStatus("已退号");
|
||||||
|
} else {
|
||||||
|
dto.setStatus("已预约");
|
||||||
|
}
|
||||||
|
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已退号");
|
||||||
|
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已停诊");
|
||||||
|
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已锁定");
|
||||||
|
} else {
|
||||||
|
dto.setStatus("未预约");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setStatus("未预约");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tickets.add(dto);
|
tickets.add(dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 封装分页响应结构
|
// 5. 按照前端组件需要的【真分页】格式进行包装,并返回
|
||||||
Map<String, Object> result = new HashMap<>();
|
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||||
|
result.put("list", tickets);
|
||||||
|
result.put("total", rawPage.getTotal()); // 这个 total 就是底层用 COUNT(*) 算出来的真实总条数!
|
||||||
|
result.put("page", query.getPage());
|
||||||
|
result.put("limit", query.getLimit());
|
||||||
|
|
||||||
|
return R.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据
|
||||||
|
*/
|
||||||
|
private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
|
||||||
|
String rawStatus = query.getStatus();
|
||||||
|
if (rawStatus == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String normalized = rawStatus.trim();
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
query.setStatus(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String lower = normalized.toLowerCase(Locale.ROOT);
|
||||||
|
switch (lower) {
|
||||||
|
case "all":
|
||||||
|
case "全部":
|
||||||
|
query.setStatus("all");
|
||||||
|
break;
|
||||||
|
case "unbooked":
|
||||||
|
case "0":
|
||||||
|
case "未预约":
|
||||||
|
query.setStatus("unbooked");
|
||||||
|
break;
|
||||||
|
case "booked":
|
||||||
|
case "1":
|
||||||
|
case "已预约":
|
||||||
|
query.setStatus("booked");
|
||||||
|
break;
|
||||||
|
case "checked":
|
||||||
|
case "checkin":
|
||||||
|
case "checkedin":
|
||||||
|
case "2":
|
||||||
|
case "已取号":
|
||||||
|
query.setStatus("checked");
|
||||||
|
break;
|
||||||
|
case "cancelled":
|
||||||
|
case "canceled":
|
||||||
|
case "3":
|
||||||
|
case "已停诊":
|
||||||
|
case "已取消":
|
||||||
|
query.setStatus("cancelled");
|
||||||
|
break;
|
||||||
|
case "returned":
|
||||||
|
case "4":
|
||||||
|
case "5":
|
||||||
|
case "已退号":
|
||||||
|
query.setStatus("returned");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 设置为 impossible 值,配合 mapper 的 otherwise 分支直接返回空
|
||||||
|
query.setStatus("__invalid__");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> listDoctorAvailability(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
|
||||||
|
if (query == null) {
|
||||||
|
query = new com.openhis.appointmentmanage.dto.TicketQueryDTO();
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.List<com.openhis.appointmentmanage.domain.DoctorAvailabilityDTO> rawList = scheduleSlotMapper
|
||||||
|
.selectDoctorAvailabilitySummary(query);
|
||||||
|
java.util.List<java.util.Map<String, Object>> doctors = new java.util.ArrayList<>();
|
||||||
|
if (rawList != null) {
|
||||||
|
for (com.openhis.appointmentmanage.domain.DoctorAvailabilityDTO item : rawList) {
|
||||||
|
java.util.Map<String, Object> row = new java.util.HashMap<>();
|
||||||
|
String doctorName = item.getDoctorName();
|
||||||
|
Long doctorId = item.getDoctorId();
|
||||||
|
row.put("id", doctorId != null ? String.valueOf(doctorId) : doctorName);
|
||||||
|
row.put("name", doctorName);
|
||||||
|
row.put("available", item.getAvailable() == null ? 0 : item.getAvailable());
|
||||||
|
row.put("type", item.getTicketType() == null ? "general" : item.getTicketType());
|
||||||
|
doctors.add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return R.ok(doctors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R<?> listAllTickets() {
|
||||||
|
// 1. 调用最新的 Mapper,直接从数据库抽出我们半成品的 DTO(强类型!)
|
||||||
|
List<com.openhis.appointmentmanage.domain.TicketSlotDTO> rawDtos = scheduleSlotMapper.selectAllTicketSlots();
|
||||||
|
|
||||||
|
// 这是真正要发给前端展示的包裹外卖盒
|
||||||
|
List<TicketDto> tickets = new ArrayList<>();
|
||||||
|
|
||||||
|
if (rawDtos != null) {
|
||||||
|
for (com.openhis.appointmentmanage.domain.TicketSlotDTO raw : rawDtos) {
|
||||||
|
TicketDto dto = new TicketDto();
|
||||||
|
|
||||||
|
// --- 基础字段处理 ---
|
||||||
|
// 注意:这里已经变成了极其舒服的 .getSlotId() 方法调用,告别魔鬼字符串!
|
||||||
|
dto.setSlot_id(raw.getSlotId());
|
||||||
|
dto.setSeqNo(raw.getSeqNo());
|
||||||
|
dto.setBusNo(String.valueOf(raw.getSlotId())); // 暂时借用真实槽位ID做唯一流水号
|
||||||
|
dto.setDoctor(raw.getDoctor());
|
||||||
|
dto.setDepartment(raw.getDepartmentName());
|
||||||
|
dto.setFee(raw.getFee());
|
||||||
|
dto.setPatientName(raw.getPatientName());
|
||||||
|
dto.setPatientId(raw.getMedicalCard());
|
||||||
|
dto.setPhone(raw.getPhone());
|
||||||
|
|
||||||
|
// --- 号源类型处理 (普通/专家) ---
|
||||||
|
// 改用底层 adm_doctor_schedule 传来的标准数字字典:0=普通,1=专家
|
||||||
|
if (raw.getRegType() != null && raw.getRegType() == 1) {
|
||||||
|
dto.setTicketType("expert");
|
||||||
|
} else {
|
||||||
|
dto.setTicketType("general");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 就诊时间严谨拼接 ---
|
||||||
|
// 拼接出来给前端展示的,如 "2026-03-20 08:30"
|
||||||
|
if (raw.getScheduleDate() != null && raw.getExpectTime() != null) {
|
||||||
|
dto.setDateTime(raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
|
||||||
|
try {
|
||||||
|
String timeStr = raw.getAppointmentTime() != null ? raw.getAppointmentTime() : (raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
|
||||||
|
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(timeStr.length() > 10 ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd");
|
||||||
|
java.util.Date date = sdf.parse(timeStr);
|
||||||
|
dto.setAppointmentDate(date);
|
||||||
|
dto.setAppointmentTime(date);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("时间解析失败", e);
|
||||||
|
dto.setAppointmentDate(new java.util.Date());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 核心逻辑:精准状态分类 ---
|
||||||
|
// 第一关:底层硬性停诊拦截
|
||||||
|
if (Boolean.TRUE.equals(raw.getIsStopped())) {
|
||||||
|
dto.setStatus("已停诊");
|
||||||
|
} else {
|
||||||
|
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已取消...)
|
||||||
|
Integer slotStatus = raw.getSlotStatus();
|
||||||
|
if (slotStatus != null) {
|
||||||
|
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已取号");
|
||||||
|
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
|
||||||
|
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) {
|
||||||
|
dto.setStatus("已取号");
|
||||||
|
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
|
||||||
|
dto.setStatus("已退号");
|
||||||
|
} else {
|
||||||
|
dto.setStatus("已预约");
|
||||||
|
}
|
||||||
|
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已退号");
|
||||||
|
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已停诊");
|
||||||
|
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
|
||||||
|
dto.setStatus("已锁定");
|
||||||
|
} else {
|
||||||
|
dto.setStatus("未预约");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setStatus("未预约");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tickets.add(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 封装分页响应结构并吐给前端
|
||||||
|
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||||
result.put("list", tickets);
|
result.put("list", tickets);
|
||||||
result.put("total", tickets.size());
|
result.put("total", tickets.size());
|
||||||
result.put("page", 1);
|
result.put("page", 1);
|
||||||
result.put("limit", 20);
|
result.put("limit", 20);
|
||||||
|
|
||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +419,7 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
dto.setBusNo(ticket.getBusNo());
|
dto.setBusNo(ticket.getBusNo());
|
||||||
dto.setDepartment(ticket.getDepartment());
|
dto.setDepartment(ticket.getDepartment());
|
||||||
dto.setDoctor(ticket.getDoctor());
|
dto.setDoctor(ticket.getDoctor());
|
||||||
|
|
||||||
// 处理号源类型(转换为英文,前端期望的是general或expert)
|
// 处理号源类型(转换为英文,前端期望的是general或expert)
|
||||||
String ticketType = ticket.getTicketType();
|
String ticketType = ticket.getTicketType();
|
||||||
if ("普通".equals(ticketType)) {
|
if ("普通".equals(ticketType)) {
|
||||||
@@ -278,10 +429,10 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
} else {
|
} else {
|
||||||
dto.setTicketType(ticketType);
|
dto.setTicketType(ticketType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理号源时间(dateTime)
|
// 处理号源时间(dateTime)
|
||||||
dto.setDateTime(ticket.getTime());
|
dto.setDateTime(ticket.getTime());
|
||||||
|
|
||||||
// 处理号源状态(转换为中文)
|
// 处理号源状态(转换为中文)
|
||||||
String status = ticket.getStatus();
|
String status = ticket.getStatus();
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -300,32 +451,29 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
default:
|
default:
|
||||||
dto.setStatus(status);
|
dto.setStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.setFee(ticket.getFee());
|
dto.setFee(ticket.getFee());
|
||||||
dto.setPatientName(ticket.getPatientName());
|
dto.setPatientName(ticket.getPatientName());
|
||||||
dto.setPatientId(ticket.getMedicalCard()); // 就诊卡号
|
dto.setPatientId(ticket.getMedicalCard()); // 就诊卡号
|
||||||
dto.setPhone(ticket.getPhone());
|
dto.setPhone(ticket.getPhone());
|
||||||
|
|
||||||
// 获取患者性别
|
// 获取患者性别
|
||||||
if (ticket.getPatientId() != null) {
|
if (ticket.getPatientId() != null) {
|
||||||
Patient patient = patientService.getById(ticket.getPatientId());
|
Patient patient = patientService.getById(ticket.getPatientId());
|
||||||
if (patient != null) {
|
if (patient != null) {
|
||||||
Integer genderEnum = patient.getGenderEnum();
|
Integer genderEnum = patient.getGenderEnum();
|
||||||
if (genderEnum != null) {
|
if (genderEnum != null) {
|
||||||
switch (genderEnum) {
|
if (Integer.valueOf(1).equals(genderEnum)) {
|
||||||
case 1:
|
dto.setGender("男");
|
||||||
dto.setGender("男");
|
} else if (Integer.valueOf(2).equals(genderEnum)) {
|
||||||
break;
|
dto.setGender("女");
|
||||||
case 2:
|
} else {
|
||||||
dto.setGender("女");
|
dto.setGender("未知");
|
||||||
break;
|
|
||||||
default:
|
|
||||||
dto.setGender("未知");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.setAppointmentDate(ticket.getAppointmentDate());
|
dto.setAppointmentDate(ticket.getAppointmentDate());
|
||||||
dto.setAppointmentTime(ticket.getAppointmentTime());
|
dto.setAppointmentTime(ticket.getAppointmentTime());
|
||||||
dto.setDepartmentId(ticket.getDepartmentId());
|
dto.setDepartmentId(ticket.getDepartmentId());
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.openhis.web.appointmentmanage.controller;
|
package com.openhis.web.appointmentmanage.controller;
|
||||||
|
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.appointmentmanage.domain.AppointmentConfig;
|
||||||
|
import com.openhis.web.appointmentmanage.appservice.IAppointmentConfigAppService;
|
||||||
import com.openhis.web.appointmentmanage.appservice.IDeptAppService;
|
import com.openhis.web.appointmentmanage.appservice.IDeptAppService;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
@@ -16,6 +15,9 @@ public class DeptController {
|
|||||||
@Resource
|
@Resource
|
||||||
private IDeptAppService deptAppService;
|
private IDeptAppService deptAppService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IAppointmentConfigAppService appointmentConfigAppService;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 获取科室列表
|
* 获取科室列表
|
||||||
*
|
*
|
||||||
@@ -38,4 +40,22 @@ public class DeptController {
|
|||||||
){
|
){
|
||||||
return R.ok(deptAppService.searchDept(pageNo,pageSize,orgName,deptName));
|
return R.ok(deptAppService.searchDept(pageNo,pageSize,orgName,deptName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 获取预约配置
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
@GetMapping("/config")
|
||||||
|
public R<?> getAppointmentConfig(){
|
||||||
|
return appointmentConfigAppService.getAppointmentConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 保存预约配置
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
@PostMapping("/config")
|
||||||
|
public R<?> saveAppointmentConfig(@RequestBody AppointmentConfig appointmentConfig){
|
||||||
|
return appointmentConfigAppService.saveAppointmentConfig(appointmentConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ public class DoctorScheduleController {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* 新增医生排班(带具体日期)
|
* 新增医生排班(带具体日期)
|
||||||
*
|
|
||||||
* */
|
* */
|
||||||
@PostMapping("/add-with-date")
|
@PostMapping("/add-with-date")
|
||||||
public R<?> addDoctorScheduleWithDate(@RequestBody DoctorSchedule doctorSchedule) {
|
public R<?> addDoctorScheduleWithDate(@RequestBody DoctorSchedule doctorSchedule) {
|
||||||
@@ -77,7 +76,7 @@ public class DoctorScheduleController {
|
|||||||
* */
|
* */
|
||||||
@DeleteMapping("/delete/{doctorScheduleId}")
|
@DeleteMapping("/delete/{doctorScheduleId}")
|
||||||
public R<?> removeDoctorSchedule(@PathVariable Integer doctorScheduleId){
|
public R<?> removeDoctorSchedule(@PathVariable Integer doctorScheduleId){
|
||||||
return R.ok(doctorScheduleAppService.removeDoctorSchedule(doctorScheduleId));
|
return doctorScheduleAppService.removeDoctorSchedule(doctorScheduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package com.openhis.web.appointmentmanage.controller;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.core.common.annotation.Anonymous;
|
import com.core.common.annotation.Anonymous;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.appointmentmanage.domain.AppointmentBookDTO;
|
||||||
|
import com.openhis.appointmentmanage.dto.TicketQueryDTO;
|
||||||
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
|
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
|
||||||
import com.openhis.web.appointmentmanage.dto.TicketDto;
|
import com.openhis.web.appointmentmanage.dto.TicketDto;
|
||||||
|
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -19,11 +23,35 @@ import java.util.Map;
|
|||||||
@RequestMapping("/appointment/ticket")
|
@RequestMapping("/appointment/ticket")
|
||||||
public class TicketController {
|
public class TicketController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询门诊号源列表 (带多条件过滤)
|
||||||
|
*
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 分页号源列表
|
||||||
|
*/
|
||||||
|
@Anonymous
|
||||||
|
@PostMapping("/list")
|
||||||
|
public R<?> listTicket(@RequestBody @Validated TicketQueryDTO query) {
|
||||||
|
return ticketAppService.listTicket(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询医生余号汇总(基于号源池,不受分页影响)
|
||||||
|
*
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 医生余号列表
|
||||||
|
*/
|
||||||
|
@Anonymous
|
||||||
|
@PostMapping("/doctorSummary")
|
||||||
|
public R<?> listDoctorAvailability(@RequestBody @Validated TicketQueryDTO query) {
|
||||||
|
return ticketAppService.listDoctorAvailability(query);
|
||||||
|
}
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ITicketAppService ticketAppService;
|
private ITicketAppService ticketAppService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询所有号源(用于测试)
|
* 查询所有号源
|
||||||
*
|
*
|
||||||
* @return 所有号源列表
|
* @return 所有号源列表
|
||||||
*/
|
*/
|
||||||
@@ -36,44 +64,44 @@ public class TicketController {
|
|||||||
/**
|
/**
|
||||||
* 预约号源
|
* 预约号源
|
||||||
*
|
*
|
||||||
* @param params 预约参数
|
* @param dto 预约参数
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/book")
|
@PostMapping("/book")
|
||||||
public R<?> bookTicket(@RequestBody Map<String, Object> params) {
|
public R<?> bookTicket(@RequestBody @Validated AppointmentBookDTO dto) {
|
||||||
return ticketAppService.bookTicket(params);
|
return ticketAppService.bookTicket(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消预约
|
* 取消预约
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/cancel")
|
@PostMapping("/cancel")
|
||||||
public R<?> cancelTicket(@RequestParam Long ticketId) {
|
public R<?> cancelTicket(@RequestParam Long slotId) {
|
||||||
return ticketAppService.cancelTicket(ticketId);
|
return ticketAppService.cancelTicket(slotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取号
|
* 取号
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/checkin")
|
@PostMapping("/checkin")
|
||||||
public R<?> checkInTicket(@RequestParam Long ticketId) {
|
public R<?> checkInTicket(@RequestParam Long slotId) {
|
||||||
return ticketAppService.checkInTicket(ticketId);
|
return ticketAppService.checkInTicket(slotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停诊
|
* 停诊
|
||||||
*
|
*
|
||||||
* @param ticketId 号源ID
|
* @param slotId 槽位ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/cancelConsultation")
|
@PostMapping("/cancelConsultation")
|
||||||
public R<?> cancelConsultation(@RequestParam Long ticketId) {
|
public R<?> cancelConsultation(@RequestParam Long slotId) {
|
||||||
return ticketAppService.cancelConsultation(ticketId);
|
return ticketAppService.cancelConsultation(slotId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ public class TicketDto {
|
|||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long slot_id;
|
private Long slot_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 号源序号(对应 adm_schedule_slot.seq_no)
|
||||||
|
*/
|
||||||
|
private Integer seqNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 号源编码
|
* 号源编码
|
||||||
*/
|
*/
|
||||||
@@ -49,7 +54,7 @@ public class TicketDto {
|
|||||||
private String dateTime;
|
private String dateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态 (unbooked:未预约, booked:已预约, checked:已取号, cancelled:已取消, locked:已锁定)
|
* 状态
|
||||||
*/
|
*/
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@@ -99,4 +104,26 @@ public class TicketDto {
|
|||||||
*/
|
*/
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long doctorId;
|
private Long doctorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真实患者ID(数据库主键,区别于 patientId 存的就诊卡号)
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long realPatientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 身份证号
|
||||||
|
*/
|
||||||
|
private String idCard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约订单ID
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long orderId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约订单号
|
||||||
|
*/
|
||||||
|
private String orderNo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.openhis.web.basedatamanage.appservice.impl;
|
package com.openhis.web.basedatamanage.appservice.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.core.common.utils.AssignSeqUtil;
|
import com.core.common.utils.AssignSeqUtil;
|
||||||
import com.core.common.utils.MessageUtils;
|
import com.core.common.utils.MessageUtils;
|
||||||
import com.core.common.utils.StringUtils;
|
import com.core.common.utils.StringUtils;
|
||||||
import com.openhis.administration.domain.Organization;
|
import com.openhis.administration.domain.Organization;
|
||||||
|
import com.openhis.administration.mapper.OrganizationMapper;
|
||||||
import com.openhis.administration.service.IOrganizationService;
|
import com.openhis.administration.service.IOrganizationService;
|
||||||
import com.openhis.common.constant.CommonConstants;
|
import com.openhis.common.constant.CommonConstants;
|
||||||
import com.openhis.common.constant.PromptMsgConstant;
|
import com.openhis.common.constant.PromptMsgConstant;
|
||||||
@@ -35,12 +37,15 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
@Resource
|
@Resource
|
||||||
private AssignSeqUtil assignSeqUtil;
|
private AssignSeqUtil assignSeqUtil;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OrganizationMapper organizationMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum,
|
public Page<OrganizationDto> getOrganizationTree(Integer pageNo, Integer pageSize, String name, Integer typeEnum,
|
||||||
List<String> classEnumList,
|
List<String> classEnumList,
|
||||||
String sortField, String sortOrder, HttpServletRequest request) {
|
String sortField, String sortOrder, HttpServletRequest request) {
|
||||||
|
|
||||||
// 使用Page对象进行分页查询
|
// 使用 Page 对象进行分页查询
|
||||||
Page<Organization> page = new Page<>(pageNo, pageSize);
|
Page<Organization> page = new Page<>(pageNo, pageSize);
|
||||||
|
|
||||||
// 创建查询条件
|
// 创建查询条件
|
||||||
@@ -54,7 +59,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
queryWrapper.eq(Organization::getTypeEnum, typeEnum);
|
queryWrapper.eq(Organization::getTypeEnum, typeEnum);
|
||||||
}
|
}
|
||||||
if (classEnumList != null && !classEnumList.isEmpty()) {
|
if (classEnumList != null && !classEnumList.isEmpty()) {
|
||||||
// 使用OR条件来匹配class_enum字段中包含任一值的记录
|
// 使用 OR 条件来匹配 class_enum 字段中包含任一值的记录
|
||||||
queryWrapper.and(wrapper -> {
|
queryWrapper.and(wrapper -> {
|
||||||
for (int i = 0; i < classEnumList.size(); i++) {
|
for (int i = 0; i < classEnumList.size(); i++) {
|
||||||
String classEnum = classEnumList.get(i);
|
String classEnum = classEnumList.get(i);
|
||||||
@@ -63,18 +68,18 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
wrapper.and(subWrapper -> {
|
wrapper.and(subWrapper -> {
|
||||||
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
||||||
.or() // 或者
|
.or() // 或者
|
||||||
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
||||||
.or() // 或者
|
.or() // 或者
|
||||||
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
||||||
.or() // 或者
|
.or() // 或者
|
||||||
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
|
.like(Organization::getClassEnum, "," + classEnum + ","); // 在中间,被逗号包围
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 后续条件使用OR连接
|
// 后续条件使用 OR 连接
|
||||||
wrapper.or(subWrapper -> {
|
wrapper.or(subWrapper -> {
|
||||||
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
subWrapper.eq(Organization::getClassEnum, classEnum) // 精确匹配
|
||||||
.or() // 或者
|
.or() // 或者
|
||||||
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
.likeRight(Organization::getClassEnum, classEnum + ",") // 以"值,"开头
|
||||||
.or() // 或者
|
.or() // 或者
|
||||||
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
.likeLeft(Organization::getClassEnum, "," + classEnum) // 以",值"结尾
|
||||||
.or() // 或者
|
.or() // 或者
|
||||||
@@ -88,7 +93,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
// 执行分页查询
|
// 执行分页查询
|
||||||
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
|
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
|
||||||
|
|
||||||
// 将查询结果转为DTO并构建树结构
|
// 将查询结果转为 DTO 并构建树结构
|
||||||
List<Organization> organizationList = resultPage.getRecords();
|
List<Organization> organizationList = resultPage.getRecords();
|
||||||
List<OrganizationDto> orgTree = buildTree(organizationList);
|
List<OrganizationDto> orgTree = buildTree(organizationList);
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
* @return tree
|
* @return tree
|
||||||
*/
|
*/
|
||||||
private List<OrganizationDto> buildTree(List<Organization> records) {
|
private List<OrganizationDto> buildTree(List<Organization> records) {
|
||||||
// 按b_no的层级排序,确保父节点先处理
|
// 按 b_no 的层级排序,确保父节点先处理
|
||||||
List<Organization> sortedRecords = records.stream()
|
List<Organization> sortedRecords = records.stream()
|
||||||
.sorted(Comparator.comparingInt(r -> r.getBusNo().split("\\.").length)).collect(Collectors.toList());
|
.sorted(Comparator.comparingInt(r -> r.getBusNo().split("\\.").length)).collect(Collectors.toList());
|
||||||
|
|
||||||
@@ -131,7 +136,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
// 根节点
|
// 根节点
|
||||||
tree.add(node);
|
tree.add(node);
|
||||||
} else {
|
} else {
|
||||||
// 获取父节点的b_no(去掉最后一部分)
|
// 获取父节点的 b_no(去掉最后一部分)
|
||||||
String parentBNo = String.join(".", Arrays.copyOf(parts, parts.length - 1));
|
String parentBNo = String.join(".", Arrays.copyOf(parts, parts.length - 1));
|
||||||
OrganizationDto parent = nodeMap.get(parentBNo);
|
OrganizationDto parent = nodeMap.get(parentBNo);
|
||||||
|
|
||||||
@@ -149,7 +154,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
/**
|
/**
|
||||||
* 机构信息详情
|
* 机构信息详情
|
||||||
*
|
*
|
||||||
* @param orgId 机构信息id
|
* @param orgId 机构信息 id
|
||||||
* @return 机构信息详情
|
* @return 机构信息详情
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -159,7 +164,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, new Object[] { "机构信息" }));
|
return R.fail(MessageUtils.createMessage(PromptMsgConstant.Common.M00006, new Object[] { "机构信息" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为DTO对象,确保数据格式一致
|
// 转换为 DTO 对象,确保数据格式一致
|
||||||
OrganizationDto organizationDto = new OrganizationDto();
|
OrganizationDto organizationDto = new OrganizationDto();
|
||||||
BeanUtils.copyProperties(organization, organizationDto);
|
BeanUtils.copyProperties(organization, organizationDto);
|
||||||
organizationDto
|
organizationDto
|
||||||
@@ -181,7 +186,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
@Override
|
@Override
|
||||||
public R<?> addOrEditOrganization(OrganizationDto organizationDto) {
|
public R<?> addOrEditOrganization(OrganizationDto organizationDto) {
|
||||||
|
|
||||||
// 新增organization信息
|
// 新增 organization 信息
|
||||||
Organization organization = new Organization();
|
Organization organization = new Organization();
|
||||||
BeanUtils.copyProperties(organizationDto, organization);
|
BeanUtils.copyProperties(organizationDto, organization);
|
||||||
|
|
||||||
@@ -191,9 +196,9 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
} else {
|
} else {
|
||||||
// 活动标识:有效
|
// 活动标识:有效
|
||||||
organization.setActiveFlag(AccountStatus.ACTIVE.getValue());
|
organization.setActiveFlag(AccountStatus.ACTIVE.getValue());
|
||||||
// 采番bus_no三位
|
// 采番 bus_no 三位
|
||||||
String code = assignSeqUtil.getSeq(AssignSeqEnum.ORGANIZATION_BUS_NO.getPrefix(), 3);
|
String code = assignSeqUtil.getSeq(AssignSeqEnum.ORGANIZATION_BUS_NO.getPrefix(), 3);
|
||||||
// 如果传了上级科室 把当前的code拼到后边
|
// 如果传了上级科室 把当前的 code 拼到后边
|
||||||
if (StringUtils.isNotEmpty(organization.getBusNo())) {
|
if (StringUtils.isNotEmpty(organization.getBusNo())) {
|
||||||
organization.setBusNo(String.format(CommonConstants.Common.MONTAGE_FORMAT, organization.getBusNo(),
|
organization.setBusNo(String.format(CommonConstants.Common.MONTAGE_FORMAT, organization.getBusNo(),
|
||||||
CommonConstants.Common.POINT, code));
|
CommonConstants.Common.POINT, code));
|
||||||
@@ -203,7 +208,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
// 生成待发送的机构信息
|
// 生成待发送的机构信息
|
||||||
organizationService.save(organization);
|
organizationService.save(organization);
|
||||||
}
|
}
|
||||||
// 返回机构id
|
// 返回机构 id
|
||||||
return R.ok(organization.getId(),
|
return R.ok(organization.getId(),
|
||||||
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息更新添加" }));
|
MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] { "机构信息更新添加" }));
|
||||||
}
|
}
|
||||||
@@ -211,7 +216,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
/**
|
/**
|
||||||
* 删除机构
|
* 删除机构
|
||||||
*
|
*
|
||||||
* @param orgIds 机构信息id
|
* @param orgIds 机构信息 id
|
||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -232,7 +237,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
/**
|
/**
|
||||||
* 机构启用
|
* 机构启用
|
||||||
*
|
*
|
||||||
* @param orgId 机构信息id
|
* @param orgId 机构信息 id
|
||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -247,7 +252,7 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
/**
|
/**
|
||||||
* 机构停用
|
* 机构停用
|
||||||
*
|
*
|
||||||
* @param orgId 机构信息id
|
* @param orgId 机构信息 id
|
||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -299,38 +304,27 @@ public class OrganizationAppServiceImpl implements IOrganizationAppService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> getRegisterOrganizations(Integer pageNo, Integer pageSize, String name, String orgName) {
|
public R<?> getRegisterOrganizations(Integer pageNo, Integer pageSize, String name, String orgName) {
|
||||||
// 使用Page对象进行分页查询
|
// 使用 Page 对象进行分页查询
|
||||||
Page<Organization> page = new Page<>(pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10);
|
Page<Organization> page = new Page<>(pageNo != null ? pageNo : 1, pageSize != null ? pageSize : 10);
|
||||||
|
|
||||||
// 创建查询条件,只查询register_flag为1的组织机构
|
// 使用 Mapper 方法关联查询 sys_tenant 表获取租户名称
|
||||||
LambdaQueryWrapper<Organization> queryWrapper = new LambdaQueryWrapper<>();
|
IPage<Organization> resultPage = organizationMapper.selectRegisterOrganizationsWithTenant(
|
||||||
queryWrapper.eq(Organization::getRegisterFlag, 1); // 只获取挂号科室
|
page,
|
||||||
queryWrapper.eq(Organization::getDeleteFlag, "0"); // 确保未删除
|
1, // register_flag = 1
|
||||||
|
"0", // delete_flag = '0'
|
||||||
|
name,
|
||||||
|
orgName
|
||||||
|
);
|
||||||
|
|
||||||
// 添加名称过滤条件
|
// 转换为 DTO 对象并设置字典文本
|
||||||
if (StringUtils.isNotEmpty(name)) {
|
|
||||||
queryWrapper.like(Organization::getName, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有机构名称筛选
|
|
||||||
if (StringUtils.isNotEmpty(orgName)) {
|
|
||||||
// 这里假设 orgName 是父机构名称,如果需要更复杂的关联查询可在此扩展
|
|
||||||
// 当前逻辑暂保持与原逻辑一致的过滤方式或根据需求调整
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按编码排序
|
|
||||||
queryWrapper.orderByAsc(Organization::getBusNo);
|
|
||||||
|
|
||||||
// 执行分页查询
|
|
||||||
Page<Organization> resultPage = organizationService.page(page, queryWrapper);
|
|
||||||
|
|
||||||
// 转换为DTO对象并设置字典文本
|
|
||||||
List<OrganizationDto> organizationDtoList = resultPage.getRecords().stream().map(org -> {
|
List<OrganizationDto> organizationDtoList = resultPage.getRecords().stream().map(org -> {
|
||||||
OrganizationDto dto = new OrganizationDto();
|
OrganizationDto dto = new OrganizationDto();
|
||||||
BeanUtils.copyProperties(org, dto);
|
BeanUtils.copyProperties(org, dto);
|
||||||
dto.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, dto.getTypeEnum()));
|
dto.setTypeEnum_dictText(EnumUtils.getInfoByValue(OrganizationType.class, dto.getTypeEnum()));
|
||||||
dto.setClassEnum_dictText(formatClassEnumDictText(dto.getClassEnum()));
|
dto.setClassEnum_dictText(formatClassEnumDictText(dto.getClassEnum()));
|
||||||
dto.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, dto.getActiveFlag()));
|
dto.setActiveFlag_dictText(EnumUtils.getInfoByValue(AccountStatus.class, dto.getActiveFlag()));
|
||||||
|
// 设置租户名称
|
||||||
|
dto.setOrgName(org.getTenantName());
|
||||||
return dto;
|
return dto;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
|||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -60,18 +59,20 @@ public class OrganizationDto {
|
|||||||
private Integer displayOrder;
|
private Integer displayOrder;
|
||||||
|
|
||||||
/** 子集合 */
|
/** 子集合 */
|
||||||
@ToString.Exclude
|
|
||||||
private List<OrganizationDto> children = new ArrayList<>();
|
private List<OrganizationDto> children = new ArrayList<>();
|
||||||
|
|
||||||
/** 挂号科室标记 */
|
/** 挂号科室标记 */
|
||||||
private Integer registerFlag;
|
private Integer registerFlag;
|
||||||
|
|
||||||
/** 科室位置 */
|
/** 科室位置 */
|
||||||
private String location;
|
private String location;
|
||||||
|
|
||||||
/** 科室简介 */
|
/** 科室简介 */
|
||||||
private String intro;
|
private String intro;
|
||||||
|
|
||||||
/** 备注 */
|
/** 备注 */
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
/** 租户名称 */
|
||||||
|
private String orgName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.core.common.utils.SecurityUtils;
|
|||||||
import com.core.common.core.domain.model.LoginUser;
|
import com.core.common.core.domain.model.LoginUser;
|
||||||
import com.openhis.infectious.domain.InfectiousAudit;
|
import com.openhis.infectious.domain.InfectiousAudit;
|
||||||
import com.openhis.infectious.domain.InfectiousCard;
|
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.appservice.ICardManageAppService;
|
||||||
import com.openhis.web.cardmanagement.dto.*;
|
import com.openhis.web.cardmanagement.dto.*;
|
||||||
import com.openhis.web.cardmanagement.mapper.InfectiousAuditMapper;
|
import com.openhis.web.cardmanagement.mapper.InfectiousAuditMapper;
|
||||||
@@ -52,6 +54,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
|
|
||||||
private final InfectiousCardMapper infectiousCardMapper;
|
private final InfectiousCardMapper infectiousCardMapper;
|
||||||
private final InfectiousAuditMapper infectiousAuditMapper;
|
private final InfectiousAuditMapper infectiousAuditMapper;
|
||||||
|
private final IPractitionerService iPractitionerService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CardStatisticsDto getStatistics() {
|
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());
|
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +130,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
if (card == null) {
|
if (card == null) {
|
||||||
return new ArrayList<>();
|
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());
|
return records.stream().map(this::convertAuditToDto).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,16 +148,16 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
for (String cardNo : batchAuditDto.getCardNos()) {
|
for (String cardNo : batchAuditDto.getCardNos()) {
|
||||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||||
if (card == null) continue;
|
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();
|
Integer oldStatus = card.getStatus();
|
||||||
card.setStatus("2");
|
card.setStatus(2);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
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());
|
null, auditorId, auditorName, true, batchAuditDto.getCardNos().size());
|
||||||
successCount++;
|
successCount++;
|
||||||
}
|
}
|
||||||
@@ -176,17 +179,17 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
for (String cardNo : batchReturnDto.getCardNos()) {
|
for (String cardNo : batchReturnDto.getCardNos()) {
|
||||||
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
InfectiousCard card = infectiousCardMapper.selectByCardNo(cardNo);
|
||||||
if (card == null) continue;
|
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();
|
Integer oldStatus = card.getStatus();
|
||||||
card.setStatus("5");
|
card.setStatus(5);
|
||||||
card.setReturnReason(batchReturnDto.getReturnReason());
|
card.setReturnReason(batchReturnDto.getReturnReason());
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
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());
|
batchReturnDto.getReturnReason(), auditorId, auditorName, true, batchReturnDto.getCardNos().size());
|
||||||
successCount++;
|
successCount++;
|
||||||
}
|
}
|
||||||
@@ -206,13 +209,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
String auditorName = SecurityUtils.getUsername();
|
String auditorName = SecurityUtils.getUsername();
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
String oldStatus = card.getStatus();
|
Integer oldStatus = card.getStatus();
|
||||||
card.setStatus("2");
|
card.setStatus(2);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
infectiousCardMapper.updateById(card);
|
||||||
|
|
||||||
// 创建审核记录
|
// 创建审核记录
|
||||||
createAuditRecord(card.getId(), oldStatus, "2", "2", auditDto.getAuditOpinion(),
|
createAuditRecord(card.getCardNo(), oldStatus, 2, 2, auditDto.getAuditOpinion(),
|
||||||
null, auditorId, auditorName, false, 1);
|
null, auditorId, auditorName, false, 1);
|
||||||
|
|
||||||
return R.ok("审核通过");
|
return R.ok("审核通过");
|
||||||
@@ -230,14 +233,14 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
String auditorName = SecurityUtils.getUsername();
|
String auditorName = SecurityUtils.getUsername();
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
String oldStatus = card.getStatus();
|
Integer oldStatus = card.getStatus();
|
||||||
card.setStatus("5");
|
card.setStatus(5);
|
||||||
card.setReturnReason(returnDto.getReturnReason());
|
card.setReturnReason(returnDto.getReturnReason());
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
infectiousCardMapper.updateById(card);
|
||||||
|
|
||||||
// 创建审核记录
|
// 创建审核记录
|
||||||
createAuditRecord(card.getId(), oldStatus, "5", "4", null,
|
createAuditRecord(card.getCardNo(), oldStatus, 5, 4, null,
|
||||||
returnDto.getReturnReason(), auditorId, auditorName, false, 1);
|
returnDto.getReturnReason(), auditorId, auditorName, false, 1);
|
||||||
|
|
||||||
return R.ok("已退回");
|
return R.ok("已退回");
|
||||||
@@ -251,7 +254,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
if (queryParams.getRegistrationSource() != null) {
|
if (queryParams.getRegistrationSource() != null) {
|
||||||
wrapper.eq(InfectiousCard::getRegistrationSource, queryParams.getRegistrationSource());
|
wrapper.eq(InfectiousCard::getRegistrationSource, queryParams.getRegistrationSource());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(queryParams.getStatus())) {
|
if (queryParams.getStatus() != null) {
|
||||||
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(queryParams.getPatientName())) {
|
if (StringUtils.hasText(queryParams.getPatientName())) {
|
||||||
@@ -292,7 +295,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
row.createCell(1).setCellValue(card.getPatName());
|
row.createCell(1).setCellValue(card.getPatName());
|
||||||
row.createCell(2).setCellValue("1".equals(card.getSex()) ? "男" : "2".equals(card.getSex()) ? "女" : "未知");
|
row.createCell(2).setCellValue("1".equals(card.getSex()) ? "男" : "2".equals(card.getSex()) ? "女" : "未知");
|
||||||
row.createCell(3).setCellValue(card.getAge() != null ? card.getAge() + "岁" : "");
|
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(5).setCellValue(card.getDeptName());
|
||||||
row.createCell(6).setCellValue(card.getCreateTime() != null ? dateFormat.format(card.getCreateTime()) : "");
|
row.createCell(6).setCellValue(card.getCreateTime() != null ? dateFormat.format(card.getCreateTime()) : "");
|
||||||
row.createCell(7).setCellValue(getStatusText(card.getStatus()));
|
row.createCell(7).setCellValue(getStatusText(card.getStatus()));
|
||||||
@@ -316,7 +319,19 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DoctorCardStatisticsDto getDoctorCardStatistics() {
|
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();
|
DoctorCardStatisticsDto dto = new DoctorCardStatisticsDto();
|
||||||
Integer totalCount = infectiousCardMapper.countByDoctorId(doctorId);
|
Integer totalCount = infectiousCardMapper.countByDoctorId(doctorId);
|
||||||
@@ -331,7 +346,18 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> getDoctorCardPage(DoctorCardQueryDto queryParams) {
|
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());
|
Page<InfectiousCard> page = new Page<>(queryParams.getPageNo(), queryParams.getPageSize());
|
||||||
LambdaQueryWrapper<InfectiousCard> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<InfectiousCard> wrapper = new LambdaQueryWrapper<>();
|
||||||
@@ -340,7 +366,7 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
wrapper.eq(InfectiousCard::getDoctorId, doctorId);
|
wrapper.eq(InfectiousCard::getDoctorId, doctorId);
|
||||||
|
|
||||||
// 状态筛选
|
// 状态筛选
|
||||||
if (StringUtils.hasText(queryParams.getStatus())) {
|
if (queryParams.getStatus() != null) {
|
||||||
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
wrapper.eq(InfectiousCard::getStatus, queryParams.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,13 +380,24 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
wrapper.le(InfectiousCard::getCreateTime, endDateTime);
|
wrapper.le(InfectiousCard::getCreateTime, endDateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键词搜索(患者姓名或报卡名称)
|
// 关键词搜索(患者姓名、疾病编码、报卡名称)
|
||||||
if (StringUtils.hasText(queryParams.getKeyword())) {
|
if (StringUtils.hasText(queryParams.getKeyword())) {
|
||||||
wrapper.and(w -> w
|
String kw = queryParams.getKeyword();
|
||||||
.like(InfectiousCard::getPatName, queryParams.getKeyword())
|
// 将关键词匹配报卡名称,找出对应的 cardNameCode 列表
|
||||||
.or()
|
List<Integer> matchedCodes = getMatchedCardNameCodes(kw);
|
||||||
.like(InfectiousCard::getDiseaseName, queryParams.getKeyword())
|
// 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("无权操作此报卡");
|
return R.fail("无权操作此报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证状态:只有暂存状态可以提交
|
// 狋证状态:只有暂存状态可以提交
|
||||||
if (!"0".equals(card.getStatus())) {
|
if (!Integer.valueOf(0).equals(card.getStatus())) {
|
||||||
return R.fail("只能提交暂存状态的报卡");
|
return R.fail("只能提交暂存状态的报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态为已提交
|
// 更新状态为已提交
|
||||||
card.setStatus("1");
|
card.setStatus(1);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
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("无权操作此报卡");
|
return R.fail("无权操作此报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证状态:只有已提交状态可以撤回
|
// 狋证状态:只有已提交状态可以撤回
|
||||||
if (!"1".equals(card.getStatus())) {
|
if (!Integer.valueOf(1).equals(card.getStatus())) {
|
||||||
return R.fail("只能撤回已提交状态的报卡");
|
return R.fail("只能撤回已提交状态的报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态为暂存
|
// 更新状态为暂存
|
||||||
card.setStatus("0");
|
card.setStatus(0);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
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("无权操作此报卡");
|
return R.fail("无权操作此报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证状态:只有暂存状态可以删除
|
// 狋证状态:只有暂存状态可以删除
|
||||||
if (!"0".equals(card.getStatus())) {
|
if (!Integer.valueOf(0).equals(card.getStatus())) {
|
||||||
return R.fail("只能删除暂存状态的报卡");
|
return R.fail("只能删除暂存状态的报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态为作废
|
// 更新状态为作废
|
||||||
card.setStatus("6");
|
card.setStatus(6);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
infectiousCardMapper.updateById(card);
|
||||||
|
|
||||||
@@ -464,7 +507,12 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
return R.fail("请选择要提交的报卡");
|
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;
|
int successCount = 0;
|
||||||
|
|
||||||
for (String cardNo : cardNos) {
|
for (String cardNo : cardNos) {
|
||||||
@@ -472,13 +520,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
if (card == null) continue;
|
if (card == null) continue;
|
||||||
|
|
||||||
// 验证权限:只能提交自己的报卡
|
// 验证权限:只能提交自己的报卡
|
||||||
if (!card.getDoctorId().equals(doctorId)) continue;
|
if (!doctorId.equals(card.getDoctorId())) continue;
|
||||||
|
|
||||||
// 验证状态:只有暂存状态可以提交
|
|
||||||
if (!"0".equals(card.getStatus())) continue;
|
|
||||||
|
|
||||||
|
// 狋证状态:只有暂存状态可以提交
|
||||||
|
if (!Integer.valueOf(0).equals(card.getStatus())) continue;
|
||||||
|
|
||||||
// 更新状态为已提交
|
// 更新状态为已提交
|
||||||
card.setStatus("1");
|
card.setStatus(1);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
infectiousCardMapper.updateById(card);
|
||||||
successCount++;
|
successCount++;
|
||||||
@@ -498,7 +546,12 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
return R.fail("请选择要删除的报卡");
|
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;
|
int successCount = 0;
|
||||||
|
|
||||||
for (String cardNo : cardNos) {
|
for (String cardNo : cardNos) {
|
||||||
@@ -506,13 +559,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
if (card == null) continue;
|
if (card == null) continue;
|
||||||
|
|
||||||
// 验证权限:只能删除自己的报卡
|
// 验证权限:只能删除自己的报卡
|
||||||
if (!card.getDoctorId().equals(doctorId)) continue;
|
if (!doctorId.equals(card.getDoctorId())) continue;
|
||||||
|
|
||||||
// 验证状态:只有暂存状态可以删除
|
|
||||||
if (!"0".equals(card.getStatus())) continue;
|
|
||||||
|
|
||||||
|
// 狋证状态:只有暂存状态可以删除
|
||||||
|
if (!Integer.valueOf(0).equals(card.getStatus())) continue;
|
||||||
|
|
||||||
// 更新状态为作废
|
// 更新状态为作废
|
||||||
card.setStatus("6");
|
card.setStatus(6);
|
||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
infectiousCardMapper.updateById(card);
|
infectiousCardMapper.updateById(card);
|
||||||
successCount++;
|
successCount++;
|
||||||
@@ -530,6 +583,13 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
// 获取当前登录用户信息
|
// 获取当前登录用户信息
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
Long currentUserId = loginUser.getUserId();
|
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());
|
InfectiousCard card = infectiousCardMapper.selectByCardNo(updateDto.getCardNo());
|
||||||
@@ -538,12 +598,12 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证是否当前医生的报卡 - 根据 doctorId 字段验证
|
// 验证是否当前医生的报卡 - 根据 doctorId 字段验证
|
||||||
if (!currentUserId.equals(card.getDoctorId())) {
|
if (!doctorId.equals(card.getDoctorId())) {
|
||||||
return R.fail("只能修改自己的报卡");
|
return R.fail("只能修改自己的报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证状态是否允许修改(只能修改暂存状态的报卡)
|
// 狋证状态是否允许修改(只能修改暂存状态的报卡)
|
||||||
if (!"0".equals(card.getStatus())) {
|
if (!Integer.valueOf(0).equals(card.getStatus())) {
|
||||||
return R.fail("只能修改暂存状态的报卡");
|
return R.fail("只能修改暂存状态的报卡");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,15 +619,6 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
card.setUpdateTime(new Date());
|
card.setUpdateTime(new Date());
|
||||||
card.setUpdateBy(loginUser.getUsername()); // 使用username作为更新者
|
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);
|
int rows = infectiousCardMapper.updateById(card);
|
||||||
if (rows > 0) {
|
if (rows > 0) {
|
||||||
return R.ok("更新成功");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证状态:只有已上报状态可以导出
|
// 狋证状态:只有已上报状态可以导出
|
||||||
if (!"3".equals(card.getStatus())) {
|
if (!Integer.valueOf(3).equals(card.getStatus())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,6 +665,8 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
private DoctorCardListDto convertToDoctorCardListDto(InfectiousCard card) {
|
private DoctorCardListDto convertToDoctorCardListDto(InfectiousCard card) {
|
||||||
DoctorCardListDto dto = new DoctorCardListDto();
|
DoctorCardListDto dto = new DoctorCardListDto();
|
||||||
BeanUtils.copyProperties(card, dto);
|
BeanUtils.copyProperties(card, dto);
|
||||||
|
// 由于数据库中没有 disease_name 字段,使用 disease_code 作为疾病名称展示
|
||||||
|
dto.setDiseaseName(card.getDiseaseCode());
|
||||||
dto.setCardName(getCardName(card.getCardNameCode()));
|
dto.setCardName(getCardName(card.getCardNameCode()));
|
||||||
dto.setSubmitTime(card.getCreateTime() != null ?
|
dto.setSubmitTime(card.getCreateTime() != null ?
|
||||||
new SimpleDateFormat("yyyy-MM-dd HH:mm").format(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
|
* 转换审核记录为 DTO
|
||||||
*/
|
*/
|
||||||
private AuditRecordDto convertAuditToDto(InfectiousAudit audit) {
|
private AuditRecordDto convertAuditToDto(InfectiousAudit audit) {
|
||||||
AuditRecordDto dto = new AuditRecordDto();
|
AuditRecordDto dto = new AuditRecordDto();
|
||||||
BeanUtils.copyProperties(audit, dto);
|
BeanUtils.copyProperties(audit, dto);
|
||||||
dto.setCardId(audit.getCardId() != null ? audit.getCardId().toString() : null);
|
dto.setCardId(audit.getCardId());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +725,8 @@ public class CardManageAppServiceImpl implements ICardManageAppService {
|
|||||||
private InfectiousCardDto convertToDto(InfectiousCard card) {
|
private InfectiousCardDto convertToDto(InfectiousCard card) {
|
||||||
InfectiousCardDto dto = new InfectiousCardDto();
|
InfectiousCardDto dto = new InfectiousCardDto();
|
||||||
BeanUtils.copyProperties(card, dto);
|
BeanUtils.copyProperties(card, dto);
|
||||||
|
// 由于数据库中没有 disease_name 字段,使用 disease_code 作为疾病名称展示
|
||||||
|
dto.setDiseaseName(card.getDiseaseCode());
|
||||||
dto.setStatusText(getStatusText(card.getStatus()));
|
dto.setStatusText(getStatusText(card.getStatus()));
|
||||||
return dto;
|
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,
|
String auditOpinion, String returnReason, String auditorId, String auditorName,
|
||||||
Boolean isBatch, Integer batchSize) {
|
Boolean isBatch, Integer batchSize) {
|
||||||
InfectiousAudit audit = new InfectiousAudit();
|
InfectiousAudit audit = new InfectiousAudit();
|
||||||
audit.setCardId(cardId);
|
audit.setCardId(cardId);
|
||||||
audit.setAuditSeq(infectiousAuditMapper.getNextAuditSeq(cardId));
|
audit.setAuditSeq(infectiousAuditMapper.getNextAuditSeq(cardId));
|
||||||
audit.setAuditType(auditType);
|
audit.setAuditType(String.valueOf(auditType));
|
||||||
audit.setAuditStatusFrom(statusFrom);
|
audit.setAuditStatusFrom(statusFrom != null ? String.valueOf(statusFrom) : null);
|
||||||
audit.setAuditStatusTo(statusTo);
|
audit.setAuditStatusTo(statusTo != null ? String.valueOf(statusTo) : null);
|
||||||
audit.setAuditTime(LocalDateTime.now());
|
audit.setAuditTime(LocalDateTime.now());
|
||||||
audit.setAuditorId(auditorId);
|
audit.setAuditorId(auditorId);
|
||||||
audit.setAuditorName(auditorName);
|
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) {
|
switch (status) {
|
||||||
case "0": return "暂存";
|
case 0: return "暂存";
|
||||||
case "1": return "已提交";
|
case 1: return "已提交";
|
||||||
case "2": return "审核通过";
|
case 2: return "审核通过";
|
||||||
case "3": return "已上报";
|
case 3: return "已上报";
|
||||||
case "4": return "失败";
|
case 4: return "失败";
|
||||||
case "5": return "审核失败";
|
case 5: return "审核失败";
|
||||||
case "6": return "作废";
|
case 6: return "作废";
|
||||||
default: return "未知";
|
default: return "未知";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ public class CardQueryDto {
|
|||||||
/** 患者姓名 */
|
/** 患者姓名 */
|
||||||
private String patientName;
|
private String patientName;
|
||||||
|
|
||||||
/** 审核状态 */
|
/** 审核状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
|
||||||
private String status;
|
private Integer status;
|
||||||
|
|
||||||
/** 科室ID */
|
/** 科室ID */
|
||||||
private Long deptId;
|
private Long deptId;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
|||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 医生个人报卡列表DTO
|
* 医生个人报卡列表DTO
|
||||||
*
|
*
|
||||||
@@ -41,6 +44,51 @@ public class DoctorCardListDto {
|
|||||||
/** 提交时间 */
|
/** 提交时间 */
|
||||||
private String submitTime;
|
private String submitTime;
|
||||||
|
|
||||||
/** 状态 */
|
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
|
||||||
private String status;
|
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;
|
||||||
}
|
}
|
||||||
@@ -26,8 +26,8 @@ public class DoctorCardQueryDto {
|
|||||||
/** 结束日期 */
|
/** 结束日期 */
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回) */
|
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
|
||||||
private String status;
|
private Integer status;
|
||||||
|
|
||||||
/** 患者姓名或报卡名称 */
|
/** 患者姓名或报卡名称 */
|
||||||
private String keyword;
|
private String keyword;
|
||||||
|
|||||||
@@ -1,18 +1,44 @@
|
|||||||
package com.openhis.web.cardmanagement.dto;
|
package com.openhis.web.cardmanagement.dto;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class DoctorCardUpdateDto {
|
public class DoctorCardUpdateDto {
|
||||||
|
@NotBlank(message = "卡片编号不能为空")
|
||||||
private String cardNo;
|
private String cardNo;
|
||||||
|
|
||||||
private String phone;
|
private String phone;
|
||||||
|
private String contactPhone; // 紧急联系人电话
|
||||||
|
|
||||||
private LocalDate onsetDate;
|
private LocalDate onsetDate;
|
||||||
private LocalDateTime diagDate;
|
private LocalDateTime diagDate;
|
||||||
private String diseaseType; // 修改为diseaseType,对应InfectiousCard中的diseaseType字段
|
|
||||||
private String addressProv;
|
private String diseaseType; // 病例分类(对应InfectiousCard中的diseaseType字段)
|
||||||
private String addressCity;
|
private String diseaseCode; // 疾病编码
|
||||||
private String addressCounty;
|
|
||||||
private String addressHouse;
|
@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; // 其他传染病名称
|
||||||
}
|
}
|
||||||
@@ -65,8 +65,8 @@ public class InfectiousCardDto {
|
|||||||
/** 现住址门牌号 */
|
/** 现住址门牌号 */
|
||||||
private String addressHouse;
|
private String addressHouse;
|
||||||
|
|
||||||
/** 病人属于 */
|
/** 病人属于(1本县区/2本市其他县区/3本省其他地市/4外省/5港澳台/6外籍) */
|
||||||
private String patientbelong;
|
private Integer patientBelong;
|
||||||
|
|
||||||
/** 职业 */
|
/** 职业 */
|
||||||
private String occupation;
|
private String occupation;
|
||||||
@@ -110,8 +110,8 @@ public class InfectiousCardDto {
|
|||||||
/** 填卡日期 */
|
/** 填卡日期 */
|
||||||
private LocalDate reportDate;
|
private LocalDate reportDate;
|
||||||
|
|
||||||
/** 状态 */
|
/** 状态(0暂存/1已提交/2已审核/3已上报/4失败/5退回/6作废) */
|
||||||
private String status;
|
private Integer status;
|
||||||
|
|
||||||
/** 状态文本 */
|
/** 状态文本 */
|
||||||
private String statusText;
|
private String statusText;
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ import java.util.List;
|
|||||||
public interface InfectiousAuditMapper extends BaseMapper<InfectiousAudit> {
|
public interface InfectiousAuditMapper extends BaseMapper<InfectiousAudit> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据报卡ID查询审核记录
|
* 根据报卡编号查询审核记录
|
||||||
*/
|
*/
|
||||||
@Select("SELECT * FROM infectious_audit WHERE card_id = #{cardId} ORDER BY audit_time DESC")
|
@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}")
|
@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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
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();
|
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();
|
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();
|
Integer countMonthReported();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,14 +55,14 @@ public interface InfectiousCardMapper extends BaseMapper<InfectiousCard> {
|
|||||||
Integer countByDoctorId(@Param("doctorId") Long doctorId);
|
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);
|
Integer countPendingFailedByDoctorId(@Param("doctorId") Long doctorId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计医生已成功上报数(状态为3已上报)
|
* 统计医生已成功上报数(状态为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);
|
Integer countReportedByDoctorId(@Param("doctorId") Long doctorId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,9 +119,11 @@ public class OutpatientChargeAppServiceImpl implements IOutpatientChargeAppServi
|
|||||||
= outpatientChargeAppMapper.selectEncounterPatientPrescription(encounterId,
|
= outpatientChargeAppMapper.selectEncounterPatientPrescription(encounterId,
|
||||||
ChargeItemContext.ACTIVITY.getValue(), ChargeItemContext.MEDICATION.getValue(),
|
ChargeItemContext.ACTIVITY.getValue(), ChargeItemContext.MEDICATION.getValue(),
|
||||||
ChargeItemContext.DEVICE.getValue(), ChargeItemContext.REGISTER.getValue(),
|
ChargeItemContext.DEVICE.getValue(), ChargeItemContext.REGISTER.getValue(),
|
||||||
|
ChargeItemContext.WESTERN_MEDICINE.getValue(), ChargeItemContext.CHINESE_PATENT_MEDICINE.getValue(),
|
||||||
ChargeItemStatus.PLANNED.getValue(), ChargeItemStatus.BILLABLE.getValue(),
|
ChargeItemStatus.PLANNED.getValue(), ChargeItemStatus.BILLABLE.getValue(),
|
||||||
ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDING.getValue(),
|
ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDING.getValue(),
|
||||||
ChargeItemStatus.REFUNDED.getValue(), ChargeItemStatus.PART_REFUND.getValue());
|
ChargeItemStatus.REFUNDED.getValue(), ChargeItemStatus.PART_REFUND.getValue(),
|
||||||
|
CommonConstants.TableName.WOR_DEVICE_REQUEST);
|
||||||
prescriptionDtoList.forEach(e -> {
|
prescriptionDtoList.forEach(e -> {
|
||||||
// 收费状态枚举
|
// 收费状态枚举
|
||||||
e.setStatusEnum_enumText(EnumUtils.getInfoByValue(ChargeItemStatus.class, e.getStatusEnum()));
|
e.setStatusEnum_enumText(EnumUtils.getInfoByValue(ChargeItemStatus.class, e.getStatusEnum()));
|
||||||
@@ -229,7 +231,8 @@ public class OutpatientChargeAppServiceImpl implements IOutpatientChargeAppServi
|
|||||||
ChargeItemStatus.REFUNDING.getValue(), ChargeItemStatus.REFUNDED.getValue(),
|
ChargeItemStatus.REFUNDING.getValue(), ChargeItemStatus.REFUNDED.getValue(),
|
||||||
ChargeItemStatus.PART_REFUND.getValue(), YbPayment.DISCOUNT_PAY.getValue(),
|
ChargeItemStatus.PART_REFUND.getValue(), YbPayment.DISCOUNT_PAY.getValue(),
|
||||||
YbPayment.SELF_CASH_VALUE.getValue(), YbPayment.SELF_CASH_VX_VALUE.getValue(),
|
YbPayment.SELF_CASH_VALUE.getValue(), YbPayment.SELF_CASH_VX_VALUE.getValue(),
|
||||||
YbPayment.SELF_CASH_ALI_VALUE.getValue(), YbPayment.SELF_CASH_UNION_VALUE.getValue());
|
YbPayment.SELF_CASH_ALI_VALUE.getValue(), YbPayment.SELF_CASH_UNION_VALUE.getValue(),
|
||||||
|
CommonConstants.TableName.WOR_DEVICE_REQUEST);
|
||||||
prescriptionDtoList.forEach(e -> {
|
prescriptionDtoList.forEach(e -> {
|
||||||
// 应收金额
|
// 应收金额
|
||||||
BigDecimal receivableAmount = e.getReceivableAmount();
|
BigDecimal receivableAmount = e.getReceivableAmount();
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ public class OutpatientPricingAppServiceImpl implements IOutpatientPricingAppSer
|
|||||||
} else {
|
} else {
|
||||||
adviceTypes = List.of(1, 2, 3);
|
adviceTypes = List.of(1, 2, 3);
|
||||||
}
|
}
|
||||||
// 门诊划价:不要强制 pricingFlag=1 参与过滤(wor_activity_definition.pricing_flag 可能为 0),
|
String categoryCode = adviceBaseDto != null ? adviceBaseDto.getCategoryCode() : null;
|
||||||
// 否则会导致诊疗项目(adviceType=3)查询结果为空 records=[]
|
// 门诊划价:仅返回划价标记为“是”的项目
|
||||||
return iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, null,
|
return iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, null,
|
||||||
organizationId, pageNo, pageSize, null, adviceTypes, null);
|
organizationId, pageNo, pageSize, Whether.YES.getValue(), adviceTypes, null, categoryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ import com.openhis.common.enums.ybenums.YbPayment;
|
|||||||
import com.openhis.common.utils.EnumUtils;
|
import com.openhis.common.utils.EnumUtils;
|
||||||
import com.openhis.common.utils.HisPageUtils;
|
import com.openhis.common.utils.HisPageUtils;
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
import com.openhis.common.utils.HisQueryUtils;
|
||||||
|
import com.openhis.appointmentmanage.domain.SchedulePool;
|
||||||
|
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||||
|
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
|
||||||
|
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||||
|
import com.openhis.clinical.domain.Order;
|
||||||
|
import com.openhis.clinical.service.IOrderService;
|
||||||
import com.openhis.financial.domain.PaymentReconciliation;
|
import com.openhis.financial.domain.PaymentReconciliation;
|
||||||
import com.openhis.financial.domain.RefundLog;
|
import com.openhis.financial.domain.RefundLog;
|
||||||
import com.openhis.financial.service.IRefundLogService;
|
import com.openhis.financial.service.IRefundLogService;
|
||||||
@@ -48,6 +54,8 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -97,6 +105,21 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
@Resource
|
@Resource
|
||||||
IRefundLogService iRefundLogService;
|
IRefundLogService iRefundLogService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
IOrderService orderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
com.openhis.triageandqueuemanage.service.TriageQueueItemService triageQueueItemService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
ScheduleSlotMapper scheduleSlotMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
SchedulePoolMapper schedulePoolMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
com.openhis.document.service.IEmrService iEmrService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 门诊挂号 - 查询患者信息
|
* 门诊挂号 - 查询患者信息
|
||||||
*
|
*
|
||||||
@@ -242,14 +265,24 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> returnRegister(CancelRegPaymentDto cancelRegPaymentDto) {
|
public R<?> returnRegister(CancelRegPaymentDto cancelRegPaymentDto) {
|
||||||
Encounter byId = iEncounterService.getById(cancelRegPaymentDto.getEncounterId());
|
Encounter byId = iEncounterService.getById(cancelRegPaymentDto.getEncounterId());
|
||||||
|
if (byId == null) {
|
||||||
|
return R.fail(null, "就诊记录不存在");
|
||||||
|
}
|
||||||
if (EncounterStatus.CANCELLED.getValue().equals(byId.getStatusEnum())) {
|
if (EncounterStatus.CANCELLED.getValue().equals(byId.getStatusEnum())) {
|
||||||
return R.fail(null, "该患者已经退号,请勿重复退号");
|
return R.fail(null, "该患者已经退号,请勿重复退号");
|
||||||
}
|
}
|
||||||
// 只有待诊状态才能退号
|
// 只有待诊状态才能退号
|
||||||
if (!EncounterStatus.PLANNED.getValue().equals(byId.getStatusEnum())) {
|
if (!EncounterStatus.PLANNED.getValue().equals(byId.getStatusEnum())) {
|
||||||
return R.fail(null, "该患者医生已接诊,不能退号!");
|
return R.fail(null, "该患者已开始就诊,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊前退号检查:病历、费用明细、班段时间
|
||||||
|
R<?> checkResult = checkPreConsultationRefund(byId);
|
||||||
|
if (checkResult != null) {
|
||||||
|
return checkResult;
|
||||||
}
|
}
|
||||||
iEncounterService.returnRegister(cancelRegPaymentDto.getEncounterId());
|
iEncounterService.returnRegister(cancelRegPaymentDto.getEncounterId());
|
||||||
// 查询账户信息
|
// 查询账户信息
|
||||||
@@ -291,6 +324,14 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果本次门诊挂号来自预约签到,同步把预约订单与号源槽位状态改为已退号
|
||||||
|
if (result != null && result.getCode() == 200) {
|
||||||
|
syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason());
|
||||||
|
|
||||||
|
// 同步移除分诊队列中的记录
|
||||||
|
removeTriageQueueItem(byId.getId());
|
||||||
|
}
|
||||||
|
|
||||||
// 记录退号日志
|
// 记录退号日志
|
||||||
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon);
|
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon);
|
||||||
|
|
||||||
@@ -298,6 +339,149 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
|
return R.ok(paymentRecon, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, new Object[] {"退号"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊前退号检查
|
||||||
|
* 检查项:病历记录、费用明细、当日就诊、班段结束时间
|
||||||
|
*
|
||||||
|
* @param encounter 就诊记录
|
||||||
|
* @return null 表示通过检查,否则返回失败原因
|
||||||
|
*/
|
||||||
|
private R<?> checkPreConsultationRefund(Encounter encounter) {
|
||||||
|
Long encounterId = encounter.getId();
|
||||||
|
|
||||||
|
// 当日时间范围:今天 00:00:00 到 明天 00:00:00
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDateTime todayStart = today.atStartOfDay();
|
||||||
|
LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay();
|
||||||
|
Date todayStartDate = Date.from(todayStart.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date tomorrowStartDate = Date.from(tomorrowStart.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
|
||||||
|
// 1. 检查是否有当日病历记录(医生已写病历则不能退号)
|
||||||
|
// 只检查当天的病历,避免误判历史数据
|
||||||
|
// 条件:(recordTime在当天范围内) OR (recordTime为空 AND createTime在当天范围内)
|
||||||
|
long emrCount = iEmrService.count(new LambdaQueryWrapper<com.openhis.document.domain.Emr>()
|
||||||
|
.eq(com.openhis.document.domain.Emr::getEncounterId, encounterId)
|
||||||
|
.and(wrapper -> wrapper
|
||||||
|
.and(w -> w
|
||||||
|
.ge(com.openhis.document.domain.Emr::getRecordTime, todayStartDate)
|
||||||
|
.lt(com.openhis.document.domain.Emr::getRecordTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
.or()
|
||||||
|
.and(w -> w
|
||||||
|
.isNull(com.openhis.document.domain.Emr::getRecordTime)
|
||||||
|
.ge(com.openhis.document.domain.Emr::getCreateTime, todayStartDate)
|
||||||
|
.lt(com.openhis.document.domain.Emr::getCreateTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
if (emrCount > 0) {
|
||||||
|
return R.fail(null, "该患者已有病历记录,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否有当日费用明细(除挂号费外的其他费用)
|
||||||
|
// 只检查当天的费用明细,避免误判历史数据
|
||||||
|
// 条件:(occurrenceTime在当天范围内) OR (occurrenceTime为空 AND createTime在当天范围内)
|
||||||
|
long chargeItemCount = iChargeItemService.count(new LambdaQueryWrapper<ChargeItem>()
|
||||||
|
.eq(ChargeItem::getEncounterId, encounterId)
|
||||||
|
.ne(ChargeItem::getContextEnum, ChargeItemContext.REGISTER.getValue())
|
||||||
|
.ne(ChargeItem::getStatusEnum, ChargeItemStatus.REFUNDED.getValue())
|
||||||
|
.and(wrapper -> wrapper
|
||||||
|
.and(w -> w
|
||||||
|
.ge(ChargeItem::getOccurrenceTime, todayStartDate)
|
||||||
|
.lt(ChargeItem::getOccurrenceTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
.or()
|
||||||
|
.and(w -> w
|
||||||
|
.isNull(ChargeItem::getOccurrenceTime)
|
||||||
|
.ge(ChargeItem::getCreateTime, todayStartDate)
|
||||||
|
.lt(ChargeItem::getCreateTime, tomorrowStartDate)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
if (chargeItemCount > 0) {
|
||||||
|
return R.fail(null, "该患者已产生诊疗费用,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否当日就诊(防止隔日财务封账)
|
||||||
|
if (encounter.getCreateTime() != null) {
|
||||||
|
LocalDate encounterDate = encounter.getCreateTime().toInstant()
|
||||||
|
.atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
if (encounterDate.isBefore(today)) {
|
||||||
|
return R.fail(null, "非当日就诊记录,不能退号!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查班段是否已结束(通过预约订单获取班段信息)
|
||||||
|
R<?> shiftCheckResult = checkShiftEnded(encounter);
|
||||||
|
if (shiftCheckResult != null) {
|
||||||
|
return shiftCheckResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // 检查通过
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查班段是否已结束
|
||||||
|
* 截止时间 = 班段结束时间
|
||||||
|
*
|
||||||
|
* @param encounter 就诊记录
|
||||||
|
* @return null 表示通过检查,否则返回失败原因
|
||||||
|
*/
|
||||||
|
private R<?> checkShiftEnded(Encounter encounter) {
|
||||||
|
try {
|
||||||
|
// 通过患者、科室、日期查找关联的预约订单
|
||||||
|
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
|
||||||
|
.eq(Order::getPatientId, encounter.getPatientId())
|
||||||
|
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
|
||||||
|
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
|
||||||
|
.orderByDesc(Order::getUpdateTime)
|
||||||
|
.orderByDesc(Order::getCreateTime)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
if (encounter.getOrganizationId() != null) {
|
||||||
|
queryWrapper.eq(Order::getDepartmentId, encounter.getOrganizationId());
|
||||||
|
}
|
||||||
|
if (encounter.getTenantId() != null) {
|
||||||
|
queryWrapper.eq(Order::getTenantId, encounter.getTenantId());
|
||||||
|
}
|
||||||
|
if (encounter.getCreateTime() != null) {
|
||||||
|
LocalDate encounterDate = encounter.getCreateTime().toInstant()
|
||||||
|
.atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
Date startOfDay = Date.from(encounterDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date nextDayStart = Date.from(encounterDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
queryWrapper.ge(Order::getAppointmentDate, startOfDay)
|
||||||
|
.lt(Order::getAppointmentDate, nextDayStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
Order appointmentOrder = orderService.getOne(queryWrapper, false);
|
||||||
|
if (appointmentOrder == null || appointmentOrder.getSlotId() == null) {
|
||||||
|
// 没有关联的预约订单,跳过班段检查(非预约挂号的场景)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取号源槽位
|
||||||
|
ScheduleSlot slot = scheduleSlotMapper.selectById(appointmentOrder.getSlotId());
|
||||||
|
if (slot == null || slot.getPoolId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取号源池(班段信息)
|
||||||
|
SchedulePool pool = schedulePoolMapper.selectById(slot.getPoolId());
|
||||||
|
if (pool == null || pool.getEndTime() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 检查当前时间是否已过班段结束时间
|
||||||
|
LocalTime now = LocalTime.now();
|
||||||
|
if (now.isAfter(pool.getEndTime())) {
|
||||||
|
return R.fail(null, "当前班段已结束,不能退号!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("检查班段结束时间失败, encounterId={}", encounter.getId(), e);
|
||||||
|
// 异常情况下允许退号,避免阻断正常业务
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询当日就诊数据
|
* 查询当日就诊数据
|
||||||
*
|
*
|
||||||
@@ -399,6 +583,74 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
return R.ok("已取消挂号");
|
return R.ok("已取消挂号");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步预约号源状态为已退号。
|
||||||
|
* 说明:
|
||||||
|
* 1) 门诊退号主流程不依赖该步骤成功与否,因此此方法内部异常仅记录日志,不向上抛出。
|
||||||
|
* 2) 通过患者、科室、日期以及状态筛选最近一条预约订单,尽量避免误匹配。
|
||||||
|
*/
|
||||||
|
private void syncAppointmentReturnStatus(Encounter encounter, String reason) {
|
||||||
|
if (encounter == null || encounter.getPatientId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
|
||||||
|
.eq(Order::getPatientId, encounter.getPatientId())
|
||||||
|
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
|
||||||
|
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
|
||||||
|
.orderByDesc(Order::getUpdateTime)
|
||||||
|
.orderByDesc(Order::getCreateTime)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
if (encounter.getOrganizationId() != null) {
|
||||||
|
queryWrapper.eq(Order::getDepartmentId, encounter.getOrganizationId());
|
||||||
|
}
|
||||||
|
if (encounter.getTenantId() != null) {
|
||||||
|
queryWrapper.eq(Order::getTenantId, encounter.getTenantId());
|
||||||
|
}
|
||||||
|
if (encounter.getCreateTime() != null) {
|
||||||
|
LocalDate encounterDate = encounter.getCreateTime().toInstant()
|
||||||
|
.atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
Date startOfDay = Date.from(encounterDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date nextDayStart = Date.from(encounterDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
queryWrapper.ge(Order::getAppointmentDate, startOfDay)
|
||||||
|
.lt(Order::getAppointmentDate, nextDayStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
Order appointmentOrder = orderService.getOne(queryWrapper, false);
|
||||||
|
if (appointmentOrder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
if (!CommonConstants.AppointmentOrderStatus.RETURNED.equals(appointmentOrder.getStatus())) {
|
||||||
|
Order updateOrder = new Order();
|
||||||
|
updateOrder.setId(appointmentOrder.getId());
|
||||||
|
updateOrder.setStatus(CommonConstants.AppointmentOrderStatus.RETURNED);
|
||||||
|
updateOrder.setCancelTime(now);
|
||||||
|
updateOrder.setCancelReason(
|
||||||
|
StringUtils.isNotEmpty(reason) ? reason : "门诊退号");
|
||||||
|
updateOrder.setUpdateTime(now);
|
||||||
|
orderService.updateById(updateOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long slotId = appointmentOrder.getSlotId();
|
||||||
|
if (slotId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.RETURNED);
|
||||||
|
if (slotRows > 0) {
|
||||||
|
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
|
||||||
|
if (poolId != null) {
|
||||||
|
schedulePoolMapper.refreshPoolStats(poolId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 补打挂号
|
* 补打挂号
|
||||||
* 补打挂号不需要修改数据库,只需要返回成功即可,前端已有所有需要的数据用于打印
|
* 补打挂号不需要修改数据库,只需要返回成功即可,前端已有所有需要的数据用于打印
|
||||||
@@ -533,4 +785,48 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除分诊队列中的记录
|
||||||
|
* 退号时同步移除患者队列记录,避免已退号患者仍在排队
|
||||||
|
*
|
||||||
|
* @param encounterId 就诊ID
|
||||||
|
*/
|
||||||
|
private void removeTriageQueueItem(Long encounterId) {
|
||||||
|
if (encounterId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 1. 移除分诊队列中的记录(必须成功,否则回滚事务)
|
||||||
|
com.openhis.triageandqueuemanage.domain.TriageQueueItem queueItem = triageQueueItemService.getOne(
|
||||||
|
new LambdaQueryWrapper<com.openhis.triageandqueuemanage.domain.TriageQueueItem>()
|
||||||
|
.eq(com.openhis.triageandqueuemanage.domain.TriageQueueItem::getEncounterId, encounterId)
|
||||||
|
.eq(com.openhis.triageandqueuemanage.domain.TriageQueueItem::getDeleteFlag, "0")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (queueItem != null) {
|
||||||
|
// 逻辑删除队列项
|
||||||
|
queueItem.setDeleteFlag("1");
|
||||||
|
queueItem.setUpdateTime(LocalDateTime.now());
|
||||||
|
triageQueueItemService.updateById(queueItem);
|
||||||
|
log.info("退号成功,已移除分诊队列记录,encounterId={}, queueItemId={}", encounterId, queueItem.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 移除候选池排除记录(非必须,即使失败也不影响主流程)
|
||||||
|
try {
|
||||||
|
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
|
||||||
|
new LambdaQueryWrapper<TriageCandidateExclusion>()
|
||||||
|
.eq(TriageCandidateExclusion::getEncounterId, encounterId)
|
||||||
|
.eq(TriageCandidateExclusion::getDeleteFlag, "0")
|
||||||
|
);
|
||||||
|
if (exclusion != null) {
|
||||||
|
exclusion.setDeleteFlag("1");
|
||||||
|
exclusion.setUpdateTime(LocalDateTime.now());
|
||||||
|
triageCandidateExclusionService.updateById(exclusion);
|
||||||
|
log.info("已移除候选池排除记录,encounterId={}", encounterId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 候选池排除记录移除失败不影响主流程,仅记录日志
|
||||||
|
log.warn("移除候选池排除记录失败,encounterId={}", encounterId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ public class OutpatientPricingController {
|
|||||||
@RequestParam(value = "locationId", required = false) Long locationId,
|
@RequestParam(value = "locationId", required = false) Long locationId,
|
||||||
@RequestParam(value = "organizationId") Long organizationId,
|
@RequestParam(value = "organizationId") Long organizationId,
|
||||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
@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) {
|
||||||
|
// 将 categoryCode 设置到 adviceBaseDto 中
|
||||||
|
if (categoryCode != null && !categoryCode.isEmpty()) {
|
||||||
|
adviceBaseDto.setCategoryCode(categoryCode);
|
||||||
|
}
|
||||||
return R.ok(iOutpatientPricingAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, organizationId,
|
return R.ok(iOutpatientPricingAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, organizationId,
|
||||||
pageNo, pageSize));
|
pageNo, pageSize));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,4 +146,28 @@ public class CurrentDayEncounterDto {
|
|||||||
*/
|
*/
|
||||||
private Integer displayOrder;
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ public class EncounterFormData {
|
|||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long organizationId;
|
private Long organizationId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约订单ID(用于预约签到时关联预约订单)
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long orderId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置默认值
|
* 设置默认值
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,19 +42,24 @@ public interface OutpatientChargeAppMapper {
|
|||||||
* @param medication 药品
|
* @param medication 药品
|
||||||
* @param device 耗材
|
* @param device 耗材
|
||||||
* @param register 挂号费
|
* @param register 挂号费
|
||||||
|
* @param westernMedicine 西药
|
||||||
|
* @param chinesePatentMedicine 中成药
|
||||||
* @param planned 收费状态:待收费
|
* @param planned 收费状态:待收费
|
||||||
* @param billable 收费状态:待结算
|
* @param billable 收费状态:待结算
|
||||||
* @param billed 收费状态:已结算
|
* @param billed 收费状态:已结算
|
||||||
* @param refunding 收费状态:退费中
|
* @param refunding 收费状态:退费中
|
||||||
* @param refunded 收费状态:全部退费
|
* @param refunded 收费状态:全部退费
|
||||||
* @param partRefund 收费状态:部分退费
|
* @param partRefund 收费状态:部分退费
|
||||||
|
* @param worDeviceRequest 耗材请求表名常量
|
||||||
* @return 患者处方列表
|
* @return 患者处方列表
|
||||||
*/
|
*/
|
||||||
List<EncounterPatientPrescriptionDto> selectEncounterPatientPrescription(@Param("encounterId") Long encounterId,
|
List<EncounterPatientPrescriptionDto> selectEncounterPatientPrescription(@Param("encounterId") Long encounterId,
|
||||||
@Param("activity") Integer activity, @Param("medication") Integer medication, @Param("device") Integer device,
|
@Param("activity") Integer activity, @Param("medication") Integer medication, @Param("device") Integer device,
|
||||||
@Param("register") Integer register, @Param("planned") Integer planned, @Param("billable") Integer billable,
|
@Param("register") Integer register, @Param("westernMedicine") Integer westernMedicine,
|
||||||
|
@Param("chinesePatentMedicine") Integer chinesePatentMedicine,
|
||||||
|
@Param("planned") Integer planned, @Param("billable") Integer billable,
|
||||||
@Param("billed") Integer billed, @Param("refunding") Integer refunding, @Param("refunded") Integer refunded,
|
@Param("billed") Integer billed, @Param("refunding") Integer refunding, @Param("refunded") Integer refunded,
|
||||||
@Param("partRefund") Integer partRefund);
|
@Param("partRefund") Integer partRefund, @Param("worDeviceRequest") String worDeviceRequest);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据就诊id查询患者处方列表并新增字段:应收金额,实收金额,优惠金额,折扣率
|
* 根据就诊id查询患者处方列表并新增字段:应收金额,实收金额,优惠金额,折扣率
|
||||||
@@ -75,6 +80,7 @@ public interface OutpatientChargeAppMapper {
|
|||||||
* @param selfVxCode 微信枚举码
|
* @param selfVxCode 微信枚举码
|
||||||
* @param selfAliCode 支付宝枚举码
|
* @param selfAliCode 支付宝枚举码
|
||||||
* @param selfUnionCode 银联枚举码
|
* @param selfUnionCode 银联枚举码
|
||||||
|
* @param worDeviceRequest 耗材请求表名常量
|
||||||
* @return 患者处方列表
|
* @return 患者处方列表
|
||||||
*/
|
*/
|
||||||
List<EncounterPatientPrescriptionDto> selectEncounterPatientPrescriptionWithPrice(
|
List<EncounterPatientPrescriptionDto> selectEncounterPatientPrescriptionWithPrice(
|
||||||
@@ -84,5 +90,5 @@ public interface OutpatientChargeAppMapper {
|
|||||||
@Param("refunding") Integer refunding, @Param("refunded") Integer refunded,
|
@Param("refunding") Integer refunding, @Param("refunded") Integer refunded,
|
||||||
@Param("partRefund") Integer partRefund, @Param("discountCode") Integer discountCode,
|
@Param("partRefund") Integer partRefund, @Param("discountCode") Integer discountCode,
|
||||||
@Param("self") Integer selfCode, @Param("selfVx") Integer selfVxCode, @Param("selfAli") Integer selfAliCode,
|
@Param("self") Integer selfCode, @Param("selfVx") Integer selfVxCode, @Param("selfAli") Integer selfAliCode,
|
||||||
@Param("selfUnion") Integer selfUnionCode);
|
@Param("selfUnion") Integer selfUnionCode, @Param("worDeviceRequest") String worDeviceRequest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ public interface ICheckMethodAppService{
|
|||||||
|
|
||||||
R<?> searchCheckMethodList(Integer pageNo, Integer pageSize, String checkType, String name, String packageName);
|
R<?> searchCheckMethodList(Integer pageNo, Integer pageSize, String checkType, String name, String packageName);
|
||||||
|
|
||||||
R<?> exportCheckMethod(String checkType, String name, String packageName, HttpServletResponse response);
|
void exportCheckMethod(String checkType, String name, String packageName, HttpServletResponse response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ public interface ICheckPartAppService {
|
|||||||
|
|
||||||
R<?> searchCheckPartList(Integer pageNo, Integer pageSize, String checkType, String name, String packageName);
|
R<?> searchCheckPartList(Integer pageNo, Integer pageSize, String checkType, String name, String packageName);
|
||||||
|
|
||||||
R<?> exportCheckPart(String checkType, String name, String packageName, HttpServletResponse response);
|
void exportCheckPart(String checkType, String name, String packageName, HttpServletResponse response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import cn.hutool.core.util.ObjectUtil;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.openhis.check.domain.CheckMethod;
|
import com.openhis.check.domain.CheckMethod;
|
||||||
|
import com.openhis.check.domain.CheckPackage;
|
||||||
import com.openhis.check.service.ICheckMethodService;
|
import com.openhis.check.service.ICheckMethodService;
|
||||||
|
import com.openhis.check.service.ICheckPackageService;
|
||||||
import com.openhis.web.check.appservice.ICheckMethodAppService;
|
import com.openhis.web.check.appservice.ICheckMethodAppService;
|
||||||
|
import com.openhis.web.check.dto.CheckMethodDto;
|
||||||
import com.openhis.web.reportmanage.utils.ExcelFillerUtil;
|
import com.openhis.web.reportmanage.utils.ExcelFillerUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -16,6 +19,7 @@ import java.io.IOException;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -24,10 +28,15 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ICheckMethodService checkMethodService;
|
private ICheckMethodService checkMethodService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ICheckPackageService checkPackageService; // Bug #384修复:注入套餐服务
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> getCheckMethodList() {
|
public R<?> getCheckMethodList() {
|
||||||
List<CheckMethod> list = checkMethodService.list();
|
List<CheckMethod> list = checkMethodService.list();
|
||||||
return R.ok(list);
|
// Bug #384修复:转换为DTO并关联套餐价格
|
||||||
|
List<CheckMethodDto> dtoList = convertToDtoWithPackagePrice(list);
|
||||||
|
return R.ok(dtoList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -43,7 +52,67 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
|||||||
wrapper.eq(CheckMethod::getPackageName, packageName);
|
wrapper.eq(CheckMethod::getPackageName, packageName);
|
||||||
}
|
}
|
||||||
List<CheckMethod> list = checkMethodService.list(wrapper);
|
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
|
@Override
|
||||||
@@ -89,7 +158,7 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> exportCheckMethod(String checkType, String name, String packageName, HttpServletResponse response) {
|
public void exportCheckMethod(String checkType, String name, String packageName, HttpServletResponse response) {
|
||||||
LambdaQueryWrapper<CheckMethod> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CheckMethod> wrapper = new LambdaQueryWrapper<>();
|
||||||
if (checkType != null && ObjectUtil.isNotEmpty(checkType)) {
|
if (checkType != null && ObjectUtil.isNotEmpty(checkType)) {
|
||||||
wrapper.eq(CheckMethod::getCheckType, checkType);
|
wrapper.eq(CheckMethod::getCheckType, checkType);
|
||||||
@@ -103,7 +172,13 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
|||||||
List<CheckMethod> list = checkMethodService.list(wrapper);
|
List<CheckMethod> list = checkMethodService.list(wrapper);
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return R.fail("导出Excel失败,无数据。");
|
try {
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write("{\"code\":500,\"msg\":\"导出Excel失败,无数据。\"}");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("写入响应失败", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -123,9 +198,12 @@ public class CheckMethodAppServiceImpl implements ICheckMethodAppService {
|
|||||||
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
||||||
} catch (IOException | IllegalAccessException e) {
|
} catch (IOException | IllegalAccessException e) {
|
||||||
log.error("导出Excel失败", e);
|
log.error("导出Excel失败", e);
|
||||||
return R.fail("导出Excel失败:" + e.getMessage());
|
try {
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write("{\"code\":500,\"msg\":\"导出Excel失败:" + e.getMessage() + "\"}");
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("写入响应失败", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return R.ok(null, "导出Excel成功");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查套餐AppService实现
|
* 检查套餐 AppService 实现
|
||||||
*
|
*
|
||||||
* @author system
|
* @author system
|
||||||
* @date 2025-11-26
|
* @date 2025-11-26
|
||||||
@@ -35,6 +35,32 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
private final ICheckPackageService checkPackageService;
|
private final ICheckPackageService checkPackageService;
|
||||||
private final ICheckPackageDetailService checkPackageDetailService;
|
private final ICheckPackageDetailService checkPackageDetailService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换明细 DTO 列表为实体列表
|
||||||
|
* @param detailDtos 明细 DTO 列表
|
||||||
|
* @param packageId 套餐 ID
|
||||||
|
* @param orderNumStart 起始序号
|
||||||
|
* @return 明细实体列表
|
||||||
|
*/
|
||||||
|
private List<CheckPackageDetail> convertToDetails(List<CheckPackageDetailDto> detailDtos, Long packageId, int orderNumStart) {
|
||||||
|
if (detailDtos == null || detailDtos.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CheckPackageDetail> details = new ArrayList<>();
|
||||||
|
int orderNum = orderNumStart;
|
||||||
|
for (CheckPackageDetailDto detailDto : detailDtos) {
|
||||||
|
CheckPackageDetail detail = new CheckPackageDetail();
|
||||||
|
BeanUtils.copyProperties(detailDto, detail);
|
||||||
|
detail.setPackageId(packageId);
|
||||||
|
detail.setOrderNum(orderNum++);
|
||||||
|
detail.setCreateTime(LocalDateTime.now());
|
||||||
|
detail.setUpdateTime(LocalDateTime.now());
|
||||||
|
details.add(detail);
|
||||||
|
}
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> getCheckPackageList() {
|
public R<?> getCheckPackageList() {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +87,7 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
.orderByAsc(CheckPackageDetail::getOrderNum)
|
.orderByAsc(CheckPackageDetail::getOrderNum)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 转换为DTO
|
// 转换为 DTO
|
||||||
CheckPackageDto dto = new CheckPackageDto();
|
CheckPackageDto dto = new CheckPackageDto();
|
||||||
BeanUtils.copyProperties(checkPackage, dto);
|
BeanUtils.copyProperties(checkPackage, dto);
|
||||||
|
|
||||||
@@ -101,28 +127,21 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
|
|
||||||
// 保存套餐明细
|
// 保存套餐明细
|
||||||
if (checkPackageDto.getItems() != null && !checkPackageDto.getItems().isEmpty()) {
|
if (checkPackageDto.getItems() != null && !checkPackageDto.getItems().isEmpty()) {
|
||||||
List<CheckPackageDetail> details = new ArrayList<>();
|
List<CheckPackageDetail> details = convertToDetails(checkPackageDto.getItems(), checkPackage.getId(), 1);
|
||||||
int orderNum = 1;
|
boolean detailSaveResult = checkPackageDetailService.saveBatch(details);
|
||||||
for (CheckPackageDetailDto detailDto : checkPackageDto.getItems()) {
|
if (!detailSaveResult) {
|
||||||
CheckPackageDetail detail = new CheckPackageDetail();
|
throw new RuntimeException("保存套餐明细失败");
|
||||||
BeanUtils.copyProperties(detailDto, detail);
|
|
||||||
detail.setPackageId(checkPackage.getId());
|
|
||||||
detail.setOrderNum(orderNum++);
|
|
||||||
detail.setCreateTime(LocalDateTime.now());
|
|
||||||
detail.setUpdateTime(LocalDateTime.now());
|
|
||||||
details.add(detail);
|
|
||||||
}
|
}
|
||||||
checkPackageDetailService.saveBatch(details);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return R.ok(checkPackage.getId(), "保存成功");
|
return R.ok(checkPackage.getId(), "保存成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("新增检查套餐失败", e);
|
log.error("新增检查套餐失败", e);
|
||||||
|
|
||||||
// 捕获PostgreSQL唯一约束冲突异常
|
// 捕获 PostgreSQL 唯一约束冲突异常
|
||||||
String errorMessage = e.getMessage();
|
String errorMessage = e.getMessage();
|
||||||
if (errorMessage != null) {
|
if (errorMessage != null) {
|
||||||
// PostgreSQL唯一约束错误通常包含 "duplicate key value" 或约束名称
|
// PostgreSQL 唯一约束错误通常包含 "duplicate key value" 或约束名称
|
||||||
if (errorMessage.contains("duplicate key value") ||
|
if (errorMessage.contains("duplicate key value") ||
|
||||||
errorMessage.contains("违反唯一约束") ||
|
errorMessage.contains("违反唯一约束") ||
|
||||||
errorMessage.contains("unique constraint")) {
|
errorMessage.contains("unique constraint")) {
|
||||||
@@ -135,7 +154,7 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return R.fail("新增检查套餐失败: " + errorMessage);
|
return R.fail("新增检查套餐失败:" + errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,24 +189,14 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
|
|
||||||
// 保存新的套餐明细
|
// 保存新的套餐明细
|
||||||
if (checkPackageDto.getItems() != null && !checkPackageDto.getItems().isEmpty()) {
|
if (checkPackageDto.getItems() != null && !checkPackageDto.getItems().isEmpty()) {
|
||||||
List<CheckPackageDetail> details = new ArrayList<>();
|
List<CheckPackageDetail> details = convertToDetails(checkPackageDto.getItems(), checkPackage.getId(), 1);
|
||||||
int orderNum = 1;
|
|
||||||
for (CheckPackageDetailDto detailDto : checkPackageDto.getItems()) {
|
|
||||||
CheckPackageDetail detail = new CheckPackageDetail();
|
|
||||||
BeanUtils.copyProperties(detailDto, detail);
|
|
||||||
detail.setPackageId(checkPackage.getId());
|
|
||||||
detail.setOrderNum(orderNum++);
|
|
||||||
detail.setCreateTime(LocalDateTime.now());
|
|
||||||
detail.setUpdateTime(LocalDateTime.now());
|
|
||||||
details.add(detail);
|
|
||||||
}
|
|
||||||
checkPackageDetailService.saveBatch(details);
|
checkPackageDetailService.saveBatch(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
return R.ok("更新成功");
|
return R.ok("更新成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新检查套餐失败", e);
|
log.error("更新检查套餐失败", e);
|
||||||
return R.fail("更新检查套餐失败: " + e.getMessage());
|
return R.fail("更新检查套餐失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,11 +210,14 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
return R.fail("套餐不存在");
|
return R.fail("套餐不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除套餐明细
|
// 删除套餐明细 - 先删除子表数据
|
||||||
checkPackageDetailService.remove(
|
boolean removeDetailsResult = checkPackageDetailService.remove(
|
||||||
new LambdaQueryWrapper<CheckPackageDetail>()
|
new LambdaQueryWrapper<CheckPackageDetail>()
|
||||||
.eq(CheckPackageDetail::getPackageId, id)
|
.eq(CheckPackageDetail::getPackageId, id)
|
||||||
);
|
);
|
||||||
|
if (!removeDetailsResult) {
|
||||||
|
log.warn("删除套餐明细失败,套餐 ID: {}", id);
|
||||||
|
}
|
||||||
|
|
||||||
// 删除套餐主表
|
// 删除套餐主表
|
||||||
boolean deleteResult = checkPackageService.removeById(id);
|
boolean deleteResult = checkPackageService.removeById(id);
|
||||||
@@ -213,11 +225,11 @@ public class CheckPackageAppServiceImpl implements ICheckPackageAppService {
|
|||||||
return R.fail("删除套餐失败");
|
return R.fail("删除套餐失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("删除检查套餐成功,套餐 ID: {}", id);
|
||||||
return R.ok("删除成功");
|
return R.ok("删除成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("删除检查套餐失败", e);
|
log.error("删除检查套餐失败", e);
|
||||||
return R.fail("删除检查套餐失败: " + e.getMessage());
|
return R.fail("删除检查套餐失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class CheckPartAppServiceImpl implements ICheckPartAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> exportCheckPart(String checkType, String name, String packageName, HttpServletResponse response) {
|
public void exportCheckPart(String checkType, String name, String packageName, HttpServletResponse response) {
|
||||||
LambdaQueryWrapper<CheckPart> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CheckPart> wrapper = new LambdaQueryWrapper<>();
|
||||||
if (checkType != null && ObjectUtil.isNotEmpty(checkType)) {
|
if (checkType != null && ObjectUtil.isNotEmpty(checkType)) {
|
||||||
wrapper.eq(CheckPart::getCheckType, checkType);
|
wrapper.eq(CheckPart::getCheckType, checkType);
|
||||||
@@ -79,7 +79,13 @@ public class CheckPartAppServiceImpl implements ICheckPartAppService {
|
|||||||
List<CheckPart> list = checkPartService.list(wrapper);
|
List<CheckPart> list = checkPartService.list(wrapper);
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return R.fail("导出Excel失败,无数据。");
|
try {
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write("{\"code\":500,\"msg\":\"导出Excel失败,无数据。\"}");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("写入响应失败", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -102,8 +108,12 @@ public class CheckPartAppServiceImpl implements ICheckPartAppService {
|
|||||||
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
ExcelFillerUtil.makeExcelFile(response, list, headers, excelName, null);
|
||||||
} catch (IOException | IllegalAccessException e) {
|
} catch (IOException | IllegalAccessException e) {
|
||||||
log.error("导出Excel失败", e);
|
log.error("导出Excel失败", e);
|
||||||
return R.fail("导出Excel失败:" + e.getMessage());
|
try {
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write("{\"code\":500,\"msg\":\"导出Excel失败:" + e.getMessage() + "\"}");
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("写入响应失败", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return R.ok(null, "导出Excel成功");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.core.common.core.controller.BaseController;
|
import com.core.common.core.controller.BaseController;
|
||||||
import com.core.common.core.domain.AjaxResult;
|
import com.core.common.core.domain.AjaxResult;
|
||||||
import com.core.common.core.page.TableDataInfo;
|
import com.core.common.core.page.TableDataInfo;
|
||||||
|
import com.core.common.exception.ServiceException;
|
||||||
import com.core.common.utils.AssignSeqUtil;
|
import com.core.common.utils.AssignSeqUtil;
|
||||||
import com.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.openhis.administration.domain.Account;
|
||||||
import com.openhis.administration.domain.ChargeItem;
|
import com.openhis.administration.domain.ChargeItem;
|
||||||
|
import com.openhis.administration.service.IAccountService;
|
||||||
import com.openhis.administration.service.IChargeItemService;
|
import com.openhis.administration.service.IChargeItemService;
|
||||||
import com.openhis.check.domain.ExamApply;
|
import com.openhis.check.domain.ExamApply;
|
||||||
import com.openhis.check.domain.ExamApplyItem;
|
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.AssignSeqEnum;
|
||||||
import com.openhis.common.enums.ChargeItemStatus;
|
import com.openhis.common.enums.ChargeItemStatus;
|
||||||
import com.openhis.common.enums.GenerateSource;
|
import com.openhis.common.enums.GenerateSource;
|
||||||
|
import com.openhis.common.enums.ItemType;
|
||||||
import com.openhis.common.enums.RequestStatus;
|
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.ExamApplyDto;
|
||||||
import com.openhis.web.check.dto.ExamApplyItemDto;
|
import com.openhis.web.check.dto.ExamApplyItemDto;
|
||||||
import com.openhis.workflow.domain.ServiceRequest;
|
import com.openhis.workflow.domain.ServiceRequest;
|
||||||
@@ -57,17 +63,41 @@ public class ExamApplyController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IChargeItemService chargeItemService;
|
private IChargeItemService chargeItemService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAccountService accountService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private AssignSeqUtil assignSeqUtil;
|
private AssignSeqUtil assignSeqUtil;
|
||||||
|
@Autowired
|
||||||
|
private IOrganizationService organizationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询检查申请单列表
|
* 查询检查申请单列表
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(ExamApply examApply) {
|
public TableDataInfo list(ExamApply examApply, @RequestParam(value = "encounterId", required = false) Long encounterId) {
|
||||||
startPage();
|
startPage();
|
||||||
LambdaQueryWrapper<ExamApply> wrapper = new LambdaQueryWrapper<>();
|
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.eq(ExamApply::getVisitNo, examApply.getVisitNo());
|
||||||
}
|
}
|
||||||
wrapper.orderByDesc(ExamApply::getApplyTime);
|
wrapper.orderByDesc(ExamApply::getApplyTime);
|
||||||
@@ -147,6 +177,8 @@ public class ExamApplyController extends BaseController {
|
|||||||
examApply.setOperatorId("system");
|
examApply.setOperatorId("system");
|
||||||
}
|
}
|
||||||
examApplyService.save(examApply);
|
examApplyService.save(examApply);
|
||||||
|
// 业务主键为 apply_no,自增 id 不会随 save 回填;列表接口依赖 wor_service_request.based_on_id=exam_apply.id 关联本次就诊,此处必须回读 id
|
||||||
|
examApply = examApplyService.getById(applyNo);
|
||||||
|
|
||||||
// ========== 2. 批量保存明细 + 写入门诊医嘱 + 写入费用项 ==========
|
// ========== 2. 批量保存明细 + 写入门诊医嘱 + 写入费用项 ==========
|
||||||
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
|
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
|
||||||
@@ -191,6 +223,9 @@ public class ExamApplyController extends BaseController {
|
|||||||
|
|
||||||
// 检查申请不走诊疗定义,设置为0占位(数据库有NOT NULL约束)
|
// 检查申请不走诊疗定义,设置为0占位(数据库有NOT NULL约束)
|
||||||
serviceRequest.setActivityId(0L);
|
serviceRequest.setActivityId(0L);
|
||||||
|
// 🔧 Bug Fix: 设置医嘱类型为诊疗(3),与检验申请保持一致
|
||||||
|
// categoryEnum=3 → SQL查询返回 adviceType=3(诊疗),避免被错误归类为药品
|
||||||
|
serviceRequest.setCategoryEnum(ItemType.ACTIVITY.getValue());
|
||||||
|
|
||||||
// 患者和就诊信息 —— 使用前端传递的数字型ID
|
// 患者和就诊信息 —— 使用前端传递的数字型ID
|
||||||
if (dto.getPatientIdNum() != null) {
|
if (dto.getPatientIdNum() != null) {
|
||||||
@@ -201,8 +236,19 @@ public class ExamApplyController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceRequest.setRequesterId(currentUserId); // 开单医生
|
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); // 签发时间
|
serviceRequest.setAuthoredTime(now); // 签发时间
|
||||||
|
// 🔧 Bug Fix: 不设置门诊类型,保留上面已设置的 categoryEnum=3(诊疗类型)
|
||||||
|
// EncounterClass.AMB.getValue()=2 表示门诊类型,会覆盖诊疗类型导致医嘱被错误归类
|
||||||
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 来源=医生开立
|
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 来源=医生开立
|
||||||
|
|
||||||
// 将项目名称存入 contentJson,使医嘱列表能通过 JSON 字段回显 adviceName
|
// 将项目名称存入 contentJson,使医嘱列表能通过 JSON 字段回显 adviceName
|
||||||
@@ -243,10 +289,17 @@ public class ExamApplyController extends BaseController {
|
|||||||
chargeItem.setRequestingOrgId(currentOrgId); // 开立科室
|
chargeItem.setRequestingOrgId(currentOrgId); // 开立科室
|
||||||
chargeItem.setEnteredDate(now); // 开立时间
|
chargeItem.setEnteredDate(now); // 开立时间
|
||||||
|
|
||||||
// 以下字段均有 NOT NULL 约束,检查申请不走定价/账户体系,用0占位
|
// 以下字段均有 NOT NULL 约束,检查申请不走定价体系,用0占位
|
||||||
chargeItem.setDefinitionId(0L); // 费用定价ID
|
chargeItem.setDefinitionId(0L); // 费用定价ID
|
||||||
chargeItem.setAccountId(0L); // 关联账户ID
|
// 🔧 BugFix#385: 获取患者真实的自费账户,预结算验证要求accountId必须真实存在
|
||||||
chargeItem.setContextEnum(2); // 类型:2=诊疗
|
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.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION); // 产品来源表
|
||||||
chargeItem.setProductId(0L); // 产品ID
|
chargeItem.setProductId(0L); // 产品ID
|
||||||
|
|
||||||
@@ -368,6 +421,9 @@ public class ExamApplyController extends BaseController {
|
|||||||
serviceRequest.setBasedOnTable("exam_apply");
|
serviceRequest.setBasedOnTable("exam_apply");
|
||||||
serviceRequest.setBasedOnId(examApply.getId());
|
serviceRequest.setBasedOnId(examApply.getId());
|
||||||
serviceRequest.setActivityId(0L);
|
serviceRequest.setActivityId(0L);
|
||||||
|
// 🔧 Bug Fix: 设置医嘱类型为诊疗(3),与检验申请保持一致
|
||||||
|
// categoryEnum=3 → SQL查询返回 adviceType=3(诊疗),避免被错误归类为药品
|
||||||
|
serviceRequest.setCategoryEnum(ItemType.ACTIVITY.getValue());
|
||||||
|
|
||||||
if (dto.getPatientIdNum() != null) {
|
if (dto.getPatientIdNum() != null) {
|
||||||
serviceRequest.setPatientId(dto.getPatientIdNum());
|
serviceRequest.setPatientId(dto.getPatientIdNum());
|
||||||
@@ -376,8 +432,19 @@ public class ExamApplyController extends BaseController {
|
|||||||
serviceRequest.setEncounterId(dto.getEncounterId());
|
serviceRequest.setEncounterId(dto.getEncounterId());
|
||||||
}
|
}
|
||||||
serviceRequest.setRequesterId(currentUserId);
|
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);
|
serviceRequest.setAuthoredTime(now);
|
||||||
|
// 🔧 Bug Fix: 不设置门诊类型,保留上面已设置的 categoryEnum=3(诊疗类型)
|
||||||
|
// EncounterClass.AMB.getValue()=2 表示门诊类型,会覆盖诊疗类型导致医嘱被错误归类
|
||||||
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
||||||
|
|
||||||
// 将项目名称存入 contentJson,使医嘱列表能通过 JSON 字段回显 adviceName
|
// 将项目名称存入 contentJson,使医嘱列表能通过 JSON 字段回显 adviceName
|
||||||
@@ -412,8 +479,14 @@ public class ExamApplyController extends BaseController {
|
|||||||
chargeItem.setRequestingOrgId(currentOrgId);
|
chargeItem.setRequestingOrgId(currentOrgId);
|
||||||
chargeItem.setEnteredDate(now);
|
chargeItem.setEnteredDate(now);
|
||||||
chargeItem.setDefinitionId(0L);
|
chargeItem.setDefinitionId(0L);
|
||||||
chargeItem.setAccountId(0L);
|
// 🔧 BugFix#385: 获取患者真实的自费账户,预结算验证要求accountId必须真实存在
|
||||||
chargeItem.setContextEnum(2);
|
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.setProductTable(CommonConstants.TableName.WOR_ACTIVITY_DEFINITION);
|
||||||
chargeItem.setProductId(0L);
|
chargeItem.setProductId(0L);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.openhis.web.check.dto;
|
package com.openhis.web.check.dto;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查方法DTO - Bug #384修复:增加套餐价格字段
|
||||||
|
* 用于API返回数据传输,不含数据库注解
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class CheckMethodDto {
|
public class CheckMethodDto {
|
||||||
@@ -14,7 +17,6 @@ public class CheckMethodDto {
|
|||||||
/**
|
/**
|
||||||
* 检查方法ID
|
* 检查方法ID
|
||||||
*/
|
*/
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/* 检查类型 */
|
/* 检查类型 */
|
||||||
@@ -29,6 +31,12 @@ public class CheckMethodDto {
|
|||||||
/* 套餐名称 */
|
/* 套餐名称 */
|
||||||
private String packageName;
|
private String packageName;
|
||||||
|
|
||||||
|
/* 套餐价格 - Bug #384修复:通过packageName匹配CheckPackage获取 */
|
||||||
|
private BigDecimal packagePrice;
|
||||||
|
|
||||||
|
/* 服务费 - Bug #384修复:通过packageName匹配CheckPackage获取 */
|
||||||
|
private BigDecimal serviceFee;
|
||||||
|
|
||||||
/* 曝光次数 */
|
/* 曝光次数 */
|
||||||
private Integer exposureNum;
|
private Integer exposureNum;
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ public class CheckPackageDetailDto {
|
|||||||
@NotNull(message = "数量不能为空")
|
@NotNull(message = "数量不能为空")
|
||||||
private Integer quantity;
|
private Integer quantity;
|
||||||
|
|
||||||
|
/** 单位 */
|
||||||
|
private String unit;
|
||||||
|
|
||||||
/** 单价 */
|
/** 单价 */
|
||||||
@NotNull(message = "单价不能为空")
|
@NotNull(message = "单价不能为空")
|
||||||
private BigDecimal unitPrice;
|
private BigDecimal unitPrice;
|
||||||
|
|||||||
@@ -343,11 +343,28 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
|||||||
serviceRequest.setEncounterId(surgeryDto.getEncounterId()); // 就诊id
|
serviceRequest.setEncounterId(surgeryDto.getEncounterId()); // 就诊id
|
||||||
serviceRequest.setAuthoredTime(curDate); // 请求签发时间
|
serviceRequest.setAuthoredTime(curDate); // 请求签发时间
|
||||||
serviceRequest.setOrgId(orgId); // 执行科室
|
serviceRequest.setOrgId(orgId); // 执行科室
|
||||||
|
// 🔧 BugFix#318: 设置 contentJson,包含手术名称
|
||||||
|
Map<String, String> serviceContentMap = new HashMap<>();
|
||||||
|
String surgeryNameFromDto = surgeryDto.getSurgeryName();
|
||||||
|
String surgeryCodeFromDto = surgeryDto.getSurgeryCode();
|
||||||
|
log.info("【DEBUG】surgeryName from DTO: {}", surgeryNameFromDto);
|
||||||
|
log.info("【DEBUG】surgeryCode from DTO: {}", surgeryCodeFromDto);
|
||||||
|
serviceContentMap.put("surgeryName", surgeryNameFromDto != null ? surgeryNameFromDto : "");
|
||||||
|
serviceContentMap.put("surgeryCode", surgeryCodeFromDto != null ? surgeryCodeFromDto : "");
|
||||||
|
try {
|
||||||
|
String contentJson = new ObjectMapper().writeValueAsString(serviceContentMap);
|
||||||
|
log.info("【DEBUG】Setting contentJson: {}", contentJson);
|
||||||
|
serviceRequest.setContentJson(contentJson);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("【DEBUG】设置手术医嘱 contentJson 失败", e);
|
||||||
|
}
|
||||||
serviceRequestService.save(serviceRequest);
|
serviceRequestService.save(serviceRequest);
|
||||||
|
log.info("【DEBUG】Saved serviceRequest with ID: {}, contentJson: {}",
|
||||||
|
serviceRequest.getId(), serviceRequest.getContentJson());
|
||||||
|
|
||||||
// 生成收费项目
|
// 生成收费项目
|
||||||
ChargeItem chargeItem = new ChargeItem();
|
ChargeItem chargeItem = new ChargeItem();
|
||||||
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 收费状态
|
chargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue()); // 收费状态:待收费
|
||||||
chargeItem.setBusNo("CI" + serviceRequest.getBusNo());
|
chargeItem.setBusNo("CI" + serviceRequest.getBusNo());
|
||||||
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
|
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
|
||||||
chargeItem.setPatientId(surgeryDto.getPatientId()); // 患者
|
chargeItem.setPatientId(surgeryDto.getPatientId()); // 患者
|
||||||
@@ -541,15 +558,33 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
|||||||
// 收集所有需要查询的ID
|
// 收集所有需要查询的ID
|
||||||
Set<Long> practitionerIds = new HashSet<>();
|
Set<Long> practitionerIds = new HashSet<>();
|
||||||
Set<Long> orgIds = new HashSet<>();
|
Set<Long> orgIds = new HashSet<>();
|
||||||
Set<Long> otherIds = new HashSet<>();
|
Set<Long> userIds = new HashSet<>(); // 用于查询sys_user表
|
||||||
|
|
||||||
// 收集Practitioner IDs
|
// 收集Practitioner IDs (医生相关)
|
||||||
if (surgery.getMainSurgeonId() != null) practitionerIds.add(surgery.getMainSurgeonId());
|
if (surgery.getMainSurgeonId() != null) {
|
||||||
if (surgery.getAnesthetistId() != null) practitionerIds.add(surgery.getAnesthetistId());
|
practitionerIds.add(surgery.getMainSurgeonId());
|
||||||
if (surgery.getAssistant1Id() != null) practitionerIds.add(surgery.getAssistant1Id());
|
userIds.add(surgery.getMainSurgeonId());
|
||||||
if (surgery.getAssistant2Id() != null) practitionerIds.add(surgery.getAssistant2Id());
|
}
|
||||||
if (surgery.getScrubNurseId() != null) practitionerIds.add(surgery.getScrubNurseId());
|
if (surgery.getAnesthetistId() != null) {
|
||||||
if (surgery.getApplyDoctorId() != null) practitionerIds.add(surgery.getApplyDoctorId());
|
practitionerIds.add(surgery.getAnesthetistId());
|
||||||
|
userIds.add(surgery.getAnesthetistId());
|
||||||
|
}
|
||||||
|
if (surgery.getAssistant1Id() != null) {
|
||||||
|
practitionerIds.add(surgery.getAssistant1Id());
|
||||||
|
userIds.add(surgery.getAssistant1Id());
|
||||||
|
}
|
||||||
|
if (surgery.getAssistant2Id() != null) {
|
||||||
|
practitionerIds.add(surgery.getAssistant2Id());
|
||||||
|
userIds.add(surgery.getAssistant2Id());
|
||||||
|
}
|
||||||
|
if (surgery.getScrubNurseId() != null) {
|
||||||
|
practitionerIds.add(surgery.getScrubNurseId());
|
||||||
|
userIds.add(surgery.getScrubNurseId());
|
||||||
|
}
|
||||||
|
if (surgery.getApplyDoctorId() != null) {
|
||||||
|
practitionerIds.add(surgery.getApplyDoctorId());
|
||||||
|
userIds.add(surgery.getApplyDoctorId());
|
||||||
|
}
|
||||||
|
|
||||||
// 收集Organization IDs
|
// 收集Organization IDs
|
||||||
if (surgery.getOrgId() != null) orgIds.add(surgery.getOrgId());
|
if (surgery.getOrgId() != null) orgIds.add(surgery.getOrgId());
|
||||||
@@ -558,69 +593,151 @@ public class SurgeryAppServiceImpl implements ISurgeryAppService {
|
|||||||
// 批量查询并缓存结果
|
// 批量查询并缓存结果
|
||||||
Map<Long, String> practitionerNameMap = new HashMap<>();
|
Map<Long, String> practitionerNameMap = new HashMap<>();
|
||||||
Map<Long, String> orgNameMap = new HashMap<>();
|
Map<Long, String> orgNameMap = new HashMap<>();
|
||||||
|
Map<Long, String> userNameMap = new HashMap<>(); // 从sys_user查询的名称
|
||||||
|
|
||||||
// 批量查询Practitioner
|
// 批量查询Practitioner
|
||||||
if (!practitionerIds.isEmpty()) {
|
if (!practitionerIds.isEmpty()) {
|
||||||
List<com.openhis.administration.domain.Practitioner> practitioners = practitionerService.listByIds(practitionerIds);
|
try {
|
||||||
for (com.openhis.administration.domain.Practitioner p : practitioners) {
|
List<com.openhis.administration.domain.Practitioner> practitioners = practitionerService.listByIds(practitionerIds);
|
||||||
practitionerNameMap.put(p.getId(), p.getName());
|
for (com.openhis.administration.domain.Practitioner p : practitioners) {
|
||||||
|
if (p.getName() != null && !p.getName().isEmpty()) {
|
||||||
|
practitionerNameMap.put(p.getId(), p.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询Practitioner名称失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询SysUser (作为备选) - 使用逐个查询
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
try {
|
||||||
|
for (Long userId : userIds) {
|
||||||
|
SysUser u = sysUserService.selectUserById(userId);
|
||||||
|
if (u != null) {
|
||||||
|
String userName = u.getNickName() != null && !u.getNickName().isEmpty()
|
||||||
|
? u.getNickName()
|
||||||
|
: u.getUserName();
|
||||||
|
if (userName != null && !userName.isEmpty()) {
|
||||||
|
userNameMap.put(u.getUserId(), userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询SysUser名称失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量查询Organization
|
// 批量查询Organization
|
||||||
if (!orgIds.isEmpty()) {
|
if (!orgIds.isEmpty()) {
|
||||||
List<Organization> orgs = organizationService.listByIds(orgIds);
|
try {
|
||||||
for (Organization o : orgs) {
|
List<Organization> orgs = organizationService.listByIds(orgIds);
|
||||||
orgNameMap.put(o.getId(), o.getName());
|
for (Organization o : orgs) {
|
||||||
|
if (o.getName() != null && !o.getName().isEmpty()) {
|
||||||
|
orgNameMap.put(o.getId(), o.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询Organization名称失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填充患者姓名
|
// 填充患者姓名
|
||||||
if (surgery.getPatientId() != null && surgery.getPatientName() == null) {
|
if (surgery.getPatientId() != null && surgery.getPatientName() == null) {
|
||||||
Patient patient = patientService.getById(surgery.getPatientId());
|
try {
|
||||||
if (patient != null) {
|
Patient patient = patientService.getById(surgery.getPatientId());
|
||||||
surgery.setPatientName(patient.getName());
|
if (patient != null) {
|
||||||
|
surgery.setPatientName(patient.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询患者名称失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用缓存填充名称
|
// 填充医生名称 - 优先使用practitioner,如果不存在则使用sys_user
|
||||||
if (surgery.getMainSurgeonId() != null && surgery.getMainSurgeonName() == null) {
|
if (surgery.getMainSurgeonId() != null && surgery.getMainSurgeonName() == null) {
|
||||||
surgery.setMainSurgeonName(practitionerNameMap.get(surgery.getMainSurgeonId()));
|
String name = practitionerNameMap.get(surgery.getMainSurgeonId());
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = userNameMap.get(surgery.getMainSurgeonId());
|
||||||
|
}
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setMainSurgeonName(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (surgery.getAnesthetistId() != null && surgery.getAnesthetistName() == null) {
|
if (surgery.getAnesthetistId() != null && surgery.getAnesthetistName() == null) {
|
||||||
surgery.setAnesthetistName(practitionerNameMap.get(surgery.getAnesthetistId()));
|
String name = practitionerNameMap.get(surgery.getAnesthetistId());
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = userNameMap.get(surgery.getAnesthetistId());
|
||||||
|
}
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setAnesthetistName(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (surgery.getAssistant1Id() != null && surgery.getAssistant1Name() == null) {
|
if (surgery.getAssistant1Id() != null && surgery.getAssistant1Name() == null) {
|
||||||
surgery.setAssistant1Name(practitionerNameMap.get(surgery.getAssistant1Id()));
|
String name = practitionerNameMap.get(surgery.getAssistant1Id());
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = userNameMap.get(surgery.getAssistant1Id());
|
||||||
|
}
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setAssistant1Name(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (surgery.getAssistant2Id() != null && surgery.getAssistant2Name() == null) {
|
if (surgery.getAssistant2Id() != null && surgery.getAssistant2Name() == null) {
|
||||||
surgery.setAssistant2Name(practitionerNameMap.get(surgery.getAssistant2Id()));
|
String name = practitionerNameMap.get(surgery.getAssistant2Id());
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = userNameMap.get(surgery.getAssistant2Id());
|
||||||
|
}
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setAssistant2Name(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (surgery.getScrubNurseId() != null && surgery.getScrubNurseName() == null) {
|
if (surgery.getScrubNurseId() != null && surgery.getScrubNurseName() == null) {
|
||||||
surgery.setScrubNurseName(practitionerNameMap.get(surgery.getScrubNurseId()));
|
String name = practitionerNameMap.get(surgery.getScrubNurseId());
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = userNameMap.get(surgery.getScrubNurseId());
|
||||||
|
}
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setScrubNurseName(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (surgery.getApplyDoctorId() != null && surgery.getApplyDoctorName() == null) {
|
if (surgery.getApplyDoctorId() != null && surgery.getApplyDoctorName() == null) {
|
||||||
surgery.setApplyDoctorName(practitionerNameMap.get(surgery.getApplyDoctorId()));
|
String name = practitionerNameMap.get(surgery.getApplyDoctorId());
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = userNameMap.get(surgery.getApplyDoctorId());
|
||||||
|
}
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setApplyDoctorName(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填充手术室名称
|
// 填充手术室名称
|
||||||
if (surgery.getOperatingRoomId() != null && surgery.getOperatingRoomName() == null) {
|
if (surgery.getOperatingRoomId() != null && surgery.getOperatingRoomName() == null) {
|
||||||
OperatingRoom operatingRoom = operatingRoomService.getById(surgery.getOperatingRoomId());
|
try {
|
||||||
if (operatingRoom != null) {
|
OperatingRoom operatingRoom = operatingRoomService.getById(surgery.getOperatingRoomId());
|
||||||
surgery.setOperatingRoomName(operatingRoom.getName());
|
if (operatingRoom != null) {
|
||||||
|
surgery.setOperatingRoomName(operatingRoom.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询手术室名称失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用缓存填充组织名称
|
// 使用缓存填充组织名称
|
||||||
if (surgery.getOrgId() != null && surgery.getOrgName() == null) {
|
if (surgery.getOrgId() != null && surgery.getOrgName() == null) {
|
||||||
surgery.setOrgName(orgNameMap.get(surgery.getOrgId()));
|
String name = orgNameMap.get(surgery.getOrgId());
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setOrgName(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (surgery.getApplyDeptId() != null && surgery.getApplyDeptName() == null) {
|
if (surgery.getApplyDeptId() != null && surgery.getApplyDeptName() == null) {
|
||||||
surgery.setApplyDeptName(orgNameMap.get(surgery.getApplyDeptId()));
|
String name = orgNameMap.get(surgery.getApplyDeptId());
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
surgery.setApplyDeptName(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("填充手术名称字段完成 - patientName: {}, mainSurgeonName: {}, orgName: {}",
|
log.debug("填充手术名称字段完成 - patientName: {}, mainSurgeonName: {}, applyDeptName: {}",
|
||||||
surgery.getPatientName(), surgery.getMainSurgeonName(), surgery.getOrgName());
|
surgery.getPatientName(), surgery.getMainSurgeonName(), surgery.getApplyDeptName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.openhis.web.clinicalmanage.appservice.impl;
|
package com.openhis.web.clinicalmanage.appservice.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.core.common.core.domain.model.LoginUser;
|
import com.core.common.core.domain.model.LoginUser;
|
||||||
import com.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.openhis.administration.domain.Patient;
|
import com.openhis.administration.domain.Patient;
|
||||||
|
import com.openhis.administration.service.IOrganizationService;
|
||||||
import com.openhis.administration.service.IPatientService;
|
import com.openhis.administration.service.IPatientService;
|
||||||
|
import com.openhis.clinical.domain.Surgery;
|
||||||
|
import com.openhis.clinical.service.ISurgeryService;
|
||||||
import com.openhis.surgicalschedule.domain.OpSchedule;
|
import com.openhis.surgicalschedule.domain.OpSchedule;
|
||||||
import com.openhis.surgicalschedule.service.IOpScheduleService;
|
import com.openhis.surgicalschedule.service.IOpScheduleService;
|
||||||
import com.openhis.web.clinicalmanage.appservice.ISurgicalScheduleAppService;
|
import com.openhis.web.clinicalmanage.appservice.ISurgicalScheduleAppService;
|
||||||
@@ -29,6 +33,8 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.core.framework.datasource.DynamicDataSourceContextHolder.log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手术安排业务层实现类
|
* 手术安排业务层实现类
|
||||||
*
|
*
|
||||||
@@ -47,6 +53,15 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
@Resource
|
@Resource
|
||||||
private SurgicalScheduleAppMapper surgicalScheduleAppMapper;
|
private SurgicalScheduleAppMapper surgicalScheduleAppMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ISurgeryService surgeryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.openhis.administration.service.IOrganizationService organizationService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.core.system.service.ISysUserService sysUserService;
|
||||||
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private RequestFormManageAppMapper requestFormManageAppMapper;
|
private RequestFormManageAppMapper requestFormManageAppMapper;
|
||||||
@@ -94,18 +109,38 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
return R.fail("患者信息不存在");
|
return R.fail("患者信息不存在");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//校验该时段内手术间是否被占用
|
|
||||||
LocalDateTime scheduleDate = opCreateScheduleDto.getEntryTime();//入室时间
|
// 校验是否重复手术安排(必须在校验手术间占用之前执行,确保能正确返回重复错误)
|
||||||
String roomCode = opCreateScheduleDto.getRoomCode();//手术室编号
|
// 同一患者 + 同一手术单号 + 同一手术名称 只能有一条有效安排记录
|
||||||
LocalDateTime endTime = opCreateScheduleDto.getEndTime();//手术结束时间
|
if (opCreateScheduleDto.getPatientId() != null
|
||||||
Boolean scheduleConflict = surgicalScheduleAppMapper.isScheduleConflict(scheduleDate, endTime, roomCode);
|
&& opCreateScheduleDto.getOperCode() != null && !opCreateScheduleDto.getOperCode().isEmpty()
|
||||||
if (scheduleConflict) {
|
&& opCreateScheduleDto.getOperName() != null && !opCreateScheduleDto.getOperName().isEmpty()) {
|
||||||
return R.fail("该时段内手术间被占用");
|
Boolean existsDuplicate = surgicalScheduleAppMapper.existsDuplicateSchedule(
|
||||||
|
opCreateScheduleDto.getPatientId(),
|
||||||
|
opCreateScheduleDto.getOperCode(),
|
||||||
|
opCreateScheduleDto.getOperName()
|
||||||
|
);
|
||||||
|
if (existsDuplicate != null && existsDuplicate) {
|
||||||
|
return R.fail("该患者此手术单号已存在手术安排,请勿重复提交");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginUser loginUser = new LoginUser();
|
// 校验该时段内手术间是否被占用
|
||||||
//获取当前登录用户信息
|
LocalDateTime startTime = opCreateScheduleDto.getEntryTime();//入室时间
|
||||||
loginUser = SecurityUtils.getLoginUser();
|
LocalDateTime endTime = opCreateScheduleDto.getEndTime();//手术结束时间
|
||||||
|
String roomCode = opCreateScheduleDto.getRoomCode();//手术室编号
|
||||||
|
if (startTime != null && endTime != null && roomCode != null && !roomCode.isEmpty()) {
|
||||||
|
Boolean scheduleConflict = surgicalScheduleAppMapper.isScheduleConflict(startTime, endTime, roomCode);
|
||||||
|
if (scheduleConflict != null && scheduleConflict) {
|
||||||
|
return R.fail("该时段内手术间被占用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug #432 修复:获取当前登录用户信息,增加null校验防止NPE
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return R.fail("用户未登录或登录已过期");
|
||||||
|
}
|
||||||
// 当前登录用户ID
|
// 当前登录用户ID
|
||||||
Long userId = loginUser.getUserId();
|
Long userId = loginUser.getUserId();
|
||||||
|
|
||||||
@@ -113,6 +148,44 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
OpSchedule opSchedule = new OpSchedule();
|
OpSchedule opSchedule = new OpSchedule();
|
||||||
BeanUtils.copyProperties(opCreateScheduleDto, opSchedule);
|
BeanUtils.copyProperties(opCreateScheduleDto, opSchedule);
|
||||||
|
|
||||||
|
// 处理可能为null的字符串字段,设置默认值为空字符串
|
||||||
|
if (opSchedule.getPreoperativeDiagnosis() == null) {
|
||||||
|
opSchedule.setPreoperativeDiagnosis("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getPostoperativeDiagnosis() == null) {
|
||||||
|
opSchedule.setPostoperativeDiagnosis("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesMethod() == null) {
|
||||||
|
opSchedule.setAnesMethod("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesDoctor1Code() == null) {
|
||||||
|
opSchedule.setAnesDoctor1Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesDoctor2Code() == null) {
|
||||||
|
opSchedule.setAnesDoctor2Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesDoctor3Code() == null) {
|
||||||
|
opSchedule.setAnesDoctor3Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getScrubNurseCode() == null) {
|
||||||
|
opSchedule.setScrubNurseCode("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getCircuNurse1Code() == null) {
|
||||||
|
opSchedule.setCircuNurse1Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getCircuNurse2Code() == null) {
|
||||||
|
opSchedule.setCircuNurse2Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getScrubNurse1Code() == null) {
|
||||||
|
opSchedule.setScrubNurse1Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getScrubNurse2Code() == null) {
|
||||||
|
opSchedule.setScrubNurse2Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getSurgeonCode() == null) {
|
||||||
|
opSchedule.setSurgeonCode("");
|
||||||
|
}
|
||||||
|
|
||||||
// 设置创建者ID
|
// 设置创建者ID
|
||||||
opSchedule.setCreatorId(userId);
|
opSchedule.setCreatorId(userId);
|
||||||
//设置创建人名称
|
//设置创建人名称
|
||||||
@@ -127,12 +200,34 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
|
|
||||||
// 保存手术安排
|
// 保存手术安排
|
||||||
boolean saved = opScheduleService.save(opSchedule);
|
boolean saved = opScheduleService.save(opSchedule);
|
||||||
//修改申请单状态为已排期
|
|
||||||
|
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
return R.fail("新增手术安排失败");
|
return R.fail("新增手术安排失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bug #247 修复:更新手术申请单状态为已排期 (1)
|
||||||
|
if (opCreateScheduleDto.getApplyId() != null) {
|
||||||
|
try {
|
||||||
|
// 通过手术单号查找手术申请记录并更新状态
|
||||||
|
LambdaQueryWrapper<com.openhis.clinical.domain.Surgery> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(com.openhis.clinical.domain.Surgery::getSurgeryNo, opSchedule.getOperCode())
|
||||||
|
.eq(com.openhis.clinical.domain.Surgery::getDeleteFlag, "0");
|
||||||
|
com.openhis.clinical.domain.Surgery surgery = surgeryService.getOne(queryWrapper);
|
||||||
|
if (surgery != null) {
|
||||||
|
surgery.setStatusEnum(1); // 1 = 已排期
|
||||||
|
surgery.setUpdateTime(new Date());
|
||||||
|
|
||||||
|
// 填充缺失的申请科室和主刀医生名称
|
||||||
|
fillSurgeryMissingNames(surgery);
|
||||||
|
|
||||||
|
surgeryService.updateById(surgery);
|
||||||
|
log.info("更新手术申请单状态为已排期 - surgeryNo: {}, surgeryId: {}", opSchedule.getOperCode(), surgery.getId());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新手术申请单状态失败 - operCode: {}", opSchedule.getOperCode(), e);
|
||||||
|
// 状态更新失败不影响主流程,只记录日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return R.ok("新增手术安排成功");
|
return R.ok("新增手术安排成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +253,44 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
OpSchedule opSchedule = new OpSchedule();
|
OpSchedule opSchedule = new OpSchedule();
|
||||||
BeanUtils.copyProperties(opScheduleDto, opSchedule);
|
BeanUtils.copyProperties(opScheduleDto, opSchedule);
|
||||||
|
|
||||||
|
// 处理可能为null的字符串字段,设置默认值为空字符串
|
||||||
|
if (opSchedule.getPreoperativeDiagnosis() == null) {
|
||||||
|
opSchedule.setPreoperativeDiagnosis("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getPostoperativeDiagnosis() == null) {
|
||||||
|
opSchedule.setPostoperativeDiagnosis("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesMethod() == null) {
|
||||||
|
opSchedule.setAnesMethod("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesDoctor1Code() == null) {
|
||||||
|
opSchedule.setAnesDoctor1Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesDoctor2Code() == null) {
|
||||||
|
opSchedule.setAnesDoctor2Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getAnesDoctor3Code() == null) {
|
||||||
|
opSchedule.setAnesDoctor3Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getScrubNurseCode() == null) {
|
||||||
|
opSchedule.setScrubNurseCode("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getCircuNurse1Code() == null) {
|
||||||
|
opSchedule.setCircuNurse1Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getCircuNurse2Code() == null) {
|
||||||
|
opSchedule.setCircuNurse2Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getScrubNurse1Code() == null) {
|
||||||
|
opSchedule.setScrubNurse1Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getScrubNurse2Code() == null) {
|
||||||
|
opSchedule.setScrubNurse2Code("");
|
||||||
|
}
|
||||||
|
if (opSchedule.getSurgeonCode() == null) {
|
||||||
|
opSchedule.setSurgeonCode("");
|
||||||
|
}
|
||||||
|
|
||||||
// 更新时间
|
// 更新时间
|
||||||
opSchedule.setUpdateTime(new Date());
|
opSchedule.setUpdateTime(new Date());
|
||||||
|
|
||||||
@@ -226,21 +359,21 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
int index = 0;
|
int index = 0;
|
||||||
for (OpScheduleDto schedule : scheduleList) {
|
for (OpScheduleDto schedule : scheduleList) {
|
||||||
index++;
|
index++;
|
||||||
|
|
||||||
// 转换手术类型
|
// 转换手术类型
|
||||||
String surgeryType = convertSurgeryNature(schedule.getSurgeryNature());
|
String surgeryType = convertSurgeryNature(schedule.getSurgeryNature());
|
||||||
|
|
||||||
// 转换麻醉方法
|
// 转换麻醉方法
|
||||||
String anesthesiaMethod = convertAnesMethod(schedule.getAnesMethod());
|
String anesthesiaMethod = convertAnesMethod(schedule.getAnesMethod());
|
||||||
|
|
||||||
// 格式化安排时间
|
// 格式化安排时间
|
||||||
String formattedDate = formatScheduleDate(schedule.getScheduleDate());
|
String formattedDate = formatScheduleDate(schedule.getScheduleDate());
|
||||||
|
|
||||||
writer.printf("%d,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
|
writer.printf("%d,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
|
||||||
index, // 序号从1开始
|
index, // 序号从1开始
|
||||||
schedule.getOrgName() != null ? schedule.getOrgName() : "",
|
schedule.getOrgName() != null ? schedule.getOrgName() : "",
|
||||||
schedule.getPatientName() != null ? schedule.getPatientName() : "",
|
schedule.getPatientName() != null ? schedule.getPatientName() : "",
|
||||||
schedule.getVisitId() != null ? schedule.getVisitId().toString() : "",
|
schedule.getIdentifierNo() != null ? schedule.getIdentifierNo() : "",
|
||||||
schedule.getOperCode() != null ? schedule.getOperCode() : "",
|
schedule.getOperCode() != null ? schedule.getOperCode() : "",
|
||||||
schedule.getOperName() != null ? schedule.getOperName() : "",
|
schedule.getOperName() != null ? schedule.getOperName() : "",
|
||||||
schedule.getApplyDeptName() != null ? schedule.getApplyDeptName() : "",
|
schedule.getApplyDeptName() != null ? schedule.getApplyDeptName() : "",
|
||||||
@@ -293,10 +426,84 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
/**
|
/**
|
||||||
* 格式化安排时间
|
* 格式化安排时间
|
||||||
*/
|
*/
|
||||||
private String formatScheduleDate(LocalDate scheduleDate) {
|
private String formatScheduleDate(LocalDateTime scheduleDate) {
|
||||||
if (scheduleDate == null) return "";
|
if (scheduleDate == null) return "";
|
||||||
|
|
||||||
// 格式化为 yyyy-MM-dd
|
// 格式化为 yyyy-MM-dd HH:mm:ss
|
||||||
return scheduleDate.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
return scheduleDate.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充手术申请中缺失的名称字段
|
||||||
|
* 在创建手术安排时调用,确保关联的cli_surgery表中的名称字段有值
|
||||||
|
*
|
||||||
|
* @param surgery 手术申请对象
|
||||||
|
*/
|
||||||
|
private void fillSurgeryMissingNames(com.openhis.clinical.domain.Surgery surgery) {
|
||||||
|
// 填充申请科室名称
|
||||||
|
if ((surgery.getApplyDeptName() == null || surgery.getApplyDeptName().isEmpty())
|
||||||
|
&& surgery.getApplyDeptId() != null) {
|
||||||
|
try {
|
||||||
|
com.openhis.administration.domain.Organization org = organizationService.getById(surgery.getApplyDeptId());
|
||||||
|
if (org != null && org.getName() != null) {
|
||||||
|
surgery.setApplyDeptName(org.getName());
|
||||||
|
log.info("填充申请科室名称 - surgeryId: {}, deptId: {}, deptName: {}",
|
||||||
|
surgery.getId(), surgery.getApplyDeptId(), org.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询申请科室名称失败 - deptId: {}, error: {}", surgery.getApplyDeptId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充主刀医生名称
|
||||||
|
if ((surgery.getMainSurgeonName() == null || surgery.getMainSurgeonName().isEmpty())
|
||||||
|
&& surgery.getMainSurgeonId() != null) {
|
||||||
|
try {
|
||||||
|
com.core.common.core.domain.entity.SysUser user = sysUserService.selectUserById(surgery.getMainSurgeonId());
|
||||||
|
if (user != null) {
|
||||||
|
String surgeonName = user.getNickName() != null && !user.getNickName().isEmpty()
|
||||||
|
? user.getNickName()
|
||||||
|
: user.getUserName();
|
||||||
|
if (surgeonName != null) {
|
||||||
|
surgery.setMainSurgeonName(surgeonName);
|
||||||
|
log.info("填充主刀医生名称 - surgeryId: {}, surgeonId: {}, surgeonName: {}",
|
||||||
|
surgery.getId(), surgery.getMainSurgeonId(), surgeonName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询主刀医生名称失败 - surgeonId: {}, error: {}", surgery.getMainSurgeonId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充麻醉医生名称
|
||||||
|
if ((surgery.getAnesthetistName() == null || surgery.getAnesthetistName().isEmpty())
|
||||||
|
&& surgery.getAnesthetistId() != null) {
|
||||||
|
try {
|
||||||
|
com.core.common.core.domain.entity.SysUser user = sysUserService.selectUserById(surgery.getAnesthetistId());
|
||||||
|
if (user != null) {
|
||||||
|
String anesthetistName = user.getNickName() != null && !user.getNickName().isEmpty()
|
||||||
|
? user.getNickName()
|
||||||
|
: user.getUserName();
|
||||||
|
if (anesthetistName != null) {
|
||||||
|
surgery.setAnesthetistName(anesthetistName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询麻醉医生名称失败 - anesthetistId: {}, error: {}", surgery.getAnesthetistId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充执行科室名称
|
||||||
|
if ((surgery.getOrgName() == null || surgery.getOrgName().isEmpty())
|
||||||
|
&& surgery.getOrgId() != null) {
|
||||||
|
try {
|
||||||
|
com.openhis.administration.domain.Organization org = organizationService.getById(surgery.getOrgId());
|
||||||
|
if (org != null && org.getName() != null) {
|
||||||
|
surgery.setOrgName(org.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询执行科室名称失败 - orgId: {}, error: {}", surgery.getOrgId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手术安排Controller
|
* 手术安排Controller
|
||||||
@@ -98,4 +99,22 @@ public class SurgicalScheduleController {
|
|||||||
surgicalScheduleAppService.exportSurgerySchedule(opScheduleDto, response);
|
surgicalScheduleAppService.exportSurgerySchedule(opScheduleDto, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证签名密码
|
||||||
|
*
|
||||||
|
* @param params 密码参数 {password: 输入的密码}
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/checkPassword")
|
||||||
|
public R<?> checkPassword(@RequestBody Map<String, String> params) {
|
||||||
|
String password = params.get("password");
|
||||||
|
com.core.common.core.domain.model.LoginUser loginUser = com.core.common.utils.SecurityUtils.getLoginUser();
|
||||||
|
String encodedPassword = loginUser.getPassword();
|
||||||
|
if (com.core.common.utils.SecurityUtils.matchesPassword(password, encodedPassword)) {
|
||||||
|
return R.ok(true, "密码验证成功");
|
||||||
|
} else {
|
||||||
|
return R.fail(false, "账户密码错误,请重新输入");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ public class OpCreateScheduleDto {
|
|||||||
*/
|
*/
|
||||||
private Long visitId;
|
private Long visitId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 就诊卡号
|
||||||
|
*/
|
||||||
|
private String identifierNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手术编码
|
* 手术编码
|
||||||
*/
|
*/
|
||||||
@@ -45,9 +50,10 @@ public class OpCreateScheduleDto {
|
|||||||
private String postoperativeDiagnosis;
|
private String postoperativeDiagnosis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手术安排日期
|
* 手术安排日期时间
|
||||||
*/
|
*/
|
||||||
private LocalDate scheduleDate;
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime scheduleDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手术台次序号
|
* 手术台次序号
|
||||||
@@ -82,11 +88,13 @@ public class OpCreateScheduleDto {
|
|||||||
/**
|
/**
|
||||||
* 入院时间
|
* 入院时间
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime admissionTime;
|
private LocalDateTime admissionTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入手术室时间
|
* 入手术室时间
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime entryTime;
|
private LocalDateTime entryTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,21 +175,25 @@ public class OpCreateScheduleDto {
|
|||||||
/**
|
/**
|
||||||
* 手术开始时间
|
* 手术开始时间
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手术结束时间
|
* 手术结束时间
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 麻醉开始时间
|
* 麻醉开始时间
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime anesStart;
|
private LocalDateTime anesStart;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 麻醉结束时间
|
* 麻醉结束时间
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime anesEnd;
|
private LocalDateTime anesEnd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.openhis.web.clinicalmanage.dto;
|
package com.openhis.web.clinicalmanage.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.openhis.surgicalschedule.domain.OpSchedule;
|
import com.openhis.surgicalschedule.domain.OpSchedule;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
@@ -17,6 +19,20 @@ import java.time.LocalDate;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class OpScheduleDto extends OpSchedule {
|
public class OpScheduleDto extends OpSchedule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手术安排日期开始(查询用)
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
|
private LocalDate scheduleDateStart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手术安排日期结束(查询用)
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
|
private LocalDate scheduleDateEnd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 患者姓名
|
* 患者姓名
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +43,11 @@ public class OpScheduleDto extends OpSchedule {
|
|||||||
*/
|
*/
|
||||||
private Long encounterId;
|
private Long encounterId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 就诊卡号
|
||||||
|
*/
|
||||||
|
private String patientCardNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 性别
|
* 性别
|
||||||
*/
|
*/
|
||||||
@@ -55,6 +76,7 @@ public class OpScheduleDto extends OpSchedule {
|
|||||||
/**
|
/**
|
||||||
* 申请时间开始
|
* 申请时间开始
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
private String applyTime;
|
private String applyTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.openhis.web.clinicalmanage.dto;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.openhis.common.annotation.Dict;
|
import com.openhis.common.annotation.Dict;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
@@ -45,6 +46,9 @@ public class SurgeryDto {
|
|||||||
/** 就诊流水号 */
|
/** 就诊流水号 */
|
||||||
private String encounterNo;
|
private String encounterNo;
|
||||||
|
|
||||||
|
/** 就诊卡号 */
|
||||||
|
private String patientCardNo;
|
||||||
|
|
||||||
/** 申请医生ID */
|
/** 申请医生ID */
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long applyDoctorId;
|
private Long applyDoctorId;
|
||||||
@@ -84,6 +88,7 @@ public class SurgeryDto {
|
|||||||
private String statusEnum_dictText;
|
private String statusEnum_dictText;
|
||||||
|
|
||||||
/** 计划手术时间 */
|
/** 计划手术时间 */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "GMT+8")
|
||||||
private Date plannedTime;
|
private Date plannedTime;
|
||||||
|
|
||||||
/** 实际开始时间 */
|
/** 实际开始时间 */
|
||||||
|
|||||||
@@ -58,4 +58,14 @@ public interface SurgicalScheduleAppMapper {
|
|||||||
* @return 是否存在冲突的手术安排
|
* @return 是否存在冲突的手术安排
|
||||||
*/
|
*/
|
||||||
Boolean isScheduleConflict(LocalDateTime startTime, LocalDateTime endTime, String surgeryRoomId);
|
Boolean isScheduleConflict(LocalDateTime startTime, LocalDateTime endTime, String surgeryRoomId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否存在重复的手术安排
|
||||||
|
*
|
||||||
|
* @param patientId 患者ID
|
||||||
|
* @param operCode 手术单号
|
||||||
|
* @param operName 手术名称
|
||||||
|
* @return 是否存在重复记录
|
||||||
|
*/
|
||||||
|
Boolean existsDuplicateSchedule(@Param("patientId") Long patientId, @Param("operCode") String operCode, @Param("operName") String operName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ public interface IConsultationAppService {
|
|||||||
* @return 会诊意见列表
|
* @return 会诊意见列表
|
||||||
*/
|
*/
|
||||||
List<ConsultationOpinionDto> getConsultationOpinions(String consultationId);
|
List<ConsultationOpinionDto> getConsultationOpinions(String consultationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询会诊申请详情
|
||||||
|
*
|
||||||
|
* @param id 会诊申请ID
|
||||||
|
* @return 会诊申请详情
|
||||||
|
*/
|
||||||
|
ConsultationRequestDto getConsultationById(Long id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ import java.util.*;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.openhis.web.consultation.enums.ConsultationStatusEnum.CANCELLED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会诊管理AppService实现类
|
* 会诊管理AppService实现类
|
||||||
*
|
*
|
||||||
@@ -134,6 +136,8 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
|
|
||||||
// 根据就诊ID,查询该患者的会诊申请
|
// 根据就诊ID,查询该患者的会诊申请
|
||||||
wrapper.eq(ConsultationRequest::getEncounterId, encounterId);
|
wrapper.eq(ConsultationRequest::getEncounterId, encounterId);
|
||||||
|
// 过滤已作废的数据
|
||||||
|
wrapper.ne(ConsultationRequest::getConsultationStatus, CANCELLED.getCode());
|
||||||
wrapper.orderByDesc(ConsultationRequest::getCreateTime);
|
wrapper.orderByDesc(ConsultationRequest::getCreateTime);
|
||||||
|
|
||||||
List<ConsultationRequest> list = consultationRequestMapper.selectList(wrapper);
|
List<ConsultationRequest> list = consultationRequestMapper.selectList(wrapper);
|
||||||
@@ -182,6 +186,11 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
wrapper.like(ConsultationRequest::getPatientName, dto.getPatientName());
|
wrapper.like(ConsultationRequest::getPatientName, dto.getPatientName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 会诊ID查询(支持模糊匹配)
|
||||||
|
if (StringUtils.hasText(dto.getConsultationId())) {
|
||||||
|
wrapper.like(ConsultationRequest::getConsultationId, dto.getConsultationId());
|
||||||
|
}
|
||||||
|
|
||||||
// 按创建时间倒序排列
|
// 按创建时间倒序排列
|
||||||
wrapper.orderByDesc(ConsultationRequest::getConsultationRequestDate);
|
wrapper.orderByDesc(ConsultationRequest::getConsultationRequestDate);
|
||||||
|
|
||||||
@@ -236,6 +245,11 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
wrapper.like(ConsultationRequest::getPatientName, dto.getPatientName());
|
wrapper.like(ConsultationRequest::getPatientName, dto.getPatientName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 会诊ID查询(支持模糊匹配)
|
||||||
|
if (StringUtils.hasText(dto.getConsultationId())) {
|
||||||
|
wrapper.like(ConsultationRequest::getConsultationId, dto.getConsultationId());
|
||||||
|
}
|
||||||
|
|
||||||
// 按创建时间倒序排列
|
// 按创建时间倒序排列
|
||||||
wrapper.orderByDesc(ConsultationRequest::getConsultationRequestDate);
|
wrapper.orderByDesc(ConsultationRequest::getConsultationRequestDate);
|
||||||
|
|
||||||
@@ -282,11 +296,15 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
entity = new ConsultationRequest();
|
entity = new ConsultationRequest();
|
||||||
entity.setConsultationId(generateConsultationId());
|
entity.setConsultationId(generateConsultationId());
|
||||||
entity.setTenantId(SecurityUtils.getLoginUser().getTenantId().longValue());
|
entity.setTenantId(SecurityUtils.getLoginUser().getTenantId().longValue());
|
||||||
entity.setConsultationRequestDate(new Date());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制基本属性(现在字段名已统一,可以直接复制)
|
// 复制基本属性(现在字段名已统一,可以直接复制)
|
||||||
BeanUtils.copyProperties(dto, entity, "id", "consultationId", "invitedList", "submitFlag", "provisionalDiagnosis", "consultationRequestDate");
|
BeanUtils.copyProperties(dto, entity, "id", "consultationId", "invitedList", "submitFlag", "provisionalDiagnosis");
|
||||||
|
|
||||||
|
// 新增时:如果前端没有传递申请时间,使用服务器时间
|
||||||
|
if (!isUpdate && entity.getConsultationRequestDate() == null) {
|
||||||
|
entity.setConsultationRequestDate(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
// 如果前端没有传递申请医生ID,使用当前登录用户
|
// 如果前端没有传递申请医生ID,使用当前登录用户
|
||||||
if (entity.getRequestingPhysicianId() == null) {
|
if (entity.getRequestingPhysicianId() == null) {
|
||||||
@@ -403,6 +421,20 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
|
|
||||||
// 新增:更新门诊医嘱表状态为已提交
|
// 新增:更新门诊医嘱表状态为已提交
|
||||||
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.ACTIVE.getValue());
|
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.ACTIVE.getValue());
|
||||||
|
|
||||||
|
// 🎯 更新会诊关联费用项状态为"待收费",提交后即可在收费界面看到
|
||||||
|
if (entity.getOrderId() != null) {
|
||||||
|
LambdaQueryWrapper<ChargeItem> chargeItemWrapper = new LambdaQueryWrapper<>();
|
||||||
|
chargeItemWrapper.eq(ChargeItem::getServiceId, entity.getOrderId())
|
||||||
|
.eq(ChargeItem::getServiceTable, "wor_service_request");
|
||||||
|
List<ChargeItem> chargeItems = iChargeItemService.list(chargeItemWrapper);
|
||||||
|
for (ChargeItem chargeItem : chargeItems) {
|
||||||
|
chargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue());
|
||||||
|
iChargeItemService.updateById(chargeItem);
|
||||||
|
}
|
||||||
|
log.info("会诊提交,更新关联费用项状态为待收费,更新数量: {}", chargeItems.size());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("提交会诊申请失败", e);
|
log.error("提交会诊申请失败", e);
|
||||||
@@ -427,7 +459,15 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 判断是"取消提交"还是"作废"
|
// 判断是"取消提交"还是"作废"
|
||||||
if ("取消提交".equals(cancelReason) && ConsultationStatusEnum.SUBMITTED.getCode().equals(entity.getConsultationStatus())) {
|
if ("取消提交".equals(cancelReason)) {
|
||||||
|
// 状态校验:禁止已确认 (20)、已签名 (30)、已完成 (40) 的会诊申请取消提交
|
||||||
|
if (entity.getConsultationStatus() >= ConsultationStatusEnum.CONFIRMED.getCode()) {
|
||||||
|
throw new IllegalArgumentException("当前状态不允许取消提交,只有已提交状态的会诊申请才能取消提交");
|
||||||
|
}
|
||||||
|
// 只有状态为 10(已提交) 才允许取消提交
|
||||||
|
if (!ConsultationStatusEnum.SUBMITTED.getCode().equals(entity.getConsultationStatus())) {
|
||||||
|
throw new IllegalArgumentException("只有已提交状态的会诊申请才能取消提交");
|
||||||
|
}
|
||||||
// 取消提交:将状态从"已提交"改回"新开"
|
// 取消提交:将状态从"已提交"改回"新开"
|
||||||
entity.setConsultationStatus(ConsultationStatusEnum.NEW.getCode());
|
entity.setConsultationStatus(ConsultationStatusEnum.NEW.getCode());
|
||||||
entity.setConfirmingPhysician(null);
|
entity.setConfirmingPhysician(null);
|
||||||
@@ -438,9 +478,27 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
// 更新门诊医嘱表状态为新开
|
// 更新门诊医嘱表状态为新开
|
||||||
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.DRAFT.getValue());
|
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.DRAFT.getValue());
|
||||||
|
|
||||||
|
// 更新关联费用项状态为草稿
|
||||||
|
if (entity.getOrderId() != null) {
|
||||||
|
LambdaQueryWrapper<ChargeItem> chargeItemWrapper = new LambdaQueryWrapper<>();
|
||||||
|
chargeItemWrapper.eq(ChargeItem::getServiceId, entity.getOrderId())
|
||||||
|
.eq(ChargeItem::getServiceTable, "wor_service_request");
|
||||||
|
List<ChargeItem> chargeItems = iChargeItemService.list(chargeItemWrapper);
|
||||||
|
for (ChargeItem chargeItem : chargeItems) {
|
||||||
|
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue());
|
||||||
|
iChargeItemService.updateById(chargeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 作废:将状态改为"已取消"
|
// 作废:状态校验 - 已确认(20)、已签名(30)、已完成(40) 状态禁止作废
|
||||||
entity.setConsultationStatus(ConsultationStatusEnum.CANCELLED.getCode());
|
ConsultationStatusEnum currentStatus = ConsultationStatusEnum.getByCode(entity.getConsultationStatus());
|
||||||
|
if (currentStatus != null && !currentStatus.canCancel()) {
|
||||||
|
throw new IllegalArgumentException("当前状态【" + currentStatus.getDescription() + "】不允许作废,只有新开或已提交状态的会诊申请才能作废");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将状态改为"已取消"
|
||||||
|
entity.setConsultationStatus(CANCELLED.getCode());
|
||||||
entity.setCancelReason(cancelReason);
|
entity.setCancelReason(cancelReason);
|
||||||
entity.setCancelNatureDate(new Date());
|
entity.setCancelNatureDate(new Date());
|
||||||
consultationRequestMapper.updateById(entity);
|
consultationRequestMapper.updateById(entity);
|
||||||
@@ -448,6 +506,18 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
// 更新门诊医嘱表状态为已作废
|
// 更新门诊医嘱表状态为已作废
|
||||||
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.CANCELLED.getValue());
|
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.CANCELLED.getValue());
|
||||||
|
|
||||||
|
// 更新关联费用项状态为终止
|
||||||
|
if (entity.getOrderId() != null) {
|
||||||
|
LambdaQueryWrapper<ChargeItem> chargeItemWrapper = new LambdaQueryWrapper<>();
|
||||||
|
chargeItemWrapper.eq(ChargeItem::getServiceId, entity.getOrderId())
|
||||||
|
.eq(ChargeItem::getServiceTable, "wor_service_request");
|
||||||
|
List<ChargeItem> chargeItems = iChargeItemService.list(chargeItemWrapper);
|
||||||
|
for (ChargeItem chargeItem : chargeItems) {
|
||||||
|
chargeItem.setStatusEnum(ChargeItemStatus.ABORTED.getValue());
|
||||||
|
iChargeItemService.updateById(chargeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -511,8 +581,33 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
.collect(Collectors.groupingBy(Practitioner::getOrgId));
|
.collect(Collectors.groupingBy(Practitioner::getOrgId));
|
||||||
|
|
||||||
// 构建树形结构
|
// 构建树形结构
|
||||||
|
// 过滤条件:科室分类只要包含"门诊(编码1)"或"住院(编码2)"其一,即可显示
|
||||||
List<DepartmentTreeDto> treeList = new ArrayList<>();
|
List<DepartmentTreeDto> treeList = new ArrayList<>();
|
||||||
for (Organization dept : deptList) {
|
for (Organization dept : deptList) {
|
||||||
|
// 过滤科室:只显示包含门诊(1)或住院(2)分类的科室
|
||||||
|
String classEnum = dept.getClassEnum();
|
||||||
|
boolean needShow = false;
|
||||||
|
|
||||||
|
if (classEnum != null && !classEnum.isEmpty()) {
|
||||||
|
// 拆分分类编码,检查是否包含 1 或 2
|
||||||
|
String[] codes = classEnum.split(",");
|
||||||
|
for (String code : codes) {
|
||||||
|
code = code.trim();
|
||||||
|
if ("1".equals(code) || "2".equals(code)) {
|
||||||
|
needShow = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有分类,默认显示
|
||||||
|
needShow = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needShow) {
|
||||||
|
// 既不包含门诊也不包含住院,跳过
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
DepartmentTreeDto treeDto = new DepartmentTreeDto();
|
DepartmentTreeDto treeDto = new DepartmentTreeDto();
|
||||||
treeDto.setId(dept.getId());
|
treeDto.setId(dept.getId());
|
||||||
treeDto.setLabel(dept.getName());
|
treeDto.setLabel(dept.getName());
|
||||||
@@ -529,11 +624,10 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
treeDto.setChildren(children);
|
treeDto.setChildren(children);
|
||||||
} else {
|
// 只添加有医生的科室
|
||||||
treeDto.setChildren(new ArrayList<>());
|
treeList.add(treeDto);
|
||||||
}
|
}
|
||||||
|
// 没有医生的科室不添加到列表中
|
||||||
treeList.add(treeDto);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -636,12 +730,14 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
@Override
|
@Override
|
||||||
public List<ConsultationRequestDto> getMyInvitations() {
|
public List<ConsultationRequestDto> getMyInvitations() {
|
||||||
try {
|
try {
|
||||||
// 获取当前登录医生ID
|
// 获取当前登录医生ID和租户ID
|
||||||
Long currentPhysicianId = SecurityUtils.getLoginUser().getPractitionerId();
|
Long currentPhysicianId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||||
|
Long tenantId = SecurityUtils.getLoginUser().getTenantId().longValue();
|
||||||
|
|
||||||
// 查询邀请我的会诊申请
|
// 查询邀请我的会诊申请
|
||||||
LambdaQueryWrapper<ConsultationInvited> invitedWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<ConsultationInvited> invitedWrapper = new LambdaQueryWrapper<>();
|
||||||
invitedWrapper.eq(ConsultationInvited::getInvitedPhysicianId, currentPhysicianId)
|
invitedWrapper.eq(ConsultationInvited::getTenantId, tenantId)
|
||||||
|
.eq(ConsultationInvited::getInvitedPhysicianId, currentPhysicianId)
|
||||||
.orderByDesc(ConsultationInvited::getCreateTime);
|
.orderByDesc(ConsultationInvited::getCreateTime);
|
||||||
|
|
||||||
List<ConsultationInvited> invitedList = consultationInvitedMapper.selectList(invitedWrapper);
|
List<ConsultationInvited> invitedList = consultationInvitedMapper.selectList(invitedWrapper);
|
||||||
@@ -709,38 +805,64 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
|
|
||||||
dto.setInvitedList(invitedDtoList);
|
dto.setInvitedList(invitedDtoList);
|
||||||
|
|
||||||
// 🎯 如果会诊已完成或已签名,填充会诊记录信息(从已签名的医生中获取)
|
|
||||||
if (entity.getConsultationStatus() != null &&
|
// 🎯 如果会诊已确认、已签名或已完成,填充会诊记录信息(从会诊确认表中获取)
|
||||||
(entity.getConsultationStatus() == ConsultationStatusEnum.SIGNED.getCode() ||
|
// 会诊状态:20=已确认,30=已签名,40=已完成
|
||||||
entity.getConsultationStatus() == ConsultationStatusEnum.COMPLETED.getCode())) {
|
if (entity.getConsultationStatus() != null &&
|
||||||
|
entity.getConsultationStatus() >= ConsultationStatusEnum.CONFIRMED.getCode()) {
|
||||||
|
|
||||||
|
// 查询会诊确认记录
|
||||||
|
LambdaQueryWrapper<ConsultationConfirmation> confirmWrapper = new LambdaQueryWrapper<>();
|
||||||
|
confirmWrapper.eq(ConsultationConfirmation::getConsultationRequestId, entity.getId());
|
||||||
|
ConsultationConfirmation confirmation = consultationConfirmationMapper.selectOne(confirmWrapper);
|
||||||
|
|
||||||
|
// 查询所有已确认和已签名的医生(invited_status >= 2)
|
||||||
|
List<ConsultationInvited> confirmedAndSignedPhysicians = invitedList.stream()
|
||||||
|
.filter(inv -> inv.getInvitedStatus() != null && inv.getInvitedStatus() >= 2)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// 查询所有已签名的医生(invited_status >= 3)
|
// 查询所有已签名的医生(invited_status >= 3)
|
||||||
List<ConsultationInvited> signedPhysicians = invitedList.stream()
|
List<ConsultationInvited> signedPhysicians = invitedList.stream()
|
||||||
.filter(inv -> inv.getInvitedStatus() != null && inv.getInvitedStatus() >= 3)
|
.filter(inv -> inv.getInvitedStatus() != null && inv.getInvitedStatus() >= 3)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if (!signedPhysicians.isEmpty()) {
|
if (confirmation != null) {
|
||||||
// 1. 会诊邀请参加医师:拼接所有已签名医生的"科室-姓名"
|
// 1. 会诊确认参加医师:优先从确认表的confirming_physicians字段取值
|
||||||
String invitedPhysiciansText = signedPhysicians.stream()
|
if (StringUtils.hasText(confirmation.getConfirmingPhysicians())) {
|
||||||
.map(inv -> inv.getInvitedDepartmentName() + "-" + inv.getInvitedPhysicianName())
|
dto.setInvitedPhysiciansText(confirmation.getConfirmingPhysicians());
|
||||||
.collect(Collectors.joining("、"));
|
} else if (!confirmedAndSignedPhysicians.isEmpty()) {
|
||||||
dto.setInvitedPhysiciansText(invitedPhysiciansText);
|
// 备用:从invitedList拼接
|
||||||
|
String invitedPhysiciansText = confirmedAndSignedPhysicians.stream()
|
||||||
// 2. 会诊意见:汇总所有已签名医生的意见
|
.map(inv -> inv.getInvitedDepartmentName() + "-" + inv.getInvitedPhysicianName())
|
||||||
String consultationOpinion = signedPhysicians.stream()
|
.collect(Collectors.joining("、"));
|
||||||
.filter(inv -> StringUtils.hasText(inv.getConfirmOpinion()))
|
dto.setInvitedPhysiciansText(invitedPhysiciansText);
|
||||||
.map(ConsultationInvited::getConfirmOpinion)
|
}
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
dto.setConsultationOpinion(consultationOpinion);
|
// 2. 会诊意见:优先从确认表取值
|
||||||
|
if (StringUtils.hasText(confirmation.getConsultationOpinion())) {
|
||||||
// 3. 所属医生、代表科室、签名医生、签名时间:使用第一个签名的医生
|
dto.setConsultationOpinion(confirmation.getConsultationOpinion());
|
||||||
ConsultationInvited firstSigned = signedPhysicians.get(0);
|
} else if (!confirmedAndSignedPhysicians.isEmpty()) {
|
||||||
dto.setAttendingPhysician(firstSigned.getInvitedPhysicianName());
|
// 备用:从invitedList汇总
|
||||||
dto.setRepresentDepartment(firstSigned.getInvitedDepartmentName());
|
String consultationOpinion = confirmedAndSignedPhysicians.stream()
|
||||||
dto.setSignPhysician(firstSigned.getInvitedPhysicianName());
|
.filter(inv -> StringUtils.hasText(inv.getConfirmOpinion()))
|
||||||
dto.setSignTime(firstSigned.getSignatureTime());
|
.map(ConsultationInvited::getConfirmOpinion)
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
log.info("填充会诊记录信息,已签名医生数:{}", signedPhysicians.size());
|
dto.setConsultationOpinion(consultationOpinion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 签名医生、签名时间:从确认表取值
|
||||||
|
dto.setSignPhysician(confirmation.getSignature());
|
||||||
|
dto.setSignTime(confirmation.getSignatureDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 所属医生、代表科室:使用第一个确认的医生(向后兼容)
|
||||||
|
if (!confirmedAndSignedPhysicians.isEmpty()) {
|
||||||
|
ConsultationInvited firstConfirmed = confirmedAndSignedPhysicians.get(0);
|
||||||
|
dto.setAttendingPhysician(firstConfirmed.getInvitedPhysicianName());
|
||||||
|
dto.setRepresentDepartment(firstConfirmed.getInvitedDepartmentName());
|
||||||
|
|
||||||
|
log.info("填充会诊记录信息,已确认和已签名医生数:{},已签名医生数:{}",
|
||||||
|
confirmedAndSignedPhysicians.size(), signedPhysicians.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1143,15 +1265,17 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
@Override
|
@Override
|
||||||
public List<ConsultationConfirmationDto> getPendingConfirmationList() {
|
public List<ConsultationConfirmationDto> getPendingConfirmationList() {
|
||||||
try {
|
try {
|
||||||
// 获取当前登录医生ID
|
// 获取当前登录医生ID和租户ID
|
||||||
Long currentPhysicianId = SecurityUtils.getLoginUser().getPractitionerId();
|
Long currentPhysicianId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||||
|
Long tenantId = SecurityUtils.getLoginUser().getTenantId().longValue();
|
||||||
log.info("获取待确认会诊列表,当前医生ID: {}", currentPhysicianId);
|
log.info("获取待确认会诊列表,当前医生ID: {}", currentPhysicianId);
|
||||||
|
|
||||||
// 🎯 关键修改:查询当前医生个人状态为"待确认"、"已确认"或"已签名"的邀请记录
|
// 🎯 关键修改:查询当前医生个人状态为"待确认"、"已确认"或"已签名"的邀请记录
|
||||||
// 10=已提交(待确认)、20=已确认(待签名)、30=已签名,排除40=已完成
|
// 10=已提交(待确认)、20=已确认(待签名)、30=已签名,排除40=已完成
|
||||||
LambdaQueryWrapper<ConsultationInvited> invitedWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<ConsultationInvited> invitedWrapper = new LambdaQueryWrapper<>();
|
||||||
invitedWrapper.eq(ConsultationInvited::getInvitedPhysicianId, currentPhysicianId)
|
invitedWrapper.eq(ConsultationInvited::getTenantId, tenantId)
|
||||||
.in(ConsultationInvited::getInvitedStatus,
|
.eq(ConsultationInvited::getInvitedPhysicianId, currentPhysicianId)
|
||||||
|
.in(ConsultationInvited::getInvitedStatus,
|
||||||
ConsultationStatusEnum.SUBMITTED.getCode(), // 10-待确认
|
ConsultationStatusEnum.SUBMITTED.getCode(), // 10-待确认
|
||||||
ConsultationStatusEnum.CONFIRMED.getCode(), // 20-已确认(待签名)
|
ConsultationStatusEnum.CONFIRMED.getCode(), // 20-已确认(待签名)
|
||||||
ConsultationStatusEnum.SIGNED.getCode()) // 30-已签名
|
ConsultationStatusEnum.SIGNED.getCode()) // 30-已签名
|
||||||
@@ -1175,7 +1299,8 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
// 🎯 查询会诊申请详情(白名单:只查询正在进行中的会诊,明确业务范围)
|
// 🎯 查询会诊申请详情(白名单:只查询正在进行中的会诊,明确业务范围)
|
||||||
// 查询已提交、已确认、已签名状态的会诊,排除已完成(40)
|
// 查询已提交、已确认、已签名状态的会诊,排除已完成(40)
|
||||||
LambdaQueryWrapper<ConsultationRequest> requestWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<ConsultationRequest> requestWrapper = new LambdaQueryWrapper<>();
|
||||||
requestWrapper.in(ConsultationRequest::getId, requestIds)
|
requestWrapper.eq(ConsultationRequest::getTenantId, tenantId)
|
||||||
|
.in(ConsultationRequest::getId, requestIds)
|
||||||
.in(ConsultationRequest::getConsultationStatus,
|
.in(ConsultationRequest::getConsultationStatus,
|
||||||
ConsultationStatusEnum.SUBMITTED.getCode(), // 10-已提交
|
ConsultationStatusEnum.SUBMITTED.getCode(), // 10-已提交
|
||||||
ConsultationStatusEnum.CONFIRMED.getCode(), // 20-已确认
|
ConsultationStatusEnum.CONFIRMED.getCode(), // 20-已确认
|
||||||
@@ -1239,9 +1364,13 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
throw new IllegalArgumentException("会诊申请不存在");
|
throw new IllegalArgumentException("会诊申请不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有已提交状态才能确认
|
// 会诊必须处于已提交或已确认状态才能确认
|
||||||
if (request.getConsultationStatus() != ConsultationStatusEnum.SUBMITTED.getCode()) {
|
// - 已提交(10):还没有医生确认
|
||||||
throw new IllegalArgumentException("只有已提交状态的会诊申请才能确认");
|
// - 已确认(20):已有部分医生确认,允许其他医生继续确认(每个医生独立确认,类似已读)
|
||||||
|
// - 已签名(30)或已完成(40):不能再确认
|
||||||
|
if (request.getConsultationStatus() != null &&
|
||||||
|
request.getConsultationStatus() >= ConsultationStatusEnum.SIGNED.getCode()) {
|
||||||
|
throw new IllegalArgumentException("会诊已签名或完成,无法再确认");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取当前登录医生信息
|
// 2. 获取当前登录医生信息
|
||||||
@@ -1259,23 +1388,28 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
throw new IllegalArgumentException("您不在被邀请的医生列表中");
|
throw new IllegalArgumentException("您不在被邀请的医生列表中");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("会诊确认检查:currentPhysicianId={}, invitedId={}, invitedStatus={}, CONFIRMED.code={}",
|
||||||
|
currentPhysicianId, invited.getId(), invited.getInvitedStatus(), ConsultationStatusEnum.CONFIRMED.getCode());
|
||||||
if (invited.getInvitedStatus() != null && invited.getInvitedStatus() >= ConsultationStatusEnum.CONFIRMED.getCode()) {
|
if (invited.getInvitedStatus() != null && invited.getInvitedStatus() >= ConsultationStatusEnum.CONFIRMED.getCode()) {
|
||||||
throw new IllegalArgumentException("您已经确认过了,无需重复确认");
|
throw new IllegalArgumentException("您已经确认过了,无需重复确认");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 更新邀请记录(存储会诊意见)
|
// 4. 更新邀请记录(存储会诊意见)
|
||||||
// 格式:科室-医生:意见内容
|
// Bug #388:格式化存储,确保回显时参加医师和意见完整
|
||||||
String formattedOpinion = String.format("%s-%s:%s",
|
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode());
|
||||||
currentDeptName,
|
|
||||||
currentPhysicianName,
|
String deptName = StringUtils.hasText(dto.getConfirmingDeptName())
|
||||||
dto.getConsultationOpinion());
|
? dto.getConfirmingDeptName() : invited.getInvitedDepartmentName();
|
||||||
|
String physician = StringUtils.hasText(dto.getConfirmingPhysician())
|
||||||
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode()); // 已确认
|
? dto.getConfirmingPhysician() : currentPhysicianName;
|
||||||
|
|
||||||
|
// 格式:科室-参加医师:意见内容
|
||||||
|
String formattedOpinion = String.format("%s-%s:%s", deptName, physician, dto.getConsultationOpinion());
|
||||||
invited.setConfirmOpinion(formattedOpinion);
|
invited.setConfirmOpinion(formattedOpinion);
|
||||||
invited.setConfirmTime(new Date());
|
invited.setConfirmTime(new Date());
|
||||||
consultationInvitedMapper.updateById(invited);
|
consultationInvitedMapper.updateById(invited);
|
||||||
|
|
||||||
log.info("医生 {} 确认会诊,意见:{}", currentPhysicianName, formattedOpinion);
|
log.info("医生 {} 确认会诊", currentPhysicianName);
|
||||||
|
|
||||||
// 5. 更新会诊申请的确认计数
|
// 5. 更新会诊申请的确认计数
|
||||||
Integer confirmedCount = (request.getConfirmedCount() == null ? 0 : request.getConfirmedCount()) + 1;
|
Integer confirmedCount = (request.getConfirmedCount() == null ? 0 : request.getConfirmedCount()) + 1;
|
||||||
@@ -1573,9 +1707,22 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
// 更新确认记录
|
// 更新确认记录
|
||||||
updateConfirmationRecord(request);
|
updateConfirmationRecord(request);
|
||||||
|
|
||||||
// 更新医嘱状态为"已完成"
|
// 🎯 需求:专家签名后会诊医嘱状态保持"已签发"(ACTIVE = 已发送/已签发),不改为已完成
|
||||||
updateServiceRequestStatus(request.getOrderId(), RequestStatus.COMPLETED.getValue());
|
updateServiceRequestStatus(request.getOrderId(), RequestStatus.ACTIVE.getValue());
|
||||||
|
|
||||||
|
// 🎯 更新会诊关联费用项状态为"待收费",这样收费界面就能看到了
|
||||||
|
if (request.getOrderId() != null) {
|
||||||
|
LambdaQueryWrapper<ChargeItem> chargeItemWrapper = new LambdaQueryWrapper<>();
|
||||||
|
chargeItemWrapper.eq(ChargeItem::getServiceId, request.getOrderId())
|
||||||
|
.eq(ChargeItem::getServiceTable, "wor_service_request");
|
||||||
|
List<ChargeItem> chargeItems = iChargeItemService.list(chargeItemWrapper);
|
||||||
|
for (ChargeItem chargeItem : chargeItems) {
|
||||||
|
chargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue());
|
||||||
|
iChargeItemService.updateById(chargeItem);
|
||||||
|
}
|
||||||
|
log.info("会诊完成,更新关联费用项状态为待收费,更新数量: {}", chargeItems.size());
|
||||||
|
}
|
||||||
|
|
||||||
log.info("所有医生都已签名,会诊申请状态更新为:已签名(30)");
|
log.info("所有医生都已签名,会诊申请状态更新为:已签名(30)");
|
||||||
} else {
|
} else {
|
||||||
// 🎯 关键修改:部分医生签名,整体状态不变(保持为10或20)
|
// 🎯 关键修改:部分医生签名,整体状态不变(保持为10或20)
|
||||||
@@ -1775,5 +1922,26 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConsultationRequestDto getConsultationById(Long id) {
|
||||||
|
try {
|
||||||
|
if (id == null) {
|
||||||
|
throw new IllegalArgumentException("会诊申请ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 查询会诊申请
|
||||||
|
ConsultationRequest request = consultationRequestMapper.selectById(id);
|
||||||
|
if (request == null) {
|
||||||
|
throw new IllegalArgumentException("会诊申请不存在,ID: " + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为DTO并返回
|
||||||
|
return convertToDto(request);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询会诊申请详情失败", e);
|
||||||
|
throw new RuntimeException("查询会诊申请详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,5 +302,21 @@ public class ConsultationController {
|
|||||||
return R.fail("获取会诊意见列表失败: " + e.getMessage());
|
return R.fail("获取会诊意见列表失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询会诊申请详情
|
||||||
|
*/
|
||||||
|
@ApiOperation("根据ID查询会诊申请详情")
|
||||||
|
@GetMapping("/detail/{id}")
|
||||||
|
public R<ConsultationRequestDto> getConsultationById(
|
||||||
|
@ApiParam("会诊申请ID") @PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
ConsultationRequestDto detail = consultationAppService.getConsultationById(id);
|
||||||
|
return R.ok(detail);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询会诊申请详情失败", e);
|
||||||
|
return R.fail("查询会诊申请详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public enum ConsultationStatusEnum {
|
|||||||
/**
|
/**
|
||||||
* 已取消
|
* 已取消
|
||||||
*/
|
*/
|
||||||
CANCELLED(50, "已取消");
|
CANCELLED(50, "已取消/作废");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态码
|
* 状态码
|
||||||
@@ -76,10 +76,12 @@ public enum ConsultationStatusEnum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断是否可以取消
|
* 判断是否可以取消/作废
|
||||||
|
* 只有新开(0)和已提交(10)状态可以作废
|
||||||
|
* 已确认(20)、已签名(30)、已完成(40)状态禁止作废
|
||||||
*/
|
*/
|
||||||
public boolean canCancel() {
|
public boolean canCancel() {
|
||||||
return this == NEW || this == SUBMITTED || this == CONFIRMED;
|
return this == NEW || this == SUBMITTED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,4 +130,13 @@ public interface IDiagTreatMAppService {
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
R<?> updatePricingFlag(List<Long> ids, Integer pricingFlag);
|
R<?> updatePricingFlag(List<Long> ids, Integer pricingFlag);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊疗目录下拉列表(轻量级,用于套餐设置)
|
||||||
|
*
|
||||||
|
* @param statusEnum 状态(2=启用)
|
||||||
|
* @param searchKey 搜索关键词(可选)
|
||||||
|
* @return 只包含 id, name, busNo, retailPrice
|
||||||
|
*/
|
||||||
|
R<?> getDiagnosisTreatmentSimpleList(Integer statusEnum, String searchKey);
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,6 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
private IOperationRecordService operationRecordService;
|
private IOperationRecordService operationRecordService;
|
||||||
@Resource
|
@Resource
|
||||||
private IServiceRequestService serviceRequestService;
|
private IServiceRequestService serviceRequestService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 诊疗目录初期查询
|
* 诊疗目录初期查询
|
||||||
*
|
*
|
||||||
@@ -186,6 +185,14 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
public R<?> getDiseaseTreatmentPage(DiagnosisTreatmentSelParam DiagnosisTreatmentSelParam, String searchKey,
|
public R<?> getDiseaseTreatmentPage(DiagnosisTreatmentSelParam DiagnosisTreatmentSelParam, String searchKey,
|
||||||
Integer pageNo, Integer pageSize, HttpServletRequest request) {
|
Integer pageNo, Integer pageSize, HttpServletRequest request) {
|
||||||
|
|
||||||
|
// 如果没有指定状态,默认只查询启用状态(status_enum=2),避免显示未启用的项目导致保存失败
|
||||||
|
if (DiagnosisTreatmentSelParam == null) {
|
||||||
|
DiagnosisTreatmentSelParam = new DiagnosisTreatmentSelParam();
|
||||||
|
}
|
||||||
|
if (DiagnosisTreatmentSelParam.getStatusEnum() == null) {
|
||||||
|
DiagnosisTreatmentSelParam.setStatusEnum(PublicationStatus.ACTIVE.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
// 临时保存ybType值并从参数对象中移除,避免HisQueryUtils构建yb_type条件
|
// 临时保存ybType值并从参数对象中移除,避免HisQueryUtils构建yb_type条件
|
||||||
String ybTypeValue = null;
|
String ybTypeValue = null;
|
||||||
if (DiagnosisTreatmentSelParam != null && StringUtils.isNotEmpty(DiagnosisTreatmentSelParam.getYbType())) {
|
if (DiagnosisTreatmentSelParam != null && StringUtils.isNotEmpty(DiagnosisTreatmentSelParam.getYbType())) {
|
||||||
@@ -200,6 +207,13 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
DiagnosisTreatmentSelParam.setInspectionTypeId(null); // 临时移除,防止HisQueryUtils处理
|
DiagnosisTreatmentSelParam.setInspectionTypeId(null); // 临时移除,防止HisQueryUtils处理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 临时保存pricingFlag值,手动添加带表别名的条件
|
||||||
|
Integer pricingFlagValue = null;
|
||||||
|
if (DiagnosisTreatmentSelParam != null && DiagnosisTreatmentSelParam.getPricingFlag() != null) {
|
||||||
|
pricingFlagValue = DiagnosisTreatmentSelParam.getPricingFlag();
|
||||||
|
DiagnosisTreatmentSelParam.setPricingFlag(null); // 临时移除,防止HisQueryUtils处理
|
||||||
|
}
|
||||||
|
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = HisQueryUtils.buildQueryWrapper(DiagnosisTreatmentSelParam,
|
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = HisQueryUtils.buildQueryWrapper(DiagnosisTreatmentSelParam,
|
||||||
searchKey, new HashSet<>(Arrays.asList("T1.bus_no", "T1.name", "T1.py_str", "T1.wb_str")), request);
|
searchKey, new HashSet<>(Arrays.asList("T1.bus_no", "T1.name", "T1.py_str", "T1.wb_str")), request);
|
||||||
@@ -218,9 +232,15 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
DiagnosisTreatmentSelParam.setInspectionTypeId(inspectionTypeIdValue);
|
DiagnosisTreatmentSelParam.setInspectionTypeId(inspectionTypeIdValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页查询
|
// 如果需要按划价标记过滤,添加带表别名的条件
|
||||||
|
if (pricingFlagValue != null) {
|
||||||
|
queryWrapper.eq("T1.pricing_flag", pricingFlagValue);
|
||||||
|
// 恢复参数对象中的值
|
||||||
|
DiagnosisTreatmentSelParam.setPricingFlag(pricingFlagValue);
|
||||||
|
}
|
||||||
|
|
||||||
IPage<DiagnosisTreatmentDto> diseaseTreatmentPage
|
IPage<DiagnosisTreatmentDto> diseaseTreatmentPage
|
||||||
= activityDefinitionManageMapper.getDiseaseTreatmentPage(new Page<DiagnosisTreatmentDto>(pageNo, pageSize), queryWrapper);
|
= activityDefinitionManageMapper.getDiseaseTreatmentPage(new Page<>(pageNo, pageSize), queryWrapper);
|
||||||
|
|
||||||
diseaseTreatmentPage.getRecords().forEach(e -> {
|
diseaseTreatmentPage.getRecords().forEach(e -> {
|
||||||
// 医保标记枚举类回显赋值
|
// 医保标记枚举类回显赋值
|
||||||
@@ -280,24 +300,19 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取诊查项目列表(医保类型为02)
|
* 获取诊查项目列表(医保类型为02,返回全量数据)
|
||||||
*
|
*
|
||||||
* @param orgId 科室ID
|
* @param orgId 科室ID(兼容保留,不参与过滤)
|
||||||
* @return 诊查项目列表
|
* @return 诊查项目列表
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> getClinicItems(Long orgId) {
|
public R<?> getClinicItems(Long orgId) {
|
||||||
// 构建查询条件,只查询医保类型为02(诊查费)的项目
|
// 构建查询条件,只查询医保类型为02(诊察费)的项目,不按科室过滤
|
||||||
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = new QueryWrapper<>();
|
||||||
queryWrapper.eq("T2.yb_type", "02"); // 使用T2表的yb_type字段,避免歧义
|
queryWrapper.eq("T2.yb_type", "02"); // 使用T2表的yb_type字段,避免歧义
|
||||||
queryWrapper.eq("T1.delete_flag", "0"); // 只查询未删除的记录
|
queryWrapper.eq("T1.delete_flag", "0"); // 只查询未删除的记录
|
||||||
queryWrapper.eq("T2.instance_table", "wor_activity_definition"); // 确保关联正确
|
queryWrapper.eq("T2.instance_table", "wor_activity_definition"); // 确保关联正确
|
||||||
|
|
||||||
// 如果提供了科室ID,则过滤该科室的项目
|
|
||||||
if (orgId != null) {
|
|
||||||
queryWrapper.eq("T1.org_id", orgId); // 使用机构ID进行过滤
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页查询,设置一个较大的页大小以获取所有诊查项目
|
// 分页查询,设置一个较大的页大小以获取所有诊查项目
|
||||||
IPage<DiagnosisTreatmentDto> diseaseTreatmentPage
|
IPage<DiagnosisTreatmentDto> diseaseTreatmentPage
|
||||||
= activityDefinitionManageMapper.getDiseaseTreatmentPage(new Page<DiagnosisTreatmentDto>(1, 100), queryWrapper);
|
= activityDefinitionManageMapper.getDiseaseTreatmentPage(new Page<DiagnosisTreatmentDto>(1, 100), queryWrapper);
|
||||||
@@ -430,24 +445,17 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> editDiseaseTreatmentStop(List<Long> ids) {
|
public R<?> editDiseaseTreatmentStop(List<Long> ids) {
|
||||||
|
List<ActivityDefinition> actList = new CopyOnWriteArrayList<>();
|
||||||
List<ActivityDefinition> ActivityDefinitionList = new CopyOnWriteArrayList<>();
|
for (Long id : ids) {
|
||||||
|
ActivityDefinition act = new ActivityDefinition();
|
||||||
// 取得更新值
|
act.setId(id);
|
||||||
for (Long detail : ids) {
|
act.setStatusEnum(PublicationStatus.RETIRED.getValue());
|
||||||
ActivityDefinition ActivityDefinition = new ActivityDefinition();
|
actList.add(act);
|
||||||
ActivityDefinition.setId(detail);
|
|
||||||
ActivityDefinition.setStatusEnum(PublicationStatus.RETIRED.getValue());
|
|
||||||
ActivityDefinitionList.add(ActivityDefinition);
|
|
||||||
}
|
}
|
||||||
// 插入操作记录
|
|
||||||
operationRecordService.addIdsOperationRecord(DbOpType.STOP.getCode(),
|
operationRecordService.addIdsOperationRecord(DbOpType.STOP.getCode(),
|
||||||
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, ids);
|
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, ids);
|
||||||
// 更新诊疗信息
|
activityDefinitionService.updateBatchById(actList);
|
||||||
return activityDefinitionService.updateBatchById(ActivityDefinitionList)
|
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"\u8bca\u7597\u76ee\u5f55"}));
|
||||||
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"诊疗目录"}))
|
|
||||||
: R.fail(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00007, null));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,24 +466,17 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> editDiseaseTreatmentStart(List<Long> ids) {
|
public R<?> editDiseaseTreatmentStart(List<Long> ids) {
|
||||||
|
List<ActivityDefinition> actList = new CopyOnWriteArrayList<>();
|
||||||
List<ActivityDefinition> ActivityDefinitionList = new CopyOnWriteArrayList<>();
|
for (Long id : ids) {
|
||||||
|
ActivityDefinition act = new ActivityDefinition();
|
||||||
// 取得更新值
|
act.setId(id);
|
||||||
for (Long detail : ids) {
|
act.setStatusEnum(PublicationStatus.ACTIVE.getValue());
|
||||||
ActivityDefinition ActivityDefinition = new ActivityDefinition();
|
actList.add(act);
|
||||||
ActivityDefinition.setId(detail);
|
|
||||||
ActivityDefinition.setStatusEnum(PublicationStatus.ACTIVE.getValue());
|
|
||||||
ActivityDefinitionList.add(ActivityDefinition);
|
|
||||||
}
|
}
|
||||||
// 插入操作记录
|
|
||||||
operationRecordService.addIdsOperationRecord(DbOpType.START.getCode(),
|
operationRecordService.addIdsOperationRecord(DbOpType.START.getCode(),
|
||||||
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, ids);
|
CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, ids);
|
||||||
// 更新诊疗信息
|
activityDefinitionService.updateBatchById(actList);
|
||||||
return activityDefinitionService.updateBatchById(ActivityDefinitionList)
|
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"诊疗目录"}));
|
||||||
? R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"诊疗目录"}))
|
|
||||||
: R.fail(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00007, null));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -813,4 +814,20 @@ public class DiagTreatMAppServiceImpl implements IDiagTreatMAppService {
|
|||||||
}
|
}
|
||||||
return activityDefinition;
|
return activityDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊疗目录下拉列表(轻量级,用于套餐设置)
|
||||||
|
* 只查询必要字段,减少JOIN,提高查询速度
|
||||||
|
* 支持搜索关键词过滤
|
||||||
|
*
|
||||||
|
* @param statusEnum 状态(2=启用)
|
||||||
|
* @param searchKey 搜索关键词
|
||||||
|
* @return 只包含 id, name, busNo, retailPrice
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public R<?> getDiagnosisTreatmentSimpleList(Integer statusEnum, String searchKey) {
|
||||||
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
List<DiagnosisTreatmentDto> list = activityDefinitionManageMapper.getDiagnosisTreatmentSimpleList(statusEnum, tenantId, searchKey);
|
||||||
|
return R.ok(list);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import java.util.List;
|
|||||||
* TODO:器材目录
|
* TODO:器材目录
|
||||||
*
|
*
|
||||||
* @author lpt
|
* @author lpt
|
||||||
* @date 2025-02-20
|
* @date 2025-02-20。
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/data-dictionary/device")
|
@RequestMapping("/data-dictionary/device")
|
||||||
|
|||||||
@@ -207,4 +207,21 @@ public class DiagnosisTreatmentController {
|
|||||||
.orderByAsc(InspectionType::getSortOrder);
|
.orderByAsc(InspectionType::getSortOrder);
|
||||||
return R.ok(inspectionTypeService.list(queryWrapper));
|
return R.ok(inspectionTypeService.list(queryWrapper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊疗目录下拉列表(轻量级,用于套餐设置)
|
||||||
|
* 只查询必要字段,减少JOIN,提高查询速度
|
||||||
|
* 支持搜索关键词过滤
|
||||||
|
*
|
||||||
|
* @param statusEnum 状态(2=启用)
|
||||||
|
* @param searchKey 搜索关键词
|
||||||
|
* @return 只包含 id, name, busNo, retailPrice
|
||||||
|
*/
|
||||||
|
@GetMapping("/simple-list")
|
||||||
|
public R<?> getDiagnosisTreatmentSimpleList(@RequestParam(required = false) Integer statusEnum, @RequestParam(required = false) String searchKey) {
|
||||||
|
if (statusEnum == null) {
|
||||||
|
statusEnum = 2;
|
||||||
|
}
|
||||||
|
return diagTreatMAppService.getDiagnosisTreatmentSimpleList(statusEnum, searchKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user