Compare commits
96 Commits
f3fd150235
...
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 |
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 "========================================"
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"tools": {
|
|
||||||
"approvalMode": "yolo"
|
|
||||||
},
|
|
||||||
"$version": 2
|
|
||||||
}
|
|
||||||
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
|
||||||
@@ -1 +1,2 @@
|
|||||||
# 赵云测试提交
|
# 赵云测试提交
|
||||||
|
赵云再次测试 - Tue Apr 14 09:36:09 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
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
|
||||||
@@ -153,4 +153,21 @@ public class CurrentDayEncounterDto {
|
|||||||
*/
|
*/
|
||||||
private Boolean isFromAppointment;
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -136,9 +136,11 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginUser loginUser = new LoginUser();
|
// Bug #432 修复:获取当前登录用户信息,增加null校验防止NPE
|
||||||
//获取当前登录用户信息
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
loginUser = SecurityUtils.getLoginUser();
|
if (loginUser == null) {
|
||||||
|
return R.fail("用户未登录或登录已过期");
|
||||||
|
}
|
||||||
// 当前登录用户ID
|
// 当前登录用户ID
|
||||||
Long userId = loginUser.getUserId();
|
Long userId = loginUser.getUserId();
|
||||||
|
|
||||||
|
|||||||
@@ -1395,9 +1395,17 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 更新邀请记录(存储会诊意见)
|
// 4. 更新邀请记录(存储会诊意见)
|
||||||
// 直接存储用户输入的原始意见内容,不添加医师姓名前缀
|
// Bug #388:格式化存储,确保回显时参加医师和意见完整
|
||||||
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode()); // 已确认
|
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode());
|
||||||
invited.setConfirmOpinion(dto.getConsultationOpinion()); // 直接存储原始意见,不添加前缀
|
|
||||||
|
String deptName = StringUtils.hasText(dto.getConfirmingDeptName())
|
||||||
|
? dto.getConfirmingDeptName() : invited.getInvitedDepartmentName();
|
||||||
|
String physician = StringUtils.hasText(dto.getConfirmingPhysician())
|
||||||
|
? dto.getConfirmingPhysician() : currentPhysicianName;
|
||||||
|
|
||||||
|
// 格式:科室-参加医师:意见内容
|
||||||
|
String formattedOpinion = String.format("%s-%s:%s", deptName, physician, dto.getConsultationOpinion());
|
||||||
|
invited.setConfirmOpinion(formattedOpinion);
|
||||||
invited.setConfirmTime(new Date());
|
invited.setConfirmTime(new Date());
|
||||||
consultationInvitedMapper.updateById(invited);
|
consultationInvitedMapper.updateById(invited);
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -116,4 +116,12 @@ public interface IDoctorStationAdviceAppService {
|
|||||||
* @return 检查url相关参数
|
* @return 检查url相关参数
|
||||||
*/
|
*/
|
||||||
R<?> getTestResult(Long encounterId);
|
R<?> getTestResult(Long encounterId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前科室已配置的药品类别列表
|
||||||
|
*
|
||||||
|
* @param organizationId 科室id
|
||||||
|
* @return 已配置的药品类别编码列表
|
||||||
|
*/
|
||||||
|
R<?> getConfiguredCategories(Long organizationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import com.openhis.administration.domain.Encounter;
|
|||||||
import com.openhis.administration.service.IAccountService;
|
import com.openhis.administration.service.IAccountService;
|
||||||
import com.openhis.administration.service.IChargeItemService;
|
import com.openhis.administration.service.IChargeItemService;
|
||||||
import com.openhis.administration.service.IEncounterService;
|
import com.openhis.administration.service.IEncounterService;
|
||||||
|
import com.openhis.administration.service.IEncounterDiagnosisService;
|
||||||
|
import com.openhis.administration.domain.EncounterDiagnosis;
|
||||||
import com.openhis.common.constant.CommonConstants;
|
import com.openhis.common.constant.CommonConstants;
|
||||||
import com.openhis.common.constant.PromptMsgConstant;
|
import com.openhis.common.constant.PromptMsgConstant;
|
||||||
import com.openhis.common.enums.*;
|
import com.openhis.common.enums.*;
|
||||||
@@ -115,6 +117,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
@Resource
|
@Resource
|
||||||
IEncounterService iEncounterService;
|
IEncounterService iEncounterService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
IEncounterDiagnosisService iEncounterDiagnosisService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
IInventoryItemService inventoryItemService;
|
IInventoryItemService inventoryItemService;
|
||||||
|
|
||||||
@@ -155,6 +160,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
String safePricingFlag = pricingFlag != null ? pricingFlag.toString() : "";
|
String safePricingFlag = pricingFlag != null ? pricingFlag.toString() : "";
|
||||||
String safePageNo = pageNo != null ? pageNo.toString() : "";
|
String safePageNo = pageNo != null ? pageNo.toString() : "";
|
||||||
String safePageSize = pageSize != null ? pageSize.toString() : "";
|
String safePageSize = pageSize != null ? pageSize.toString() : "";
|
||||||
|
String safeCategoryCode = categoryCode != null ? categoryCode : "";
|
||||||
|
|
||||||
// 设置默认科室:仅当前端/调用方未传 organizationId 时才回退到登录人科室
|
// 设置默认科室:仅当前端/调用方未传 organizationId 时才回退到登录人科室
|
||||||
// 否则会导致门诊划价等场景(按患者挂号科室查询)返回空
|
// 否则会导致门诊划价等场景(按患者挂号科室查询)返回空
|
||||||
@@ -169,7 +175,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
String cacheKey = null;
|
String cacheKey = null;
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
// 生成缓存 key:无搜索关键字时按科室缓存
|
// 生成缓存 key:无搜索关键字时按科室缓存
|
||||||
cacheKey = ADVICE_BASE_INFO_CACHE_PREFIX + organizationId + ":" + safeAdviceTypesStr + ":" + safePageNo + ":" + safePageSize;
|
cacheKey = ADVICE_BASE_INFO_CACHE_PREFIX + organizationId + ":" + safeAdviceTypesStr + ":" + safeCategoryCode + ":" + safePageNo + ":" + safePageSize;
|
||||||
|
|
||||||
// 先清除可能存在的无效缓存(JSONObject类型)
|
// 先清除可能存在的无效缓存(JSONObject类型)
|
||||||
if (redisCache.hasKey(cacheKey)) {
|
if (redisCache.hasKey(cacheKey)) {
|
||||||
@@ -281,6 +287,8 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
}
|
}
|
||||||
String unitCode = ""; // 包装单位
|
String unitCode = ""; // 包装单位
|
||||||
Long chargeItemDefinitionId; // 费用定价主表ID
|
Long chargeItemDefinitionId; // 费用定价主表ID
|
||||||
|
// 检查是否有取药科室配置(用于药品类型)
|
||||||
|
boolean hasPharmacyConfig = medLocationConfig != null && !medLocationConfig.isEmpty();
|
||||||
for (AdviceBaseDto baseDto : adviceBaseDtoList) {
|
for (AdviceBaseDto baseDto : adviceBaseDtoList) {
|
||||||
String tableName = baseDto.getAdviceTableName();
|
String tableName = baseDto.getAdviceTableName();
|
||||||
if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(tableName)) { // 药品
|
if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(tableName)) { // 药品
|
||||||
@@ -290,6 +298,9 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
// 是否为注射药物
|
// 是否为注射药物
|
||||||
baseDto.setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getInjectFlag()));
|
baseDto.setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getInjectFlag()));
|
||||||
|
|
||||||
|
// 设置是否缺少取药科室配置标志
|
||||||
|
baseDto.setPharmacyConfigMissing(!hasPharmacyConfig);
|
||||||
|
|
||||||
// fallthrough to 耗材处理逻辑(保持原有逻辑)
|
// fallthrough to 耗材处理逻辑(保持原有逻辑)
|
||||||
// 每一条医嘱的库存集合信息 , 包装单位库存前端计算
|
// 每一条医嘱的库存集合信息 , 包装单位库存前端计算
|
||||||
List<AdviceInventoryDto> inventoryList = adviceInventory
|
List<AdviceInventoryDto> inventoryList = adviceInventory
|
||||||
@@ -600,26 +611,24 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 药品(前端adviceType=1=西药, 2=中成药 → 都属于药品后端分类)
|
// 按后端 ItemType 枚举标准分类:
|
||||||
|
// MEDICINE=1(药品)、DEVICE=2(耗材)、ACTIVITY=3(诊疗)、SURGERY=6(手术)
|
||||||
|
|
||||||
|
// 药品分类:adviceType == 1
|
||||||
List<AdviceSaveDto> medicineList = adviceSaveList.stream()
|
List<AdviceSaveDto> medicineList = adviceSaveList.stream()
|
||||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())
|
.filter(e -> e.getAdviceType() != null && e.getAdviceType() == ItemType.MEDICINE.getValue())
|
||||||
|| e.getAdviceType() == 1
|
|
||||||
|| e.getAdviceType() == 2) // 前端中成药类型值为2 → 也属于药品分类
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
// 耗材(前端adviceType=4,后端ItemType.DEVICE=2)
|
|
||||||
List<AdviceSaveDto> deviceList = adviceSaveList.stream()
|
|
||||||
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())
|
|
||||||
|| e.getAdviceType() == 4) // 前端耗材类型值为4
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// 诊疗活动(前端adviceType=3诊疗、adviceType=5会诊、adviceType=6手术、adviceType=23检查 → 都属于诊疗后端分类)
|
// 耗材分类:adviceType == 2
|
||||||
|
List<AdviceSaveDto> deviceList = adviceSaveList.stream()
|
||||||
|
.filter(e -> e.getAdviceType() != null && e.getAdviceType() == ItemType.DEVICE.getValue())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 诊疗分类:adviceType == 3
|
||||||
List<AdviceSaveDto> activityList = adviceSaveList.stream()
|
List<AdviceSaveDto> activityList = adviceSaveList.stream()
|
||||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
.filter(e -> e.getAdviceType() != null
|
||||||
|| e.getAdviceType() == 3 // 前端诊疗类型值为3
|
&& (e.getAdviceType() == ItemType.ACTIVITY.getValue()
|
||||||
|| e.getAdviceType() == 5 // 前端会诊类型值为5
|
|| e.getAdviceType() == ItemType.SURGERY.getValue())) // 手术(6)也走诊疗流程
|
||||||
|| e.getAdviceType() == 6 // 前端手术类型值为6
|
|
||||||
|| e.getAdviceType() == 23 // 前端检查类型值为23
|
|
||||||
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())) // 后端手术类型值为6
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// 🔍 Debug日志日志: 记录分类结果
|
// 🔍 Debug日志日志: 记录分类结果
|
||||||
@@ -796,12 +805,71 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
// 就诊id
|
// 就诊id
|
||||||
Long encounterId = adviceSaveList.get(0).getEncounterId();
|
Long encounterId = adviceSaveList.get(0).getEncounterId();
|
||||||
|
|
||||||
// 使用安全的更新方法,避免并发冲突 - 更新费用项状态
|
// 🔧 BugFix#385: 签发时更新诊疗医嘱关联的费用项诊断信息
|
||||||
|
// 检查申请创建的费用项没有诊断关联,需要在签发时补充
|
||||||
|
if (!activityList.isEmpty()) {
|
||||||
|
// 先从就诊中获取主诊断,用于补充没有诊断关联的费用项
|
||||||
|
List<EncounterDiagnosis> encounterDiagList = iEncounterDiagnosisService.getDiagnosisList(encounterId);
|
||||||
|
EncounterDiagnosis mainDiagnosis = iEncounterDiagnosisService.getMainDiagnosis(encounterDiagList);
|
||||||
|
Long mainConditionId = mainDiagnosis != null ? mainDiagnosis.getConditionId() : null;
|
||||||
|
Long mainEncounterDiagId = mainDiagnosis != null ? mainDiagnosis.getId() : null;
|
||||||
|
|
||||||
|
log.info("BugFix#385: 签发时获取就诊主诊断, encounterId={}, mainConditionId={}, mainEncounterDiagId={}",
|
||||||
|
encounterId, mainConditionId, mainEncounterDiagId);
|
||||||
|
|
||||||
|
for (AdviceSaveDto adviceDto : activityList) {
|
||||||
|
if (adviceDto.getRequestId() != null) {
|
||||||
|
// 查询诊疗医嘱关联的费用项
|
||||||
|
List<ChargeItem> chargeItems = iChargeItemService.getChargeItemInfoByReqId(
|
||||||
|
Arrays.asList(adviceDto.getRequestId()));
|
||||||
|
if (chargeItems != null && !chargeItems.isEmpty()) {
|
||||||
|
// 过滤只保留诊疗类型的费用项
|
||||||
|
ChargeItem chargeItem = chargeItems.stream()
|
||||||
|
.filter(ci -> CommonConstants.TableName.WOR_SERVICE_REQUEST.equals(ci.getServiceTable()))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
if (chargeItem != null) {
|
||||||
|
// 🔧 BugFix#385: 如果费用项没有诊断关联,使用主诊断补充
|
||||||
|
Long conditionId = adviceDto.getConditionId();
|
||||||
|
Long encounterDiagId = adviceDto.getEncounterDiagnosisId();
|
||||||
|
|
||||||
|
// 如果传入的诊断为空,使用主诊断
|
||||||
|
if (conditionId == null) {
|
||||||
|
conditionId = mainConditionId;
|
||||||
|
}
|
||||||
|
if (encounterDiagId == null) {
|
||||||
|
encounterDiagId = mainEncounterDiagId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新诊断关联
|
||||||
|
if (conditionId != null || encounterDiagId != null) {
|
||||||
|
chargeItem.setConditionId(conditionId);
|
||||||
|
chargeItem.setEncounterDiagnosisId(encounterDiagId);
|
||||||
|
iChargeItemService.updateById(chargeItem);
|
||||||
|
log.info("BugFix#385: 签发时更新诊疗费用项诊断关联, chargeItemId={}, conditionId={}, encounterDiagnosisId={}",
|
||||||
|
chargeItem.getId(), conditionId, encounterDiagId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 BugFix#385: 使用安全的更新方法,避免并发冲突 - 更新费用项状态
|
||||||
|
// 需要处理两种情况:
|
||||||
|
// 1. 从 DRAFT (0) → PLANNED (1):新创建的收费项目
|
||||||
|
// 2. 从 BILLABLE (2) → PLANNED (1):保存时已设为待结算的项目
|
||||||
iChargeItemService.updateChargeStatusByConditionSafe(
|
iChargeItemService.updateChargeStatusByConditionSafe(
|
||||||
encounterId,
|
encounterId,
|
||||||
ChargeItemStatus.DRAFT.getValue(),
|
ChargeItemStatus.DRAFT.getValue(),
|
||||||
ChargeItemStatus.PLANNED.getValue(),
|
ChargeItemStatus.PLANNED.getValue(),
|
||||||
requestIds);
|
requestIds);
|
||||||
|
|
||||||
|
// 🔧 BugFix#385: 同时处理 BILLABLE 状态的收费项目
|
||||||
|
iChargeItemService.updateChargeStatusByConditionSafe(
|
||||||
|
encounterId,
|
||||||
|
ChargeItemStatus.BILLABLE.getValue(),
|
||||||
|
ChargeItemStatus.PLANNED.getValue(),
|
||||||
|
requestIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据变更后清理相关缓存
|
// 数据变更后清理相关缓存
|
||||||
@@ -935,12 +1003,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
insertOrUpdateList = uniqueInsertOrUpdateList;
|
insertOrUpdateList = uniqueInsertOrUpdateList;
|
||||||
|
|
||||||
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
|
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
|
||||||
// 🔧 Bug Fix: 确保accountId不为null,与handleBoundDevices保持一致
|
// 🔧 Bug Fix: 确保accountId有效(不为null且账户存在)
|
||||||
|
boolean needNewAccount = false;
|
||||||
if (adviceSaveDto.getAccountId() == null) {
|
if (adviceSaveDto.getAccountId() == null) {
|
||||||
|
needNewAccount = true;
|
||||||
|
} else {
|
||||||
|
// 验证账户是否存在且有效(未被删除,租户匹配)
|
||||||
|
Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId());
|
||||||
|
if (existingAccount == null) {
|
||||||
|
log.warn("handMedication - 前端传入的accountId无效(账户不存在),accountId={},将重新获取或创建账户",
|
||||||
|
adviceSaveDto.getAccountId());
|
||||||
|
needNewAccount = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needNewAccount) {
|
||||||
// 尝试从患者就诊中获取默认账户ID(自费账户)
|
// 尝试从患者就诊中获取默认账户ID(自费账户)
|
||||||
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
|
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
|
||||||
if (selfAccount != null) {
|
if (selfAccount != null) {
|
||||||
adviceSaveDto.setAccountId(selfAccount.getId());
|
adviceSaveDto.setAccountId(selfAccount.getId());
|
||||||
|
log.info("handMedication - 使用现有自费账户,accountId={}", selfAccount.getId());
|
||||||
} else {
|
} else {
|
||||||
// 自动创建自费账户
|
// 自动创建自费账户
|
||||||
Account newAccount = new Account();
|
Account newAccount = new Account();
|
||||||
@@ -954,6 +1036,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
||||||
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
||||||
adviceSaveDto.setAccountId(newAccountId);
|
adviceSaveDto.setAccountId(newAccountId);
|
||||||
|
log.info("handMedication - 自动创建自费账户,newAccountId={}", newAccountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,7 +1144,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
|
|
||||||
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
||||||
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
||||||
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
|
// #415 价格非负验证
|
||||||
|
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
|
||||||
|
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
unitPrice = unitPrice.abs(); // 负数取绝对值
|
||||||
|
}
|
||||||
|
chargeItem.setUnitPrice(unitPrice); // 单价
|
||||||
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
||||||
|
|
||||||
// 显式设置tenantId、createBy和createTime字段,防止自动填充机制失效
|
// 显式设置tenantId、createBy和createTime字段,防止自动填充机制失效
|
||||||
@@ -1344,12 +1432,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
log.info("BugFix#219: ========== handDevice END ==========");
|
log.info("BugFix#219: ========== handDevice END ==========");
|
||||||
|
|
||||||
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
|
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
|
||||||
// 🔧 Bug Fix: 确保accountId不为null
|
// 🔧 Bug Fix: 确保accountId有效(不为null且账户存在)
|
||||||
|
boolean needNewAccount = false;
|
||||||
if (adviceSaveDto.getAccountId() == null) {
|
if (adviceSaveDto.getAccountId() == null) {
|
||||||
|
needNewAccount = true;
|
||||||
|
} else {
|
||||||
|
// 验证账户是否存在且有效(未被删除,租户匹配)
|
||||||
|
Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId());
|
||||||
|
if (existingAccount == null) {
|
||||||
|
log.warn("handDevice - 前端传入的accountId无效(账户不存在),accountId={},将重新获取或创建账户",
|
||||||
|
adviceSaveDto.getAccountId());
|
||||||
|
needNewAccount = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needNewAccount) {
|
||||||
// 尝试从患者就诊中获取默认账户ID(自费账户)
|
// 尝试从患者就诊中获取默认账户ID(自费账户)
|
||||||
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
|
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
|
||||||
if (selfAccount != null) {
|
if (selfAccount != null) {
|
||||||
adviceSaveDto.setAccountId(selfAccount.getId());
|
adviceSaveDto.setAccountId(selfAccount.getId());
|
||||||
|
log.info("handDevice - 使用现有自费账户,accountId={}", selfAccount.getId());
|
||||||
} else {
|
} else {
|
||||||
// 自动创建自费账户
|
// 自动创建自费账户
|
||||||
Account newAccount = new Account();
|
Account newAccount = new Account();
|
||||||
@@ -1363,6 +1465,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
||||||
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
||||||
adviceSaveDto.setAccountId(newAccountId);
|
adviceSaveDto.setAccountId(newAccountId);
|
||||||
|
log.info("handDevice - 自动创建自费账户,newAccountId={}", newAccountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1518,7 +1621,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
|
|
||||||
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
||||||
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
||||||
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
|
// #415 价格非负验证
|
||||||
|
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
|
||||||
|
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
unitPrice = unitPrice.abs(); // 负数取绝对值
|
||||||
|
}
|
||||||
|
chargeItem.setUnitPrice(unitPrice); // 单价
|
||||||
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
||||||
|
|
||||||
// 显式设置审计字段,防止自动填充机制失效
|
// 显式设置审计字段,防止自动填充机制失效
|
||||||
@@ -1600,12 +1708,26 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
|
for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) {
|
||||||
// 🔧 Bug Fix: 确保accountId不为null
|
// 🔧 Bug Fix: 确保accountId有效(不为null且账户存在)
|
||||||
|
boolean needNewAccount = false;
|
||||||
if (adviceSaveDto.getAccountId() == null) {
|
if (adviceSaveDto.getAccountId() == null) {
|
||||||
|
needNewAccount = true;
|
||||||
|
} else {
|
||||||
|
// 验证账户是否存在且有效(未被删除,租户匹配)
|
||||||
|
Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId());
|
||||||
|
if (existingAccount == null) {
|
||||||
|
log.warn("handService - 前端传入的accountId无效(账户不存在),accountId={},将重新获取或创建账户",
|
||||||
|
adviceSaveDto.getAccountId());
|
||||||
|
needNewAccount = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needNewAccount) {
|
||||||
// 尝试从患者就诊中获取默认账户ID(自费账户)
|
// 尝试从患者就诊中获取默认账户ID(自费账户)
|
||||||
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
|
Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId());
|
||||||
if (selfAccount != null) {
|
if (selfAccount != null) {
|
||||||
adviceSaveDto.setAccountId(selfAccount.getId());
|
adviceSaveDto.setAccountId(selfAccount.getId());
|
||||||
|
log.info("handService - 使用现有自费账户,accountId={}", selfAccount.getId());
|
||||||
} else {
|
} else {
|
||||||
// 自动创建自费账户
|
// 自动创建自费账户
|
||||||
Account newAccount = new Account();
|
Account newAccount = new Account();
|
||||||
@@ -1619,6 +1741,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
||||||
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
||||||
adviceSaveDto.setAccountId(newAccountId);
|
adviceSaveDto.setAccountId(newAccountId);
|
||||||
|
log.info("handService - 自动创建自费账户,newAccountId={}", newAccountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,7 +1794,13 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
serviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum());
|
serviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum());
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceRequest.setActivityId(adviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
|
// 🔧 BugFix#385: 检查类型(adviceType=2)不走定价体系,activityId设置为0L占位
|
||||||
|
// 与ExamApplyController保持一致,数据库有NOT NULL约束
|
||||||
|
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 2) {
|
||||||
|
serviceRequest.setActivityId(0L);
|
||||||
|
} else {
|
||||||
|
serviceRequest.setActivityId(adviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
|
||||||
|
}
|
||||||
serviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者
|
serviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者
|
||||||
serviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生
|
serviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生
|
||||||
serviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id
|
serviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id
|
||||||
@@ -1723,7 +1852,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
|
chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
|
||||||
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
||||||
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
||||||
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
|
// #415 价格非负验证
|
||||||
|
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
|
||||||
|
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
unitPrice = unitPrice.abs(); // 负数取绝对值
|
||||||
|
}
|
||||||
|
chargeItem.setUnitPrice(unitPrice); // 单价
|
||||||
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
||||||
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
iChargeItemService.saveOrUpdate(chargeItem);
|
||||||
@@ -2147,4 +2281,23 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
|||||||
return searchKey.matches("[a-zA-Z]+");
|
return searchKey.matches("[a-zA-Z]+");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前科室已配置的药品类别列表
|
||||||
|
*
|
||||||
|
* @param organizationId 科室id
|
||||||
|
* @return 已配置的药品类别编码列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public R<?> getConfiguredCategories(Long organizationId) {
|
||||||
|
// 查询取药科室配置
|
||||||
|
List<AdviceInventoryDto> medLocationConfig = doctorStationAdviceAppMapper.getMedLocationConfig(organizationId);
|
||||||
|
// 提取不重复的 categoryCode
|
||||||
|
List<String> categoryCodes = medLocationConfig.stream()
|
||||||
|
.map(AdviceInventoryDto::getCategoryCode)
|
||||||
|
.filter(code -> code != null && !code.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return R.ok(categoryCodes);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
|
|||||||
// 诊断定义集合
|
// 诊断定义集合
|
||||||
List<SaveDiagnosisChildParam> diagnosisChildList = saveDiagnosisParam.getDiagnosisChildList();
|
List<SaveDiagnosisChildParam> diagnosisChildList = saveDiagnosisParam.getDiagnosisChildList();
|
||||||
// 先删除再保存
|
// 先删除再保存
|
||||||
// iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
|
iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
|
||||||
// 保存诊断管理
|
// 保存诊断管理
|
||||||
Condition condition;
|
Condition condition;
|
||||||
for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) {
|
for (SaveDiagnosisChildParam saveDiagnosisChildParam : diagnosisChildList) {
|
||||||
@@ -296,7 +296,7 @@ public class DoctorStationDiagnosisAppServiceImpl implements IDoctorStationDiagn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 先删除再保存
|
// 先删除再保存
|
||||||
// iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
|
iEncounterDiagnosisService.deleteEncounterDiagnosisInfos(encounterId);
|
||||||
|
|
||||||
// 如果本次保存中有设置主诊断,则先清空该就诊下所有的主诊断标记,确保唯一性
|
// 如果本次保存中有设置主诊断,则先清空该就诊下所有的主诊断标记,确保唯一性
|
||||||
boolean hasMain = diagnosisChildList.stream().anyMatch(d -> Integer.valueOf(1).equals(d.getMaindiseFlag()));
|
boolean hasMain = diagnosisChildList.stream().anyMatch(d -> Integer.valueOf(1).equals(d.getMaindiseFlag()));
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import com.core.common.utils.SecurityUtils;
|
|||||||
import com.openhis.common.enums.DbOpType;
|
import com.openhis.common.enums.DbOpType;
|
||||||
import com.openhis.administration.service.IAccountService;
|
import com.openhis.administration.service.IAccountService;
|
||||||
import com.openhis.administration.domain.Account;
|
import com.openhis.administration.domain.Account;
|
||||||
|
import com.openhis.administration.service.IChargeItemService; // Bug #386修复: 添加 ChargeItemService
|
||||||
import com.openhis.lab.domain.InspectionLabApply;
|
import com.openhis.lab.domain.InspectionLabApply;
|
||||||
import com.openhis.lab.domain.InspectionLabApplyItem;
|
import com.openhis.lab.domain.InspectionLabApplyItem;
|
||||||
import com.openhis.lab.domain.BarCode;
|
import com.openhis.lab.domain.BarCode;
|
||||||
import com.openhis.lab.domain.InspectionPackage;
|
import com.openhis.lab.domain.InspectionPackage;
|
||||||
|
import com.openhis.lab.domain.LabActivityDefinition;
|
||||||
import com.openhis.lab.service.IInspectionLabApplyItemService;
|
import com.openhis.lab.service.IInspectionLabApplyItemService;
|
||||||
import com.openhis.lab.service.IInspectionLabApplyService;
|
import com.openhis.lab.service.IInspectionLabApplyService;
|
||||||
import com.openhis.lab.service.IInspectionLabBarCodeService;
|
import com.openhis.lab.service.IInspectionLabBarCodeService;
|
||||||
import com.openhis.lab.service.IInspectionPackageService;
|
import com.openhis.lab.service.IInspectionPackageService;
|
||||||
|
import com.openhis.lab.service.ILabActivityDefinitionService;
|
||||||
import com.openhis.workflow.domain.ServiceRequest;
|
import com.openhis.workflow.domain.ServiceRequest;
|
||||||
import com.openhis.workflow.service.IServiceRequestService;
|
import com.openhis.workflow.service.IServiceRequestService;
|
||||||
import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService;
|
import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService;
|
||||||
@@ -45,6 +48,9 @@ import java.util.Date;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据检验申请单开单信息系统自动插入门诊医嘱表(与检验申请主表申请单号进行关联),
|
* 根据检验申请单开单信息系统自动插入门诊医嘱表(与检验申请主表申请单号进行关联),
|
||||||
@@ -88,6 +94,14 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IInspectionPackageService inspectionPackageService;
|
private IInspectionPackageService inspectionPackageService;
|
||||||
|
|
||||||
|
// Bug #326: 检验项目定义服务(用于回充时获取套餐信息)
|
||||||
|
@Autowired
|
||||||
|
private ILabActivityDefinitionService labActivityDefinitionService;
|
||||||
|
|
||||||
|
// Bug #386修复: ChargeItemService 用于删除收费项目
|
||||||
|
@Autowired
|
||||||
|
private IChargeItemService chargeItemService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存检验申请单信息
|
* 保存检验申请单信息
|
||||||
* @param doctorStationLabApplyDto
|
* @param doctorStationLabApplyDto
|
||||||
@@ -160,6 +174,28 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
InspectionLabApplyItem inspectionLabApplyItem = new InspectionLabApplyItem();
|
InspectionLabApplyItem inspectionLabApplyItem = new InspectionLabApplyItem();
|
||||||
BeanUtils.copyProperties(doctorStationLabApplyItemDto, inspectionLabApplyItem);
|
BeanUtils.copyProperties(doctorStationLabApplyItemDto, inspectionLabApplyItem);
|
||||||
|
|
||||||
|
// 前端选择检验项目时已携带完整的关联信息(activityId、feePackageId、itemCode等)
|
||||||
|
// Bug #326修复: 只使用 activityId 直接查询,不使用名称反查
|
||||||
|
Long activityId = doctorStationLabApplyItemDto.getActivityId();
|
||||||
|
if (activityId != null) {
|
||||||
|
// 使用 activityId 直接查询检验项目定义
|
||||||
|
LabActivityDefinition labActivityDefinition = labActivityDefinitionService.getById(activityId);
|
||||||
|
if (labActivityDefinition != null && DelFlag.NO.getCode().equals(labActivityDefinition.getDeleteFlag())) {
|
||||||
|
// 补充编码(如果前端未传或为空)
|
||||||
|
if (inspectionLabApplyItem.getItemCode() == null || inspectionLabApplyItem.getItemCode().isEmpty()) {
|
||||||
|
inspectionLabApplyItem.setItemCode(labActivityDefinition.getBusNo());
|
||||||
|
}
|
||||||
|
// 补充套餐ID(如果前端未传)
|
||||||
|
if (inspectionLabApplyItem.getFeePackageId() == null) {
|
||||||
|
inspectionLabApplyItem.setFeePackageId(labActivityDefinition.getFeePackageId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有 activityId 时记录警告,不使用名称反查
|
||||||
|
log.warn("检验项目 [{}] 未传入 activityId,无法获取完整关联信息",
|
||||||
|
doctorStationLabApplyItemDto.getItemName());
|
||||||
|
}
|
||||||
|
|
||||||
// 后端重新计算金额:金额 = 单价 × 数量
|
// 后端重新计算金额:金额 = 单价 × 数量
|
||||||
java.math.BigDecimal itemPrice = doctorStationLabApplyItemDto.getItemPrice();
|
java.math.BigDecimal itemPrice = doctorStationLabApplyItemDto.getItemPrice();
|
||||||
java.math.BigDecimal itemQty = doctorStationLabApplyItemDto.getItemQty();
|
java.math.BigDecimal itemQty = doctorStationLabApplyItemDto.getItemQty();
|
||||||
@@ -241,13 +277,16 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
);
|
);
|
||||||
if (organization != null) {
|
if (organization != null) {
|
||||||
positionId = organization.getId();
|
positionId = organization.getId();
|
||||||
|
} else {
|
||||||
|
// Bug #329: 执行科室代码无法匹配到科室,记录警告日志
|
||||||
|
log.warn("执行科室代码 [{}] 在科室表中未找到对应记录", performDeptCode);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Bug #329: 未指定执行科室,记录警告日志
|
||||||
|
log.warn("检验项目 [{}] 未指定执行科室", itemName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有指定执行科室,使用当前医生所在的科室作为默认执行科室
|
// Bug #329: 移除默认执行科室逻辑,必须由前端明确指定执行科室
|
||||||
if (positionId == null) {
|
|
||||||
positionId = SecurityUtils.getDeptId();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 创建医嘱保存对象
|
// 4. 创建医嘱保存对象
|
||||||
AdviceSaveDto adviceSaveDto = new AdviceSaveDto();
|
AdviceSaveDto adviceSaveDto = new AdviceSaveDto();
|
||||||
@@ -324,7 +363,6 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
throw new RuntimeException("套餐项目 '" + itemName + "' 未设置有效价格,请先配置套餐金额");
|
throw new RuntimeException("套餐项目 '" + itemName + "' 未设置有效价格,请先配置套餐金额");
|
||||||
}
|
}
|
||||||
unitPrice = packageInfo.getPackageAmount();
|
unitPrice = packageInfo.getPackageAmount();
|
||||||
log.info("套餐项目 '{}' 使用套餐价格: {}", itemName, unitPrice);
|
|
||||||
} else {
|
} else {
|
||||||
// 普通项目:使用前端传入的价格
|
// 普通项目:使用前端传入的价格
|
||||||
unitPrice = labApplyItemDto.getItemPrice();
|
unitPrice = labApplyItemDto.getItemPrice();
|
||||||
@@ -367,30 +405,84 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Object getInspectionApplyByApplyNo(String applyNo) {
|
public Object getInspectionApplyByApplyNo(String applyNo) {
|
||||||
// 查询主表数据
|
// 使用MyBatis-Plus查询主表数据
|
||||||
DoctorStationLabApplyDto applyDto = (DoctorStationLabApplyDto) doctorStationLabApplyMapper.getInspectionApplyByApplyNo(applyNo);
|
InspectionLabApply mainEntity = inspectionLabApplyService.getOne(
|
||||||
if (applyDto == null) {
|
new QueryWrapper<InspectionLabApply>()
|
||||||
|
.eq("apply_no", applyNo)
|
||||||
|
.eq("delete_flag", DelFlag.NO.getCode())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mainEntity == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用BeanUtils进行基础字段映射
|
||||||
|
DoctorStationLabApplyDto applyDto = new DoctorStationLabApplyDto();
|
||||||
|
BeanUtils.copyProperties(mainEntity, applyDto);
|
||||||
|
// 由于字段名称映射关系(如 id -> applicationId),需要单独设置
|
||||||
|
applyDto.setApplicationId(mainEntity.getId());
|
||||||
|
|
||||||
// 查询检验项目明细
|
// 查询检验项目明细
|
||||||
List<InspectionLabApplyItem> itemList = inspectionLabApplyItemService.list(
|
List<InspectionLabApplyItem> itemList = inspectionLabApplyItemService.list(
|
||||||
new QueryWrapper<InspectionLabApplyItem>()
|
new QueryWrapper<InspectionLabApplyItem>()
|
||||||
.eq("apply_no", applyNo)
|
.eq("apply_no", applyNo)
|
||||||
.eq("delete_flag", "0")
|
.eq("delete_flag", DelFlag.NO.getCode())
|
||||||
.orderByAsc("item_seq")
|
.orderByAsc("item_seq")
|
||||||
);
|
);
|
||||||
|
|
||||||
// 转换为 DTO 列表
|
// 转换为 DTO 列表
|
||||||
List<DoctorStationLabApplyItemDto> itemDtoList = new ArrayList<>();
|
List<DoctorStationLabApplyItemDto> itemDtoList = new ArrayList<>();
|
||||||
if (itemList != null && !itemList.isEmpty()) {
|
if (itemList != null && !itemList.isEmpty()) {
|
||||||
|
// 提取所有不同的 itemCode,用于批量查询
|
||||||
|
Set<String> itemCodes = itemList.stream()
|
||||||
|
.filter(item -> item.getItemCode() != null && !item.getItemCode().isEmpty())
|
||||||
|
.map(InspectionLabApplyItem::getItemCode)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 批量查询所有关联的检验项目定义(使用MyBatis-Plus)
|
||||||
|
Map<String, LabActivityDefinition> definitionMap = new HashMap<>();
|
||||||
|
if (!itemCodes.isEmpty()) {
|
||||||
|
List<LabActivityDefinition> labActivityDefinitions = labActivityDefinitionService.list(
|
||||||
|
new QueryWrapper<LabActivityDefinition>()
|
||||||
|
.in("bus_no", itemCodes)
|
||||||
|
.eq("delete_flag", DelFlag.NO.getCode())
|
||||||
|
);
|
||||||
|
// 构建 itemCode 到定义的映射
|
||||||
|
definitionMap = labActivityDefinitions.stream()
|
||||||
|
.collect(Collectors.toMap(LabActivityDefinition::getBusNo, Function.identity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每个明细项
|
||||||
for (InspectionLabApplyItem item : itemList) {
|
for (InspectionLabApplyItem item : itemList) {
|
||||||
DoctorStationLabApplyItemDto itemDto = new DoctorStationLabApplyItemDto();
|
DoctorStationLabApplyItemDto itemDto = new DoctorStationLabApplyItemDto();
|
||||||
|
// 使用BeanUtils进行基础字段映射
|
||||||
BeanUtils.copyProperties(item, itemDto);
|
BeanUtils.copyProperties(item, itemDto);
|
||||||
|
|
||||||
|
// feePackageId 在保存时已存储,直接使用
|
||||||
|
itemDto.setFeePackageId(item.getFeePackageId());
|
||||||
|
// 判断是否是套餐项目(根据 feePackageId 是否存在)
|
||||||
|
itemDto.setIsPackage(item.getFeePackageId() != null);
|
||||||
|
|
||||||
|
// 从批量查询结果中获取关联信息
|
||||||
|
if (item.getItemCode() != null && !item.getItemCode().isEmpty()) {
|
||||||
|
LabActivityDefinition labActivityDefinition = definitionMap.get(item.getItemCode());
|
||||||
|
if (labActivityDefinition != null) {
|
||||||
|
itemDto.setActivityId(labActivityDefinition.getId());
|
||||||
|
itemDto.setSampleType(labActivityDefinition.getSpecimenCode());
|
||||||
|
itemDto.setUnit(labActivityDefinition.getPermittedUnitCode());
|
||||||
|
// 补充检验类型ID,用于前端自动设置执行科室
|
||||||
|
itemDto.setInspectionTypeId(labActivityDefinition.getInspectionTypeId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("检验项目 [{}] 未存储 itemCode,无法获取完整关联信息", item.getItemName());
|
||||||
|
}
|
||||||
|
|
||||||
itemDtoList.add(itemDto);
|
itemDtoList.add(itemDto);
|
||||||
}
|
}
|
||||||
// 从第一个明细项获取执行科室代码
|
// 从第一个明细项获取执行科室代码
|
||||||
applyDto.setExecuteDepartment(itemList.get(0).getPerformDeptCode());
|
if (!itemList.isEmpty() && itemList.get(0).getPerformDeptCode() != null) {
|
||||||
|
applyDto.setExecuteDepartment(itemList.get(0).getPerformDeptCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
applyDto.setLabApplyItemList(itemDtoList);
|
applyDto.setLabApplyItemList(itemDtoList);
|
||||||
|
|
||||||
@@ -409,8 +501,9 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
// 使用 PageHelper 进行分页查询
|
// 使用 PageHelper 进行分页查询
|
||||||
PageHelper.startPage(pageNo, pageSize);
|
PageHelper.startPage(pageNo, pageSize);
|
||||||
|
|
||||||
// 查询检验申请单列表
|
// 使用MyBatis-Plus查询检验申请单列表
|
||||||
log.debug("查询申请单数据前");
|
// 需要关联adm_encounter表,仍使用原Mapper方法或重构为MyBatis-Plus方式
|
||||||
|
// 为保持一致性和考虑到复杂的关联查询,暂时保留原有实现方式
|
||||||
List<DoctorStationLabApplyDto> list = doctorStationLabApplyMapper.getInspectionApplyListPage(encounterId);
|
List<DoctorStationLabApplyDto> list = doctorStationLabApplyMapper.getInspectionApplyListPage(encounterId);
|
||||||
log.debug("查询申请单数据后");
|
log.debug("查询申请单数据后");
|
||||||
|
|
||||||
@@ -512,6 +605,12 @@ public class DoctorStationLabApplyServiceImpl implements IDoctorStationInspectio
|
|||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
log.debug("成功将申请单号 [{}] 关联的 {} 条门诊医嘱的删除状态更新为1,更新人:{},更新时间:{}",
|
log.debug("成功将申请单号 [{}] 关联的 {} 条门诊医嘱的删除状态更新为1,更新人:{},更新时间:{}",
|
||||||
applyNo, requestIds.size(), currentUsername, currentTime);
|
applyNo, requestIds.size(), currentUsername, currentTime);
|
||||||
|
|
||||||
|
// Bug #386修复: 同步删除关联的收费项目
|
||||||
|
for (Long requestId : requestIds) {
|
||||||
|
chargeItemService.deleteByServiceTableAndId("wor_service_request", requestId);
|
||||||
|
}
|
||||||
|
log.debug("成功删除申请单号 [{}] 关联的 {} 条收费项目", applyNo, requestIds.size());
|
||||||
} else {
|
} else {
|
||||||
log.warn("更新申请单号 [{}] 关联的门诊医嘱删除状态失败", applyNo);
|
log.warn("更新申请单号 [{}] 关联的门诊医嘱删除状态失败", applyNo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,12 +147,19 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
|||||||
return R.fail("已接诊,请勿重复点击,已为您刷新");
|
return R.fail("已接诊,请勿重复点击,已为您刷新");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 允许从「待诊/暂离/诊毕」重新接诊(用于队列弹窗的完诊患者重新进入在诊)
|
||||||
|
Date now = new Date();
|
||||||
int update = encounterMapper.update(null,
|
int update = encounterMapper.update(null,
|
||||||
new LambdaUpdateWrapper<Encounter>().eq(Encounter::getId, encounterId)
|
new LambdaUpdateWrapper<Encounter>().eq(Encounter::getId, encounterId)
|
||||||
.eq(Encounter::getStatusEnum, EncounterStatus.PLANNED.getValue()) // 只更新待诊状态的患者
|
.in(Encounter::getStatusEnum,
|
||||||
.set(Encounter::getReceptionTime, new Date())
|
EncounterStatus.PLANNED.getValue(),
|
||||||
|
EncounterStatus.ON_HOLD.getValue(),
|
||||||
|
EncounterStatus.DISCHARGED.getValue())
|
||||||
|
.set(Encounter::getReceptionTime, now)
|
||||||
|
.set(Encounter::getEndTime, null)
|
||||||
.set(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
|
.set(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())
|
||||||
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.RECEIVING_CARE.getValue()));
|
.set(Encounter::getSubjectStatusEnum, EncounterSubjectStatus.RECEIVING_CARE.getValue())
|
||||||
|
.set(Encounter::getUpdateTime, now));
|
||||||
|
|
||||||
// 如果更新失败,说明状态已被其他医生修改
|
// 如果更新失败,说明状态已被其他医生修改
|
||||||
if (update <= 0) {
|
if (update <= 0) {
|
||||||
@@ -181,7 +188,7 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
|||||||
encounterParticipant.setCreateTime(new Date());
|
encounterParticipant.setCreateTime(new Date());
|
||||||
iEncounterParticipantService.save(encounterParticipant);
|
iEncounterParticipantService.save(encounterParticipant);
|
||||||
|
|
||||||
// 更新 triage_queue_item 队列记录状态为 CALLING
|
// 更新 triage_queue_item 队列记录状态为 IN_CLINIC(诊中)
|
||||||
try {
|
try {
|
||||||
TriageQueueItem queueItem = triageQueueItemService.getOne(
|
TriageQueueItem queueItem = triageQueueItemService.getOne(
|
||||||
new LambdaQueryWrapper<TriageQueueItem>()
|
new LambdaQueryWrapper<TriageQueueItem>()
|
||||||
@@ -190,10 +197,10 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
|||||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||||
);
|
);
|
||||||
if (queueItem != null) {
|
if (queueItem != null) {
|
||||||
queueItem.setStatus("CALLING");
|
queueItem.setStatus(20); // 20=IN_CLINIC(诊中),患者进入诊室接诊
|
||||||
queueItem.setUpdateTime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
queueItem.setUpdateTime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||||
triageQueueItemService.updateById(queueItem);
|
triageQueueItemService.updateById(queueItem);
|
||||||
log.info("接诊时更新队列状态为CALLING,encounterId={}, queueItemId={}", encounterId, queueItem.getId());
|
log.info("接诊时更新队列状态为IN_CLINIC(诊中),encounterId={}, queueItemId={}", encounterId, queueItem.getId());
|
||||||
} else {
|
} else {
|
||||||
log.warn("接诊时未找到队列记录,encounterId={}", encounterId);
|
log.warn("接诊时未找到队列记录,encounterId={}", encounterId);
|
||||||
}
|
}
|
||||||
@@ -256,10 +263,13 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 如果队列项存在,检查状态并更新
|
// 如果队列项存在,检查状态并更新
|
||||||
if (queueItem != null && "CALLING".equals(queueItem.getStatus())) {
|
// 允许从 CALLING(10) 或 IN_CLINIC(20) 完成就诊
|
||||||
|
if (queueItem != null &&
|
||||||
|
(Integer.valueOf(10).equals(queueItem.getStatus()) ||
|
||||||
|
Integer.valueOf(20).equals(queueItem.getStatus()))) {
|
||||||
// 更新队列状态为已完成
|
// 更新队列状态为已完成
|
||||||
java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
|
java.time.LocalDateTime nowLocal = java.time.LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
|
||||||
queueItem.setStatus("COMPLETED");
|
queueItem.setStatus(30); // 30=COMPLETED(已完成)
|
||||||
queueItem.setUpdateTime(nowLocal);
|
queueItem.setUpdateTime(nowLocal);
|
||||||
triageQueueItemService.updateById(queueItem);
|
triageQueueItemService.updateById(queueItem);
|
||||||
|
|
||||||
@@ -268,13 +278,15 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
|||||||
Long userId = SecurityUtils.getLoginUser().getUserId();
|
Long userId = SecurityUtils.getLoginUser().getUserId();
|
||||||
String divLogSql = "INSERT INTO hisdev.div_log "
|
String divLogSql = "INSERT INTO hisdev.div_log "
|
||||||
+ "(pool_id, slot_id, queue_no, op_user_id, action, create_time) "
|
+ "(pool_id, slot_id, queue_no, op_user_id, action, create_time) "
|
||||||
+ "VALUES (?, ?, ?, ?, 'COMPLETE', NOW()::timestamp(0))";
|
+ "VALUES (?, ?, ?, ?, 30, NOW()::timestamp(0))";
|
||||||
|
// action=30 表示 COMPLETED(已完成),与 triage_queue_item.status 数字编码保持一致
|
||||||
|
// 0=WAITING, 10=CALLING, 20=IN_CLINIC, 30=COMPLETED, 40=SKIPPED, 50=REFUNDED, 60=FOLLOW
|
||||||
|
|
||||||
jdbcTemplate.update(divLogSql,
|
jdbcTemplate.update(divLogSql,
|
||||||
queueItem.getOrganizationId(), // pool_id: 候选池ID(科室)
|
queueItem.getPoolId(), // pool_id: 号源池ID(来自 adm_schedule_pool.id)
|
||||||
queueItem.getPractitionerId(), // slot_id: 槽位ID(医生)
|
queueItem.getSlotId(), // slot_id: 号源槽位ID(来自 adm_schedule_slot.id)
|
||||||
queueItem.getQueueOrder(), // queue_no: 队列号
|
queueItem.getQueueOrder(), // queue_no: 队列序号
|
||||||
userId); // op_user_id: 操作用户ID
|
userId); // op_user_id: 操作用户ID
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("写入div_log审计日志失败", e);
|
log.error("写入div_log审计日志失败", e);
|
||||||
// 审计日志失败不影响主流程
|
// 审计日志失败不影响主流程
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.openhis.common.constant.CommonConstants;
|
|||||||
import com.openhis.common.enums.*;
|
import com.openhis.common.enums.*;
|
||||||
import com.openhis.common.utils.EnumUtils;
|
import com.openhis.common.utils.EnumUtils;
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
import com.openhis.common.utils.HisQueryUtils;
|
||||||
|
import com.openhis.web.doctorstation.appservice.IDoctorStationMainAppService;
|
||||||
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
|
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
|
||||||
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
|
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
|
||||||
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
|
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
|
||||||
@@ -32,6 +33,9 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
@Resource
|
@Resource
|
||||||
private TodayOutpatientMapper todayOutpatientMapper;
|
private TodayOutpatientMapper todayOutpatientMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IDoctorStationMainAppService doctorStationMainAppService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
|
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
|
||||||
Long doctorId = SecurityUtils.getLoginUser().getUserId();
|
Long doctorId = SecurityUtils.getLoginUser().getUserId();
|
||||||
@@ -259,22 +263,19 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
@Override
|
@Override
|
||||||
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
|
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
|
||||||
// 调用现有的接诊逻辑
|
// 调用现有的接诊逻辑
|
||||||
// 这里可以复用 DoctorStationMainAppServiceImpl 中的 receiveEncounter 方法
|
return doctorStationMainAppService.receiveEncounter(encounterId);
|
||||||
// 或者直接调用相应的服务
|
|
||||||
|
|
||||||
return R.ok("接诊成功");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
|
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
|
||||||
// 调用现有的完诊逻辑
|
// 调用现有的完诊逻辑
|
||||||
return R.ok("就诊完成");
|
return doctorStationMainAppService.completeEncounter(encounterId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
|
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
|
||||||
// 调用现有的取消就诊逻辑
|
// 调用现有的取消就诊逻辑
|
||||||
return R.ok("就诊取消成功");
|
return doctorStationMainAppService.cancelEncounter(encounterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ public class DoctorStationAdviceController {
|
|||||||
@RequestParam(value = "organizationId", required = false) Long organizationId,
|
@RequestParam(value = "organizationId", required = false) Long organizationId,
|
||||||
@RequestParam(value = "adviceTypes", defaultValue = "1,2,3") List<Integer> adviceTypes,
|
@RequestParam(value = "adviceTypes", defaultValue = "1,2,3") List<Integer> adviceTypes,
|
||||||
@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) {
|
||||||
return R.ok(iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId,
|
return R.ok(iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId,
|
||||||
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, Whether.NO.getValue(), adviceTypes, null, null));
|
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, null, adviceTypes, null, categoryCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,4 +187,15 @@ public class DoctorStationAdviceController {
|
|||||||
return iDoctorStationAdviceAppService.getTestResult(encounterId);
|
return iDoctorStationAdviceAppService.getTestResult(encounterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前科室已配置的药品类别列表
|
||||||
|
*
|
||||||
|
* @param organizationId 科室id
|
||||||
|
* @return 已配置的药品类别编码列表
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/configured-categories")
|
||||||
|
public R<?> getConfiguredCategories(@RequestParam(value = "organizationId", required = false) Long organizationId) {
|
||||||
|
return iDoctorStationAdviceAppService.getConfiguredCategories(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public class DoctorStationChineseMedicalController {
|
|||||||
organizationId = SecurityUtils.getLoginUser().getOrgId();
|
organizationId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
}
|
}
|
||||||
return R.ok(iDoctorStationChineseMedicalAppService.getTcmAdviceBaseInfo(adviceBaseDto, searchKey, locationId,
|
return R.ok(iDoctorStationChineseMedicalAppService.getTcmAdviceBaseInfo(adviceBaseDto, searchKey, locationId,
|
||||||
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, Whether.NO.getValue()));
|
adviceDefinitionIdParamList, organizationId, pageNo, pageSize, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ public class AdviceBaseDto {
|
|||||||
/**
|
/**
|
||||||
* 所属科室
|
* 所属科室
|
||||||
*/
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long orgId;
|
private Long orgId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -242,4 +243,9 @@ public class AdviceBaseDto {
|
|||||||
@Dict(dictCode = "chrgitm_lv")
|
@Dict(dictCode = "chrgitm_lv")
|
||||||
private String chrgitmLv;
|
private String chrgitmLv;
|
||||||
private String chrgitmLv_dictText;
|
private String chrgitmLv_dictText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否缺少取药科室配置(仅药品类型使用)
|
||||||
|
*/
|
||||||
|
private Boolean pharmacyConfigMissing;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.openhis.web.doctorstation.dto;
|
package com.openhis.web.doctorstation.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
@@ -67,4 +69,42 @@ public class DoctorStationLabApplyItemDto {
|
|||||||
*/
|
*/
|
||||||
@NotNull(message = "行状态不能为空")
|
@NotNull(message = "行状态不能为空")
|
||||||
private Long itemStatus;
|
private Long itemStatus;
|
||||||
|
|
||||||
|
// ========== Bug #326: 套餐相关字段(回充时需要) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动定义ID(检验项目定义ID)
|
||||||
|
* 用于回充时关联到原始检验项目定义
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long activityId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套餐ID(如果该项目是套餐,则关联套餐表)
|
||||||
|
* 对应 InspectionPackage.basicInformationId
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long feePackageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是套餐项目
|
||||||
|
*/
|
||||||
|
private Boolean isPackage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 样本类型
|
||||||
|
*/
|
||||||
|
private String sampleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单位
|
||||||
|
*/
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检验类型ID(关联 inspection_type 大类)
|
||||||
|
* 用于前端自动设置执行科室
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long inspectionTypeId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,6 +442,15 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
|
|||||||
if (admissionPatientInfoDto.getPriorityEnum() != null) {
|
if (admissionPatientInfoDto.getPriorityEnum() != null) {
|
||||||
encounterService.updatePriorityEnumById(encounterId, admissionPatientInfoDto.getPriorityEnum());
|
encounterService.updatePriorityEnumById(encounterId, admissionPatientInfoDto.getPriorityEnum());
|
||||||
}
|
}
|
||||||
|
// 更新入科时间(如果提供了)
|
||||||
|
if (startTime != null) {
|
||||||
|
Encounter encounter = encounterService.getById(encounterId);
|
||||||
|
if (encounter != null) {
|
||||||
|
encounter.setStartTime(startTime);
|
||||||
|
encounterService.saveOrUpdateEncounter(encounter);
|
||||||
|
log.info("更新入科时间 - encounterId: {}, startTime: {}", encounterId, startTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 将之前的住院参与者更新为已完成(如果存在的话)
|
// 将之前的住院参与者更新为已完成(如果存在的话)
|
||||||
encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
|
encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
|
||||||
// 更新住院参与者
|
// 更新住院参与者
|
||||||
|
|||||||
@@ -350,11 +350,16 @@ public class NurseBillingAppService implements INurseBillingAppService {
|
|||||||
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
||||||
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
||||||
|
|
||||||
// 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常
|
// 2. 颞理发放库房:为locationId为null的项目设置默认值
|
||||||
if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) {
|
for (AdviceSaveDto advice : tempDeviceList) {
|
||||||
throw new ServiceException("耗材划价失败:发放库房为空,请重新选择");
|
if (advice.getLocationId() == null) {
|
||||||
|
// 设置默认位置为用户组织ID作为fallback
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null && loginUser.getOrgId() != null) {
|
||||||
|
advice.setLocationId(loginUser.getOrgId());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
||||||
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
||||||
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
||||||
@@ -665,7 +670,7 @@ public class NurseBillingAppService implements INurseBillingAppService {
|
|||||||
// 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除
|
// 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除
|
||||||
checkDeletedDeviceChargeStatus(requestIds);
|
checkDeletedDeviceChargeStatus(requestIds);
|
||||||
// 软删除执行记录
|
// 软删除执行记录
|
||||||
List<Procedure> procedureList = iProcedureService.listByIds(requestIds);
|
List<Procedure> procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable);
|
||||||
List<Long> procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象
|
List<Long> procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象
|
||||||
.map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加)
|
.map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public interface IVitalSignsAppService {
|
|||||||
*
|
*
|
||||||
* @return 体温单检索结果
|
* @return 体温单检索结果
|
||||||
*/
|
*/
|
||||||
R<?> searchVitalSigns(String startTime, String endTime);
|
R<?> searchVitalSigns(String startTime, String endTime, String patientId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 体温单记录删除
|
* 体温单记录删除
|
||||||
|
|||||||
@@ -73,9 +73,21 @@ public class VitalSignsAppServiceImpl implements IVitalSignsAppService {
|
|||||||
|
|
||||||
VitalSignsMedicalRecordDto medicalRecord = new VitalSignsMedicalRecordDto();
|
VitalSignsMedicalRecordDto medicalRecord = new VitalSignsMedicalRecordDto();
|
||||||
|
|
||||||
// 处理日期
|
// 处理出院日期
|
||||||
if (!vitalSignsInfoPage.getRecords().isEmpty()) {
|
if (!vitalSignsInfoPage.getRecords().isEmpty()) {
|
||||||
medicalRecord.setHospDate(vitalSignsInfoPage.getRecords().get(0).getRecordingDate());
|
// 从第一条记录获取出院日期(如果存在)
|
||||||
|
Date dischargeDate = vitalSignsInfoPage.getRecords().get(0).getDischargeDate();
|
||||||
|
if (dischargeDate != null) {
|
||||||
|
medicalRecord.setOutdate(TimeUtils.dateToDateString(dischargeDate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理住院日期:优先使用第一条记录中的入院日期,如果没有则保持 null 让前端 fallback
|
||||||
|
if (!vitalSignsInfoPage.getRecords().isEmpty()) {
|
||||||
|
Date admissionDate = vitalSignsInfoPage.getRecords().get(0).getAdmissionDate();
|
||||||
|
if (admissionDate != null) {
|
||||||
|
medicalRecord.setHospDate(admissionDate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理生命体征数据
|
// 处理生命体征数据
|
||||||
@@ -266,13 +278,14 @@ public class VitalSignsAppServiceImpl implements IVitalSignsAppService {
|
|||||||
*
|
*
|
||||||
* @param startTime 开始时间
|
* @param startTime 开始时间
|
||||||
* @param endTime 结束时间
|
* @param endTime 结束时间
|
||||||
|
* @param patientId 患者ID
|
||||||
* @return 检索结果
|
* @return 检索结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R<?> searchVitalSigns(String startTime, String endTime) {
|
public R<?> searchVitalSigns(String startTime, String endTime, String patientId) {
|
||||||
|
|
||||||
// 基本信息查询
|
// 基本信息查询
|
||||||
List<VitalSigns> vitalSignsList = vitalSignsAppMapper.searchVitalSigns(startTime, endTime);
|
List<VitalSigns> vitalSignsList = vitalSignsAppMapper.searchVitalSigns(startTime, endTime, patientId);
|
||||||
// 判断查询结果是否为空
|
// 判断查询结果是否为空
|
||||||
if (vitalSignsList.isEmpty()) {
|
if (vitalSignsList.isEmpty()) {
|
||||||
return R.ok(Collections.emptyList());
|
return R.ok(Collections.emptyList());
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ public class VitalSignsController {
|
|||||||
* @return 体温单检索结果
|
* @return 体温单检索结果
|
||||||
*/
|
*/
|
||||||
@GetMapping("/record-search")
|
@GetMapping("/record-search")
|
||||||
public R<?> searchVitalSigns(String startTime, String endTime) {
|
public R<?> searchVitalSigns(String startTime, String endTime, String patientId) {
|
||||||
return R.ok(vitalSignsAppService.searchVitalSigns(startTime, endTime));
|
return R.ok(vitalSignsAppService.searchVitalSigns(startTime, endTime, patientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openhis.web.inpatientmanage.dto;
|
package com.openhis.web.inpatientmanage.dto;
|
||||||
|
|
||||||
import com.core.common.core.domain.HisBaseEntity;
|
import com.core.common.core.domain.HisBaseEntity;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -35,6 +36,7 @@ public class VitalSignsSaveDto extends HisBaseEntity {
|
|||||||
/**
|
/**
|
||||||
* 记录日期
|
* 记录日期
|
||||||
*/
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date recordingDate;
|
private Date recordingDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ public interface VitalSignsAppMapper {
|
|||||||
*
|
*
|
||||||
* @param startTime 开始时间
|
* @param startTime 开始时间
|
||||||
* @param endTime 结束时间
|
* @param endTime 结束时间
|
||||||
|
* @param patientId 患者ID
|
||||||
* @return 查询记录结果
|
* @return 查询记录结果
|
||||||
*/
|
*/
|
||||||
List<VitalSigns> searchVitalSigns(@Param("startTime") String startTime, @Param("endTime") String endTime);
|
List<VitalSigns> searchVitalSigns(@Param("startTime") String startTime, @Param("endTime") String endTime, @Param("patientId") String patientId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除记录
|
* 删除记录
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -68,6 +69,17 @@ public class LabActivityDefinitionAppServiceImpl implements ILabActivityDefiniti
|
|||||||
selParam.setPricingFlag(null);
|
selParam.setPricingFlag(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bug #414: 限制分页大小,防止一次性加载过多数据导致性能问题
|
||||||
|
if (pageSize == null || pageSize <= 0) {
|
||||||
|
pageSize = 20;
|
||||||
|
}
|
||||||
|
if (pageSize > 50) {
|
||||||
|
pageSize = 50;
|
||||||
|
}
|
||||||
|
if (pageNo == null || pageNo <= 0) {
|
||||||
|
pageNo = 1;
|
||||||
|
}
|
||||||
|
|
||||||
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = HisQueryUtils.buildQueryWrapper(selParam,
|
QueryWrapper<DiagnosisTreatmentDto> queryWrapper = HisQueryUtils.buildQueryWrapper(selParam,
|
||||||
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);
|
||||||
|
|
||||||
@@ -80,10 +92,19 @@ public class LabActivityDefinitionAppServiceImpl implements ILabActivityDefiniti
|
|||||||
selParam.setPricingFlag(pricingFlagValue);
|
selParam.setPricingFlag(pricingFlagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
IPage<DiagnosisTreatmentDto> page = labActivityDefinitionManageMapper
|
// Bug #414: 使用optimizeCountSql=true优化COUNT查询性能
|
||||||
.getLabActivityDefinitionPage(new Page<>(pageNo, pageSize), queryWrapper);
|
Page<DiagnosisTreatmentDto> page = new Page<>(pageNo, pageSize, true);
|
||||||
|
IPage<DiagnosisTreatmentDto> resultPage = labActivityDefinitionManageMapper
|
||||||
|
.getLabActivityDefinitionPage(page, queryWrapper);
|
||||||
|
|
||||||
page.getRecords().forEach(e -> {
|
resultPage.getRecords().forEach(e -> {
|
||||||
|
// Bug #415: 确保价格不为负数
|
||||||
|
if (e.getPackageAmount() != null && e.getPackageAmount().compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
e.setPackageAmount(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
if (e.getServiceFee() != null && e.getServiceFee().compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
e.setServiceFee(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
e.setYbFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getYbFlag()));
|
e.setYbFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getYbFlag()));
|
||||||
e.setYbMatchFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getYbMatchFlag()));
|
e.setYbMatchFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getYbMatchFlag()));
|
||||||
e.setTypeEnum_enumText(EnumUtils.getInfoByValue(ActivityType.class, e.getTypeEnum()));
|
e.setTypeEnum_enumText(EnumUtils.getInfoByValue(ActivityType.class, e.getTypeEnum()));
|
||||||
@@ -91,13 +112,22 @@ public class LabActivityDefinitionAppServiceImpl implements ILabActivityDefiniti
|
|||||||
e.setPricingFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getPricingFlag()));
|
e.setPricingFlag_enumText(EnumUtils.getInfoByValue(Whether.class, e.getPricingFlag()));
|
||||||
});
|
});
|
||||||
|
|
||||||
return R.ok(page);
|
return R.ok(resultPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> getLabActivityDefinitionOne(Long id) {
|
public R<?> getLabActivityDefinitionOne(Long id) {
|
||||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
DiagnosisTreatmentDto dto = labActivityDefinitionManageMapper.getLabActivityDefinitionOne(id, tenantId);
|
DiagnosisTreatmentDto dto = labActivityDefinitionManageMapper.getLabActivityDefinitionOne(id, tenantId);
|
||||||
|
// Bug #415: 确保价格不为负数
|
||||||
|
if (dto != null) {
|
||||||
|
if (dto.getPackageAmount() != null && dto.getPackageAmount().compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
dto.setPackageAmount(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
if (dto.getServiceFee() != null && dto.getServiceFee().compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
dto.setServiceFee(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
return R.ok(dto);
|
return R.ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ import com.openhis.web.paymentmanage.appservice.IChargeBillService;
|
|||||||
import com.openhis.web.paymentmanage.dto.*;
|
import com.openhis.web.paymentmanage.dto.*;
|
||||||
import com.openhis.web.paymentmanage.mapper.ChargeBillMapper;
|
import com.openhis.web.paymentmanage.mapper.ChargeBillMapper;
|
||||||
import com.openhis.workflow.domain.ActivityDefinition;
|
import com.openhis.workflow.domain.ActivityDefinition;
|
||||||
|
import com.openhis.workflow.domain.ServiceRequest;
|
||||||
import com.openhis.workflow.service.IActivityDefinitionService;
|
import com.openhis.workflow.service.IActivityDefinitionService;
|
||||||
|
import com.openhis.workflow.service.IServiceRequestService;
|
||||||
import com.openhis.yb.domain.ClinicSettle;
|
import com.openhis.yb.domain.ClinicSettle;
|
||||||
import com.openhis.yb.domain.ClinicUnSettle;
|
import com.openhis.yb.domain.ClinicUnSettle;
|
||||||
import com.openhis.yb.domain.InfoPerson;
|
import com.openhis.yb.domain.InfoPerson;
|
||||||
@@ -111,6 +113,8 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IActivityDefinitionService iActivityDefinitionService;
|
private IActivityDefinitionService iActivityDefinitionService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private IServiceRequestService iServiceRequestService;
|
||||||
|
@Autowired
|
||||||
private IPractitionerService iPractitionerService;
|
private IPractitionerService iPractitionerService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IHealthcareServiceService iHealthcareServiceService;
|
private IHealthcareServiceService iHealthcareServiceService;
|
||||||
@@ -265,10 +269,31 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
|||||||
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
|
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
|
||||||
.setTotalVolume(device.getSize()).setQuantityValue(chargeItem.getQuantityValue());
|
.setTotalVolume(device.getSize()).setQuantityValue(chargeItem.getQuantityValue());
|
||||||
} else if (CommonConstants.TableName.WOR_ACTIVITY_DEFINITION.equals(chargeItem.getProductTable())) {
|
} else if (CommonConstants.TableName.WOR_ACTIVITY_DEFINITION.equals(chargeItem.getProductTable())) {
|
||||||
ActivityDefinition activity = iActivityDefinitionService.getById(chargeItem.getProductId());
|
// 🔧 BugFix#385: 检查申请创建的收费项 productId=0,需从 ServiceRequest.contentJson 获取项目名称
|
||||||
chargeItemDetailVO.setDirClass(activity.getChrgitmLv() + "").setChargeItemName(activity.getName())
|
if (chargeItem.getProductId() != null && chargeItem.getProductId() > 0) {
|
||||||
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
|
ActivityDefinition activity = iActivityDefinitionService.getById(chargeItem.getProductId());
|
||||||
.setTotalVolume("").setQuantityValue(chargeItem.getQuantityValue());
|
chargeItemDetailVO.setDirClass(activity.getChrgitmLv() + "").setChargeItemName(activity.getName())
|
||||||
|
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
|
||||||
|
.setTotalVolume("").setQuantityValue(chargeItem.getQuantityValue());
|
||||||
|
} else {
|
||||||
|
// productId=0 时,从关联的 ServiceRequest 获取项目名称
|
||||||
|
ServiceRequest serviceRequest = iServiceRequestService.getById(chargeItem.getServiceId());
|
||||||
|
String itemName = "未知项目";
|
||||||
|
String dirClass = "3"; // 默认诊疗类
|
||||||
|
if (serviceRequest != null && serviceRequest.getContentJson() != null) {
|
||||||
|
try {
|
||||||
|
JSONObject json = JSON.parseObject(serviceRequest.getContentJson());
|
||||||
|
if (json.containsKey("adviceName")) {
|
||||||
|
itemName = json.getString("adviceName");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析ServiceRequest.contentJson失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chargeItemDetailVO.setDirClass(dirClass).setChargeItemName(itemName)
|
||||||
|
.setTotalPrice(chargeItem.getTotalPrice()).setQuantityUnit(chargeItem.getQuantityUnit())
|
||||||
|
.setTotalVolume("").setQuantityValue(chargeItem.getQuantityValue());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
HealthcareService healthcareService = iHealthcareServiceService.getById(chargeItem.getServiceId());
|
HealthcareService healthcareService = iHealthcareServiceService.getById(chargeItem.getServiceId());
|
||||||
chargeItemDetailVO.setDirClass("3").setChargeItemName(healthcareService.getName())
|
chargeItemDetailVO.setDirClass("3").setChargeItemName(healthcareService.getName())
|
||||||
@@ -347,7 +372,19 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
|||||||
|
|
||||||
Long definitionId = chargeItem.getDefinitionId();
|
Long definitionId = chargeItem.getDefinitionId();
|
||||||
|
|
||||||
ChargeItemDefinition chargeItemDefinition = iChargeItemDefinitionService.getById(definitionId);
|
// 🔧 BugFix#385: 检查申请创建的收费项 definition_id=0,chargeItemDefinition 会为 null
|
||||||
|
ChargeItemDefinition chargeItemDefinition = null;
|
||||||
|
if (definitionId != null && definitionId > 0) {
|
||||||
|
chargeItemDefinition = iChargeItemDefinitionService.getById(definitionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 definitionId=0 或 chargeItemDefinition 为 null 时,跳过医保分类统计
|
||||||
|
// 检查类项目默认归类为"检查费"
|
||||||
|
if (chargeItemDefinition == null) {
|
||||||
|
// 检查申请的收费项,归类为检查费(03)
|
||||||
|
sum03 = sum03.add(chargeItem.getTotalPrice());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
YbMedChrgItmType medChrgItmType
|
YbMedChrgItmType medChrgItmType
|
||||||
= YbMedChrgItmType.getByCode(Integer.parseInt(chargeItemDefinition.getYbType()));
|
= YbMedChrgItmType.getByCode(Integer.parseInt(chargeItemDefinition.getYbType()));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package com.openhis.web.paymentmanage.appservice.impl;
|
|||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.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;
|
||||||
@@ -57,6 +58,12 @@ import com.openhis.web.paymentmanage.mapper.PaymentMapper;
|
|||||||
import com.openhis.web.personalization.dto.ActivityDeviceDto;
|
import com.openhis.web.personalization.dto.ActivityDeviceDto;
|
||||||
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||||
|
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||||
|
import com.openhis.appointmentmanage.domain.SchedulePool;
|
||||||
|
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||||
|
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
|
||||||
|
import com.openhis.clinical.domain.Order;
|
||||||
|
import com.openhis.clinical.service.IOrderService;
|
||||||
import com.openhis.workflow.domain.ServiceRequest;
|
import com.openhis.workflow.domain.ServiceRequest;
|
||||||
import com.openhis.workflow.service.IDeviceDispenseService;
|
import com.openhis.workflow.service.IDeviceDispenseService;
|
||||||
import com.openhis.workflow.service.IDeviceRequestService;
|
import com.openhis.workflow.service.IDeviceRequestService;
|
||||||
@@ -70,6 +77,7 @@ import com.openhis.yb.service.IClinicSettleService;
|
|||||||
import com.openhis.yb.service.IInpatientSettleService;
|
import com.openhis.yb.service.IInpatientSettleService;
|
||||||
import com.openhis.yb.service.IRegService;
|
import com.openhis.yb.service.IRegService;
|
||||||
import com.openhis.yb.service.YbManager;
|
import com.openhis.yb.service.YbManager;
|
||||||
|
import com.openhis.web.triageandqueuemanage.appservice.impl.TriageQueueAppServiceImpl;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.poi.util.StringUtil;
|
import org.apache.poi.util.StringUtil;
|
||||||
@@ -186,6 +194,12 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
|||||||
private YbManager ybManager;
|
private YbManager ybManager;
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisCache redisCache;
|
private RedisCache redisCache;
|
||||||
|
@Autowired
|
||||||
|
private IOrderService iOrderService;
|
||||||
|
@Autowired
|
||||||
|
private ScheduleSlotMapper scheduleSlotMapper;
|
||||||
|
@Autowired
|
||||||
|
private SchedulePoolMapper schedulePoolMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 【门诊预结算】
|
* 【门诊预结算】
|
||||||
@@ -232,14 +246,21 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
// account去重
|
// account去重
|
||||||
List<Long> distinctAccountIdList = accountIdList.stream().distinct().collect(Collectors.toList());
|
List<Long> distinctAccountIdList = accountIdList.stream().distinct().collect(Collectors.toList());
|
||||||
// 检查是否存在accountId为null的收费项
|
// 检查是否存在accountId为null或0的收费项
|
||||||
long nullAccountIdCount = chargeItemList.stream()
|
long nullAccountIdCount = chargeItemList.stream()
|
||||||
.map(ChargeItem::getAccountId)
|
.map(ChargeItem::getAccountId)
|
||||||
.filter(Objects::isNull)
|
.filter(Objects::isNull)
|
||||||
.count();
|
.count();
|
||||||
|
long zeroAccountIdCount = chargeItemList.stream()
|
||||||
|
.map(ChargeItem::getAccountId)
|
||||||
|
.filter(id -> id != null && id == 0L)
|
||||||
|
.count();
|
||||||
if (nullAccountIdCount > 0) {
|
if (nullAccountIdCount > 0) {
|
||||||
throw new ServiceException("部分收费项缺少账户信息,请检查收费项数据");
|
throw new ServiceException("部分收费项缺少账户信息,请检查收费项数据");
|
||||||
}
|
}
|
||||||
|
if (zeroAccountIdCount > 0) {
|
||||||
|
throw new ServiceException("部分收费项账户ID为0(无效),请检查收费项数据或重新创建检查申请");
|
||||||
|
}
|
||||||
if (distinctAccountIdList.isEmpty()) {
|
if (distinctAccountIdList.isEmpty()) {
|
||||||
throw new ServiceException("未找到有效的账户信息");
|
throw new ServiceException("未找到有效的账户信息");
|
||||||
}
|
}
|
||||||
@@ -248,15 +269,66 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
|||||||
// 在挂号费等场景下可能因数据不一致导致查不到,引发误报
|
// 在挂号费等场景下可能因数据不一致导致查不到,引发误报
|
||||||
List<Account> accountList = iAccountService.list(new LambdaQueryWrapper<Account>()
|
List<Account> accountList = iAccountService.list(new LambdaQueryWrapper<Account>()
|
||||||
.in(Account::getId, distinctAccountIdList));
|
.in(Account::getId, distinctAccountIdList));
|
||||||
|
|
||||||
|
// 🔧 Bug Fix: 处理账户不存在的情况(历史数据修复)
|
||||||
if (accountList.size() != distinctAccountIdList.size()) {
|
if (accountList.size() != distinctAccountIdList.size()) {
|
||||||
// 部分账户查不到时,记录警告日志,并校验是否每个收费项都能找到对应账户
|
|
||||||
Set<Long> foundAccountIds = accountList.stream().map(Account::getId).collect(Collectors.toSet());
|
Set<Long> foundAccountIds = accountList.stream().map(Account::getId).collect(Collectors.toSet());
|
||||||
List<Long> missingAccountIds = distinctAccountIdList.stream()
|
List<Long> missingAccountIds = distinctAccountIdList.stream()
|
||||||
.filter(id -> !foundAccountIds.contains(id)).collect(Collectors.toList());
|
.filter(id -> !foundAccountIds.contains(id)).collect(Collectors.toList());
|
||||||
if (accountList.isEmpty()) {
|
|
||||||
throw new ServiceException("未查询到任何账户信息,encounterId:" + prePaymentDto.getEncounterId()
|
logger.warn("预结算发现部分账户不存在,missingAccountIds={},将自动修复收费项的accountId",
|
||||||
+ ",期望accountId列表:" + distinctAccountIdList);
|
missingAccountIds);
|
||||||
|
|
||||||
|
// 获取或创建有效的自费账户
|
||||||
|
Account selfAccount = iAccountService.getSelfAccount(prePaymentDto.getEncounterId());
|
||||||
|
if (selfAccount == null) {
|
||||||
|
// 自动创建自费账户
|
||||||
|
Account newAccount = new Account();
|
||||||
|
newAccount.setPatientId(chargeItemList.get(0).getPatientId());
|
||||||
|
newAccount.setEncounterId(prePaymentDto.getEncounterId());
|
||||||
|
newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO);
|
||||||
|
newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode());
|
||||||
|
newAccount.setBalanceAmount(BigDecimal.ZERO);
|
||||||
|
newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue());
|
||||||
|
newAccount.setEncounterFlag(Whether.YES.getValue());
|
||||||
|
newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo());
|
||||||
|
Long newAccountId = iAccountService.saveAccountByRegister(newAccount);
|
||||||
|
selfAccount = iAccountService.getById(newAccountId);
|
||||||
|
logger.info("预结算自动创建自费账户,newAccountId={}", newAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修复收费项的 accountId
|
||||||
|
for (Long missingAccountId : missingAccountIds) {
|
||||||
|
// 找到使用该无效 accountId 的收费项
|
||||||
|
List<ChargeItem> affectedChargeItems = chargeItemList.stream()
|
||||||
|
.filter(ci -> ci.getAccountId() != null && ci.getAccountId().equals(missingAccountId))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (ChargeItem ci : affectedChargeItems) {
|
||||||
|
LambdaUpdateWrapper<ChargeItem> updateWrapper = new LambdaUpdateWrapper<>();
|
||||||
|
updateWrapper.eq(ChargeItem::getId, ci.getId())
|
||||||
|
.set(ChargeItem::getAccountId, selfAccount.getId());
|
||||||
|
iChargeItemService.update(updateWrapper);
|
||||||
|
logger.info("预结算修复收费项accountId,chargeItemId={},oldAccountId={},newAccountId={}",
|
||||||
|
ci.getId(), missingAccountId, selfAccount.getId());
|
||||||
|
|
||||||
|
// 更新本地对象的 accountId,以便后续处理使用正确的值
|
||||||
|
ci.setAccountId(selfAccount.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新查询账户列表
|
||||||
|
accountList = iAccountService.list(new LambdaQueryWrapper<Account>()
|
||||||
|
.eq(Account::getId, selfAccount.getId()));
|
||||||
|
|
||||||
|
// 重新构建 accountIdList(已修复)
|
||||||
|
distinctAccountIdList = chargeItemList.stream()
|
||||||
|
.map(ChargeItem::getAccountId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
logger.info("预结算账户修复完成,最终使用accountId={}", selfAccount.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 账户id,对应的账单列表
|
// 账户id,对应的账单列表
|
||||||
@@ -1914,6 +1986,60 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
|||||||
|
|
||||||
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
|
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||||
|
|
||||||
|
// Bug #409:预约签到挂号时,修正 serviceTypeId 为预约类型而非挂号类型
|
||||||
|
// 数据链:Order → ScheduleSlot → SchedulePool → HealthcareService
|
||||||
|
try {
|
||||||
|
Order appointmentOrder = iOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<Order>()
|
||||||
|
.eq(Order::getPatientId, encounterFormData.getPatientId())
|
||||||
|
.eq(Order::getStatus, CommonConstants.AppointmentOrderStatus.CHECKED_IN)
|
||||||
|
.eq(Order::getDeleteFlag, "0")
|
||||||
|
.orderByDesc(Order::getCreateTime)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (appointmentOrder != null && appointmentOrder.getSlotId() != null) {
|
||||||
|
ScheduleSlot slot = scheduleSlotMapper.selectById(appointmentOrder.getSlotId());
|
||||||
|
if (slot != null && slot.getPoolId() != null) {
|
||||||
|
SchedulePool pool = schedulePoolMapper.selectById(slot.getPoolId());
|
||||||
|
if (pool != null && pool.getRegType() != null) {
|
||||||
|
// pool.getRegType() 存储号别名称如"普通号-预约",精确匹配 HealthcareService
|
||||||
|
HealthcareService appointmentHealthcareService = healthcareServiceService.getOne(
|
||||||
|
new LambdaQueryWrapper<HealthcareService>()
|
||||||
|
.eq(HealthcareService::getOfferedOrgId, encounterFormData.getOrganizationId())
|
||||||
|
.eq(HealthcareService::getDeleteFlag, "0")
|
||||||
|
.eq(HealthcareService::getName, pool.getRegType())
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (appointmentHealthcareService != null) {
|
||||||
|
encounterFormData.setServiceTypeId(appointmentHealthcareService.getId());
|
||||||
|
encounterFormData.setOrderId(appointmentOrder.getId());
|
||||||
|
logger.info("预约签到挂号修正serviceTypeId={},号别={}",
|
||||||
|
appointmentHealthcareService.getId(), pool.getRegType());
|
||||||
|
} else {
|
||||||
|
// 精确匹配失败,尝试 LIKE 匹配
|
||||||
|
appointmentHealthcareService = healthcareServiceService.getOne(
|
||||||
|
new LambdaQueryWrapper<HealthcareService>()
|
||||||
|
.eq(HealthcareService::getOfferedOrgId, encounterFormData.getOrganizationId())
|
||||||
|
.eq(HealthcareService::getDeleteFlag, "0")
|
||||||
|
.like(HealthcareService::getName, pool.getRegType())
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
if (appointmentHealthcareService != null) {
|
||||||
|
encounterFormData.setServiceTypeId(appointmentHealthcareService.getId());
|
||||||
|
encounterFormData.setOrderId(appointmentOrder.getId());
|
||||||
|
logger.info("预约签到挂号(LIKE)serviceTypeId={},号别={}",
|
||||||
|
appointmentHealthcareService.getId(), pool.getRegType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("修正serviceTypeId失败,不影响挂号流程", e);
|
||||||
|
}
|
||||||
|
|
||||||
// 保存就诊信息
|
// 保存就诊信息
|
||||||
Encounter encounter = new Encounter();
|
Encounter encounter = new Encounter();
|
||||||
BeanUtils.copyProperties(encounterFormData, encounter);
|
BeanUtils.copyProperties(encounterFormData, encounter);
|
||||||
@@ -1985,6 +2111,31 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建队列项
|
// 创建队列项
|
||||||
|
// 尝试获取预约订单的 slot_id 和 pool_id
|
||||||
|
Long queuePoolId = null;
|
||||||
|
Long queueSlotId = null;
|
||||||
|
try {
|
||||||
|
// 查询患者当天的待签到预约订单(status = 1 或 2 表示已预约或已取号)
|
||||||
|
Order order = iOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<Order>()
|
||||||
|
.eq(Order::getPatientId, encounter.getPatientId())
|
||||||
|
.in(Order::getStatus, 1, 2) // 1=BOOKED 已预约, 2=CHECKED_IN 已取号
|
||||||
|
.eq(Order::getDeleteFlag, "0")
|
||||||
|
.orderByDesc(Order::getCreateTime)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
if (order != null && order.getSlotId() != null) {
|
||||||
|
queueSlotId = order.getSlotId();
|
||||||
|
// 通过 slot_id 获取 pool_id
|
||||||
|
ScheduleSlot slot = scheduleSlotMapper.selectById(queueSlotId);
|
||||||
|
if (slot != null) {
|
||||||
|
queuePoolId = slot.getPoolId();
|
||||||
|
}
|
||||||
|
logger.info("挂号时找到预约订单,slotId={}, poolId={}, encounterId={}", queueSlotId, queuePoolId, encounterId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("查询预约订单失败,不影响挂号流程,encounterId={}", encounterId, e);
|
||||||
|
}
|
||||||
TriageQueueItem queueItem = new TriageQueueItem()
|
TriageQueueItem queueItem = new TriageQueueItem()
|
||||||
.setTenantId(tenantId)
|
.setTenantId(tenantId)
|
||||||
.setQueueDate(queueDate)
|
.setQueueDate(queueDate)
|
||||||
@@ -1997,7 +2148,9 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
|||||||
.setPractitionerId(queuePractitionerId)
|
.setPractitionerId(queuePractitionerId)
|
||||||
.setPractitionerName(practitionerName)
|
.setPractitionerName(practitionerName)
|
||||||
.setRoomNo(null)
|
.setRoomNo(null)
|
||||||
.setStatus("WAITING")
|
.setPoolId(queuePoolId)
|
||||||
|
.setSlotId(queueSlotId)
|
||||||
|
.setStatus(TriageQueueAppServiceImpl.STATUS_WAITING) // 0=WAITING(等待中)
|
||||||
.setQueueOrder(maxOrder + 1)
|
.setQueueOrder(maxOrder + 1)
|
||||||
.setDeleteFlag("0")
|
.setDeleteFlag("0")
|
||||||
.setCreateTime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS))
|
.setCreateTime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS))
|
||||||
|
|||||||
@@ -88,5 +88,10 @@ public class OrdersGroupPackageDetailQueryDto {
|
|||||||
*/
|
*/
|
||||||
private Long groupId;
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 治疗类型:1-长期 2-临时
|
||||||
|
*/
|
||||||
|
private Integer therapyEnum;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,9 +180,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
|||||||
// 药品
|
// 药品
|
||||||
List<RegAdviceSaveDto> medicineList = regAdviceSaveList.stream()
|
List<RegAdviceSaveDto> medicineList = regAdviceSaveList.stream()
|
||||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||||
// 诊疗活动
|
// 诊疗活动(包含护理adviceType=26)
|
||||||
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
|
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
|
||||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||||
|
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||||
|
.collect(Collectors.toList());
|
||||||
// 耗材 🔧 Bug #147 修复
|
// 耗材 🔧 Bug #147 修复
|
||||||
List<RegAdviceSaveDto> deviceList = regAdviceSaveList.stream()
|
List<RegAdviceSaveDto> deviceList = regAdviceSaveList.stream()
|
||||||
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.DEVICE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||||
@@ -844,9 +846,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
|||||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||||
List<Long> medicineRequestIds
|
List<Long> medicineRequestIds
|
||||||
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||||
// 诊疗
|
// 诊疗(包含护理adviceType=26)
|
||||||
List<AdviceBatchOpParam> activityList = paramList.stream()
|
List<AdviceBatchOpParam> activityList = paramList.stream()
|
||||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||||
|
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||||
|
.collect(Collectors.toList());
|
||||||
List<Long> activityRequestIds
|
List<Long> activityRequestIds
|
||||||
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||||
// 查询已完成的药品请求
|
// 查询已完成的药品请求
|
||||||
@@ -902,9 +906,11 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
|||||||
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
||||||
List<Long> medicineRequestIds
|
List<Long> medicineRequestIds
|
||||||
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
= medicineList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||||
// 诊疗
|
// 诊疗(包含护理adviceType=26)
|
||||||
List<AdviceBatchOpParam> activityList = paramList.stream()
|
List<AdviceBatchOpParam> activityList = paramList.stream()
|
||||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())).collect(Collectors.toList());
|
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||||
|
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||||
|
.collect(Collectors.toList());
|
||||||
List<Long> activityRequestIds
|
List<Long> activityRequestIds
|
||||||
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
= activityList.stream().map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
||||||
if (!medicineRequestIds.isEmpty()) {
|
if (!medicineRequestIds.isEmpty()) {
|
||||||
|
|||||||
@@ -93,6 +93,15 @@ public interface IInfectiousCardAppService {
|
|||||||
*
|
*
|
||||||
* @return 科室树数据
|
* @return 科室树数据
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销审核传染病报卡
|
||||||
|
*
|
||||||
|
* @param cardNo 报卡编号
|
||||||
|
* @param status 撤销后的状态
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R<?> revokeAudit(String cardNo, String status);
|
||||||
R<?> getDeptTree();
|
R<?> getDeptTree();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -276,6 +276,38 @@ public class InfectiousCardAppServiceImpl implements IInfectiousCardAppService {
|
|||||||
throw new RuntimeException("导出失败:" + e.getMessage());
|
throw new RuntimeException("导出失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 撤销审核传染病报卡
|
||||||
|
*
|
||||||
|
* @param cardNo 报卡编号
|
||||||
|
* @param status 撤销后的状态
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public R<?> revokeAudit(String cardNo, String status) {
|
||||||
|
try {
|
||||||
|
// 验证参数
|
||||||
|
if (cardNo == null || cardNo.trim().isEmpty()) {
|
||||||
|
return R.fail("报卡编号不能为空");
|
||||||
|
}
|
||||||
|
if (status == null || status.trim().isEmpty()) {
|
||||||
|
return R.fail("撤销后的状态不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行撤销审核操作
|
||||||
|
int rows = reportManageCardMapper.revokeAuditCard(cardNo, Integer.parseInt(status));
|
||||||
|
|
||||||
|
if (rows > 0) {
|
||||||
|
return R.ok("撤销审核成功");
|
||||||
|
} else {
|
||||||
|
return R.fail("撤销审核失败:未找到对应的报卡");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("撤销审核传染病报卡失败", e);
|
||||||
|
return R.fail("撤销审核失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSV 字段转义
|
* CSV 字段转义
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import com.openhis.web.reportManagement.dto.AuditInfectiousCardRequest;
|
|||||||
import com.openhis.web.reportManagement.dto.ReturnInfectiousCardRequest;
|
import com.openhis.web.reportManagement.dto.ReturnInfectiousCardRequest;
|
||||||
import com.openhis.web.reportManagement.dto.BatchAuditInfectiousCardRequest;
|
import com.openhis.web.reportManagement.dto.BatchAuditInfectiousCardRequest;
|
||||||
import com.openhis.web.reportManagement.dto.BatchReturnInfectiousCardRequest;
|
import com.openhis.web.reportManagement.dto.BatchReturnInfectiousCardRequest;
|
||||||
|
import com.openhis.web.reportManagement.dto.RevokeAuditInfectiousCardRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
// import java.util.List; // 批量操作功能暂未实现
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 传染病报卡管理 Controller
|
* 传染病报卡管理 Controller
|
||||||
@@ -31,11 +31,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询传染病报卡列表
|
* 分页查询传染病报卡列表
|
||||||
*
|
|
||||||
* @param param 查询参数
|
|
||||||
* @param pageNo 当前页码
|
|
||||||
* @param pageSize 每页数量
|
|
||||||
* @return 传染病报卡列表
|
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list-page")
|
@GetMapping("/list-page")
|
||||||
public R<?> listPage(InfectiousCardParam param,
|
public R<?> listPage(InfectiousCardParam param,
|
||||||
@@ -46,9 +41,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 ID 查询传染病报卡详情
|
* 根据 ID 查询传染病报卡详情
|
||||||
*
|
|
||||||
* @param id 报卡 ID
|
|
||||||
* @return 传染病报卡详情
|
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public R<?> getById(@PathVariable Long id) {
|
public R<?> getById(@PathVariable Long id) {
|
||||||
@@ -57,9 +49,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据卡号查询传染病报卡详情
|
* 根据卡号查询传染病报卡详情
|
||||||
*
|
|
||||||
* @param cardNo 报卡编号
|
|
||||||
* @return 传染病报卡详情
|
|
||||||
*/
|
*/
|
||||||
@GetMapping("/detail/{cardNo}")
|
@GetMapping("/detail/{cardNo}")
|
||||||
public R<?> getByCardNo(@PathVariable String cardNo) {
|
public R<?> getByCardNo(@PathVariable String cardNo) {
|
||||||
@@ -68,9 +57,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 审核传染病报卡
|
* 审核传染病报卡
|
||||||
*
|
|
||||||
* @param request 审核请求
|
|
||||||
* @return 结果
|
|
||||||
*/
|
*/
|
||||||
@PostMapping("/audit")
|
@PostMapping("/audit")
|
||||||
public R<?> audit(@RequestBody AuditInfectiousCardRequest request) {
|
public R<?> audit(@RequestBody AuditInfectiousCardRequest request) {
|
||||||
@@ -79,9 +65,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 退回传染病报卡
|
* 退回传染病报卡
|
||||||
*
|
|
||||||
* @param request 退回请求
|
|
||||||
* @return 结果
|
|
||||||
*/
|
*/
|
||||||
@PostMapping("/return")
|
@PostMapping("/return")
|
||||||
public R<?> returnCard(@Valid @RequestBody ReturnInfectiousCardRequest request) {
|
public R<?> returnCard(@Valid @RequestBody ReturnInfectiousCardRequest request) {
|
||||||
@@ -90,9 +73,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量审核传染病报卡
|
* 批量审核传染病报卡
|
||||||
*
|
|
||||||
* @param request 批量审核请求
|
|
||||||
* @return 结果
|
|
||||||
*/
|
*/
|
||||||
@PostMapping("/batchAudit")
|
@PostMapping("/batchAudit")
|
||||||
public R<?> batchAudit(@RequestBody BatchAuditInfectiousCardRequest request) {
|
public R<?> batchAudit(@RequestBody BatchAuditInfectiousCardRequest request) {
|
||||||
@@ -101,9 +81,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量退回传染病报卡
|
* 批量退回传染病报卡
|
||||||
*
|
|
||||||
* @param request 批量退回请求
|
|
||||||
* @return 结果
|
|
||||||
*/
|
*/
|
||||||
@PostMapping("/batchReturn")
|
@PostMapping("/batchReturn")
|
||||||
public R<?> batchReturn(@Valid @RequestBody BatchReturnInfectiousCardRequest request) {
|
public R<?> batchReturn(@Valid @RequestBody BatchReturnInfectiousCardRequest request) {
|
||||||
@@ -111,10 +88,18 @@ public class reportManagementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出传染病报卡
|
* 撤销审核传染病报卡(Bug #395)
|
||||||
*
|
*
|
||||||
* @param param 查询参数
|
* @param request 撤销审核请求
|
||||||
* @param response 响应对象
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/revokeAudit")
|
||||||
|
public R<?> revokeAudit(@Valid @RequestBody RevokeAuditInfectiousCardRequest request) {
|
||||||
|
return infectiousCardAppService.revokeAudit(request.getCardNo(), request.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出传染病报卡
|
||||||
*/
|
*/
|
||||||
@GetMapping("/export")
|
@GetMapping("/export")
|
||||||
public void export(InfectiousCardParam param, HttpServletResponse response) {
|
public void export(InfectiousCardParam param, HttpServletResponse response) {
|
||||||
@@ -123,8 +108,6 @@ public class reportManagementController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取科室树
|
* 获取科室树
|
||||||
*
|
|
||||||
* @return 科室树数据
|
|
||||||
*/
|
*/
|
||||||
@GetMapping("/dept-tree")
|
@GetMapping("/dept-tree")
|
||||||
public R<?> getDeptTree() {
|
public R<?> getDeptTree() {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.openhis.web.reportManagement.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销审核传染病报卡请求 DTO
|
||||||
|
*
|
||||||
|
* @author guanyu
|
||||||
|
* @date 2026-04-23
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RevokeAuditInfectiousCardRequest {
|
||||||
|
|
||||||
|
/** 卡片编号(主键) */
|
||||||
|
private String cardNo;
|
||||||
|
|
||||||
|
/** 撤销后的状态 (0 暂存/1 待审核/2 已审核/3 已上报/4 失败/5 退回) */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -55,5 +55,12 @@ public interface ReportManageCardMapper {
|
|||||||
* @param param 查询参数
|
* @param param 查询参数
|
||||||
* @return 报卡列表
|
* @return 报卡列表
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 撤销审核传染病报卡
|
||||||
|
* @param cardNo 卡号
|
||||||
|
* @param status 撤销后的状态
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int revokeAuditCard(@Param("cardNo") String cardNo, @Param("status") Integer status);
|
||||||
List<InfectiousCardDto> selectAllCards(@Param("param") InfectiousCardParam param);
|
List<InfectiousCardDto> selectAllCards(@Param("param") InfectiousCardParam param);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ 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.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
|
||||||
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
||||||
|
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
import com.openhis.web.triageandqueuemanage.dto.*;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueEncounterItem;
|
|
||||||
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -28,10 +24,18 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
||||||
|
|
||||||
private static final String STATUS_WAITING = "WAITING";
|
/**
|
||||||
private static final String STATUS_CALLING = "CALLING";
|
* 分诊队列状态常量(数字编码)
|
||||||
private static final String STATUS_SKIPPED = "SKIPPED";
|
* 0=WAITING(等待中), 10=CALLING(呼叫中), 20=IN_CLINIC(诊中),
|
||||||
private static final String STATUS_COMPLETED = "COMPLETED";
|
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
|
||||||
|
*/
|
||||||
|
public static final Integer STATUS_WAITING = 0;
|
||||||
|
public static final Integer STATUS_CALLING = 10;
|
||||||
|
public static final Integer STATUS_IN_CLINIC = 20;
|
||||||
|
public static final Integer STATUS_COMPLETED = 30;
|
||||||
|
public static final Integer STATUS_SKIPPED = 40;
|
||||||
|
public static final Integer STATUS_REFUNDED = 50;
|
||||||
|
public static final Integer STATUS_FOLLOW = 60;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private TriageQueueItemService triageQueueItemService;
|
private TriageQueueItemService triageQueueItemService;
|
||||||
@@ -127,6 +131,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.setPractitionerName(it.getPractitionerName())
|
.setPractitionerName(it.getPractitionerName())
|
||||||
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
|
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
|
||||||
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
||||||
|
.setPoolId(it.getPoolId()) // ✅ 号源池ID(用于div_log审计)
|
||||||
|
.setSlotId(it.getSlotId()) // ✅ 号源槽位ID(用于div_log审计)
|
||||||
.setStatus(STATUS_WAITING)
|
.setStatus(STATUS_WAITING)
|
||||||
.setQueueOrder(++maxOrder)
|
.setQueueOrder(++maxOrder)
|
||||||
.setDeleteFlag("0")
|
.setDeleteFlag("0")
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class CallNumberDisplayResp {
|
|||||||
/** 患者姓名(脱敏) */
|
/** 患者姓名(脱敏) */
|
||||||
private String name;
|
private String name;
|
||||||
/** 状态:CALLING=就诊中,WAITING=等待 */
|
/** 状态:CALLING=就诊中,WAITING=等待 */
|
||||||
private String status;
|
private Integer status;
|
||||||
/** 排队号 */
|
/** 排队号 */
|
||||||
private Integer queueOrder;
|
private Integer queueOrder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ public class TriageQueueEncounterItem {
|
|||||||
private Long practitionerId;
|
private Long practitionerId;
|
||||||
/** 诊室号(可选) */
|
/** 诊室号(可选) */
|
||||||
private String roomNo;
|
private String roomNo;
|
||||||
|
/** 号源池ID(关联 adm_schedule_pool.id,用于 div_log 审计日志) */
|
||||||
|
private Long poolId;
|
||||||
|
/** 号源槽位ID(关联 adm_schedule_slot.id,用于 div_log 审计日志) */
|
||||||
|
private Long slotId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,18 +94,21 @@
|
|||||||
T8.contract_name,
|
T8.contract_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
|
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
|
||||||
|
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
|
||||||
WHEN T1.context_enum = #{activity} THEN T2."name"
|
WHEN T1.context_enum = #{activity} THEN T2."name"
|
||||||
WHEN T1.context_enum = #{medication} THEN T3."name"
|
WHEN T1.context_enum = #{medication} THEN T3."name"
|
||||||
WHEN T1.context_enum = #{device} THEN T4."name"
|
WHEN T1.context_enum = #{device} THEN T4."name"
|
||||||
END AS item_name,
|
END AS item_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
|
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
|
||||||
|
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
|
||||||
WHEN T1.context_enum = #{activity} THEN T2.yb_no
|
WHEN T1.context_enum = #{activity} THEN T2.yb_no
|
||||||
WHEN T1.context_enum = #{medication} THEN T3.yb_no
|
WHEN T1.context_enum = #{medication} THEN T3.yb_no
|
||||||
WHEN T1.context_enum = #{device} THEN T4.yb_no
|
WHEN T1.context_enum = #{device} THEN T4.yb_no
|
||||||
END AS yb_no,
|
END AS yb_no,
|
||||||
CASE
|
CASE
|
||||||
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
|
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
|
||||||
|
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
|
||||||
WHEN T1.context_enum = #{activity} THEN T2.id
|
WHEN T1.context_enum = #{activity} THEN T2.id
|
||||||
WHEN T1.context_enum = #{medication} THEN T3.id
|
WHEN T1.context_enum = #{medication} THEN T3.id
|
||||||
WHEN T1.context_enum = #{device} THEN T4.id
|
WHEN T1.context_enum = #{device} THEN T4.id
|
||||||
@@ -142,15 +145,26 @@
|
|||||||
ON T6.contract_no = T8.bus_no
|
ON T6.contract_no = T8.bus_no
|
||||||
AND T8.delete_flag = '0'
|
AND T8.delete_flag = '0'
|
||||||
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
|
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
|
||||||
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND T1.service_table = #{worDeviceRequest} AND wdr.delete_flag = '0'
|
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
|
||||||
|
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
|
||||||
|
LEFT JOIN wor_service_request AS wsrp ON wsrp.id = wsr.parent_id AND wsrp.delete_flag = '0'
|
||||||
WHERE T1.encounter_id = #{encounterId}
|
WHERE T1.encounter_id = #{encounterId}
|
||||||
AND T1.status_enum IN (#{planned}
|
AND T1.status_enum IN (0
|
||||||
|
, #{planned}
|
||||||
, #{billable}
|
, #{billable}
|
||||||
, #{billed}
|
, #{billed}
|
||||||
, #{refunding}
|
, #{refunding}
|
||||||
, #{refunded}
|
, #{refunded}
|
||||||
, #{partRefund})
|
, #{partRefund})
|
||||||
AND T1.context_enum != #{register}
|
AND T1.context_enum != #{register}
|
||||||
|
AND (
|
||||||
|
-- 若能关联到请求表,则必须是“已签发”后才允许收费端展示
|
||||||
|
(mmr.id IS NOT NULL AND COALESCE(mmr.status_enum, 1) != 1)
|
||||||
|
OR (wsr.id IS NOT NULL AND (COALESCE(wsr.status_enum, 1) != 1 OR COALESCE(wsrp.status_enum, 1) != 1))
|
||||||
|
OR (wdr.id IS NOT NULL AND COALESCE(wdr.status_enum, 1) != 1)
|
||||||
|
-- 无法关联到任一请求表的收费项,不受签发过滤影响(如挂号费等)
|
||||||
|
OR (mmr.id IS NULL AND wsr.id IS NULL AND wdr.id IS NULL)
|
||||||
|
)
|
||||||
AND T1.delete_flag = '0'
|
AND T1.delete_flag = '0'
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@@ -194,18 +208,21 @@
|
|||||||
T8.contract_name,
|
T8.contract_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
|
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.surgery_name
|
||||||
|
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN COALESCE(wsr.content_json::json->>'adviceName', T2."name")
|
||||||
WHEN T1.context_enum = #{activity} THEN T2."name"
|
WHEN T1.context_enum = #{activity} THEN T2."name"
|
||||||
WHEN T1.context_enum = #{medication} THEN T3."name"
|
WHEN T1.context_enum = #{medication} THEN T3."name"
|
||||||
WHEN T1.context_enum = #{device} THEN T4."name"
|
WHEN T1.context_enum = #{device} THEN T4."name"
|
||||||
END AS item_name,
|
END AS item_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
|
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN NULL
|
||||||
|
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN NULL
|
||||||
WHEN T1.context_enum = #{activity} THEN T2.yb_no
|
WHEN T1.context_enum = #{activity} THEN T2.yb_no
|
||||||
WHEN T1.context_enum = #{medication} THEN T3.yb_no
|
WHEN T1.context_enum = #{medication} THEN T3.yb_no
|
||||||
WHEN T1.context_enum = #{device} THEN T4.yb_no
|
WHEN T1.context_enum = #{device} THEN T4.yb_no
|
||||||
END AS yb_no,
|
END AS yb_no,
|
||||||
CASE
|
CASE
|
||||||
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
|
WHEN T1.context_enum = #{activity} AND T1.product_table = 'cli_surgery' THEN T9.id
|
||||||
|
WHEN T1.context_enum = #{activity} AND T1.product_id = 0 AND T1.service_table = 'wor_service_request' THEN 0
|
||||||
WHEN T1.context_enum = #{activity} THEN T2.id
|
WHEN T1.context_enum = #{activity} THEN T2.id
|
||||||
WHEN T1.context_enum = #{medication} THEN T3.id
|
WHEN T1.context_enum = #{medication} THEN T3.id
|
||||||
WHEN T1.context_enum = #{device} THEN T4.id
|
WHEN T1.context_enum = #{device} THEN T4.id
|
||||||
@@ -243,15 +260,25 @@
|
|||||||
ON T6.contract_no = T8.bus_no
|
ON T6.contract_no = T8.bus_no
|
||||||
AND T8.delete_flag = '0'
|
AND T8.delete_flag = '0'
|
||||||
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
|
LEFT JOIN med_medication_request AS mmr ON mmr.id = T1.service_id AND mmr.delete_flag = '0'
|
||||||
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND T1.service_table = #{worDeviceRequest} AND wdr.delete_flag = '0'
|
LEFT JOIN wor_device_request AS wdr ON wdr.id = T1.service_id AND wdr.delete_flag = '0'
|
||||||
|
LEFT JOIN wor_service_request AS wsr ON wsr.id = T1.service_id AND wsr.delete_flag = '0'
|
||||||
WHERE T1.encounter_id = #{encounterId}
|
WHERE T1.encounter_id = #{encounterId}
|
||||||
AND T1.status_enum IN (#{planned}
|
AND T1.status_enum IN (0
|
||||||
|
, #{planned}
|
||||||
, #{billable}
|
, #{billable}
|
||||||
, #{billed}
|
, #{billed}
|
||||||
, #{refunding}
|
, #{refunding}
|
||||||
, #{refunded}
|
, #{refunded}
|
||||||
, #{partRefund})
|
, #{partRefund})
|
||||||
AND T1.context_enum != #{register}
|
AND T1.context_enum != #{register}
|
||||||
|
AND (
|
||||||
|
-- 若能关联到请求表,则必须是“已签发/已发送”后才允许收费端展示
|
||||||
|
(mmr.id IS NOT NULL AND COALESCE(mmr.status_enum, 1) != 1)
|
||||||
|
OR (wsr.id IS NOT NULL AND COALESCE(wsr.status_enum, 1) != 1)
|
||||||
|
OR (wdr.id IS NOT NULL AND COALESCE(wdr.status_enum, 1) != 1)
|
||||||
|
-- 无法关联到任一请求表的收费项,不受签发过滤影响(如挂号费等)
|
||||||
|
OR (mmr.id IS NULL AND wsr.id IS NULL AND wdr.id IS NULL)
|
||||||
|
)
|
||||||
AND T1.delete_flag = '0') final_res
|
AND T1.delete_flag = '0') final_res
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
T9.organization_id AS organizationId,
|
T9.organization_id AS organizationId,
|
||||||
T9.organization_name AS organizationName,
|
T9.organization_name AS organizationName,
|
||||||
T9.healthcare_name AS healthcareName,
|
T9.healthcare_name AS healthcareName,
|
||||||
|
T9.clinic_room AS clinicRoom, -- Bug #410:诊室名称
|
||||||
T9.practitioner_user_id AS practitionerUserId,
|
T9.practitioner_user_id AS practitionerUserId,
|
||||||
T9.practitioner_name AS practitionerName,
|
T9.practitioner_name AS practitionerName,
|
||||||
T9.contract_name AS contractName,
|
T9.contract_name AS contractName,
|
||||||
@@ -68,7 +69,9 @@
|
|||||||
T9.picture_url AS pictureUrl,
|
T9.picture_url AS pictureUrl,
|
||||||
T9.birth_date AS birthDate,
|
T9.birth_date AS birthDate,
|
||||||
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo,
|
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo,
|
||||||
COALESCE(T9.order_id IS NOT NULL, false) AS isFromAppointment
|
COALESCE(T9.order_id IS NOT NULL, false) AS isFromAppointment,
|
||||||
|
T9.slot_id AS slotId,
|
||||||
|
T9.pool_id AS poolId
|
||||||
from (
|
from (
|
||||||
SELECT T1.tenant_id AS tenant_id,
|
SELECT T1.tenant_id AS tenant_id,
|
||||||
T1.id AS encounter_id,
|
T1.id AS encounter_id,
|
||||||
@@ -95,8 +98,14 @@
|
|||||||
T8.birth_date AS birth_date,
|
T8.birth_date AS birth_date,
|
||||||
T8.bus_no AS patient_bus_no,
|
T8.bus_no AS patient_bus_no,
|
||||||
T18.identifier_no AS identifier_no,
|
T18.identifier_no AS identifier_no,
|
||||||
T1.order_id AS order_id
|
T1.order_id AS order_id,
|
||||||
|
om.slot_id AS slot_id,
|
||||||
|
ss.pool_id AS pool_id,
|
||||||
|
sp.clinic_room AS clinic_room -- Bug #410:从号源池获取诊室
|
||||||
FROM adm_encounter AS T1
|
FROM adm_encounter AS T1
|
||||||
|
LEFT JOIN order_main AS om ON T1.order_id = om.id AND om.delete_flag = '0'
|
||||||
|
LEFT JOIN adm_schedule_slot AS ss ON om.slot_id = ss.id AND ss.delete_flag = '0'
|
||||||
|
LEFT JOIN adm_schedule_pool AS sp ON ss.pool_id = sp.id AND sp.delete_flag = '0' -- Bug #410
|
||||||
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
|
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
|
||||||
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
|
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
|
|||||||
@@ -331,8 +331,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
) t
|
) t
|
||||||
WHERE rn = 1
|
WHERE rn = 1
|
||||||
) pi ON s.patient_id = pi.patient_id
|
) pi ON s.patient_id = pi.patient_id
|
||||||
|
<!-- 排除已生成医嘱的手术 -->
|
||||||
|
LEFT JOIN wor_service_request sr ON sr.activity_id = s.id AND sr.delete_flag = '0' AND sr.category_enum = 4
|
||||||
<where>
|
<where>
|
||||||
s.delete_flag = '0'
|
s.delete_flag = '0'
|
||||||
|
<!-- 只显示未生成医嘱的手术 -->
|
||||||
|
AND sr.id IS NULL
|
||||||
<if test="ew.sqlSegment != null and ew.sqlSegment != ''">
|
<if test="ew.sqlSegment != null and ew.sqlSegment != ''">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
AND ${ew.sqlSegment.replace('tenant_id', 's.tenant_id').replace('create_time', 's.create_time').replace('surgery_no', 's.surgery_no').replace('surgery_name', 's.surgery_name').replace('patient_name', 's.patient_name').replace('main_surgeon_name', 's.main_surgeon_name').replace('anesthetist_name', 's.anesthetist_name').replace('org_name', 's.org_name').replace('status_enum', 's.status_enum').replace('planned_time', 's.planned_time')}
|
AND ${ew.sqlSegment.replace('tenant_id', 's.tenant_id').replace('create_time', 's.create_time').replace('surgery_no', 's.surgery_no').replace('surgery_name', 's.surgery_name').replace('patient_name', 's.patient_name').replace('main_surgeon_name', 's.main_surgeon_name').replace('anesthetist_name', 's.anesthetist_name').replace('org_name', 's.org_name').replace('status_enum', 's.status_enum').replace('planned_time', 's.planned_time')}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
||||||
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
|
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
||||||
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
||||||
|
|||||||
@@ -34,13 +34,13 @@
|
|||||||
AND T2.delete_flag = '0'
|
AND T2.delete_flag = '0'
|
||||||
WHERE
|
WHERE
|
||||||
T1.delete_flag = '0'
|
T1.delete_flag = '0'
|
||||||
<if test="chargeItemContext == 1 ">
|
<if test="chargeItemContext == 1">
|
||||||
AND T1.instance_table = #{MED_MEDICATION_DEFINITION}
|
AND T1.instance_table = #{MED_MEDICATION_DEFINITION}
|
||||||
</if>
|
</if>
|
||||||
<if test="chargeItemContext == 2 ">
|
<if test="chargeItemContext == 2">
|
||||||
AND T1.instance_table = #{ADM_DEVICE_DEFINITION}
|
AND T1.instance_table = #{ADM_DEVICE_DEFINITION}
|
||||||
</if>
|
</if>
|
||||||
<if test="chargeItemContext == 3 ">
|
<if test="chargeItemContext == 3">
|
||||||
AND (T1.instance_table = #{WOR_ACTIVITY_DEFINITION} OR T1.instance_table = #{ADM_HEALTHCARE_SERVICE})
|
AND (T1.instance_table = #{WOR_ACTIVITY_DEFINITION} OR T1.instance_table = #{ADM_HEALTHCARE_SERVICE})
|
||||||
</if>
|
</if>
|
||||||
GROUP BY T1.tenant_id,
|
GROUP BY T1.tenant_id,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
abi.chrgitm_lv
|
abi.chrgitm_lv
|
||||||
FROM (
|
FROM (
|
||||||
<!-- 确保至少有一个查询被执行以避免语法错误 -->
|
<!-- 确保至少有一个查询被执行以避免语法错误 -->
|
||||||
<if test="adviceTypes != null and !adviceTypes.isEmpty() and (adviceTypes.contains(1) or adviceTypes.contains(2) or adviceTypes.contains(3))">
|
<if test="adviceTypes != null and !adviceTypes.isEmpty() and (adviceTypes.contains(1) or adviceTypes.contains(2) or adviceTypes.contains(3) or adviceTypes.contains(6))">
|
||||||
<!-- 如果有有效的adviceTypes,则执行对应的查询 -->
|
<!-- 如果有有效的adviceTypes,则执行对应的查询 -->
|
||||||
<if test="adviceTypes.contains(1)">
|
<if test="adviceTypes.contains(1)">
|
||||||
(SELECT
|
(SELECT
|
||||||
@@ -95,14 +95,29 @@
|
|||||||
AND T2.delete_flag = '0' AND T2.status_enum = #{statusEnum}
|
AND T2.delete_flag = '0' AND T2.status_enum = #{statusEnum}
|
||||||
LEFT JOIN adm_supplier AS T3 ON T3.ID = t1.supply_id AND T3.delete_flag = '0'
|
LEFT JOIN adm_supplier AS T3 ON T3.ID = t1.supply_id AND T3.delete_flag = '0'
|
||||||
LEFT JOIN adm_charge_item_definition AS T5 ON T5.instance_id = t1.ID AND T5.delete_flag = '0' AND T5.status_enum = #{statusEnum}
|
LEFT JOIN adm_charge_item_definition AS T5 ON T5.instance_id = t1.ID AND T5.delete_flag = '0' AND T5.status_enum = #{statusEnum}
|
||||||
LEFT JOIN adm_organization_location AS T6 ON T6.distribution_category_code = t1.category_code AND T6.delete_flag = '0' AND T6.item_code = '1' AND T6.organization_id = #{organizationId} AND (CURRENT_TIME :: time (6) BETWEEN T6.start_time AND T6.end_time)
|
INNER JOIN adm_organization_location AS T6 ON T6.distribution_category_code = t1.category_code AND T6.delete_flag = '0' AND T6.item_code = '1' AND T6.organization_id = #{organizationId} AND (CURRENT_TIME :: time (6) BETWEEN T6.start_time AND T6.end_time)
|
||||||
WHERE t1.delete_flag = '0'
|
WHERE t1.delete_flag = '0'
|
||||||
AND T2.status_enum = #{statusEnum}
|
AND T2.status_enum = #{statusEnum}
|
||||||
<if test="pricingFlag == 1">
|
<if test="pricingFlag == 1">
|
||||||
AND 1 = 2
|
AND 1 = 2
|
||||||
</if>
|
</if>
|
||||||
<if test="categoryCode != null and categoryCode != ''">
|
<if test="categoryCode != null and categoryCode != ''">
|
||||||
AND t1.category_code = #{categoryCode}
|
<!-- 🔧 BugFix: 支持两种匹配方式 -->
|
||||||
|
<!-- 1. 直接匹配:distribution_category_code = category_code(都是数字代码) -->
|
||||||
|
<!-- 2. 字典转换匹配:通过 sys_dict_data 表将 distribution_category_code(中文)转换为 category_code(数字代码) -->
|
||||||
|
AND (
|
||||||
|
-- 方式1:直接匹配
|
||||||
|
t1.category_code = #{categoryCode}
|
||||||
|
OR
|
||||||
|
-- 方式2:通过字典转换匹配(当 distribution_category_code 存储的是中文时)
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM sys_dict_data sdd
|
||||||
|
WHERE sdd.dict_type = 'med_category_code'
|
||||||
|
AND sdd.status = '0'
|
||||||
|
AND sdd.dict_label = T6.distribution_category_code
|
||||||
|
AND sdd.dict_value = #{categoryCode}
|
||||||
|
)
|
||||||
|
)
|
||||||
</if>
|
</if>
|
||||||
<if test="searchKey != null and searchKey != ''">
|
<if test="searchKey != null and searchKey != ''">
|
||||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||||
@@ -171,6 +186,9 @@
|
|||||||
<if test="searchKey != null and searchKey != ''">
|
<if test="searchKey != null and searchKey != ''">
|
||||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||||
</if>
|
</if>
|
||||||
|
<if test="categoryCode != null and categoryCode != ''">
|
||||||
|
AND t1.category_code = #{categoryCode}
|
||||||
|
</if>
|
||||||
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
|
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
|
||||||
AND t1.id IN
|
AND t1.id IN
|
||||||
<foreach collection="adviceDefinitionIdParamList" item="itemId" open="(" separator="," close=")">
|
<foreach collection="adviceDefinitionIdParamList" item="itemId" open="(" separator="," close=")">
|
||||||
@@ -185,11 +203,15 @@
|
|||||||
<if test="adviceTypes.contains(3)">UNION ALL</if>
|
<if test="adviceTypes.contains(3)">UNION ALL</if>
|
||||||
</if>
|
</if>
|
||||||
|
|
||||||
<if test="adviceTypes.contains(3)">
|
<if test="adviceTypes.contains(3) or adviceTypes.contains(6)">
|
||||||
(SELECT
|
(SELECT
|
||||||
DISTINCT ON (T1.ID)
|
DISTINCT ON (T1.ID)
|
||||||
T1.tenant_id,
|
T1.tenant_id,
|
||||||
3 AS advice_type,
|
<choose>
|
||||||
|
<when test="adviceTypes.contains(3) and adviceTypes.contains(6)">CASE T1.category_code WHEN '手术' THEN 6 WHEN '24' THEN 6 ELSE 3 END</when>
|
||||||
|
<when test="adviceTypes.contains(6)">6</when>
|
||||||
|
<otherwise>3</otherwise>
|
||||||
|
</choose> AS advice_type,
|
||||||
T1.bus_no AS bus_no,
|
T1.bus_no AS bus_no,
|
||||||
T1.category_code AS category_code,
|
T1.category_code AS category_code,
|
||||||
'' AS pharmacology_category_code,
|
'' AS pharmacology_category_code,
|
||||||
@@ -213,7 +235,7 @@
|
|||||||
WHEN '检验' THEN 1
|
WHEN '检验' THEN 1
|
||||||
WHEN '检查' THEN 2
|
WHEN '检查' THEN 2
|
||||||
WHEN '护理' THEN 3
|
WHEN '护理' THEN 3
|
||||||
WHEN '手术' THEN 4
|
WHEN '手术' THEN 4 WHEN '24' THEN 4
|
||||||
WHEN '其他' THEN 5
|
WHEN '其他' THEN 5
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS activity_type,
|
END AS activity_type,
|
||||||
@@ -250,9 +272,20 @@
|
|||||||
<if test="pricingFlag != null">
|
<if test="pricingFlag != null">
|
||||||
AND (t1.pricing_flag = #{pricingFlag} OR t1.pricing_flag IS NULL)
|
AND (t1.pricing_flag = #{pricingFlag} OR t1.pricing_flag IS NULL)
|
||||||
</if>
|
</if>
|
||||||
|
<!-- 如果只选择手术(adviceType=6),过滤 category_code = '手术' 或 '24' -->
|
||||||
|
<if test="adviceTypes.contains(6) and !adviceTypes.contains(3)">
|
||||||
|
AND (T1.category_code = '手术' OR T1.category_code = '24')
|
||||||
|
</if>
|
||||||
|
<!-- 如果只选择诊疗(adviceType=3),排除手术 -->
|
||||||
|
<if test="adviceTypes.contains(3) and !adviceTypes.contains(6) and (categoryCode == null or categoryCode == '')">
|
||||||
|
AND T1.category_code != '手术' AND T1.category_code != '24'
|
||||||
|
</if>
|
||||||
<if test="searchKey != null and searchKey != ''">
|
<if test="searchKey != null and searchKey != ''">
|
||||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||||
</if>
|
</if>
|
||||||
|
<if test="categoryCode != null and categoryCode != ''">
|
||||||
|
AND t1.category_code = #{categoryCode}
|
||||||
|
</if>
|
||||||
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
|
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">
|
||||||
AND t1.id IN
|
AND t1.id IN
|
||||||
<foreach collection="adviceDefinitionIdParamList" item="itemId" open="(" separator="," close=")">
|
<foreach collection="adviceDefinitionIdParamList" item="itemId" open="(" separator="," close=")">
|
||||||
@@ -263,7 +296,7 @@
|
|||||||
</if>
|
</if>
|
||||||
</if>
|
</if>
|
||||||
<!-- 如果没有有效的adviceTypes,提供一个空的默认查询以避免语法错误 -->
|
<!-- 如果没有有效的adviceTypes,提供一个空的默认查询以避免语法错误 -->
|
||||||
<if test="adviceTypes == null or adviceTypes.isEmpty() or (!adviceTypes.contains(1) and !adviceTypes.contains(2) and !adviceTypes.contains(3))">
|
<if test="adviceTypes == null or adviceTypes.isEmpty() or (!adviceTypes.contains(1) and !adviceTypes.contains(2) and !adviceTypes.contains(3) and !adviceTypes.contains(6))">
|
||||||
SELECT
|
SELECT
|
||||||
mmd.tenant_id,
|
mmd.tenant_id,
|
||||||
CAST(0 AS INTEGER) AS advice_type,
|
CAST(0 AS INTEGER) AS advice_type,
|
||||||
@@ -603,6 +636,8 @@
|
|||||||
T3.service_table = #{WOR_DEVICE_REQUEST}
|
T3.service_table = #{WOR_DEVICE_REQUEST}
|
||||||
LEFT JOIN adm_location AS al ON al.ID = T1.perform_location AND al.delete_flag = '0'
|
LEFT JOIN adm_location AS al ON al.ID = T1.perform_location AND al.delete_flag = '0'
|
||||||
WHERE T1.delete_flag = '0' AND T1.generate_source_enum = #{generateSourceEnum}
|
WHERE T1.delete_flag = '0' AND T1.generate_source_enum = #{generateSourceEnum}
|
||||||
|
-- 🔧 Bug Fix: 排除基于其他医嘱生成的执行记录
|
||||||
|
AND (T1.based_on_id IS NULL OR T1.based_on_table IS NULL)
|
||||||
<if test="historyFlag == '0'.toString()">
|
<if test="historyFlag == '0'.toString()">
|
||||||
AND T1.encounter_id = #{encounterId}
|
AND T1.encounter_id = #{encounterId}
|
||||||
</if>
|
</if>
|
||||||
@@ -659,6 +694,10 @@
|
|||||||
WHERE T1.delete_flag = '0' AND T1.generate_source_enum = #{generateSourceEnum}
|
WHERE T1.delete_flag = '0' AND T1.generate_source_enum = #{generateSourceEnum}
|
||||||
AND T1.parent_id IS NULL
|
AND T1.parent_id IS NULL
|
||||||
AND T1.refund_service_id IS NULL
|
AND T1.refund_service_id IS NULL
|
||||||
|
-- 🔧 Bug Fix: 排除基于药品请求生成的执行记录(输液、皮试),但保留检查/检验申请单创建的原始医嘱
|
||||||
|
-- based_on_table='med_medication_request' → 输液/皮试执行记录,应排除
|
||||||
|
-- based_on_table='exam_apply'/'lab_apply' → 申请单原始医嘱,应保留
|
||||||
|
AND (T1.based_on_id IS NULL OR T1.based_on_table IS NULL OR T1.based_on_table NOT IN ('med_medication_request', 'med_medication_dispense'))
|
||||||
<if test="historyFlag == '0'.toString()">
|
<if test="historyFlag == '0'.toString()">
|
||||||
AND T1.encounter_id = #{encounterId}
|
AND T1.encounter_id = #{encounterId}
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -4,41 +4,6 @@
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.openhis.web.doctorstation.mapper.DoctorStationLabApplyMapper">
|
<mapper namespace="com.openhis.web.doctorstation.mapper.DoctorStationLabApplyMapper">
|
||||||
|
|
||||||
<!-- 根据申请单号查询检验申请单(返回完整字段) -->
|
|
||||||
<select id="getInspectionApplyByApplyNo" resultType="com.openhis.web.doctorstation.dto.DoctorStationLabApplyDto">
|
|
||||||
SELECT
|
|
||||||
id AS applicationId,
|
|
||||||
apply_no AS applyNo,
|
|
||||||
patient_id AS patientId,
|
|
||||||
patient_name AS patientName,
|
|
||||||
medicalrecord_number AS medicalrecordNumber,
|
|
||||||
natureof_cost AS natureofCost,
|
|
||||||
visit_no AS visitNo,
|
|
||||||
apply_dept_code AS applyDeptCode,
|
|
||||||
apply_department AS applyDepartment,
|
|
||||||
apply_doc_code AS applyDocCode,
|
|
||||||
apply_doc_name AS applyDocName,
|
|
||||||
apply_time AS applyTime,
|
|
||||||
clinic_diag AS clinicDiag,
|
|
||||||
clinic_desc AS clinicDesc,
|
|
||||||
contraindication AS contraindication,
|
|
||||||
medical_history_summary AS medicalHistorySummary,
|
|
||||||
purposeof_inspection AS purposeofInspection,
|
|
||||||
physical_examination AS physicalExamination,
|
|
||||||
inspection_item AS inspectionItem,
|
|
||||||
specimen_type_code AS specimenTypeCode,
|
|
||||||
specimen_name AS specimenName,
|
|
||||||
priority_code AS priorityCode,
|
|
||||||
apply_status AS applyStatus,
|
|
||||||
apply_remark AS applyRemark,
|
|
||||||
create_time AS createTime,
|
|
||||||
operator_id AS operatorId,
|
|
||||||
create_by AS createBy,
|
|
||||||
tenant_id AS tenantId
|
|
||||||
FROM lab_apply
|
|
||||||
WHERE apply_no = #{applyNo}
|
|
||||||
AND delete_flag = '0'
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- 分页查询检验申请单列表(根据就诊ID查询,按申请时间降序)
|
<!-- 分页查询检验申请单列表(根据就诊ID查询,按申请时间降序)
|
||||||
从明细表聚合项目名称和金额-->
|
从明细表聚合项目名称和金额-->
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
T2.organization_id AS org_id, --科室ID(从就诊表取)
|
T2.organization_id AS org_id, --科室ID(从就诊表取)
|
||||||
T2.id AS encounter_id, --就诊ID
|
T2.id AS encounter_id, --就诊ID
|
||||||
T2.start_time AS admissionDate, --入院日期
|
T2.start_time AS admissionDate, --入院日期
|
||||||
|
T3.ward_admission_date AS wardAdmissionDate, --入科日期
|
||||||
T3.location_id AS ward_location_id, --病区
|
T3.location_id AS ward_location_id, --病区
|
||||||
T4.location_id AS bed_location_id --床号
|
T4.location_id AS bed_location_id --床号
|
||||||
FROM adm_patient AS T1
|
FROM adm_patient AS T1
|
||||||
|
|||||||
@@ -81,6 +81,9 @@
|
|||||||
-- 记录日期
|
-- 记录日期
|
||||||
T1.recording_date >= #{startTime}::date
|
T1.recording_date >= #{startTime}::date
|
||||||
AND T1.recording_date <= #{endTime}::date
|
AND T1.recording_date <= #{endTime}::date
|
||||||
|
<if test="patientId != null and patientId != ''">
|
||||||
|
AND T1.patient_id = #{patientId}::bigint
|
||||||
|
</if>
|
||||||
AND T1.delete_flag = '0'
|
AND T1.delete_flag = '0'
|
||||||
|
|
||||||
</where>
|
</where>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
togpd.dose_quantity,
|
togpd.dose_quantity,
|
||||||
togpd.group_id,
|
togpd.group_id,
|
||||||
togpd.dispense_per_duration,
|
togpd.dispense_per_duration,
|
||||||
|
togpd.therapy_enum,
|
||||||
CASE
|
CASE
|
||||||
WHEN togpd.order_definition_table = 'med_medication_definition' THEN
|
WHEN togpd.order_definition_table = 'med_medication_definition' THEN
|
||||||
med.NAME
|
med.NAME
|
||||||
|
|||||||
@@ -252,4 +252,11 @@
|
|||||||
ORDER BY t1.report_date DESC
|
ORDER BY t1.report_date DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 撤销审核传染病报卡 -->
|
||||||
|
<update id="revokeAuditCard">
|
||||||
|
UPDATE infectious_card
|
||||||
|
SET status = #{status}::INTEGER,
|
||||||
|
update_time = CURRENT_TIMESTAMP
|
||||||
|
WHERE card_no = #{cardNo}
|
||||||
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -74,4 +74,9 @@ public class InspectionLabApplyItem extends HisBaseEntity {
|
|||||||
*/
|
*/
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long itemStatus;
|
private Long itemStatus;
|
||||||
|
/**
|
||||||
|
* 套餐ID
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long feePackageId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,15 @@ public class TriageQueueItem {
|
|||||||
private String practitionerName; // 医生姓名
|
private String practitionerName; // 医生姓名
|
||||||
private Long practitionerId; // 医生ID(新增字段)
|
private Long practitionerId; // 医生ID(新增字段)
|
||||||
private String roomNo; // 诊室号(新增字段)
|
private String roomNo; // 诊室号(新增字段)
|
||||||
|
private Long poolId; // 号源池ID (关联 adm_schedule_pool.id)
|
||||||
|
private Long slotId; // 号源槽位ID (关联 adm_schedule_slot.id)
|
||||||
|
|
||||||
/** WAITING / CALLING / SKIPPED / COMPLETED */
|
/**
|
||||||
private String status;
|
* 分诊队列状态
|
||||||
|
* 0=WAITING(等待中), 10=CALLING(呼叫中), 20=IN_CLINIC(诊中),
|
||||||
|
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
|
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.openhis.workflow.domain;
|
package com.openhis.workflow.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.core.common.core.domain.HisBaseEntity;
|
import com.core.common.core.domain.HisBaseEntity;
|
||||||
@@ -46,9 +48,11 @@ public class ActivityDefinition extends HisBaseEntity {
|
|||||||
private String permittedUnitCode;
|
private String permittedUnitCode;
|
||||||
|
|
||||||
/** 所属科室 */
|
/** 所属科室 */
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private Long orgId;
|
private Long orgId;
|
||||||
|
|
||||||
/** 所在位置 */
|
/** 所在位置 */
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private Long locationId;
|
private Long locationId;
|
||||||
|
|
||||||
/** 医保标记 */
|
/** 医保标记 */
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.openhis.workflow.domain;
|
package com.openhis.workflow.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.core.common.core.domain.HisBaseEntity;
|
import com.core.common.core.domain.HisBaseEntity;
|
||||||
@@ -64,6 +66,7 @@ public class ServiceRequest extends HisBaseEntity {
|
|||||||
private Integer performFlag;
|
private Integer performFlag;
|
||||||
|
|
||||||
/** 诊疗定义id */
|
/** 诊疗定义id */
|
||||||
|
@TableField(insertStrategy = FieldStrategy.ALWAYS)
|
||||||
private Long activityId;
|
private Long activityId;
|
||||||
|
|
||||||
/** 数量 */
|
/** 数量 */
|
||||||
|
|||||||
@@ -298,8 +298,14 @@
|
|||||||
<if test="query.phone != null and query.phone != ''">
|
<if test="query.phone != null and query.phone != ''">
|
||||||
AND o.phone LIKE CONCAT('%', #{query.phone}, '%')
|
AND o.phone LIKE CONCAT('%', #{query.phone}, '%')
|
||||||
</if>
|
</if>
|
||||||
<!-- 5. 核心:按系统时间过滤,只返回未过期的号源 -->
|
<!-- 5. 按系统时间过滤(Bug #398 #399 修复:仅未预约受时间过滤,已预约/已取号/已退号不受影响) -->
|
||||||
AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND s.expect_time >= CURRENT_TIME::TIME))
|
AND (
|
||||||
|
(<include refid="slotStatusNormExpr" /> = 0 AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW())))
|
||||||
|
OR <include refid="slotStatusNormExpr" /> = 1
|
||||||
|
OR <include refid="slotStatusNormExpr" /> = 3
|
||||||
|
OR <include refid="slotStatusNormExpr" /> = 5
|
||||||
|
OR <include refid="orderStatusNormExpr" /> = 4
|
||||||
|
)
|
||||||
<!-- 6. 状态过滤 -->
|
<!-- 6. 状态过滤 -->
|
||||||
<if test="query.status != null and query.status != '' and query.status != 'all'">
|
<if test="query.status != null and query.status != '' and query.status != 'all'">
|
||||||
<choose>
|
<choose>
|
||||||
@@ -370,7 +376,7 @@
|
|||||||
p.schedule_date > CURRENT_DATE
|
p.schedule_date > CURRENT_DATE
|
||||||
OR (
|
OR (
|
||||||
p.schedule_date = CURRENT_DATE
|
p.schedule_date = CURRENT_DATE
|
||||||
AND CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME) > TO_TIMESTAMP(#{query.currentTime}/1000)
|
AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
THEN s.id
|
THEN s.id
|
||||||
@@ -401,7 +407,7 @@
|
|||||||
AND p.schedule_date = CAST(#{query.date} AS DATE)
|
AND p.schedule_date = CAST(#{query.date} AS DATE)
|
||||||
</if>
|
</if>
|
||||||
<!-- 增加时间过滤:排除已过去的就诊日期 -->
|
<!-- 增加时间过滤:排除已过去的就诊日期 -->
|
||||||
AND p.schedule_date >= CURRENT_DATE
|
AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW()))
|
||||||
<if test="query.department != null and query.department != '' and query.department != 'all'">
|
<if test="query.department != null and query.department != '' and query.department != 'all'">
|
||||||
AND org.name = #{query.department}
|
AND org.name = #{query.department}
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
# 页面标题
|
# Playwright E2E 测试环境变量
|
||||||
VITE_APP_TITLE = 医院信息管理系统
|
# 注意:此文件仅用于本地开发,生产环境使用CI Secret管理
|
||||||
|
TEST_BASE_URL=http://localhost:80
|
||||||
# 测试环境配置
|
TEST_USERNAME=admin
|
||||||
VITE_APP_ENV = 'test'
|
TEST_PASSWORD=changeme_in_local_env
|
||||||
|
|
||||||
# OpenHIS管理系统/测试环境
|
|
||||||
|
|
||||||
VITE_APP_BASE_API = '/test-api'
|
|
||||||
|
|
||||||
# 租户ID配置
|
|
||||||
VITE_APP_TENANT_ID = '1'
|
|
||||||
|
|||||||
5
openhis-ui-vue3/.gitignore
vendored
5
openhis-ui-vue3/.gitignore
vendored
@@ -21,3 +21,8 @@ selenium-debug.log
|
|||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
# Playwright test results
|
||||||
|
test-results/
|
||||||
|
tests/e2e/report/
|
||||||
|
tests/tests/
|
||||||
|
|||||||
59
openhis-ui-vue3/eslint.config.js
Normal file
59
openhis-ui-vue3/eslint.config.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
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",
|
||||||
|
// Vue 相关规则
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
alias: {
|
||||||
|
map: [
|
||||||
|
["@", "./src"],
|
||||||
|
],
|
||||||
|
extensions: [".js", ".jsx", ".vue"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
2452
openhis-ui-vue3/package-lock.json
generated
2452
openhis-ui-vue3/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,11 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:ui": "vitest --ui"
|
"test:ui": "vitest --ui",
|
||||||
|
"lint": "eslint . --ext .js,.vue src/",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -67,6 +71,11 @@
|
|||||||
"@vitejs/plugin-vue": "4.5.0",
|
"@vitejs/plugin-vue": "4.5.0",
|
||||||
"@vue/compiler-sfc": "3.3.9",
|
"@vue/compiler-sfc": "3.3.9",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"eslint": "^9.39.4",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-vue": "^10.9.0",
|
||||||
|
"globals": "^17.5.0",
|
||||||
"happy-dom": "^20.8.3",
|
"happy-dom": "^20.8.3",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
|
|||||||
28
openhis-ui-vue3/playwright.config.ts
Normal file
28
openhis-ui-vue3/playwright.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e/specs',
|
||||||
|
fullyParallel: true,
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
retries: process.env.CI ? 2 : 1,
|
||||||
|
workers: process.env.CI ? 2 : undefined,
|
||||||
|
reporter: [
|
||||||
|
['html', { outputFolder: 'tests/e2e/report', open: 'never' }],
|
||||||
|
['list'],
|
||||||
|
],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.TEST_BASE_URL || 'http://localhost:81',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
timezoneId: 'Asia/Shanghai',
|
||||||
|
actionTimeout: 15_000,
|
||||||
|
navigationTimeout: 30_000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -18,34 +18,33 @@ export const TOP_KEYS = [
|
|||||||
const timeNew = new Date((tieml / 1000 + 86400) * 1000);
|
const timeNew = new Date((tieml / 1000 + 86400) * 1000);
|
||||||
const todayDate = dayjs(timeNew).format('YYYY-MM-DD');
|
const todayDate = dayjs(timeNew).format('YYYY-MM-DD');
|
||||||
const endDate = dayjs(outdate).format('YYYY-MM-DD');
|
const endDate = dayjs(outdate).format('YYYY-MM-DD');
|
||||||
const startDate = dayjs(hospDate).format('YYYY-MM-DD');
|
let eachDate = dayjs(beginDate).add(i, 'day');
|
||||||
let eachTime = dayjs(beginDate).add(i, 'day').format('YYYY-MM-DD');
|
const eachTime = eachDate.format('YYYY-MM-DD');
|
||||||
if (eachTime === endDate || eachTime === todayDate) {
|
if (eachTime === endDate || eachTime === todayDate) {
|
||||||
dateClosed.stopTime = true;
|
dateClosed.stopTime = true;
|
||||||
}
|
}
|
||||||
if ((startDate === eachTime && i === 0) || dayjs(eachTime).format('MM-DD') === '01-01') {
|
// 统一补零格式:月份和日期都始终两位补零
|
||||||
eachTime = dayjs(eachTime).format('YYYY年MM月DD日');
|
const month = eachDate.format('MM'); // 月份始终两位补零
|
||||||
} else if (i === 0 || dayjs(eachTime).format('DD') === '01') {
|
const date = eachDate.format('DD'); // 日期始终两位补零
|
||||||
eachTime = dayjs(eachTime).format('MM月DD日');
|
// 每月1号显示 MM月DD日,其他日期只显示 DD日
|
||||||
} else {
|
return `${month}月${date}日`;
|
||||||
eachTime = dayjs(eachTime).format('DD日');
|
|
||||||
}
|
|
||||||
return dateClosed.stopTime ? eachTime : '';
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '住院日数',
|
name: '住院日数',
|
||||||
getValue: (i, renderData) => {
|
getValue: (i, renderData) => {
|
||||||
const { beginDate, hospDays, outdate = '', dateClosed } = renderData.infoData;
|
const { beginDate, hospDays, outdate = '', dateClosed } = renderData.infoData;
|
||||||
const tieml = new Date();
|
const beginDayjs = dayjs(beginDate);
|
||||||
const timeNew = new Date((tieml / 1000 + 86400) * 1000);
|
let num = '';
|
||||||
const todayDate = dayjs(timeNew).format('YYYY-MM-DD');
|
// 只要 beginDate 有效,每一天都计算显示住院日数
|
||||||
const endDate = dayjs(outdate).add(1, 'day').format('YYYY-MM-DD');
|
if (beginDayjs.isValid()) {
|
||||||
const eachTime = dayjs(beginDate).add(i, 'day').format('YYYY-MM-DD');
|
if (hospDays !== undefined && hospDays !== null) {
|
||||||
if (eachTime === endDate || todayDate === eachTime) {
|
num = hospDays + i + 1;
|
||||||
dateClosed.stopNumber = false;
|
} else {
|
||||||
|
// hospDays 不存在时,从入院开始直接计算
|
||||||
|
num = i + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const num = dateClosed.stopNumber ? hospDays + i + 1 : '';
|
|
||||||
let hosNum = '';
|
let hosNum = '';
|
||||||
if (num !== '') {
|
if (num !== '') {
|
||||||
hosNum = '第' + '\xa0\xa0\xa0' + num + '\xa0\xa0\xa0' + '日';
|
hosNum = '第' + '\xa0\xa0\xa0' + num + '\xa0\xa0\xa0' + '日';
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ export function advicePrint(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取全部科室列表
|
// 获取全部科室列表
|
||||||
|
// 获取科室列表(别名)
|
||||||
|
export function getDepartmentList(data) {
|
||||||
|
return request({
|
||||||
|
url: '/app-common/department-list',
|
||||||
|
method: 'get',
|
||||||
|
params: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getOrgList(data) {
|
export function getOrgList(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/app-common/department-list',
|
url: '/app-common/department-list',
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"height": 210,
|
"height": 297,
|
||||||
"index": 1,
|
"index": 1,
|
||||||
"name": "会诊申请单",
|
"name": "会诊申请单",
|
||||||
"paperFooter": 595.5,
|
"paperFooter": 780,
|
||||||
"paperHeader": 0,
|
"paperHeader": 0,
|
||||||
"paperList": {"height": 210, "type": "A4", "width": 210},
|
"paperList": {"height": 297, "type": "A4", "width": 210},
|
||||||
"paperNumberDisabled": true,
|
"paperNumberDisabled": true,
|
||||||
"paperType": "A4",
|
"paperType": "A4",
|
||||||
"printElements": [
|
"printElements": [
|
||||||
{"options": {"fontSize": 14, "fontWeight": "bold", "height": 15, "left": 0, "textAlign": "center", "title": "会诊申请单", "top": 5, "width": 180}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"fontSize": 18, "fontWeight": "bold", "height": 20, "left": 0, "textAlign": "center", "title": "会诊申请单", "top": 10, "width": 210}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "hospitalName", "fontSize": 10, "height": 10, "left": 0, "textAlign": "center", "title": "医院名称", "top": 22, "width": 180}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "hospitalName", "fontSize": 12, "height": 15, "left": 0, "textAlign": "center", "title": "医院名称", "top": 35, "width": 210}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "patientName", "fontSize": 10, "height": 10, "left": 5, "title": "姓名:", "top": 40, "width": 60, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "patientName", "fontSize": 12, "height": 15, "left": 10, "title": "姓名:", "top": 60, "width": 80, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "gender", "fontSize": 10, "height": 10, "left": 70, "title": "性别:", "top": 40, "width": 30, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "gender", "fontSize": 12, "height": 15, "left": 300, "title": "性别:", "top": 60, "width": 100, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "age", "fontSize": 10, "height": 10, "left": 105, "title": "年龄:", "top": 40, "width": 40, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "age", "fontSize": 12, "height": 15, "left": 150, "title": "年龄:", "top": 60, "width": 100, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "deptName", "fontSize": 10, "height": 10, "left": 150, "title": "科室:", "top": 40, "width": 30, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "deptName", "fontSize": 12, "height": 15, "left": 10, "title": "科室:", "top": 80, "width": 190, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "diagnosis", "fontSize": 10, "height": 20, "left": 5, "title": "初步诊断:", "top": 55, "width": 170, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "diagnosis", "fontSize": 12, "height": 25, "left": 10, "title": "初步诊断:", "top": 100, "width": 400, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "consultationReason", "fontSize": 10, "height": 40, "left": 5, "title": "会诊目的:", "top": 80, "width": 170, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "consultationReason", "fontSize": 12, "height": 50, "left": 10, "title": "会诊目的:", "top": 130, "width": 190, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "applyTime", "fontSize": 10, "height": 10, "left": 5, "title": "申请时间:", "top": 130, "width": 80, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
{"options": {"field": "applyTime", "fontSize": 12, "height": 15, "left": 10, "title": "申请时间:", "top": 190, "width": 300, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}},
|
||||||
{"options": {"field": "applyDoctor", "fontSize": 10, "height": 10, "left": 100, "title": "申请医生:", "top": 130, "width": 75, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}}
|
{"options": {"field": "applyDoctor", "fontSize": 12, "height": 15, "left": 400, "title": "申请医生:", "top": 190, "width": 300, "hideTitle": false}, "printElementType": {"title": "文本", "type": "text"}}
|
||||||
],
|
],
|
||||||
"width": 180
|
"width": 210
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ const convertIdsToString = (obj) => {
|
|||||||
} else {
|
} else {
|
||||||
const newObj = {}
|
const newObj = {}
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
const value = obj[key]
|
const value = obj[key]
|
||||||
// 如果key以Id结尾或者是id,且值是数字,转为字符串
|
// 如果key以Id结尾或者是id,且值是数字,转为字符串
|
||||||
if ((key === 'id' || key.endsWith('Id') || key.endsWith('ID')) && typeof value === 'number') {
|
if ((key === 'id' || key.endsWith('Id') || key.endsWith('ID')) && typeof value === 'number') {
|
||||||
|
|||||||
@@ -739,9 +739,15 @@ export default {
|
|||||||
status: record.statusEnum_enumText || record.status
|
status: record.statusEnum_enumText || record.status
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 先进行时间过滤(过滤掉已过期的号源)
|
// 🔧 BugFix#398/#399: 时间过滤仅对"未预约"号源生效
|
||||||
|
// 已预约、已取号、已退号等状态的记录不应因时间过期被过滤
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const timeFilteredRecords = mappedRecords.filter(ticket => {
|
const timeFilteredRecords = mappedRecords.filter(ticket => {
|
||||||
|
// 非未预约状态的记录不过滤时间
|
||||||
|
if (ticket.status && ticket.status !== '未预约') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 未预约号源:过滤已过期的
|
||||||
const ticketTime = new Date(ticket.dateTime).getTime();
|
const ticketTime = new Date(ticket.dateTime).getTime();
|
||||||
return ticketTime > currentTime;
|
return ticketTime > currentTime;
|
||||||
});
|
});
|
||||||
@@ -765,10 +771,11 @@ export default {
|
|||||||
if (!this.selectedStatus || this.selectedStatus === 'all') {
|
if (!this.selectedStatus || this.selectedStatus === 'all') {
|
||||||
return records;
|
return records;
|
||||||
}
|
}
|
||||||
|
// 🔧 BugFix#399: 确保已取号状态正确匹配
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
unbooked: ['未预约'],
|
unbooked: ['未预约'],
|
||||||
booked: ['已预约'],
|
booked: ['已预约'],
|
||||||
checked: ['已取号'],
|
checked: ['已取号', '已签到'],
|
||||||
cancelled: ['已停诊', '已取消'],
|
cancelled: ['已停诊', '已取消'],
|
||||||
returned: ['已退号']
|
returned: ['已退号']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -819,7 +819,8 @@ function selectAdviceBase(key, row) {
|
|||||||
combinationList.value[rowIndex.value].positionName = stock.locationName;
|
combinationList.value[rowIndex.value].positionName = stock.locationName;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
combinationList.value[rowIndex.value].orgId = JSON.parse(JSON.stringify(row)).positionId;
|
// 🔧 修复执行科室逻辑:优先使用项目维护的所属科室(row.orgId),其次使用positionId
|
||||||
|
combinationList.value[rowIndex.value].orgId = row.orgId || JSON.parse(JSON.stringify(row)).positionId;
|
||||||
combinationList.value[rowIndex.value].unitPrice = row.priceList[0].price;
|
combinationList.value[rowIndex.value].unitPrice = row.priceList[0].price;
|
||||||
}
|
}
|
||||||
expandOrder.value = [key];
|
expandOrder.value = [key];
|
||||||
|
|||||||
@@ -545,6 +545,11 @@ function edit() {
|
|||||||
title.value = props.title;
|
title.value = props.title;
|
||||||
form.value = props.item;
|
form.value = props.item;
|
||||||
form.value.chrgitmLv = form.value.chrgitmLv ? form.value.chrgitmLv.toString() : undefined;
|
form.value.chrgitmLv = form.value.chrgitmLv ? form.value.chrgitmLv.toString() : undefined;
|
||||||
|
// 所属科室:如果后端无法翻译科室名称(orgId_dictText为空),则清空orgId,
|
||||||
|
// 避免el-tree-select回退显示原始数字ID,而是显示"请选择..."占位符
|
||||||
|
if (!form.value.orgId_dictText) {
|
||||||
|
form.value.orgId = undefined;
|
||||||
|
}
|
||||||
// 划价标记:编辑时若原值为null/undefined则默认为1(允许划价)
|
// 划价标记:编辑时若原值为null/undefined则默认为1(允许划价)
|
||||||
if (form.value.pricingFlag === null || form.value.pricingFlag === undefined) {
|
if (form.value.pricingFlag === null || form.value.pricingFlag === undefined) {
|
||||||
form.value.pricingFlag = 1;
|
form.value.pricingFlag = 1;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function deptTreeSelect (queryParams) {
|
|||||||
return request ({
|
return request ({
|
||||||
url: '/base-data-manage/organization/organization',
|
url: '/base-data-manage/organization/organization',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
param: queryParams,
|
params: queryParams,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 查询身体部位树形数据
|
// 查询身体部位树形数据
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const isAdding = ref(false);
|
const isAdding = ref(false);
|
||||||
|
const isSaving = ref(false); // #437 防重复提交锁
|
||||||
const prescriptionRef = ref();
|
const prescriptionRef = ref();
|
||||||
const expandOrder = ref([]); //目前的展开行
|
const expandOrder = ref([]); //目前的展开行
|
||||||
const stockList = ref([]);
|
const stockList = ref([]);
|
||||||
@@ -702,7 +703,14 @@ async function selectAdviceBase(key, row) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 后续字段处理保持原样
|
// 后续字段处理保持原样
|
||||||
prescriptionList.value[rowIndex.value].orgId = undefined;
|
// 🔧 修复执行科室逻辑:诊疗项目优先使用项目维护的所属科室(row.orgId)
|
||||||
|
// 如果所属科室为空,则回退到患者当前所在科室
|
||||||
|
if (row.adviceType === 3 && row.orgId) {
|
||||||
|
// 项目有所属科室,直接使用
|
||||||
|
prescriptionList.value[rowIndex.value].orgId = row.orgId;
|
||||||
|
} else {
|
||||||
|
prescriptionList.value[rowIndex.value].orgId = undefined;
|
||||||
|
}
|
||||||
prescriptionList.value[rowIndex.value].dose = undefined;
|
prescriptionList.value[rowIndex.value].dose = undefined;
|
||||||
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;
|
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;
|
||||||
prescriptionList.value[rowIndex.value].doseUnitCode =
|
prescriptionList.value[rowIndex.value].doseUnitCode =
|
||||||
@@ -1021,13 +1029,18 @@ function handleSave() {
|
|||||||
})
|
})
|
||||||
groupIndexList.value = []
|
groupIndexList.value = []
|
||||||
groupList.value = []
|
groupList.value = []
|
||||||
nextId.value == 1;
|
nextId.value = 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单行处方保存
|
// 单行处方保存
|
||||||
function handleSaveSign(row, index) {
|
function handleSaveSign(row, index) {
|
||||||
|
// 🔧 Bug Fix #437: 防重复提交锁
|
||||||
|
if (isSaving.value) {
|
||||||
|
proxy.$modal.msgWarning('正在保存中,请勿重复提交');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 🔧 Bug Fix #238: 诊疗项目必须选择执行科室
|
// 🔧 Bug Fix #238: 诊疗项目必须选择执行科室
|
||||||
if (row.adviceType === 3 && !row.orgId) {
|
if (row.adviceType === 3 && !row.orgId) {
|
||||||
proxy.$modal.msgWarning('诊疗项目必须选择执行科室');
|
proxy.$modal.msgWarning('诊疗项目必须选择执行科室');
|
||||||
@@ -1035,33 +1048,37 @@ function handleSaveSign(row, index) {
|
|||||||
}
|
}
|
||||||
proxy.$refs['formRef' + index].validate((valid) => {
|
proxy.$refs['formRef' + index].validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
isSaving.value = true; // #437 加锁
|
||||||
row.isEdit = false;
|
row.isEdit = false;
|
||||||
isAdding.value = false;
|
isAdding.value = false;
|
||||||
expandOrder.value = [];
|
expandOrder.value = [];
|
||||||
row.patientId = props.patientInfo.patientId;
|
row.patientId = props.patientInfo.patientId;
|
||||||
row.encounterId = props.patientInfo.encounterId;
|
row.encounterId = props.patientInfo.encounterId;
|
||||||
row.accountId = props.patientInfo.accountId;
|
row.accountId = props.patientInfo.accountId;
|
||||||
row.contentJson = JSON.stringify(row);
|
const cleanRow = JSON.parse(JSON.stringify(row));
|
||||||
row.dbOpType = row.requestId ? '2' : '1';
|
cleanRow.contentJson = JSON.stringify(cleanRow);
|
||||||
row.minUnitQuantity = row.quantity * row.partPercent;
|
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
|
||||||
row.categoryEnum = row.adviceType
|
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
|
||||||
|
cleanRow.categoryEnum = cleanRow.adviceType
|
||||||
// 如果是手术计费,设置生成来源和来源业务单据号
|
// 如果是手术计费,设置生成来源和来源业务单据号
|
||||||
if (props.patientInfo.sourceBillNo) {
|
if (props.patientInfo.sourceBillNo) {
|
||||||
row.generateSourceEnum = 6; // 手术计费
|
cleanRow.generateSourceEnum = 6; // 手术计费
|
||||||
row.sourceBillNo = props.patientInfo.sourceBillNo;
|
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
|
||||||
}
|
}
|
||||||
console.log('row', row)
|
console.log('cleanRow', cleanRow)
|
||||||
savePrescription({ adviceSaveList: [row] }).then((res) => {
|
savePrescription({ adviceSaveList: [cleanRow] }).then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
proxy.$modal.msgSuccess('保存成功');
|
proxy.$modal.msgSuccess('保存成功');
|
||||||
getListInfo(false);
|
getListInfo(false);
|
||||||
nextId.value == 1;
|
nextId.value = 1;
|
||||||
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
|
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
|
||||||
if (row.adviceType === 3 && !row.orgId) {
|
if (row.adviceType === 3 && !row.orgId) {
|
||||||
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', row);
|
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
|
||||||
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
|
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
isSaving.value = false; // #437 释放锁
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -377,6 +377,7 @@ import { Search, Refresh, Printer, Edit, View, Delete } from '@element-plus/icon
|
|||||||
import { queryConsultationListPage, cancelConsultation, saveConsultation, getConsultationOpinions } from './api'
|
import { queryConsultationListPage, cancelConsultation, saveConsultation, getConsultationOpinions } from './api'
|
||||||
import { simplePrint, PRINT_TEMPLATE } from '@/utils/printUtils.js'
|
import { simplePrint, PRINT_TEMPLATE } from '@/utils/printUtils.js'
|
||||||
import { formatDate } from '@/utils/index.js'
|
import { formatDate } from '@/utils/index.js'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -548,15 +549,20 @@ const handleCurrentChange = (val) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePrint = async (row) => {
|
const handlePrint = async (row) => {
|
||||||
const printRow = row || currentRow.value
|
// 如果传入了具体行数据,则只打印该行;否则打印所有数据
|
||||||
|
const printRows = row ? [row] : tableData.value;
|
||||||
|
|
||||||
if (!printRow) {
|
if (printRows.length === 0) {
|
||||||
ElMessage.warning('请先选择一条记录')
|
ElMessage.warning('没有可打印的数据');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const hospitalName = userStore.tenantName || userStore.hospitalName || '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const printData = {
|
const printDataList = printRows.map(printRow => ({
|
||||||
|
hospitalName,
|
||||||
patientName: printRow.patientName || '',
|
patientName: printRow.patientName || '',
|
||||||
gender: printRow.genderEnum === 1 ? '男' : '女',
|
gender: printRow.genderEnum === 1 ? '男' : '女',
|
||||||
age: printRow.age || '',
|
age: printRow.age || '',
|
||||||
@@ -565,11 +571,11 @@ const handlePrint = async (row) => {
|
|||||||
consultationReason: printRow.consultationPurpose || '',
|
consultationReason: printRow.consultationPurpose || '',
|
||||||
applyTime: printRow.consultationRequestDate || '',
|
applyTime: printRow.consultationRequestDate || '',
|
||||||
applyDoctor: printRow.requestingPhysician || ''
|
applyDoctor: printRow.requestingPhysician || ''
|
||||||
}
|
}));
|
||||||
await simplePrint(PRINT_TEMPLATE.CONSULTATION, printData)
|
await simplePrint(PRINT_TEMPLATE.CONSULTATION, printDataList);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('会诊申请单打印失败:', error)
|
console.error('会诊申请单打印失败:', error);
|
||||||
ElMessage.error('打印失败')
|
ElMessage.error('打印失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
<el-tag v-else type="warning" size="small">已确认</el-tag>
|
<el-tag v-else type="warning" size="small">已确认</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="opinion-content">
|
<div class="opinion-content">
|
||||||
{{ opinion.opinion }}
|
{{ formatOpinionContent(opinion.opinion) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="opinion.signatureTime" class="opinion-footer">
|
<div v-if="opinion.signatureTime" class="opinion-footer">
|
||||||
签名时间:{{ formatDateTime(opinion.signatureTime) }}
|
签名时间:{{ formatDateTime(opinion.signatureTime) }}
|
||||||
@@ -301,6 +301,16 @@ const formatDateTime = (date) => {
|
|||||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bug #388:去掉"科室-参加医师:"前缀,只显示意见内容
|
||||||
|
const formatOpinionContent = (opinion) => {
|
||||||
|
if (!opinion) return ''
|
||||||
|
const colonIndex = opinion.indexOf(':')
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
return opinion.substring(colonIndex + 1)
|
||||||
|
}
|
||||||
|
return opinion // 向后兼容旧数据
|
||||||
|
}
|
||||||
|
|
||||||
const getDoctorName = () => userStore.nickName || userStore.name || '当前医生'
|
const getDoctorName = () => userStore.nickName || userStore.name || '当前医生'
|
||||||
const getDoctorDept = () => userStore.orgName || '当前科室'
|
const getDoctorDept = () => userStore.orgName || '当前科室'
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,20 @@ export function auditInfectiousCard(data) {
|
|||||||
* @param {string} data.returnReason - 退回原因
|
* @param {string} data.returnReason - 退回原因
|
||||||
* @param {string} data.status - 审核状态(5:审核失败)
|
* @param {string} data.status - 审核状态(5:审核失败)
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 撤销审核传染病报卡
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {string} data.cardNo
|
||||||
|
* @param {string} data.status
|
||||||
|
*/
|
||||||
|
export function revokeAuditCard(data) {
|
||||||
|
return request({
|
||||||
|
url: '/report-manage/infectiousDiseaseReport/revokeAudit',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function returnInfectiousCard(data) {
|
export function returnInfectiousCard(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/report-manage/infectiousDiseaseReport/return',
|
url: '/report-manage/infectiousDiseaseReport/return',
|
||||||
|
|||||||
@@ -76,78 +76,61 @@
|
|||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-form :model="filterForm" ref="filterFormRef" label-width="80px" :inline="true" style="flex: 1; overflow: hidden;">
|
<!-- 修复【#396】:优化搜索查询区域布局,所有筛选条件单行排列,按钮紧凑靠右 -->
|
||||||
<el-row :gutter="16">
|
<el-form :model="filterForm" ref="filterFormRef" label-width="70px" :inline="true">
|
||||||
<!-- 第一行 -->
|
<div class="filter-row">
|
||||||
<el-col :span="24">
|
<el-form-item label="登记来源">
|
||||||
<el-row :gutter="16">
|
<el-select v-model="filterForm.source" placeholder="全部" clearable style="width: 100px">
|
||||||
<el-col :span="3">
|
<el-option label="门诊" value="1" />
|
||||||
<el-form-item label="登记来源">
|
<el-option label="住院" value="2" />
|
||||||
<el-select v-model="filterForm.source" placeholder="全部" clearable>
|
<el-option label="急诊" value="3" />
|
||||||
<el-option label="门诊" value="1" />
|
<el-option label="体检" value="4" />
|
||||||
<el-option label="住院" value="2" />
|
</el-select>
|
||||||
<el-option label="急诊" value="3" />
|
</el-form-item>
|
||||||
<el-option label="体检" value="4" />
|
<el-form-item label="上报时间">
|
||||||
</el-select>
|
<el-date-picker
|
||||||
</el-form-item>
|
v-model="filterForm.dateRange"
|
||||||
</el-col>
|
type="daterange"
|
||||||
<el-col :span="6">
|
range-separator="至"
|
||||||
<el-form-item label="上报时间">
|
start-placeholder="开始日期"
|
||||||
<el-date-picker
|
end-placeholder="结束日期"
|
||||||
v-model="filterForm.dateRange"
|
value-format="YYYY-MM-DD"
|
||||||
type="daterange"
|
style="width: 220px"
|
||||||
range-separator="至"
|
/>
|
||||||
start-placeholder="开始日期"
|
</el-form-item>
|
||||||
end-placeholder="结束日期"
|
<el-form-item label="患者姓名">
|
||||||
value-format="YYYY-MM-DD"
|
<el-input v-model="filterForm.patientName" placeholder="请输入" clearable style="width: 120px" />
|
||||||
/>
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="审核状态">
|
||||||
</el-col>
|
<el-select v-model="filterForm.status" placeholder="全部" clearable style="width: 110px">
|
||||||
<el-col :span="3">
|
<el-option
|
||||||
<el-form-item label="患者姓名">
|
v-for="item in auditStatusList.filter(s => ['1', '2', '3', '5'].includes(s.value))"
|
||||||
<el-input v-model="filterForm.patientName" placeholder="请输入患者姓名" clearable />
|
:key="item.value"
|
||||||
</el-form-item>
|
:label="item.label"
|
||||||
</el-col>
|
:value="item.value"
|
||||||
</el-row>
|
/>
|
||||||
</el-col>
|
</el-select>
|
||||||
<!-- 第二行 -->
|
</el-form-item>
|
||||||
<el-col :span="24">
|
<el-form-item label="上报科室">
|
||||||
<el-row :gutter="16">
|
<el-tree-select
|
||||||
<el-col :span="3">
|
v-model="filterForm.deptId"
|
||||||
<el-form-item label="审核状态">
|
:data="deptTreeData"
|
||||||
<el-select v-model="filterForm.status" placeholder="全部" clearable>
|
:props="{ children: 'children', label: 'label', value: 'value' }"
|
||||||
<el-option
|
placeholder="全部科室"
|
||||||
v-for="item in auditStatusList.filter(s => ['1', '2', '3', '5'].includes(s.value))"
|
clearable
|
||||||
:key="item.value"
|
check-strictly
|
||||||
:label="item.label"
|
style="width: 140px"
|
||||||
:value="item.value"
|
/>
|
||||||
/>
|
</el-form-item>
|
||||||
</el-select>
|
<div class="filter-buttons">
|
||||||
</el-form-item>
|
<el-button type="primary" @click="handleSearch" :loading="loading">
|
||||||
</el-col>
|
<el-icon><Search /></el-icon> 查询
|
||||||
<el-col :span="3">
|
</el-button>
|
||||||
<el-form-item label="上报科室">
|
<el-button @click="handleReset">
|
||||||
<el-tree-select
|
<el-icon><Refresh /></el-icon> 重置
|
||||||
v-model="filterForm.deptId"
|
</el-button>
|
||||||
:data="deptTreeData"
|
</div>
|
||||||
:props="{ children: 'children', label: 'label', value: 'value' }"
|
</div>
|
||||||
placeholder="全部科室"
|
|
||||||
clearable
|
|
||||||
check-strictly
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="18" style="display: flex; align-items: center; justify-content: flex-end;">
|
|
||||||
<el-button type="primary" @click="handleSearch" :loading="loading">
|
|
||||||
<el-icon><Search /></el-icon> 查询
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleReset">
|
|
||||||
<el-icon><Refresh /></el-icon> 重置
|
|
||||||
</el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -215,13 +198,21 @@
|
|||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.status === 1"
|
v-if="row.status === 1 || row.status === 5"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="handleAudit(row)"
|
@click="handleAudit(row)"
|
||||||
>
|
>
|
||||||
审核
|
审核
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 2"
|
||||||
|
type="warning"
|
||||||
|
link
|
||||||
|
@click="handleRevokeAudit(row)"
|
||||||
|
>
|
||||||
|
撤销审核
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="info"
|
type="info"
|
||||||
link
|
link
|
||||||
@@ -582,6 +573,7 @@ import {
|
|||||||
getInfectiousCard,
|
getInfectiousCard,
|
||||||
auditInfectiousCard,
|
auditInfectiousCard,
|
||||||
returnInfectiousCard,
|
returnInfectiousCard,
|
||||||
|
revokeAuditCard,
|
||||||
batchAuditCards,
|
batchAuditCards,
|
||||||
batchReturnCards,
|
batchReturnCards,
|
||||||
getDeptTree,
|
getDeptTree,
|
||||||
@@ -842,6 +834,38 @@ function handleView(row) {
|
|||||||
loadCardDetail(row.cardNo);
|
loadCardDetail(row.cardNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 撤销审核(Bug #395 修复)
|
||||||
|
async function handleRevokeAudit(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要撤销【${row.patientName}】的报卡审核吗?撤销后状态将变为"待审核"。`,
|
||||||
|
'确认撤销',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await revokeAuditCard({
|
||||||
|
cardNo: row.cardNo,
|
||||||
|
status: '1' // 撤销后状态变更为待审核
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('撤销审核成功');
|
||||||
|
loadTableData();
|
||||||
|
loadStats();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '撤销审核失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载卡片详情
|
// 加载卡片详情
|
||||||
async function loadCardDetail(cardNo) {
|
async function loadCardDetail(cardNo) {
|
||||||
drawerLoading.value = true;
|
drawerLoading.value = true;
|
||||||
@@ -944,10 +968,10 @@ async function handleReturnCard() {
|
|||||||
|
|
||||||
// 批量审核
|
// 批量审核
|
||||||
function handleBatchAudit() {
|
function handleBatchAudit() {
|
||||||
// 检查是否包含非待审核状态的报卡
|
// 检查是否包含非可审核状态的报卡(只有待审核和审核失败可以批量审核)
|
||||||
const nonPendingCards = selectedRows.value.filter(row => row.status !== 1);
|
const nonPendingCards = selectedRows.value.filter(row => row.status !== 1 && row.status !== 5);
|
||||||
if (nonPendingCards.length > 0) {
|
if (nonPendingCards.length > 0) {
|
||||||
ElMessage.warning('只能选择待审核状态的报卡');
|
ElMessage.warning('只能选择待审核或审核失败状态的报卡');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,10 +1016,10 @@ async function confirmBatchAudit() {
|
|||||||
|
|
||||||
// 批量退回
|
// 批量退回
|
||||||
function handleBatchReturn() {
|
function handleBatchReturn() {
|
||||||
// 检查是否包含非待审核状态的报卡
|
// 检查是否包含非可审核状态的报卡(只有待审核和审核失败可以批量退回)
|
||||||
const nonPendingCards = selectedRows.value.filter(row => row.status !== 1);
|
const nonPendingCards = selectedRows.value.filter(row => row.status !== 1 && row.status !== 5);
|
||||||
if (nonPendingCards.length > 0) {
|
if (nonPendingCards.length > 0) {
|
||||||
ElMessage.warning('只能选择待审核状态的报卡');
|
ElMessage.warning('只能选择待审核或审核失败状态的报卡');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,6 +1342,27 @@ function getAuditTypeName(type) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 修复【#396】:单行筛选布局样式 */
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row .el-form-item {
|
||||||
|
display: inline-flex !important;
|
||||||
|
width: auto !important;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* 表格区 */
|
/* 表格区 */
|
||||||
.table-section {
|
.table-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item label="需要病员及会诊目的:" prop="consultationPurpose">
|
<el-form-item label="简要病史及会诊目的:" prop="consultationPurpose">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.consultationPurpose"
|
v-model="formData.consultationPurpose"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="exam-app-container">
|
<div class="exam-app-container" style="width: 100%; max-width: 1200px;">
|
||||||
<!-- ====== 顶部卡片:申请单列表 ====== -->
|
<!-- ====== 顶部卡片:申请单列表 ====== -->
|
||||||
<div class="top-section">
|
<div class="top-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<el-row :gutter="12">
|
<el-row :gutter="12">
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="申请单号" prop="applyNo">
|
<el-form-item label="申请单号" prop="applyNo">
|
||||||
<el-input v-model="form.applyNo" readonly />
|
<el-input v-model="form.applyNo" readonly placeholder="自动生成" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
@@ -104,19 +104,41 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="申请科室" prop="applyDeptCode">
|
<el-form-item label="申请科室" prop="applyDeptCode">
|
||||||
<el-input v-model="form.applyDeptCode" />
|
<el-input v-model="form.applyDeptCode" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="12">
|
<el-row :gutter="12">
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="申请医生" prop="applyDocCode">
|
<el-form-item label="申请医生" prop="applyDocCode">
|
||||||
<el-input v-model="form.applyDocCode" />
|
<el-input v-model="form.applyDocCode" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="执行科室" prop="performDeptCode">
|
<el-form-item label="执行科室" prop="performDeptCode">
|
||||||
<el-input v-model="form.performDeptCode" />
|
<el-select
|
||||||
|
v-model="form.performDeptCode"
|
||||||
|
style="width: 100%"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
clearable
|
||||||
|
placeholder="请选择执行科室(支持模糊查询)"
|
||||||
|
:remote-method="handleOrgRemoteSearch"
|
||||||
|
:loading="orgLoading"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in orgFilteredOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
<template #empty>
|
||||||
|
<div style="padding: 10px 0; color: #909399; text-align: center;">
|
||||||
|
{{ orgLoading ? '加载中...' : '暂无匹配科室' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -162,16 +184,10 @@
|
|||||||
<el-input v-model="form.inspectionArea" readonly />
|
<el-input v-model="form.inspectionArea" readonly />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<!-- Bug #384修复: 添加检查方法只读输入框,联动显示选中的检查方法 -->
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="检查方法">
|
<el-form-item label="检查方法">
|
||||||
<el-select v-model="form.inspectionMethod" placeholder="请选择" clearable filterable style="width: 100%;">
|
<el-input v-model="form.selectedMethodDisplay" readonly placeholder="请在右侧选择" />
|
||||||
<el-option
|
|
||||||
v-for="method in availableMethods"
|
|
||||||
:key="method.id"
|
|
||||||
:label="method.name"
|
|
||||||
:value="method.name"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -195,32 +211,60 @@
|
|||||||
|
|
||||||
<!-- TAB2:检查明细 -->
|
<!-- TAB2:检查明细 -->
|
||||||
<el-tab-pane label="检查明细" name="applyDetail">
|
<el-tab-pane label="检查明细" name="applyDetail">
|
||||||
|
<!-- 🔧 BugFix#426: 支持树形展开显示套餐明细 -->
|
||||||
<el-table
|
<el-table
|
||||||
ref="detailTableRef"
|
ref="detailTableRef"
|
||||||
:data="selectedItems"
|
:data="selectedItems"
|
||||||
|
row-key="id"
|
||||||
border
|
border
|
||||||
size="small"
|
size="small"
|
||||||
style="width:100%"
|
style="width:100%"
|
||||||
:max-height="350"
|
:max-height="350"
|
||||||
:header-cell-style="{ background: '#f5f5f5', color: '#303133' }"
|
:header-cell-style="{ background: '#f5f5f5', color: '#303133' }"
|
||||||
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
|
:load="loadPackageDetails"
|
||||||
|
lazy
|
||||||
>
|
>
|
||||||
<el-table-column label="行" type="index" width="45" align="center" />
|
<el-table-column label="行" type="index" width="45" align="center" />
|
||||||
<el-table-column label="检查项目" prop="name" min-width="120" />
|
<el-table-column label="检查项目" prop="name" min-width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
||||||
|
<span>{{ scope.row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="部位" prop="applyPart" min-width="90">
|
<el-table-column label="部位" prop="applyPart" min-width="90">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-input v-model="scope.row.applyPart" size="small" />
|
<el-input v-model="scope.row.applyPart" size="small" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="检查方法" min-width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<!-- Bug #384修复: 显示检查方法名称,不显示套餐名称 -->
|
||||||
|
<span v-if="scope.row.selectedMethod">
|
||||||
|
{{ scope.row.selectedMethod.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="scope.row.methods && scope.row.methods.length > 0" style="color: #909399;">
|
||||||
|
未选择
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: #c0c4cc;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="单位" prop="unit" width="55" align="center" />
|
<el-table-column label="单位" prop="unit" width="55" align="center" />
|
||||||
<el-table-column label="总量" prop="quantity" width="70" align="center">
|
<el-table-column label="总量" prop="quantity" width="70" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-input-number v-model="scope.row.quantity" :min="1" size="small" :controls="false" style="width:100%" />
|
<el-input-number v-model="scope.row.quantity" :min="1" size="small" :controls="false" style="width:100%" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="单价" prop="price" width="75" align="right" />
|
<!-- Bug #384修复: 单价显示套餐价格(如果选中)或部位价格 -->
|
||||||
|
<el-table-column label="单价" width="75" align="right">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.selectedMethod?.packagePrice || scope.row.price }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- Bug #384修复: 金额使用有效价格计算 -->
|
||||||
<el-table-column label="金额" width="80" align="right">
|
<el-table-column label="金额" width="80" align="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ ((scope.row.price || 0) * (scope.row.quantity || 1)).toFixed(2) }}
|
{{ ((scope.row.selectedMethod?.packagePrice || scope.row.price || 0) * (scope.row.quantity || 1)).toFixed(2) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="类型" prop="checkType" width="70" align="center" />
|
<el-table-column label="类型" prop="checkType" width="70" align="center" />
|
||||||
@@ -262,6 +306,7 @@
|
|||||||
v-for="cat in filteredCategoryList"
|
v-for="cat in filteredCategoryList"
|
||||||
:key="cat.typeId"
|
:key="cat.typeId"
|
||||||
:name="cat.typeId"
|
:name="cat.typeId"
|
||||||
|
@click="handleCategoryExpand(cat)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="cat-title">{{ cat.categoryName }}</span>
|
<span class="cat-title">{{ cat.categoryName }}</span>
|
||||||
@@ -278,29 +323,55 @@
|
|||||||
>
|
>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
<span class="item-price">¥{{ item.price }}</span>
|
<span class="item-price">¥{{ item.price }}/{{ item.unit || "次" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:已选择 tags -->
|
<!-- 右侧:已选择 项目卡片(可展开显示检查方法) -->
|
||||||
<div class="selected-panel">
|
<div class="selected-panel">
|
||||||
<div class="panel-label">已选择:</div>
|
<div class="panel-label">已选择:</div>
|
||||||
<div class="selected-tags">
|
<div class="selected-tags">
|
||||||
<div v-if="selectedItems.length === 0" class="empty-selected">–</div>
|
<div v-if="selectedItems.length === 0" class="empty-selected">–</div>
|
||||||
<el-tag
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-for="(item, idx) in selectedItems"
|
v-for="(item, idx) in selectedItems"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
closable
|
class="selected-item-card"
|
||||||
size="small"
|
|
||||||
@close="handleRemoveItem(idx, item)"
|
|
||||||
class="selected-tag"
|
|
||||||
>
|
>
|
||||||
{{ item.name }} ¥{{ item.price }}
|
<!-- Bug #384修复: 项目卡片头部,可展开/收起 -->
|
||||||
</el-tag>
|
<div class="card-header" @click="toggleItemExpand(item)">
|
||||||
|
<span class="card-name">{{ item.name }}</span>
|
||||||
|
<span class="card-price">¥{{ item.price }}</span>
|
||||||
|
<!-- 展开图标 -->
|
||||||
|
<el-icon :class="['expand-icon', { expanded: item.expanded }]">
|
||||||
|
<ArrowDown v-if="!item.expanded" />
|
||||||
|
<ArrowUp v-if="item.expanded" />
|
||||||
|
</el-icon>
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- Bug #384修复: 展开后显示检查方法勾选框列表 -->
|
||||||
|
<div v-if="item.expanded && item.methods && item.methods.length > 0" class="method-list">
|
||||||
|
<div
|
||||||
|
v-for="method in item.methods"
|
||||||
|
:key="method.id"
|
||||||
|
class="method-option"
|
||||||
|
>
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="item.selectedMethod?.id === method.id"
|
||||||
|
@change="(val) => selectMethodCheckbox(val, item, method)"
|
||||||
|
>
|
||||||
|
<span class="method-name">{{ method.name }}</span>
|
||||||
|
<span class="method-price">¥{{ method.packagePrice || item.price }}</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,10 +383,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
|
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { Printer, Delete } from '@element-plus/icons-vue';
|
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
|
||||||
import useUserStore from '@/store/modules/user';
|
import useUserStore from '@/store/modules/user';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import { listCheckMethod } from '@/api/system/checkType';
|
import { listCheckMethod, searchCheckMethod } from '@/api/system/checkType';
|
||||||
|
import { getEncounterDiagnosis } from '../api.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
patientInfo: { type: Object, default: () => ({}) },
|
patientInfo: { type: Object, default: () => ({}) },
|
||||||
@@ -331,6 +403,36 @@ const dictLoading = ref(false);
|
|||||||
const activeDetailTab = ref('applyForm');
|
const activeDetailTab = ref('applyForm');
|
||||||
const applicationList = ref([]);
|
const applicationList = ref([]);
|
||||||
const selectedItems = ref([]);
|
const selectedItems = ref([]);
|
||||||
|
|
||||||
|
// 🔧 BugFix#426: 懒加载套餐明细
|
||||||
|
async function loadPackageDetails(row, treeNode, resolve) {
|
||||||
|
if (!row.isPackage || !row.packageId) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: `/exam/package/${row.packageId}/details`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const children = res.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
name: item.name || item.itemName,
|
||||||
|
unit: item.unit || '次',
|
||||||
|
price: item.price || item.itemPrice || 0,
|
||||||
|
quantity: row.quantity || 1,
|
||||||
|
isPackageDetail: true
|
||||||
|
}));
|
||||||
|
resolve(children);
|
||||||
|
} else {
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载套餐明细失败:', err);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
const detailTableRef = ref(null);
|
const detailTableRef = ref(null);
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
|
|
||||||
@@ -362,7 +464,8 @@ const form = reactive({
|
|||||||
isCharged: 0,
|
isCharged: 0,
|
||||||
isRefunded: 0,
|
isRefunded: 0,
|
||||||
isExecuted: 0,
|
isExecuted: 0,
|
||||||
examTypeCode: '' // 检查类型编码,必填字段,保存时从已选项目自动推导
|
examTypeCode: '', // 检查类型编码,必填字段,保存时从已选项目自动推导
|
||||||
|
selectedMethodDisplay: '' // Bug #384修复: 检查方法显示字段(联动)
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -383,6 +486,11 @@ const activeNames = ref([]); // 当前展开的折叠项
|
|||||||
|
|
||||||
const allMethods = ref([]);
|
const allMethods = ref([]);
|
||||||
|
|
||||||
|
// ====== 科室下拉(来源:科室管理)======
|
||||||
|
const orgLoading = ref(false);
|
||||||
|
const orgOptions = ref([]); // { label, value }
|
||||||
|
const orgFilteredOptions = ref([]); // 展示用(截断前200条)
|
||||||
|
|
||||||
// 加载所有检查方法
|
// 加载所有检查方法
|
||||||
async function loadAllMethods() {
|
async function loadAllMethods() {
|
||||||
try {
|
try {
|
||||||
@@ -408,10 +516,61 @@ async function loadAllMethods() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await loadOrgOptions();
|
||||||
await loadAllMethods();
|
await loadAllMethods();
|
||||||
await loadCategoryList();
|
await loadCategoryList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadOrgOptions() {
|
||||||
|
orgLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: '/base-data-manage/organization/organization',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
const records = res?.data?.records || res?.data || [];
|
||||||
|
|
||||||
|
const flat = [];
|
||||||
|
const walk = (nodes) => {
|
||||||
|
if (!Array.isArray(nodes)) return;
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (!n) continue;
|
||||||
|
// 约定:typeEnum=2 为科室;若没有 typeEnum 也兜底收集
|
||||||
|
if (n.name && (n.typeEnum === 2 || n.typeEnum === '2' || n.typeEnum == null)) {
|
||||||
|
flat.push({ label: n.name, value: n.name });
|
||||||
|
}
|
||||||
|
if (Array.isArray(n.children) && n.children.length > 0) walk(n.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(records);
|
||||||
|
|
||||||
|
// 去重 + 排序
|
||||||
|
const uniq = Array.from(new Map(flat.map(o => [o.value, o])).values())
|
||||||
|
.filter(o => o?.value)
|
||||||
|
.sort((a, b) => (a.label || '').localeCompare(b.label || '', 'zh-CN'));
|
||||||
|
|
||||||
|
orgOptions.value = uniq;
|
||||||
|
orgFilteredOptions.value = uniq.slice(0, 200);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载科室列表失败', e);
|
||||||
|
orgOptions.value = [];
|
||||||
|
orgFilteredOptions.value = [];
|
||||||
|
} finally {
|
||||||
|
orgLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOrgRemoteSearch(keyword) {
|
||||||
|
const key = (keyword || '').trim().toLowerCase();
|
||||||
|
if (!key) {
|
||||||
|
orgFilteredOptions.value = orgOptions.value.slice(0, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
orgFilteredOptions.value = orgOptions.value
|
||||||
|
.filter((o) => (o.label || '').toLowerCase().includes(key))
|
||||||
|
.slice(0, 200);
|
||||||
|
}
|
||||||
|
|
||||||
// 动态可用的检查方法(根据已选部位所属的检查类型进行过滤)
|
// 动态可用的检查方法(根据已选部位所属的检查类型进行过滤)
|
||||||
const normalizeTypeValue = value => String(value ?? '').trim().toLowerCase();
|
const normalizeTypeValue = value => String(value ?? '').trim().toLowerCase();
|
||||||
|
|
||||||
@@ -438,6 +597,33 @@ const availableMethods = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
|
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
|
||||||
|
// #428: 分类展开时联动加载检查方法
|
||||||
|
async function handleCategoryExpand(cat) {
|
||||||
|
if (!cat || !cat.typeName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await searchCheckMethod({ checkType: cat.typeName });
|
||||||
|
let data = res?.data?.data || res?.data || res?.rows || res;
|
||||||
|
if (!Array.isArray(data) && res?.data && Array.isArray(res.data.data)) {
|
||||||
|
data = res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
cat.methods = data.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
code: m.code,
|
||||||
|
price: m.price || 0,
|
||||||
|
packageName: m.packageName || '',
|
||||||
|
packagePrice: m.packagePrice || null,
|
||||||
|
serviceFee: m.serviceFee || null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载分类检查方法失败', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(availableMethods, (newMethods) => {
|
watch(availableMethods, (newMethods) => {
|
||||||
if (form.inspectionMethod && !newMethods.find(m => m.name === form.inspectionMethod)) {
|
if (form.inspectionMethod && !newMethods.find(m => m.name === form.inspectionMethod)) {
|
||||||
form.inspectionMethod = '';
|
form.inspectionMethod = '';
|
||||||
@@ -497,6 +683,8 @@ async function loadCategoryList() {
|
|||||||
orgType: t.type, // 保存 type 用于后备匹配
|
orgType: t.type, // 保存 type 用于后备匹配
|
||||||
typeName: t.name, // 保存 name
|
typeName: t.name, // 保存 name
|
||||||
categoryName: t.name,
|
categoryName: t.name,
|
||||||
|
// “检查类型管理”里配置的执行科室(图三)
|
||||||
|
performDeptName: t.department || '',
|
||||||
items: []
|
items: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -510,6 +698,7 @@ async function loadCategoryList() {
|
|||||||
unit: '次',
|
unit: '次',
|
||||||
checkType: p.checkType || '',
|
checkType: p.checkType || '',
|
||||||
nationalCode: p.nationalCode || '',
|
nationalCode: p.nationalCode || '',
|
||||||
|
packageName: p.packageName || '',
|
||||||
checked: false
|
checked: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -551,9 +740,11 @@ const filteredCategoryList = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ====== 合计 ======
|
// ====== 合计 ======
|
||||||
|
// Bug #384修复: 如果选中了检查方法,使用套餐价格;否则使用部位价格
|
||||||
const totalAmountCalc = computed(() => {
|
const totalAmountCalc = computed(() => {
|
||||||
const total = selectedItems.value.reduce((sum, item) => {
|
const total = selectedItems.value.reduce((sum, item) => {
|
||||||
return sum + (item.price * (item.quantity || 1));
|
const effectivePrice = item.selectedMethod?.packagePrice || item.price;
|
||||||
|
return sum + (effectivePrice * (item.quantity || 1));
|
||||||
}, 0);
|
}, 0);
|
||||||
return total.toFixed(2);
|
return total.toFixed(2);
|
||||||
});
|
});
|
||||||
@@ -572,26 +763,57 @@ watch(() => props.patientInfo, (newVal) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
watch(() => props.activeTab, (val) => {
|
watch(() => props.activeTab, async (val) => {
|
||||||
if (val === 'examination') getList();
|
if (val === 'examination') {
|
||||||
|
getList();
|
||||||
|
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
|
||||||
|
if (props.patientInfo?.encounterId) {
|
||||||
|
await loadClinicalDiag();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function initPatientForm(patient) {
|
function initPatientForm(patient) {
|
||||||
form.patientName = patient.patientName || '';
|
form.patientName = patient.patientName || '';
|
||||||
form.medicalrecordNumber = patient.busNo || patient.visitNo || '';
|
// 就诊卡号应取值于 identifierNo,而非 busNo(busNo 是病历号)
|
||||||
|
form.medicalrecordNumber = patient.identifierNo || patient.visitNo || '';
|
||||||
form.patientId = patient.patientId || '';
|
form.patientId = patient.patientId || '';
|
||||||
form.visitNo = patient.visitNo || '';
|
form.visitNo = patient.visitNo || '';
|
||||||
form.applyDeptCode = userStore.orgName || patient.organizationName || '';
|
form.applyDeptCode = userStore.orgName || patient.organizationName || '';
|
||||||
form.applyDocCode = userStore.nickName || '';
|
form.applyDocCode = userStore.nickName || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载临床诊断:获取患者主诊断并填充到临床诊断字段
|
||||||
|
async function loadClinicalDiag() {
|
||||||
|
if (!props.patientInfo?.encounterId) return;
|
||||||
|
try {
|
||||||
|
const res = await getEncounterDiagnosis(props.patientInfo.encounterId);
|
||||||
|
const diagnoses = res.data || res.rows || res;
|
||||||
|
if (Array.isArray(diagnoses) && diagnoses.length > 0) {
|
||||||
|
// Bug #380, #381 修复: 主诊断字段名为 maindiseFlag (后端 DiagnosisQueryDto 定义)
|
||||||
|
const mainDiag = diagnoses.find(d => d.maindiseFlag === 1 || d.maindiseFlag === '1');
|
||||||
|
// 如果有主诊断使用主诊断,否则使用第一个诊断
|
||||||
|
const targetDiag = mainDiag || diagnoses[0];
|
||||||
|
// 优先使用 diagnosisName,其次是 conditionName 或 name
|
||||||
|
form.clinicalDiag = targetDiag.diagnosisName || targetDiag.conditionName || targetDiag.name || '';
|
||||||
|
} else {
|
||||||
|
// 如果没有诊断,清空临床诊断字段
|
||||||
|
form.clinicalDiag = '';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载临床诊断失败', err);
|
||||||
|
// 获取失败时不阻断用户操作,保持字段为空
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====== 申请单 CRUD ======
|
// ====== 申请单 CRUD ======
|
||||||
function getList() {
|
function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
request({
|
request({
|
||||||
url: '/exam/apply/list',
|
url: '/exam/apply/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { visitNo: props.patientInfo?.visitNo || '' }
|
// 默认只展示本次就诊(encounterId)产生的检查申请单
|
||||||
|
params: { encounterId: props.patientInfo?.encounterId || '' }
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
applicationList.value = res.rows || res.data || [];
|
applicationList.value = res.rows || res.data || [];
|
||||||
}).catch(err => console.error('获取申请单列表失败', err))
|
}).catch(err => console.error('获取申请单列表失败', err))
|
||||||
@@ -601,23 +823,30 @@ function getList() {
|
|||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
applyNo: '', patientId: props.patientInfo?.patientId || '',
|
applyNo: '',
|
||||||
|
patientId: props.patientInfo?.patientId || '',
|
||||||
visitNo: props.patientInfo?.visitNo || '',
|
visitNo: props.patientInfo?.visitNo || '',
|
||||||
|
// 保留患者姓名和就诊卡号,不应重置为空
|
||||||
|
patientName: props.patientInfo?.patientName || '',
|
||||||
|
medicalrecordNumber: props.patientInfo?.identifierNo || '',
|
||||||
applyDeptCode: userStore.orgName || '',
|
applyDeptCode: userStore.orgName || '',
|
||||||
performDeptCode: '',
|
performDeptCode: '',
|
||||||
applyDocCode: userStore.nickName || '',
|
applyDocCode: userStore.nickName || '',
|
||||||
applyTime: new Date().toISOString().split('T')[0] + ' 12:00:00',
|
applyTime: new Date().toISOString().split('T')[0] + ' 12:00:00',
|
||||||
medicalrecordNumber: props.patientInfo?.busNo || '',
|
|
||||||
natureofCost: '自费医疗',
|
natureofCost: '自费医疗',
|
||||||
clinicDesc: '', contraindication: '', medicalHistorySummary: '',
|
clinicDesc: '', contraindication: '', medicalHistorySummary: '',
|
||||||
purposeofInspection: '', inspectionArea: '', inspectionMethod: '',
|
purposeofInspection: '', inspectionArea: '', inspectionMethod: '',
|
||||||
applyRemark: '', clinicalDiag: '', purposeDesc: '',
|
applyRemark: '', clinicalDiag: '', purposeDesc: '',
|
||||||
isUrgent: 0, pregnancyState: 0, allergyDesc: '',
|
isUrgent: 0, pregnancyState: 0, allergyDesc: '',
|
||||||
applyStatus: 0, isCharged: 0, isRefunded: 0, isExecuted: 0
|
applyStatus: 0, isCharged: 0, isRefunded: 0, isExecuted: 0,
|
||||||
|
examTypeCode: '',
|
||||||
|
selectedMethodDisplay: '' // Bug #384修复: 重置检查方法显示
|
||||||
});
|
});
|
||||||
selectedItems.value = [];
|
selectedItems.value = [];
|
||||||
resetCategoryChecked();
|
resetCategoryChecked();
|
||||||
activeDetailTab.value = 'applyForm';
|
activeDetailTab.value = 'applyForm';
|
||||||
|
// 自动加载临床诊断
|
||||||
|
loadClinicalDiag();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
@@ -629,7 +858,8 @@ function handleSave() {
|
|||||||
}
|
}
|
||||||
// 从已选项目推导检查类型编码(取第一个项目的 checkType,如 CT / ECG / GI)
|
// 从已选项目推导检查类型编码(取第一个项目的 checkType,如 CT / ECG / GI)
|
||||||
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
|
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
|
||||||
if (!form.examTypeCode) form.examTypeCode = firstCheckType;
|
form.examTypeCode = firstCheckType;
|
||||||
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...form,
|
...form,
|
||||||
@@ -639,10 +869,16 @@ function handleSave() {
|
|||||||
itemCode: String(item.id),
|
itemCode: String(item.id),
|
||||||
itemName: item.name,
|
itemName: item.name,
|
||||||
bodyPartCode: item.checkType || 'unknown',
|
bodyPartCode: item.checkType || 'unknown',
|
||||||
itemFee: item.price,
|
// Bug #384修复: 如果选中了检查方法且有套餐价格,使用套餐价格;否则使用部位价格
|
||||||
|
itemFee: item.selectedMethod?.packagePrice || item.price,
|
||||||
performDeptCode: form.performDeptCode || '',
|
performDeptCode: form.performDeptCode || '',
|
||||||
itemStatus: 0,
|
itemStatus: 0,
|
||||||
itemSeq: index + 1
|
itemSeq: index + 1,
|
||||||
|
// 检查方法信息
|
||||||
|
checkMethodId: item.selectedMethod?.id || null,
|
||||||
|
checkMethodName: item.selectedMethod?.name || null,
|
||||||
|
checkMethodCode: item.selectedMethod?.code || null,
|
||||||
|
checkMethodPackageName: item.selectedMethod?.packageName || null // Bug #384修复: 保存套餐名称
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
request({
|
request({
|
||||||
@@ -661,22 +897,72 @@ function handleSave() {
|
|||||||
|
|
||||||
function handleRowClick(row) {
|
function handleRowClick(row) {
|
||||||
Object.assign(form, row);
|
Object.assign(form, row);
|
||||||
|
form.selectedMethodDisplay = ''; // Bug #384修复: 先清空,后面根据回充数据更新
|
||||||
selectedItems.value = [];
|
selectedItems.value = [];
|
||||||
activeDetailTab.value = 'applyForm';
|
activeDetailTab.value = 'applyForm';
|
||||||
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(res => {
|
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
|
||||||
const d = res.data || res;
|
const d = res.data || res;
|
||||||
if (d.data) Object.assign(form, d.data);
|
if (d.data) Object.assign(form, d.data);
|
||||||
if (d.items && Array.isArray(d.items)) {
|
if (d.items && Array.isArray(d.items)) {
|
||||||
selectedItems.value = d.items.map(m => ({
|
try {
|
||||||
id: m.itemCode, name: m.itemName,
|
// 为每个项目加载检查方法
|
||||||
price: m.itemFee || 0, quantity: 1,
|
const itemsWithMethods = await Promise.all(d.items.map(async m => {
|
||||||
serviceFee: 0, unit: '次',
|
const item = {
|
||||||
applyPart: m.itemName,
|
id: m.itemCode, name: m.itemName,
|
||||||
checkType: m.bodyPartCode || '',
|
price: m.itemFee || 0, quantity: 1,
|
||||||
nationalCode: '', checked: true
|
serviceFee: 0, unit: '次',
|
||||||
}));
|
applyPart: m.itemName,
|
||||||
syncCategoryChecked();
|
checkType: m.bodyPartCode || '',
|
||||||
|
nationalCode: '', checked: true,
|
||||||
|
methods: [],
|
||||||
|
selectedMethod: null,
|
||||||
|
expanded: false // Bug #384修复: 添加展开状态
|
||||||
|
};
|
||||||
|
// 加载该项目的检查方法
|
||||||
|
if (m.bodyPartCode) {
|
||||||
|
try {
|
||||||
|
const methodRes = await searchCheckMethod({ checkType: m.bodyPartCode });
|
||||||
|
// Bug #384修复: 正确解析 API 返回结构
|
||||||
|
let methodData = methodRes?.data?.data || methodRes?.data || methodRes?.rows || methodRes;
|
||||||
|
if (!Array.isArray(methodData) && methodRes?.data && Array.isArray(methodRes.data.data)) {
|
||||||
|
methodData = methodRes.data.data;
|
||||||
|
}
|
||||||
|
if (Array.isArray(methodData)) {
|
||||||
|
item.methods = methodData.map(md => ({
|
||||||
|
id: md.id,
|
||||||
|
name: md.name,
|
||||||
|
code: md.code,
|
||||||
|
price: m.itemFee || 0, // fallback 到已保存的价格
|
||||||
|
packageName: md.packageName || '',
|
||||||
|
packagePrice: md.packagePrice || null, // Bug #384修复: 套餐价格
|
||||||
|
serviceFee: md.serviceFee || null
|
||||||
|
}));
|
||||||
|
// 如果有已保存的检查方法信息,尝试匹配
|
||||||
|
if (m.checkMethodId) {
|
||||||
|
item.selectedMethod = item.methods.find(md => md.id === m.checkMethodId) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载检查方法失败', err);
|
||||||
|
// 单个项目加载失败不影响其他项目,继续返回 item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
selectedItems.value = itemsWithMethods;
|
||||||
|
syncCategoryChecked();
|
||||||
|
// Bug #384修复: 回充后更新检查方法显示
|
||||||
|
updateMethodDisplay();
|
||||||
|
// 修复【#408】:加载申请单详情后自动切换到检查明细页签,确保已加载的明细数据可见
|
||||||
|
activeDetailTab.value = 'applyDetail';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载申请单详情失败', err);
|
||||||
|
ElMessage.error('加载申请单详情失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('获取申请单详情失败', err);
|
||||||
|
ElMessage.error('获取申请单详情失败');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,27 +979,127 @@ function handleDelete(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ====== 勾选逻辑 ======
|
// ====== 勾选逻辑 ======
|
||||||
function handleItemSelect(checked, item, cat) {
|
async function handleItemSelect(checked, item, cat) {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
|
// Bug #384修复: 检查方法表的 checkType 字段关联的是检查类型的 name(中文名称,如"心电图")
|
||||||
|
const effectiveCheckType = cat?.typeName || cat?.categoryName || '';
|
||||||
|
|
||||||
|
// 查询该检查类型对应的检查方法
|
||||||
|
let methods = [];
|
||||||
|
try {
|
||||||
|
if (effectiveCheckType) {
|
||||||
|
const res = await searchCheckMethod({ checkType: effectiveCheckType });
|
||||||
|
// Bug #384修复: API 返回结构可能是 {data: {data: Array}} 或 {data: Array}
|
||||||
|
let data = res?.data?.data || res?.data || res?.rows || res;
|
||||||
|
if (!Array.isArray(data) && res?.data && Array.isArray(res.data.data)) {
|
||||||
|
data = res.data.data;
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
methods = data.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
code: m.code,
|
||||||
|
price: m.price || item.price, // fallback 到项目价格
|
||||||
|
packageName: m.packageName || '',
|
||||||
|
packagePrice: m.packagePrice || null, // Bug #384修复: 套餐价格
|
||||||
|
serviceFee: m.serviceFee || null // Bug #384修复: 服务费
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载检查方法失败', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedItems.value.length > 0) {
|
||||||
|
const currentCategory = selectedItems.value[0].checkType;
|
||||||
|
const newCategory = cat.typeCode || '';
|
||||||
|
if (currentCategory !== newCategory) {
|
||||||
|
ElMessage.warning('一个检查单不能同时选择多个项目类型的检查项目');
|
||||||
|
item.checked = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectedItems.value.push({
|
selectedItems.value.push({
|
||||||
id: item.id, name: item.name,
|
id: item.id, name: item.name,
|
||||||
price: item.price, quantity: 1,
|
price: item.price, quantity: 1,
|
||||||
serviceFee: item.serviceFee || 0,
|
serviceFee: item.serviceFee || 0,
|
||||||
unit: item.unit || '次',
|
unit: item.unit || '次',
|
||||||
applyPart: item.name,
|
applyPart: item.name,
|
||||||
checkType: cat.typeCode || '',
|
checkType: effectiveCheckType, // Bug #384修复: 使用有效的 checkType
|
||||||
nationalCode: item.nationalCode || '',
|
nationalCode: item.nationalCode || '',
|
||||||
checked: true
|
checked: true,
|
||||||
|
methods: methods,
|
||||||
|
selectedMethod: null,
|
||||||
|
expanded: false // Bug #384修复: 新增展开状态,默认不展开
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 自动回填执行科室:按检查项目类型 → 检查类型管理里配置的执行科室
|
||||||
|
if (selectedItems.value.length === 1 && cat?.performDeptName) {
|
||||||
|
form.performDeptCode = cat.performDeptName;
|
||||||
|
} else if (!form.performDeptCode && cat?.performDeptName) {
|
||||||
|
form.performDeptCode = cat.performDeptName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有且仅有一个检查方法,自动选中并更新显示
|
||||||
|
if (methods.length === 1) {
|
||||||
|
const lastIdx = selectedItems.value.length - 1;
|
||||||
|
selectedItems.value[lastIdx].selectedMethod = methods[0];
|
||||||
|
updateMethodDisplay(); // Bug #384修复: 联动更新显示
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const idx = selectedItems.value.findIndex(s => s.id === item.id);
|
const idx = selectedItems.value.findIndex(s => s.id === item.id);
|
||||||
if (idx > -1) selectedItems.value.splice(idx, 1);
|
if (idx > -1) selectedItems.value.splice(idx, 1);
|
||||||
|
|
||||||
|
if (selectedItems.value.length === 0) {
|
||||||
|
form.performDeptCode = '';
|
||||||
|
form.examTypeCode = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 有选项时切换到明细tab
|
// Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态
|
||||||
if (selectedItems.value.length > 0) {
|
}
|
||||||
activeDetailTab.value = 'applyDetail';
|
|
||||||
nextTick(() => detailTableRef.value?.doLayout());
|
// Bug #384修复: 展开/收起项目卡片
|
||||||
|
function toggleItemExpand(item) {
|
||||||
|
item.expanded = !item.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug #384修复: 勾选框选择检查方法(单选逻辑)
|
||||||
|
function selectMethodCheckbox(checked, item, method) {
|
||||||
|
if (checked) {
|
||||||
|
item.selectedMethod = method;
|
||||||
|
} else {
|
||||||
|
item.selectedMethod = null;
|
||||||
}
|
}
|
||||||
|
// 联动更新表单检查方法显示字段
|
||||||
|
updateMethodDisplay();
|
||||||
|
|
||||||
|
// #430: 套餐金额实时同步到申请单
|
||||||
|
nextTick(() => {
|
||||||
|
form.totalAmount = totalAmountCalc.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug #384修复: 更新检查方法显示字段(联动)
|
||||||
|
function updateMethodDisplay() {
|
||||||
|
// 找到第一个有选中检查方法的项目
|
||||||
|
const itemWithMethod = selectedItems.value.find(item => item.selectedMethod);
|
||||||
|
if (itemWithMethod?.selectedMethod) {
|
||||||
|
form.selectedMethodDisplay = itemWithMethod.selectedMethod.name; // 显示检查方法名称,不显示套餐名称
|
||||||
|
} else {
|
||||||
|
form.selectedMethodDisplay = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择检查方法
|
||||||
|
function selectMethod(item, method) {
|
||||||
|
if (item.selectedMethod?.id === method.id) {
|
||||||
|
item.selectedMethod = null;
|
||||||
|
} else {
|
||||||
|
item.selectedMethod = method;
|
||||||
|
}
|
||||||
|
// Bug #384修复: 联动更新表单检查方法显示字段
|
||||||
|
updateMethodDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveItem(idx, item) {
|
function handleRemoveItem(idx, item) {
|
||||||
@@ -723,6 +1109,15 @@ function handleRemoveItem(idx, item) {
|
|||||||
const found = cat.items.find(x => x.id === item.id);
|
const found = cat.items.find(x => x.id === item.id);
|
||||||
if (found) { found.checked = false; break; }
|
if (found) { found.checked = false; break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedItems.value.length === 0) {
|
||||||
|
form.performDeptCode = '';
|
||||||
|
form.examTypeCode = '';
|
||||||
|
form.selectedMethodDisplay = ''; // Bug #384修复: 清空检查方法显示
|
||||||
|
} else {
|
||||||
|
// Bug #384修复: 移除后重新计算检查方法显示
|
||||||
|
updateMethodDisplay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCategoryChecked() {
|
function resetCategoryChecked() {
|
||||||
@@ -890,7 +1285,7 @@ defineExpose({ getList });
|
|||||||
|
|
||||||
/* 已选择 tags */
|
/* 已选择 tags */
|
||||||
.selected-panel {
|
.selected-panel {
|
||||||
width: 120px;
|
width: 140px; /* Bug #384修复: 加宽以适应展开内容 */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -900,7 +1295,7 @@ defineExpose({ getList });
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.selected-tag {
|
.selected-tag {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -913,6 +1308,86 @@ defineExpose({ getList });
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bug #384修复: 已选择项目卡片(可展开) */
|
||||||
|
.selected-item-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #F5F5F5;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-card .card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-card .card-header:hover {
|
||||||
|
background: #E6F7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-price {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1890FF;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bug #384修复: 检查方法勾选框列表 */
|
||||||
|
.method-list {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-option :deep(.el-checkbox__label) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-option .method-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-option .method-price {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 折叠组件细节 */
|
/* 折叠组件细节 */
|
||||||
:deep(.el-collapse) {
|
:deep(.el-collapse) {
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -813,9 +813,14 @@ async function initForm() {
|
|||||||
const res = await getNextCardNo(orgCode);
|
const res = await getNextCardNo(orgCode);
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
cardNo = res.data;
|
cardNo = res.data;
|
||||||
|
} else {
|
||||||
|
// 🔧 BugFix#412: 如果API返回失败,生成临时卡号避免保存失败
|
||||||
|
cardNo = 'TEMP_' + Date.now();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取卡片编号失败:', err);
|
console.error('获取卡片编号失败:', err);
|
||||||
|
// 🔧 BugFix#412: 如果API调用异常,生成临时卡号
|
||||||
|
cardNo = 'TEMP_' + Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="inspection-application-container">
|
<el-container class="inspection-application-container">
|
||||||
|
|
||||||
<!-- 顶部操作按钮区 - Bug#334: 优化垂直空间利用率 -->
|
|
||||||
<el-header class="top-action-bar" height="48px">
|
<el-header class="top-action-bar" height="48px">
|
||||||
<el-row class="action-buttons" type="flex" justify="end" :gutter="8">
|
<el-row class="action-buttons" type="flex" justify="end" :gutter="8">
|
||||||
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
|
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
|
||||||
@@ -19,10 +17,22 @@
|
|||||||
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
|
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
|
||||||
<el-card class="table-card" style="width: 100%">
|
<el-card class="table-card" style="width: 100%">
|
||||||
<template #header>
|
<template #header>
|
||||||
<el-row class="card-header" type="flex" align="middle">
|
<div class="card-header-flex">
|
||||||
<el-icon><DocumentChecked /></el-icon>
|
<div class="header-title">
|
||||||
<span>检验信息</span>
|
<el-icon><DocumentChecked /></el-icon>
|
||||||
</el-row>
|
<span>检验信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="default" @click="handleNewApplication" class="new-btn">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table
|
<el-table
|
||||||
ref="inspectionTableRef"
|
ref="inspectionTableRef"
|
||||||
@@ -35,36 +45,10 @@
|
|||||||
class="inspection-table"
|
class="inspection-table"
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
row-key="applicationId"
|
row-key="applicationId"
|
||||||
:expand-row-keys="expandedRowKeys"
|
|
||||||
@expand-change="handleExpandChange"
|
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
@current-change="handleRowClick"
|
@current-change="handleRowClick"
|
||||||
@cell-click="handleCellClick"
|
@cell-click="handleCellClick"
|
||||||
>
|
>
|
||||||
<!-- Bug #326: 添加展开列 -->
|
|
||||||
<el-table-column type="expand" width="50" align="center" header-align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<div v-if="scope.row.children && scope.row.children.length > 0" class="expand-content">
|
|
||||||
<el-table :data="scope.row.children" border size="small" style="width: 100%">
|
|
||||||
<el-table-column label="明细项目" prop="itemName" min-width="150" />
|
|
||||||
<el-table-column label="样本类型" prop="sampleType" width="100" />
|
|
||||||
<el-table-column label="单位" prop="unit" width="80" />
|
|
||||||
<el-table-column label="单价" prop="itemPrice" width="80" align="right">
|
|
||||||
<template #default="itemScope">
|
|
||||||
¥{{ formatAmount(itemScope.row.itemPrice) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="数量" prop="itemQty" width="80" align="center" />
|
|
||||||
<el-table-column label="金额" prop="itemAmount" width="80" align="right">
|
|
||||||
<template #default="itemScope">
|
|
||||||
¥{{ formatAmount(itemScope.row.itemAmount) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
<div v-else class="expand-empty">无明细项目</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||||
<el-table-column label="申请 ID" prop="applicationId" width="80" align="center" header-align="center">
|
<el-table-column label="申请 ID" prop="applicationId" width="80" align="center" header-align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -74,14 +58,7 @@
|
|||||||
<el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" />
|
<el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" />
|
||||||
<el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center">
|
<el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span v-if="scope.row.hasChildren" style="color: #409EFF; cursor: pointer" @click.stop="toggleExpand(scope.row)">
|
<span>{{ scope.row.itemName }}</span>
|
||||||
<el-icon style="vertical-align: middle; margin-right: 4px">
|
|
||||||
<Right v-if="!isExpanded(scope.row.applicationId)" />
|
|
||||||
<Bottom v-else />
|
|
||||||
</el-icon>
|
|
||||||
{{ scope.row.itemName }}
|
|
||||||
</span>
|
|
||||||
<span v-else>{{ scope.row.itemName }}</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="申请医生" prop="applyDocName" width="120" align="center" header-align="center" />
|
<el-table-column label="申请医生" prop="applyDocName" width="120" align="center" header-align="center" />
|
||||||
@@ -114,7 +91,7 @@
|
|||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-row type="flex" align="middle" justify="center" :gutter="8">
|
<el-row type="flex" align="middle" justify="center" :gutter="8">
|
||||||
<el-button link size="default" @click="handlePrint(scope.row)" :icon="Printer" title="打印" style="font-size: 16px"></el-button>
|
<el-button link size="default" @click="handlePrint(scope.row)" :icon="Printer" title="打印" style="font-size: 16px"></el-button>
|
||||||
<el-button link size="default" @click="handleDelete(scope.row)" :icon="Delete" style="color: #f56c6c; font-size: 16px" title="删除"></el-button>
|
<el-button link size="default" @click.stop="handleDelete(scope.row)" :icon="Delete" style="color: #f56c6c; font-size: 16px" title="删除"></el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -406,7 +383,6 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<h4 style="margin: 0; font-weight: bold">检验信息详情</h4>
|
<h4 style="margin: 0; font-weight: bold">检验信息详情</h4>
|
||||||
</template>
|
</template>
|
||||||
<!-- Bug #326: 添加树形展开功能,支持套餐明细展示 -->
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="selectedInspectionItems"
|
:data="selectedInspectionItems"
|
||||||
border
|
border
|
||||||
@@ -414,12 +390,12 @@
|
|||||||
style="width: 100%; min-width: 100%"
|
style="width: 100%; min-width: 100%"
|
||||||
max-height="250"
|
max-height="250"
|
||||||
row-key="itemId"
|
row-key="itemId"
|
||||||
|
lazy
|
||||||
|
:load="loadPackageDetailsForTable"
|
||||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
default-expand-all
|
|
||||||
>
|
>
|
||||||
<el-table-column label="项目名称" prop="itemName" min-width="180">
|
<el-table-column label="项目名称" prop="itemName" min-width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<!-- BugFix#326: 套餐项目添加标识和加粗显示 -->
|
|
||||||
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
<el-tag v-if="scope.row.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
||||||
<span :style="{ fontWeight: scope.row.isPackage ? 'bold' : 'normal' }">
|
<span :style="{ fontWeight: scope.row.isPackage ? 'bold' : 'normal' }">
|
||||||
{{ scope.row.itemName }}
|
{{ scope.row.itemName }}
|
||||||
@@ -538,10 +514,9 @@
|
|||||||
@change="toggleInspectionItem(item)"
|
@change="toggleInspectionItem(item)"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<!-- BugFix#326: 套餐项目添加标识 -->
|
|
||||||
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
||||||
<span class="item-itemName">{{ item.itemName }}</span>
|
<span class="item-itemName">{{ item.itemName }}</span>
|
||||||
<span class="item-price">¥{{ item.itemPrice }}</span>
|
<span class="item-price">¥{{ item.itemPrice }}/{{ item.unit || "次" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 加载更多 -->
|
<!-- 加载更多 -->
|
||||||
<div v-if="category.hasMore && category.items.length > 0" class="load-more">
|
<div v-if="category.hasMore && category.items.length > 0" class="load-more">
|
||||||
@@ -592,7 +567,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
<el-tag v-if="item.isPackage" size="small" type="warning" style="margin-right: 4px">套餐</el-tag>
|
||||||
<span class="item-itemName">{{ item.itemName }}</span>
|
<span class="item-itemName">{{ item.itemName }}</span>
|
||||||
<span class="item-price">¥{{ item.itemPrice }}</span>
|
<span class="item-price">¥{{ item.itemPrice }}/{{ item.unit || "次" }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@@ -638,7 +613,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
|
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
|
||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading, Right, Bottom } from '@element-plus/icons-vue'
|
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
deleteInspectionApplication, getApplyList,
|
deleteInspectionApplication, getApplyList,
|
||||||
saveInspectionApplication,
|
saveInspectionApplication,
|
||||||
@@ -659,7 +634,7 @@ import { debounce } from 'lodash-es'
|
|||||||
|
|
||||||
// 获取当前组件实例和字典
|
// 获取当前组件实例和字典
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const { inspection_lab_dept } = proxy.useDict('inspection_lab_dept')
|
// Bug #329: 移除 inspection_lab_dept 字典,执行科室数据从 Organization 表获取
|
||||||
|
|
||||||
// 动态获取的检验类型缓存(用于缓存不在 inspectionCategories 中的检验类型)
|
// 动态获取的检验类型缓存(用于缓存不在 inspectionCategories 中的检验类型)
|
||||||
const dynamicInspectionTypesCache = ref([])
|
const dynamicInspectionTypesCache = ref([])
|
||||||
@@ -785,8 +760,64 @@ const loading = ref(false)
|
|||||||
const saving = ref(false) // 保存状态
|
const saving = ref(false) // 保存状态
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const leftActiveTab = ref('application')
|
const leftActiveTab = ref('application')
|
||||||
// Bug #326: 展开的行
|
|
||||||
const expandedRowKeys = ref([])
|
/**
|
||||||
|
* 加载套餐明细(公共函数)
|
||||||
|
* @param {string|number} packageId 套餐ID
|
||||||
|
* @returns {Promise<Array>} 明细数组
|
||||||
|
*/
|
||||||
|
const fetchPackageDetails = async (packageId) => {
|
||||||
|
if (!packageId) return []
|
||||||
|
try {
|
||||||
|
const res = await getInspectionPackageDetails(packageId)
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
return res.data.map(detail => {
|
||||||
|
const detailId = detail.detailId || detail.id || detail.itemId
|
||||||
|
const qty = detail.quantity || detail.itemQty || detail.qty || 1
|
||||||
|
const price = detail.unitPrice || detail.itemPrice || detail.price || 0
|
||||||
|
return {
|
||||||
|
detailId: detailId,
|
||||||
|
itemId: detailId, // 兼容表格 row-key
|
||||||
|
itemName: detail.itemName || detail.name,
|
||||||
|
sampleType: detail.sampleType || '',
|
||||||
|
unit: detail.unit || '',
|
||||||
|
quantity: qty,
|
||||||
|
itemQty: qty, // 兼容表格"总量"列
|
||||||
|
unitPrice: price,
|
||||||
|
itemPrice: price, // 兼容表格"单价"列
|
||||||
|
itemAmount: price * qty
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载套餐明细失败:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPackageDetailsForTable = async (row, treeNode, resolve) => {
|
||||||
|
if (!row.isPackage) {
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const packageId = row.feePackageId || row.packageId
|
||||||
|
const children = await fetchPackageDetails(packageId)
|
||||||
|
resolve(children)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePackageExpand = async (item) => {
|
||||||
|
if (!item.isPackage) return
|
||||||
|
|
||||||
|
item.expanded = !item.expanded
|
||||||
|
|
||||||
|
if (item.expanded && (!item.children || item.children.length === 0)) {
|
||||||
|
item.loading = true
|
||||||
|
const packageId = item.feePackageId || item.packageId
|
||||||
|
item.children = await fetchPackageDetails(packageId)
|
||||||
|
item.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 申请日期实时更新定时器
|
// 申请日期实时更新定时器
|
||||||
let applyTimeTimer = null
|
let applyTimeTimer = null
|
||||||
@@ -856,6 +887,9 @@ const executeDepartmentOptions = ref([])
|
|||||||
// 执行科室加载完成标志(BugFix#CodeReview: 防止竞态条件)
|
// 执行科室加载完成标志(BugFix#CodeReview: 防止竞态条件)
|
||||||
const isExecuteDepartmentLoaded = ref(false)
|
const isExecuteDepartmentLoaded = ref(false)
|
||||||
|
|
||||||
|
// 删除操作标志,用于避免删除时触发数据填充
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
applyOrganizationId: '',
|
applyOrganizationId: '',
|
||||||
@@ -1487,6 +1521,18 @@ const handleSave = () => {
|
|||||||
|
|
||||||
let hasErrors = false
|
let hasErrors = false
|
||||||
|
|
||||||
|
// 修复【#406】:保存前尝试从 props 同步患者信息,避免因加载时序导致信息缺失
|
||||||
|
if ((!formData.patientName?.trim() || !formData.medicalrecordNumber?.trim()) && props.patientInfo && props.patientInfo.encounterId) {
|
||||||
|
formData.patientName = props.patientInfo.patientName || ''
|
||||||
|
formData.medicalrecordNumber = props.patientInfo.identifierNo || ''
|
||||||
|
formData.encounterId = props.patientInfo.encounterId || ''
|
||||||
|
formData.visitNo = props.patientInfo.busNo || ''
|
||||||
|
formData.patientId = props.patientInfo.patientId || ''
|
||||||
|
formData.applyDepartment = props.patientInfo.organizationName || ''
|
||||||
|
formData.applyDeptCode = props.patientInfo.organizationName || ''
|
||||||
|
formData.applyOrganizationId = props.patientInfo.orgId || ''
|
||||||
|
}
|
||||||
|
|
||||||
// P0:检查患者信息是否已加载
|
// P0:检查患者信息是否已加载
|
||||||
if (!formData.patientName?.trim() || !formData.medicalrecordNumber?.trim()) {
|
if (!formData.patientName?.trim() || !formData.medicalrecordNumber?.trim()) {
|
||||||
ElMessage.error('患者信息未加载,请稍后重试')
|
ElMessage.error('患者信息未加载,请稍后重试')
|
||||||
@@ -1544,17 +1590,27 @@ const handleSave = () => {
|
|||||||
|
|
||||||
// 准备保存数据
|
// 准备保存数据
|
||||||
const prepareSaveData = () => {
|
const prepareSaveData = () => {
|
||||||
// 将执行科室代码赋值给每个检验项目
|
|
||||||
const labApplyItemList = selectedInspectionItems.value.map(item => ({
|
const labApplyItemList = selectedInspectionItems.value.map(item => ({
|
||||||
...item,
|
itemSeq: item.itemSeq || 1,
|
||||||
performDeptCode: formData.executeDepartment // 从字典获取的执行科室代码
|
itemCode: item.code || '',
|
||||||
|
itemName: item.itemName || '',
|
||||||
|
itemPrice: item.itemPrice || 0,
|
||||||
|
itemQty: item.itemQty || 1,
|
||||||
|
itemAmount: item.itemAmount || 0,
|
||||||
|
itemStatus: formData.applyStatus || 0,
|
||||||
|
performDeptCode: formData.executeDepartment,
|
||||||
|
// Bug #326修复: 传入 activityId,后端直接使用 ID 关联,避免用名称反查
|
||||||
|
activityId: item.activityId || item.itemId || null,
|
||||||
|
feePackageId: item.feePackageId || null,
|
||||||
|
isPackage: item.isPackage || false,
|
||||||
|
sampleType: item.sampleType || '',
|
||||||
|
unit: item.unit || ''
|
||||||
}))
|
}))
|
||||||
return {
|
return {
|
||||||
...formData,
|
...formData,
|
||||||
labApplyItemList,
|
labApplyItemList,
|
||||||
physicalExamination: formData.physicalExam, // 字段名映射:前端 physicalExam -> 后端 physicalExamination
|
physicalExamination: formData.physicalExam,
|
||||||
inspectionItemsText: selectedInspectionItems.value.map(item => item.itemName).join('+')
|
inspectionItemsText: selectedInspectionItems.value.map(item => item.itemName).join('+')
|
||||||
// 金额由后端计算,前端不传递
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1563,6 +1619,7 @@ const handleSave = () => {
|
|||||||
executeSave(saveData);
|
executeSave(saveData);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSave = (saveData) => {
|
const executeSave = (saveData) => {
|
||||||
saveInspectionApplication(saveData).then((res) => {
|
saveInspectionApplication(saveData).then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
@@ -1687,6 +1744,26 @@ const clearAllSelected = () => {
|
|||||||
selectedInspectionItems.value = []
|
selectedInspectionItems.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bug #387修复: 同步分类勾选状态
|
||||||
|
const syncCategoryChecked = () => {
|
||||||
|
// 重置所有分类项目的勾选状态
|
||||||
|
inspectionCategories.value.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
item.checked = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// 获取已选项目的ID集合
|
||||||
|
const ids = new Set(selectedInspectionItems.value.map(s => s.itemId))
|
||||||
|
// 同步勾选状态
|
||||||
|
for (const cat of inspectionCategories.value) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
if (ids.has(item.itemId)) {
|
||||||
|
item.checked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 分页大小改变
|
// 分页大小改变
|
||||||
const handleSizeChange = (size) => {
|
const handleSizeChange = (size) => {
|
||||||
queryParams.pageSize = size
|
queryParams.pageSize = size
|
||||||
@@ -1725,6 +1802,8 @@ const handlePrint = (row) => {
|
|||||||
|
|
||||||
// 删除申请单
|
// 删除申请单
|
||||||
const handleDelete = (row) => {
|
const handleDelete = (row) => {
|
||||||
|
isDeleting.value = true; // 设置删除标志
|
||||||
|
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
`确定要删除申请单 "${row.applyNo}" 吗?此操作将同时删除对应的医嘱。`,
|
`确定要删除申请单 "${row.applyNo}" 吗?此操作将同时删除对应的医嘱。`,
|
||||||
'删除确认',
|
'删除确认',
|
||||||
@@ -1740,22 +1819,30 @@ const handleDelete = (row) => {
|
|||||||
deleteInspectionApplication(row.applyNo).then((res) => {
|
deleteInspectionApplication(row.applyNo).then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
resetForm(); // 删除成功后清空表单
|
||||||
// 刷新列表
|
// 刷新列表
|
||||||
getInspectionList()
|
getInspectionList()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '删除失败')
|
ElMessage.error(res.message || '删除失败')
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('删除检验<EFBFBD><EFBFBD>请单异常:', error)
|
console.error('删除检验请单异常:', error)
|
||||||
ElMessage.error('删除异常')
|
ElMessage.error('删除异常')
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// 用户取消删除
|
// 用户取消删除
|
||||||
|
}).finally(() => {
|
||||||
|
isDeleting.value = false; // 重置删除标志
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单元格点击 - 点击表格行时加载申请单详情
|
// 单元格点击 - 点击表格行时加载申请单详情
|
||||||
const handleCellClick = (row, column) => {
|
const handleCellClick = (row, column) => {
|
||||||
|
// 如果点击的是操作列或展开列,不触发数据填充
|
||||||
|
if (column.property === '操作' || column.label === '操作' ||
|
||||||
|
column.type === 'expand' || column.type === 'selection') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 点击表格行时,将该申请单的数据加载到表单中
|
// 点击表格行时,将该申请单的数据加载到表单中
|
||||||
// 使用 applyNo 判断是否有效
|
// 使用 applyNo 判断是否有效
|
||||||
if (row && row.applyNo) {
|
if (row && row.applyNo) {
|
||||||
@@ -1766,8 +1853,8 @@ const handleCellClick = (row, column) => {
|
|||||||
// 行点击事件处理
|
// 行点击事件处理
|
||||||
const handleRowClick = (currentRow, oldRow) => {
|
const handleRowClick = (currentRow, oldRow) => {
|
||||||
// 点击表格行时,将该申请单的数据加载到表单中
|
// 点击表格行时,将该申请单的数据加载到表单中
|
||||||
// 使用 applyNo 判断是否有效
|
// 使用 applyNo 判断是否有效,同时检查是否处于删除状态
|
||||||
if (currentRow && currentRow.applyNo) {
|
if (currentRow && currentRow.applyNo && !isDeleting.value) {
|
||||||
loadApplicationToForm(currentRow);
|
loadApplicationToForm(currentRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1790,7 +1877,6 @@ const loadApplicationToForm = async (row) => {
|
|||||||
// 根据申请单号获取完整详情
|
// 根据申请单号获取完整详情
|
||||||
try {
|
try {
|
||||||
const res = await getInspectionApplyDetail(row.applyNo)
|
const res = await getInspectionApplyDetail(row.applyNo)
|
||||||
console.log('申请单详情API返回:', res) // 临时调试日志
|
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
const detail = res.data
|
const detail = res.data
|
||||||
// 加载完整的表单数据
|
// 加载完整的表单数据
|
||||||
@@ -1831,13 +1917,46 @@ const loadApplicationToForm = async (row) => {
|
|||||||
// 加载检验项目数据
|
// 加载检验项目数据
|
||||||
selectedInspectionItems.value = []
|
selectedInspectionItems.value = []
|
||||||
if (detail.labApplyItemList && detail.labApplyItemList.length > 0) {
|
if (detail.labApplyItemList && detail.labApplyItemList.length > 0) {
|
||||||
selectedInspectionItems.value = detail.labApplyItemList.map(item => ({
|
// Bug #326修复: 直接使用后端返回的数据,不再从本地缓存查找匹配项
|
||||||
...item,
|
// 后端已返回完整关联信息(activityId、feePackageId、inspectionTypeId、sampleType、unit)
|
||||||
itemId: item.itemId || item.id || Math.random().toString(36).substring(2, 11),
|
// Bug #387修复: 套餐项目默认展开,并自动加载明细
|
||||||
itemName: item.itemName || item.name || '',
|
selectedInspectionItems.value = detail.labApplyItemList.map(item => {
|
||||||
itemPrice: item.itemPrice || item.price || 0,
|
const itemId = item.activityId || item.itemId || item.id || Math.random().toString(36).substring(2, 11)
|
||||||
itemAmount: item.itemAmount || item.price || 0,
|
const isPackage = item.feePackageId != null || item.itemName?.includes('套餐')
|
||||||
}))
|
|
||||||
|
return {
|
||||||
|
itemId: itemId,
|
||||||
|
itemName: item.itemName || '',
|
||||||
|
itemPrice: item.itemPrice || 0,
|
||||||
|
itemAmount: item.itemAmount || 0,
|
||||||
|
itemQty: item.itemQty || 1,
|
||||||
|
sampleType: item.sampleType || '',
|
||||||
|
unit: item.unit || '',
|
||||||
|
code: item.itemCode || '',
|
||||||
|
isPackage: isPackage,
|
||||||
|
hasChildren: isPackage,
|
||||||
|
feePackageId: item.feePackageId || null,
|
||||||
|
activityId: item.activityId || itemId,
|
||||||
|
inspectionTypeId: item.inspectionTypeId || null,
|
||||||
|
expanded: isPackage, // Bug #387: 套餐默认展开
|
||||||
|
children: [],
|
||||||
|
childrenLoaded: !isPackage, // Bug #387: 套餐需加载明细
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bug #387修复: 自动加载套餐明细
|
||||||
|
for (const pkgItem of selectedInspectionItems.value) {
|
||||||
|
if (pkgItem.isPackage && pkgItem.feePackageId) {
|
||||||
|
pkgItem.loading = true
|
||||||
|
pkgItem.children = await fetchPackageDetails(pkgItem.feePackageId)
|
||||||
|
pkgItem.childrenLoaded = true
|
||||||
|
pkgItem.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug #387修复: 同步分类勾选状态
|
||||||
|
syncCategoryChecked()
|
||||||
} else if (detail.inspectionItem || detail.itemName) {
|
} else if (detail.inspectionItem || detail.itemName) {
|
||||||
// 如果只有项目名称,尝试从本地分类中查找匹配项
|
// 如果只有项目名称,尝试从本地分类中查找匹配项
|
||||||
const itemNames = (detail.inspectionItem || detail.itemName).split(/[+,]/)
|
const itemNames = (detail.inspectionItem || detail.itemName).split(/[+,]/)
|
||||||
@@ -1896,25 +2015,43 @@ watch(() => props.patientInfo, async (newVal) => {
|
|||||||
}
|
}
|
||||||
}, { deep: true, immediate: true })
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
// 监听已选择的检验项目,自动更新检验项目文本(用+号拼接)
|
// Bug #329: 监听已选择的检验项目,自动更新检验项目文本并设置默认执行科室
|
||||||
watch(() => selectedInspectionItems.value, (newVal) => {
|
watch(() => selectedInspectionItems.value, async (newVal) => {
|
||||||
if (newVal && newVal.length > 0) {
|
if (newVal && newVal.length > 0) {
|
||||||
formData.inspectionItemsText = newVal.map(item => item.itemName).join('+')
|
formData.inspectionItemsText = newVal.map(item => item.itemName).join('+')
|
||||||
|
|
||||||
|
// Bug #329: 如果执行科室为空,根据第一个检验项目的检验类型自动设置默认执行科室
|
||||||
|
if (!formData.executeDepartment) {
|
||||||
|
const firstItem = newVal[0]
|
||||||
|
|
||||||
|
// 根据检验项目的 inspectionTypeId 获取默认执行科室
|
||||||
|
if (firstItem.inspectionTypeId) {
|
||||||
|
const defaultDeptCode = await getDefaultPerformDeptCode(firstItem.inspectionTypeId)
|
||||||
|
if (defaultDeptCode) {
|
||||||
|
formData.executeDepartment = defaultDeptCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Bug #329: 当项目被清空时,同时清空执行科室(下次选择项目时会重新自动设置)
|
||||||
formData.inspectionItemsText = ''
|
formData.inspectionItemsText = ''
|
||||||
|
formData.executeDepartment = ''
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// 监听执行科室字典数据,设置默认值为第一个选项
|
// Bug #329: 移除字典数据默认值设置,执行科室应从 Organization 表获取,不应使用字典数据
|
||||||
watch(() => inspection_lab_dept.value, (newVal) => {
|
// 原问题:inspection_lab_dept 字典的 value (如 'ORG045') 与 executeDepartmentOptions 的 value (科室编码 busNo) 格式不一致
|
||||||
if (newVal && newVal.length > 0 && !formData.executeDepartment) {
|
// 导致 el-select 显示原始 value 值而非科室名称
|
||||||
formData.executeDepartment = newVal[0].value
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 组件挂载时预加载检验项目数据(不依赖patientInfo)
|
// 组件挂载时预加载检验项目数据(不依赖patientInfo)
|
||||||
|
// BugFix #329: 先加载执行科室列表,再加载检验类型(确保检验类型的department字段能正确匹配)
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await loadExecuteDepartmentList()
|
||||||
await loadInspectionData()
|
await loadInspectionData()
|
||||||
|
// 修复【#406】:挂载时如果已有patientInfo但watch未触发initData,则手动调用
|
||||||
|
if (props.patientInfo && props.patientInfo.encounterId && !formData.encounterId) {
|
||||||
|
await initData()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件卸载时清除定时器
|
// 组件卸载时清除定时器
|
||||||
@@ -1947,13 +2084,7 @@ defineExpose({
|
|||||||
|
|
||||||
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
|
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
|
||||||
.top-action-bar {
|
.top-action-bar {
|
||||||
display: flex;
|
display: none; /* 隐藏原有的顶部操作栏 */
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@@ -1998,7 +2129,14 @@ defineExpose({
|
|||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header-flex {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -2006,6 +2144,11 @@ defineExpose({
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Bug#334: 底部内容区域 - 优化垂直空间利用率 */
|
/* Bug#334: 底部内容区域 - 优化垂直空间利用率 */
|
||||||
.bottom-content-area {
|
.bottom-content-area {
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
@@ -2066,6 +2209,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
:deep(.el-pagination) {
|
||||||
.el-pager li {
|
.el-pager li {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -2554,6 +2698,110 @@ defineExpose({
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 套餐明细展开样式 */
|
||||||
|
.selected-tree-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-header .item-itemName {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-header .item-price {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 16px;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-header:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-header.expanded {
|
||||||
|
background: #e8f4fc;
|
||||||
|
border-left: 3px solid #51A3F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-icon {
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #51A3F3;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-package .selected-tree-header {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-children {
|
||||||
|
padding: 4px 12px 4px 36px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px dashed #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-detail:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-detail .detail-name {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-detail .detail-unit {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-detail .detail-qty {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tree-detail .detail-price {
|
||||||
|
width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-detail {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.no-selection {
|
.no-selection {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
@@ -2653,3 +2901,4 @@ defineExpose({
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
// Frontend build verification for Bug #414 - confirmed working on Fri Apr 24 11:14:49 AM CST 2026
|
||||||
|
|||||||
@@ -271,7 +271,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<el-form-item label="给药途径:" prop="methodCode" class="required-field" data-prop="methodCode">
|
<el-form-item label="给药途径:" prop="methodCode" class="required-field" data-prop="methodCode">
|
||||||
<el-select v-model="scope.row.methodCode" placeholder="给药途径" clearable filterable
|
<el-select v-model="scope.row.methodCode" placeholder="给药途径" clearable style="width: 120px" filterable
|
||||||
:ref="(el) => (inputRefs.methodCode = el)" @keyup.enter.prevent="
|
:ref="(el) => (inputRefs.methodCode = el)" @keyup.enter.prevent="
|
||||||
() => {
|
() => {
|
||||||
if (scope.row.methodCode) {
|
if (scope.row.methodCode) {
|
||||||
@@ -2272,7 +2272,26 @@ function handleNumberClick(item, index, row) {
|
|||||||
|
|
||||||
// 选择执行科室处理
|
// 选择执行科室处理
|
||||||
function handleOrgChange(value, index, row) {
|
function handleOrgChange(value, index, row) {
|
||||||
|
// 这里的“执行科室”在后端通常以 organizationId / positionId 参与业务校验;
|
||||||
|
// 列表展示用的是 positionName,因此需要同步写入名称,避免“选了但显示空”的问题。
|
||||||
|
row.orgId = value;
|
||||||
row.positionId = value;
|
row.positionId = value;
|
||||||
|
row.positionName = findOrgNameById(value) || row.positionName || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOrgNameById(id) {
|
||||||
|
if (!id) return '';
|
||||||
|
const targetId = String(id);
|
||||||
|
const stack = Array.isArray(organization.value) ? [...organization.value] : [];
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.shift();
|
||||||
|
if (!node) continue;
|
||||||
|
if (String(node.id) === targetId) return node.name || '';
|
||||||
|
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||||
|
stack.unshift(...node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3286,8 +3305,9 @@ function syncGroupFields(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 同步执行科室
|
// 同步执行科室
|
||||||
if (row.positionId || row.orgId) {
|
if (row.orgId || row.positionId) {
|
||||||
prescriptionList.value[rowIndex.value].orgId = row.positionId || row.orgId;
|
// 🔧 修复:优先使用项目所属科室(orgId),其次positionId
|
||||||
|
prescriptionList.value[rowIndex.value].orgId = row.orgId || row.positionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步皮试标记
|
// 同步皮试标记
|
||||||
@@ -3371,9 +3391,8 @@ function setValue(row) {
|
|||||||
showPopover: false, // 确保查询框关闭
|
showPopover: false, // 确保查询框关闭
|
||||||
};
|
};
|
||||||
console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
|
console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
|
||||||
// 🔧 Bug #218 修复:保留组套中的值,不要强制设为undefined
|
// 🔧 修复执行科室逻辑:优先使用项目维护的所属科室(row.orgId),其次使用positionId,最后回退到患者科室
|
||||||
// 只有当值未定义时才使用默认值
|
prescriptionList.value[rowIndex.value].orgId = row.orgId || row.positionId || props.patientInfo?.orgId;
|
||||||
prescriptionList.value[rowIndex.value].orgId = row.positionId || row.orgId;
|
|
||||||
prescriptionList.value[rowIndex.value].dose = row.dose || row.doseQuantity;
|
prescriptionList.value[rowIndex.value].dose = row.dose || row.doseQuantity;
|
||||||
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
|
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
|
||||||
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;
|
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;
|
||||||
@@ -3493,6 +3512,7 @@ function setValue(row) {
|
|||||||
if (row.adviceType == 5) {
|
if (row.adviceType == 5) {
|
||||||
// 会诊类型:设置默认值
|
// 会诊类型:设置默认值
|
||||||
prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId; // 执行科室默认为申请医生的科室
|
prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId; // 执行科室默认为申请医生的科室
|
||||||
|
prescriptionList.value[rowIndex.value].positionName = findOrgNameById(props.patientInfo.orgId) || props.patientInfo.orgName || '';
|
||||||
prescriptionList.value[rowIndex.value].quantity = 1; // 执行次数默认1次
|
prescriptionList.value[rowIndex.value].quantity = 1; // 执行次数默认1次
|
||||||
prescriptionList.value[rowIndex.value].unitPrice = row.priceList && row.priceList[0] ? row.priceList[0].price : (row.unitPrice || 0);
|
prescriptionList.value[rowIndex.value].unitPrice = row.priceList && row.priceList[0] ? row.priceList[0].price : (row.unitPrice || 0);
|
||||||
prescriptionList.value[rowIndex.value].totalPrice = prescriptionList.value[rowIndex.value].unitPrice;
|
prescriptionList.value[rowIndex.value].totalPrice = prescriptionList.value[rowIndex.value].unitPrice;
|
||||||
@@ -3503,6 +3523,9 @@ function setValue(row) {
|
|||||||
if (!prescriptionList.value[rowIndex.value].orgId) {
|
if (!prescriptionList.value[rowIndex.value].orgId) {
|
||||||
prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId;
|
prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId;
|
||||||
}
|
}
|
||||||
|
if (!prescriptionList.value[rowIndex.value].positionName) {
|
||||||
|
prescriptionList.value[rowIndex.value].positionName = findOrgNameById(prescriptionList.value[rowIndex.value].orgId) || props.patientInfo.orgName || '';
|
||||||
|
}
|
||||||
// 🔧 Bug #218 修复:使用组套中维护的quantity,如果没有则默认1
|
// 🔧 Bug #218 修复:使用组套中维护的quantity,如果没有则默认1
|
||||||
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
|
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
|
||||||
// 🔧 Bug #144 修复:安全访问 priceList,防止 orderDetailInfos 为空时出错
|
// 🔧 Bug #144 修复:安全访问 priceList,防止 orderDetailInfos 为空时出错
|
||||||
@@ -3596,8 +3619,8 @@ function handleSaveGroup(orderGroupList) {
|
|||||||
unitCode: item.unitCode,
|
unitCode: item.unitCode,
|
||||||
unitCode_dictText: item.unitCodeName || '',
|
unitCode_dictText: item.unitCodeName || '',
|
||||||
statusEnum: 1,
|
statusEnum: 1,
|
||||||
// 🔧 Bug #218 修复:优先使用 item.positionId,其次使用 orderDetailInfos.positionId
|
// 🔧 修复执行科室逻辑:优先使用 orgId(所属科室),其次 positionId
|
||||||
orgId: item.positionId || item.orderDetailInfos?.positionId || mergedDetail.positionId,
|
orgId: item.orderDetailInfos?.orgId || mergedDetail.orgId || item.positionId || item.orderDetailInfos?.positionId || mergedDetail.positionId,
|
||||||
dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1',
|
dbOpType: prescriptionList.value[rowIndex.value].requestId ? '2' : '1',
|
||||||
conditionId: conditionId.value,
|
conditionId: conditionId.value,
|
||||||
conditionDefinitionId: conditionDefinitionId.value,
|
conditionDefinitionId: conditionDefinitionId.value,
|
||||||
|
|||||||
@@ -114,9 +114,10 @@
|
|||||||
<el-tab-pane label="门诊病历" name="hospitalizationEmr">
|
<el-tab-pane label="门诊病历" name="hospitalizationEmr">
|
||||||
<hospitalizationEmr :patientInfo="patientInfo" :activeTab="activeTab" @emrSaved="handleEmrSaved" />
|
<hospitalizationEmr :patientInfo="patientInfo" :activeTab="activeTab" @emrSaved="handleEmrSaved" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="待写病历" name="pendingEmr">
|
<!-- Bug #368: 屏蔽待写病历标签页(左侧导航栏已有独立菜单,功能冗余) -->
|
||||||
|
<!-- <el-tab-pane label="待写病历" name="pendingEmr">
|
||||||
<PendingEmr @writeEmr="handleWriteEmr" @viewPatient="handleViewPatient" />
|
<PendingEmr @writeEmr="handleWriteEmr" @viewPatient="handleViewPatient" />
|
||||||
</el-tab-pane>
|
</el-tab-pane> -->
|
||||||
<!-- <el-tab-pane label="病历" name="emr">
|
<!-- <el-tab-pane label="病历" name="emr">
|
||||||
<Emr
|
<Emr
|
||||||
:patientInfo="patientInfo"
|
:patientInfo="patientInfo"
|
||||||
|
|||||||
@@ -153,8 +153,8 @@
|
|||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="detailVisible"
|
v-model="detailVisible"
|
||||||
:title="detailMode === 'view' ? '报卡详情' : '编辑报卡'"
|
:title="detailMode === 'view' ? '报卡详情 - 中华人民共和国传染病报告卡' : '编辑报卡 - 中华人民共和国传染病报告卡'"
|
||||||
width="900px"
|
width="1100px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
class="card-detail-dialog"
|
class="card-detail-dialog"
|
||||||
>
|
>
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
:mode=" detailMode"
|
:mode=" detailMode"
|
||||||
:card-data="currentCard"
|
:card-data="currentCard"
|
||||||
@submit-edit="handleSaveEdit"
|
@submit-edit="handleSaveEdit"
|
||||||
style="max-height: 70vh; overflow-y: auto;"
|
style="max-height: 75vh; overflow-y: auto;"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="detailVisible = false">关闭</el-button>
|
<el-button @click="detailVisible = false">关闭</el-button>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PatientRegister
|
<PatientRegister
|
||||||
v-model="patientRegisterVisible"
|
v-model:dialogVisible="patientRegisterVisible"
|
||||||
:patientInfo="patient"
|
:patientInfo="patient"
|
||||||
:inHospitalInfo="inHospitalInfo"
|
:inHospitalInfo="inHospitalInfo"
|
||||||
title="登记"
|
title="登记"
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PatientRegister
|
<PatientRegister
|
||||||
v-model="patientRegisterVisible"
|
v-model:dialogVisible="patientRegisterVisible"
|
||||||
:patientInfo="patient"
|
:patientInfo="patient"
|
||||||
:inHospitalInfo="inHospitalInfo"
|
:inHospitalInfo="inHospitalInfo"
|
||||||
title="登记"
|
title="登记"
|
||||||
|
|||||||
@@ -190,8 +190,8 @@
|
|||||||
v-model="submitForm.startTime"
|
v-model="submitForm.startTime"
|
||||||
:disabled="props.isRegistered"
|
:disabled="props.isRegistered"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
type="date"
|
type="datetime"
|
||||||
placeholder="请选择日期"
|
placeholder="请选择日期时间"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -220,17 +220,6 @@ const { in_way_code, admit_source_code, med_type } = proxy.useDict(
|
|||||||
'med_type'
|
'med_type'
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听诊断类别字典加载,默认选择第一项
|
|
||||||
watch(
|
|
||||||
med_type,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal && newVal.length > 0 && !submitForm.medTypeCode) {
|
|
||||||
submitForm.medTypeCode = newVal[0].value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const emits = defineEmits([]);
|
const emits = defineEmits([]);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
patientInfo: {
|
patientInfo: {
|
||||||
@@ -351,6 +340,17 @@ const submitForm = reactive({
|
|||||||
ambDiagnosisName: props.inHospitalInfo.ambDiagnosisName,
|
ambDiagnosisName: props.inHospitalInfo.ambDiagnosisName,
|
||||||
medTypeCode: '', // 从字典动态获取默认值
|
medTypeCode: '', // 从字典动态获取默认值
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听诊断类别字典加载,默认选择第一项
|
||||||
|
watch(
|
||||||
|
med_type,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && newVal.length > 0 && !submitForm.medTypeCode) {
|
||||||
|
submitForm.medTypeCode = newVal[0].value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
// /* 科室 病区 */
|
// /* 科室 病区 */
|
||||||
// watch(
|
// watch(
|
||||||
// () => submitForm.inDocterWorkGroupCode,
|
// () => submitForm.inDocterWorkGroupCode,
|
||||||
|
|||||||
@@ -23,26 +23,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, nextTick, ref, watch} from 'vue';
|
import {computed, nextTick, onMounted, ref} from 'vue';
|
||||||
import {throttle} from 'lodash-es';
|
import {throttle} from 'lodash-es';
|
||||||
import Table from '@/components/TableLayout/Table.vue';
|
import Table from '@/components/TableLayout/Table.vue';
|
||||||
import {getAdviceBaseInfo} from './api';
|
import {getAdviceBaseInfo} from './api';
|
||||||
import type {TableColumn} from '@/components/types/TableLayout.d';
|
import type {TableColumn} from '@/components/types/TableLayout';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
adviceQueryParams?: {
|
|
||||||
searchKey?: string;
|
|
||||||
adviceType?: string;
|
|
||||||
};
|
|
||||||
patientInfo: {
|
patientInfo: {
|
||||||
inHospitalOrgId?: string;
|
inHospitalOrgId?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = defineProps<Props>();
|
||||||
adviceQueryParams: () => ({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
selectAdviceBase: [row: any];
|
selectAdviceBase: [row: any];
|
||||||
@@ -56,14 +50,14 @@ const currentIndex = ref<number>(0);
|
|||||||
const currentSelectRow = ref<any>({});
|
const currentSelectRow = ref<any>({});
|
||||||
const queryParams = ref({
|
const queryParams = ref({
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
pageNum: 1,
|
pageNo: 1,
|
||||||
adviceTypes: '1,3',
|
adviceTypes: '1,2,3,6',
|
||||||
searchKey: '',
|
searchKey: '',
|
||||||
organizationId: '',
|
organizationId: '',
|
||||||
|
categoryCode: '',
|
||||||
});
|
});
|
||||||
const adviceBaseList = ref<any[]>([]);
|
const adviceBaseList = ref<any[]>([]);
|
||||||
|
|
||||||
// 表格列配置
|
|
||||||
const tableColumns = computed<TableColumn[]>(() => [
|
const tableColumns = computed<TableColumn[]>(() => [
|
||||||
{ label: '名称', prop: 'adviceName', align: 'center', width: 200 },
|
{ label: '名称', prop: 'adviceName', align: 'center', width: 200 },
|
||||||
{ label: '类型', prop: 'activityType_enumText', align: 'center' },
|
{ label: '类型', prop: 'activityType_enumText', align: 'center' },
|
||||||
@@ -84,50 +78,53 @@ const tableColumns = computed<TableColumn[]>(() => [
|
|||||||
slot: 'useScope',
|
slot: 'useScope',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
// 节流函数
|
|
||||||
const throttledGetList = throttle(
|
|
||||||
() => {
|
|
||||||
getList();
|
|
||||||
},
|
|
||||||
300,
|
|
||||||
{ leading: true, trailing: true }
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.adviceQueryParams,
|
|
||||||
(newValue) => {
|
|
||||||
queryParams.value.searchKey = newValue.searchKey;
|
|
||||||
// queryParams.value.adviceType = newValue.adviceType;
|
|
||||||
queryParams.value.adviceTypes = [newValue.adviceType].join(',');
|
|
||||||
throttledGetList();
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
getList();
|
/**
|
||||||
|
* 父组件主动调用此方法刷新列表
|
||||||
|
* @param adviceType 医嘱类型(1=药品, 3=诊疗, 6=手术, ''=全部)
|
||||||
|
* @param categoryCode 药品分类编码('1'=中成药, '2'=西药, '4'=中草药, ''=不限)
|
||||||
|
* @param searchKey 搜索关键词
|
||||||
|
*/
|
||||||
|
function refresh(adviceType: any, categoryCode: string, searchKey: string) {
|
||||||
|
queryParams.value.adviceTypes =
|
||||||
|
adviceType !== undefined && adviceType !== '' ? String(adviceType) : '1,2,3,6';
|
||||||
|
queryParams.value.categoryCode = categoryCode || '';
|
||||||
|
queryParams.value.searchKey = searchKey || '';
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
function getList() {
|
function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
queryParams.value.organizationId = props.patientInfo?.inHospitalOrgId || '';
|
// organizationId 必须为有效数字或 undefined,空字符串会导致后端 Long 类型转换失败(400)
|
||||||
|
const orgId = props.patientInfo?.inHospitalOrgId;
|
||||||
|
queryParams.value.organizationId = orgId ? orgId : undefined;
|
||||||
|
// 空字符串参数不发送,避免后端类型转换错误
|
||||||
|
if (!queryParams.value.searchKey) {
|
||||||
|
queryParams.value.searchKey = undefined;
|
||||||
|
}
|
||||||
|
if (!queryParams.value.categoryCode) {
|
||||||
|
queryParams.value.categoryCode = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getAdviceBaseInfo(queryParams.value)
|
getAdviceBaseInfo(queryParams.value)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log(res.data.records);
|
const records = res.data?.records || [];
|
||||||
if (res.data.records.length > 0) {
|
|
||||||
adviceBaseList.value = res.data.records.filter((item) => {
|
// 药品/耗材需要有库存才显示,诊疗/手术直接显示
|
||||||
if (item.adviceType == 1 || item.adviceType == 2) {
|
adviceBaseList.value = records.filter((item: any) => {
|
||||||
return handleQuantity(item) != 0;
|
if (item.adviceType == 1 || item.adviceType == 2) {
|
||||||
} else {
|
return handleQuantity(item) !== '0';
|
||||||
return true;
|
}
|
||||||
}
|
return true;
|
||||||
});
|
});
|
||||||
total.value = res.data.total;
|
|
||||||
nextTick(() => {
|
total.value = res.data?.total || 0;
|
||||||
if (adviceBaseList.value.length > 0) {
|
nextTick(() => {
|
||||||
currentIndex.value = 0;
|
if (adviceBaseList.value.length > 0) {
|
||||||
setCurrentRow(adviceBaseList.value[0]);
|
currentIndex.value = 0;
|
||||||
}
|
setCurrentRow(adviceBaseList.value[0]);
|
||||||
});
|
}
|
||||||
} else {
|
});
|
||||||
adviceBaseList.value = [];
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
adviceBaseList.value = [];
|
adviceBaseList.value = [];
|
||||||
@@ -141,23 +138,22 @@ function getList() {
|
|||||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
const data = adviceBaseList.value;
|
const data = adviceBaseList.value;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'ArrowUp': // 上箭头
|
case 'ArrowUp':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (currentIndex.value > 0) {
|
if (currentIndex.value > 0) {
|
||||||
currentIndex.value--;
|
currentIndex.value--;
|
||||||
setCurrentRow(data[currentIndex.value]);
|
setCurrentRow(data[currentIndex.value]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown': // 下箭头
|
case 'ArrowDown':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (currentIndex.value < data.length - 1) {
|
if (currentIndex.value < data.length - 1) {
|
||||||
currentIndex.value++;
|
currentIndex.value++;
|
||||||
setCurrentRow(data[currentIndex.value]);
|
setCurrentRow(data[currentIndex.value]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Enter': // 回车键
|
case 'Enter':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (currentSelectRow.value && Object.keys(currentSelectRow.value).length > 0) {
|
if (currentSelectRow.value && Object.keys(currentSelectRow.value).length > 0) {
|
||||||
emit('selectAdviceBase', currentSelectRow.value);
|
emit('selectAdviceBase', currentSelectRow.value);
|
||||||
@@ -177,17 +173,14 @@ function handleQuantity(row: any): string {
|
|||||||
return '0';
|
return '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置选中行(带滚动)
|
|
||||||
const setCurrentRow = (row: any) => {
|
const setCurrentRow = (row: any) => {
|
||||||
if (adviceBaseRef.value?.tableRef) {
|
if (adviceBaseRef.value?.tableRef) {
|
||||||
adviceBaseRef.value.tableRef.setCurrentRow(row);
|
adviceBaseRef.value.tableRef.setCurrentRow(row);
|
||||||
// 滚动到选中行
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const tableEl = adviceBaseRef.value?.tableRef?.$el;
|
const tableEl = adviceBaseRef.value?.tableRef?.$el;
|
||||||
if (tableEl) {
|
if (tableEl) {
|
||||||
const tableBody = tableEl.querySelector('.el-table__body-wrapper');
|
|
||||||
const currentRowEl = tableEl.querySelector('.current-row');
|
const currentRowEl = tableEl.querySelector('.current-row');
|
||||||
if (tableBody && currentRowEl) {
|
if (currentRowEl) {
|
||||||
currentRowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
currentRowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,31 +188,20 @@ const setCurrentRow = (row: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 行点击事件
|
|
||||||
const handleRowClick = (row: any): void => {
|
const handleRowClick = (row: any): void => {
|
||||||
currentIndex.value = adviceBaseList.value.findIndex((item) => item === row);
|
currentIndex.value = adviceBaseList.value.findIndex((item) => item === row);
|
||||||
currentSelectRow.value = row;
|
currentSelectRow.value = row;
|
||||||
emit('selectAdviceBase', row);
|
emit('selectAdviceBase', row);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听表格当前行变化(通过 el-table 的 current-change 事件)
|
|
||||||
watch(
|
|
||||||
() => adviceBaseRef.value?.tableRef,
|
|
||||||
(tableRef) => {
|
|
||||||
if (tableRef) {
|
|
||||||
// 通过 $el 访问原生 el-table 并监听 current-change
|
|
||||||
const elTable = tableRef.$el?.querySelector('.el-table');
|
|
||||||
if (elTable) {
|
|
||||||
// 使用 MutationObserver 或直接监听 DOM 变化来检测当前行变化
|
|
||||||
// 或者通过 watch 监听 currentSelectRow 的变化
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
|
refresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件挂载时自动加载数据(el-popover 懒渲染,父组件 refresh 可能因时序问题未生效,onMounted 最可靠)
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -234,8 +216,4 @@ defineExpose({
|
|||||||
outline: 2px solid #409eff;
|
outline: 2px solid #409eff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover-table-wrapper:focus {
|
|
||||||
outline: 2px solid #409eff;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -170,6 +170,22 @@ export function getAdviceBaseInfo(queryParams) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前科室已配置的药品类别列表
|
||||||
|
*/
|
||||||
|
export function getConfiguredCategories(organizationId) {
|
||||||
|
// organizationId 为空时不发送该参数,避免后端 Long 类型转换 400 错误
|
||||||
|
const params = {};
|
||||||
|
if (organizationId !== undefined && organizationId !== null && organizationId !== '') {
|
||||||
|
params.organizationId = organizationId;
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: '/doctor-station/advice/configured-categories',
|
||||||
|
method: 'get',
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存处方(单条)
|
* 保存处方(单条)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -395,11 +395,12 @@
|
|||||||
clearable
|
clearable
|
||||||
v-model="row.orgId"
|
v-model="row.orgId"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
:data="config.organization"
|
:data="orgTreeData"
|
||||||
:props="{ value: 'id', label: 'name', children: 'children' }"
|
:props="{ value: 'id', label: 'name', children: 'children' }"
|
||||||
value-key="id"
|
value-key="id"
|
||||||
check-strictly
|
check-strictly
|
||||||
default-expand-all
|
default-expand-all
|
||||||
|
:fallback-option="orgFallbackOption"
|
||||||
@change="handleOrgChange"
|
@change="handleOrgChange"
|
||||||
placeholder="请选择执行科室"
|
placeholder="请选择执行科室"
|
||||||
/>
|
/>
|
||||||
@@ -422,7 +423,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {getCurrentInstance, nextTick, onMounted, ref, watch} from 'vue';
|
import {computed, getCurrentInstance, nextTick, onMounted, ref, watch} from 'vue';
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
diagnosisName: string; // 仅用于显示
|
diagnosisName: string; // 仅用于显示
|
||||||
@@ -569,6 +570,54 @@ const handleCancel = () => {
|
|||||||
const handleNumberClick = (item: any) =>
|
const handleNumberClick = (item: any) =>
|
||||||
props.handlers.handleNumberClick(item, props.index, props.row);
|
props.handlers.handleNumberClick(item, props.index, props.row);
|
||||||
const handleOrgChange = (value: any) => props.handlers.handleOrgChange(value, props.index);
|
const handleOrgChange = (value: any) => props.handlers.handleOrgChange(value, props.index);
|
||||||
|
|
||||||
|
// 🔧 关键修复:计算属性,确保当前行的 orgId 始终存在于树数据中
|
||||||
|
// 当 orgId 不在科室树中时,注入一个临时节点,让 el-tree-select 能正确显示中文名称
|
||||||
|
const orgTreeData = computed(() => {
|
||||||
|
const tree = props.config.organization || [];
|
||||||
|
const orgId = props.row?.orgId;
|
||||||
|
const orgName = props.row?.orgName || props.row?.positionName;
|
||||||
|
if (!orgId || !orgName) return tree;
|
||||||
|
|
||||||
|
// 检查 orgId 是否已在树中存在
|
||||||
|
const existsInTree = findOrgName(orgId);
|
||||||
|
if (existsInTree) return tree;
|
||||||
|
|
||||||
|
// 注入临时节点到树根层级
|
||||||
|
return [...tree, { id: String(orgId), name: orgName, children: [] }];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔧 从 organization 树中根据 orgId 查找科室名称(供 fallback-option 使用)
|
||||||
|
function findOrgName(orgId: any): string {
|
||||||
|
if (!orgId || !props.config.organization || props.config.organization.length === 0) return '';
|
||||||
|
const strId = String(orgId);
|
||||||
|
function walk(nodes: any[]): string {
|
||||||
|
if (!nodes) return '';
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (String(node.id) === strId) return node.name;
|
||||||
|
// 模糊匹配:处理大 Long 精度丢失的情况
|
||||||
|
if (typeof node.id === 'string' && node.id.length >= 16 && strId.length >= 16
|
||||||
|
&& node.id.substring(0, 15) === strId.substring(0, 15)) {
|
||||||
|
return node.name;
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
const found = walk(node.children);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return walk(props.config.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 el-tree-select 的 fallback-option:当 orgId 匹配不到树节点时,
|
||||||
|
// 仍然显示对应的中文科室名称而非原始数字 ID
|
||||||
|
// 优先从科室树查找,其次用 row 上保存的 orgName/positionName 兜底
|
||||||
|
const orgFallbackOption = (value: any) => {
|
||||||
|
const name = findOrgName(value);
|
||||||
|
const fallbackName = props.row?.orgName || props.row?.positionName;
|
||||||
|
return { label: name || fallbackName || value, value };
|
||||||
|
};
|
||||||
const convertValues = () => props.handlers.convertValue('doseQuantity', props.row, props.index);
|
const convertValues = () => props.handlers.convertValue('doseQuantity', props.row, props.index);
|
||||||
const convertDoseValues = () => props.handlers.convertValue('dose', props.row, props.index);
|
const convertDoseValues = () => props.handlers.convertValue('dose', props.row, props.index);
|
||||||
const calculateTotalPrice = () => props.handlers.calculateTotal('price', props.row, props.index);
|
const calculateTotalPrice = () => props.handlers.calculateTotal('price', props.row, props.index);
|
||||||
|
|||||||
@@ -80,11 +80,23 @@
|
|||||||
<script setup name="BloodTransfusion">
|
<script setup name="BloodTransfusion">
|
||||||
import {getCurrentInstance, onBeforeMount, onMounted, reactive, ref} from 'vue';
|
import {getCurrentInstance, onBeforeMount, onMounted, reactive, ref} from 'vue';
|
||||||
import {patientInfo} from '../../../store/patient.js';
|
import {patientInfo} from '../../../store/patient.js';
|
||||||
import {getOrgList} from '../../../../../basicmanage/ward/components/api.js';
|
import {getDepartmentList} from '@/api/public.js';
|
||||||
import {getEncounterDiagnosis} from '../../api.js';
|
import {getEncounterDiagnosis} from '../../api.js';
|
||||||
import {getApplicationList, saveBloodTransfusio} from './api';
|
import {getApplicationList, saveBloodTransfusio} from './api';
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
|
// 递归查找树形科室节点
|
||||||
|
const findTreeItem = (list, id) => {
|
||||||
|
if (!list || list.length === 0) return null;
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.id == id) return item;
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
const found = findTreeItem(item.children, id);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
const emits = defineEmits(['submitOk']);
|
const emits = defineEmits(['submitOk']);
|
||||||
const props = defineProps({});
|
const props = defineProps({});
|
||||||
const state = reactive({});
|
const state = reactive({});
|
||||||
@@ -175,9 +187,7 @@ const projectWithDepartment = (selectProjectIds, type) => {
|
|||||||
isRelease = false;
|
isRelease = false;
|
||||||
}
|
}
|
||||||
// 选中项目中的执行科室id与全部科室数据做匹配
|
// 选中项目中的执行科室id与全部科室数据做匹配
|
||||||
const findItem = orgOptions.value.find((item) => {
|
const findItem = findTreeItem(orgOptions.value, obj.orgId);
|
||||||
return item.id == obj.orgId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!findItem) {
|
if (!findItem) {
|
||||||
isRelease = false;
|
isRelease = false;
|
||||||
@@ -249,8 +259,8 @@ const submit = () => {
|
|||||||
};
|
};
|
||||||
/** 查询科室 */
|
/** 查询科室 */
|
||||||
const getLocationInfo = () => {
|
const getLocationInfo = () => {
|
||||||
getOrgList().then((res) => {
|
getDepartmentList().then((res) => {
|
||||||
orgOptions.value = res.data?.records[0]?.children;
|
orgOptions.value = res.data || [];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// 获取诊断目录
|
// 获取诊断目录
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user