Compare commits
6 Commits
fix-668-te
...
681107ca64
| Author | SHA1 | Date | |
|---|---|---|---|
| 681107ca64 | |||
| f75133369a | |||
| ca812421d2 | |||
| ae12cb2135 | |||
| d2a1cd6f29 | |||
| d9d2b83c5b |
544
AGENTS.md
544
AGENTS.md
@@ -107,6 +107,143 @@
|
|||||||
- **违规判定**: 因修改导致原有代码编译失败或运行报错,视为违反铁律18,必须立即回滚修复
|
- **违规判定**: 因修改导致原有代码编译失败或运行报错,视为违反铁律18,必须立即回滚修复
|
||||||
|
|
||||||
|
|
||||||
|
**铁律19: 编译错误不区分来源(Bug #698 教训)**
|
||||||
|
- `mvn compile`、`vite build`、`vue-tsc` 等构建命令报错 = 不过关,**不管是自己引入的还是历史遗留的**
|
||||||
|
- 禁止说"这是预存问题""不是我改的""原有bug"——构建通不过就不能宣称完成
|
||||||
|
- 正确做法:定位错误 → 修复 → 重新构建确认通过 → 然后才能继续
|
||||||
|
- **违规判定**: 构建命令有 ERROR 但未修复就报告"编译通过",视为违反铁律
|
||||||
|
|
||||||
|
**铁律20: 数据来源必须验证(Bug #698 教训)**
|
||||||
|
- 涉及数据查询/提取时,必须先确认数据实际存储位置,不能假设
|
||||||
|
- 案例:从 `raw_steps_html` 提取 fileID,而不是从 `steps`(纯文本,已被 strip)
|
||||||
|
- 修复前必须:打印/检查原始数据结构 → 确认字段存在 → 再写提取逻辑
|
||||||
|
- 禁止:凭代码推断数据位置、假设"应该在这里"
|
||||||
|
|
||||||
|
**铁律21: 外部配置值必须实测验证(Bug #698 教训)**
|
||||||
|
- 使用外部服务(API、模型、数据库)的配置值,必须实际调用验证,不能仅凭记忆或推测
|
||||||
|
- 案例:模型名 `mino-v2.5` 应为 `mimo-v2.5`,拼写错误导致 400
|
||||||
|
- 配置变更后必须:发起一次真实请求 → 确认返回 200 → 再宣称配置正确
|
||||||
|
- 禁止:改完配置不测试、假设"应该能用"
|
||||||
|
|
||||||
|
**铁律22: 端到端验证必须有实际输出证据(Bug #698 教训)**
|
||||||
|
- 声称功能生效前,必须有实际的端到端输出证据
|
||||||
|
- 不能仅凭代码路径推断"应该走了 vision"——必须看到实际返回内容
|
||||||
|
- 验证方式:运行命令 → 检查输出中包含预期关键词(如 vision 分析结果、图片识别文字)
|
||||||
|
- 禁止:只检查代码路径可达就算"验证通
|
||||||
|
**铁律23: 文件读写强制 UTF-8 编码(必遵守)**
|
||||||
|
- **禁止**使用 Get-Content -Raw(不带 -Encoding UTF8)读取源文件
|
||||||
|
- **禁止**使用 Out-File -Encoding utf8(会写 BOM)
|
||||||
|
- **正确写法**:
|
||||||
|
- 读取:[System.IO.File]::ReadAllText(, [System.Text.Encoding]::UTF8)
|
||||||
|
- 写入:[System.IO.File]::WriteAllText(, # HealthLink-HIS — AI 开发规范
|
||||||
|
|
||||||
|
> 🤖 本文件由 Codex CLI、Claude Code 等工具自动读取。
|
||||||
|
> 工具进入项目目录时会自动加载此文件作为开发规范上下文。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# HealthLink-HIS — AI 开发规范(自动加载)
|
||||||
|
|
||||||
|
> 🤖 **本文件供所有 AI 编码工具自动读取**。进入本项目后必须遵守以下规范。
|
||||||
|
>
|
||||||
|
> **模型决定上限,Harness 决定底线。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目概览
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|------|------|
|
||||||
|
| 项目名 | HealthLink-HIS(医院信息系统) |
|
||||||
|
| 后端路径 | `healthlink-his-server/` |
|
||||||
|
| 前端路径 | `healthlink-his-ui/` |
|
||||||
|
| 文档路径 | `MD/` |
|
||||||
|
| JDK | 25 (OpenJDK) |
|
||||||
|
| Spring Boot | 4.0.6 |
|
||||||
|
| MyBatis-Plus | 3.5.16 |
|
||||||
|
| Vue | 3.x + Vite + Element Plus |
|
||||||
|
| 数据库 | PostgreSQL 15+ |
|
||||||
|
| 包名 | `com.healthlink.his` |
|
||||||
|
| 后端端口 | 18082 |
|
||||||
|
| 前端端口 | 81 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、铁律(必须遵守,违反即失败)
|
||||||
|
|
||||||
|
### 🔴 P0 铁律 — 不可违反
|
||||||
|
|
||||||
|
**铁律1: 修改完必须测试**
|
||||||
|
```
|
||||||
|
后端: mvn clean compile -DskipTests → mvn install -DskipTests → mvn test
|
||||||
|
前端: npm run build:dev → npm run lint
|
||||||
|
```
|
||||||
|
- 白盒:编译通过,无 ERROR
|
||||||
|
- 黑盒:关键接口返回 `{code:200, data:...}`,验证业务逻辑
|
||||||
|
- 冒烟:应用正常启动,核心流程通畅
|
||||||
|
|
||||||
|
**铁律2: Flyway 数据库迁移**
|
||||||
|
- 凡是新建表、新增字段,必须创建 Flyway 迁移脚本
|
||||||
|
- 路径:`healthlink-his-domain/src/main/resources/db/migration/`
|
||||||
|
- 命名:`V{版本号}__{描述}.sql`(双下划线)
|
||||||
|
|
||||||
|
**铁律3: 测试通过后才提交**
|
||||||
|
- 编译 + 测试全部通过后才能 git commit
|
||||||
|
- 不提交未完成的功能、调试代码、临时文件
|
||||||
|
|
||||||
|
**铁律4: 前后端API路径对齐**
|
||||||
|
- 后端前缀:`/healthlink-his/api/v1/`
|
||||||
|
- 前端 `request.js` 的 baseURL 必须与后端匹配
|
||||||
|
|
||||||
|
**铁律5: 状态值一致性(Bug #574 教训)**
|
||||||
|
- 修改任何状态值前,必须先列出完整的状态流转链路
|
||||||
|
- 检查项:枚举定义 → Service 设置 → 查询映射 → 前端 STATUS_CLASS_MAP → 前端 v-if → 统计SQL
|
||||||
|
- 禁止:只改一端不检查其他端
|
||||||
|
|
||||||
|
**铁律6: 禁止删除源文件(Bug #574 教训)**
|
||||||
|
- 绝对禁止删除项目中已有的 Java/Vue/SQL 源文件
|
||||||
|
- 编译错误 → 修复错误;重复文件 → 重构合并
|
||||||
|
- 唯一例外:明确由人类确认删除的文件
|
||||||
|
|
||||||
|
**铁律7: 禁止修改已有公开方法签名**
|
||||||
|
- 不能删除/重命名已有的 public 方法,不能修改参数列表
|
||||||
|
- 需要新功能 → 添加重载方法;需要改行为 → 修改内部实现
|
||||||
|
|
||||||
|
**铁律8: 验证后才宣称完成(Verification Before Completion)**
|
||||||
|
- **没有跑过验证命令,就不能说"完成了""通过了""没问题"**
|
||||||
|
- 禁止使用"应该可以""大概没问题""看起来正确"
|
||||||
|
- 必须:运行命令 → 读取输出 → 确认结果 → 才能宣称
|
||||||
|
- 这是诚实原则,不是效率问题
|
||||||
|
|
||||||
|
|
||||||
|
**铁律9: 开发前必须审核原有代码(P0 — 铁律)**
|
||||||
|
- **任何新功能开发前,必须先搜索项目中是否已有相关代码**
|
||||||
|
- 搜索路径:Controller / AppService / Service / Mapper / Entity / 前端页面 / API接口
|
||||||
|
- 如果已有部分功能 → 在原有代码基础上**升级优化完善**,禁止另起炉灶
|
||||||
|
- 如果已有接口但前端缺失 → 只补前端,不重复建后端
|
||||||
|
- 如果已有前端但后端缺失 → 只补后端,不重写前端
|
||||||
|
- 搜索命令:`rg -l "关键词" healthlink-his-server/ healthlink-his-ui/src/`
|
||||||
|
- 禁止:不看代码就新建模块、重复实现已有功能、废弃原有代码另写一套
|
||||||
|
|
||||||
|
|
||||||
|
**铁律12: 设计文档确认后自主开发(铁律)**
|
||||||
|
- 设计文档(如 `MD/architecture/GRADE3A_GAP_ANALYSIS_AND_DESIGN.md`)一旦确认,后续开发**必须按文档自主执行**
|
||||||
|
- **禁止反复询问"是否继续""下一步做什么""是否开始"**——直接按计划推进
|
||||||
|
- 每完成一个 Sprint,自动提交推送,然后立即开始下一个 Sprint
|
||||||
|
- 只在遇到**无法解决的阻塞**(如技术选型冲突、需求不明确、第三方依赖不可用)时才暂停询问
|
||||||
|
- 设计文档是"**已签合同**",不是"参考意见"。铁律执行优先级:设计文档 > 人类临时指令 > AI 自行判断
|
||||||
|
|
||||||
|
**铁律18: 禁止破坏原有功能(P0绝对铁律)**
|
||||||
|
- **完善增加功能和流程时,绝对不能破坏或者让原有功能不能用**
|
||||||
|
- 修改已有实体前必须对比原始文件(`git show HEAD~N:./file.java`),保留所有原有字段和方法
|
||||||
|
- 新增字段只能追加,不能删除或重命名已有字段
|
||||||
|
- SQL迁移只允许 `ALTER TABLE ADD COLUMN`,不允许 `DROP COLUMN` 或 `RENAME COLUMN`
|
||||||
|
- Controller新端点不能修改已有端点的路径或参数
|
||||||
|
- 前端新页面不能修改已有页面的组件结构
|
||||||
|
- 每次修改后必须 `mvn clean compile -DskipTests` 验证
|
||||||
|
- **违规判定**: 因修改导致原有代码编译失败或运行报错,视为违反铁律18,必须立即回滚修复
|
||||||
|
|
||||||
|
|
||||||
**铁律19: 编译错误不区分来源(Bug #698 教训)**
|
**铁律19: 编译错误不区分来源(Bug #698 教训)**
|
||||||
- `mvn compile`、`vite build`、`vue-tsc` 等构建命令报错 = 不过关,**不管是自己引入的还是历史遗留的**
|
- `mvn compile`、`vite build`、`vue-tsc` 等构建命令报错 = 不过关,**不管是自己引入的还是历史遗留的**
|
||||||
- 禁止说"这是预存问题""不是我改的""原有bug"——构建通不过就不能宣称完成
|
- 禁止说"这是预存问题""不是我改的""原有bug"——构建通不过就不能宣称完成
|
||||||
@@ -533,3 +670,410 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
|||||||
---
|
---
|
||||||
|
|
||||||
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||||
|
, [System.Text.UTF8Encoding]::new(False))
|
||||||
|
- git提取:先 Out-File -Encoding utf8 保存到临时文件,再用 [System.IO.File]::ReadAllText() 读取
|
||||||
|
- **根因**:PowerShell 管道会丢失换行符,git show | Out-File 会将多行文件压缩为一行
|
||||||
|
过"
|
||||||
|
|
||||||
|
|
||||||
|
### 🟡 P1 铁律 — 强烈建议
|
||||||
|
|
||||||
|
**铁律9: 先分解再行动**
|
||||||
|
- 修改超过3个文件、涉及多模块、数据库变更,必须先制定计划
|
||||||
|
|
||||||
|
**铁律10: 验证后信**
|
||||||
|
- 每次修改后必须验证编译通过,不信记忆
|
||||||
|
|
||||||
|
**铁律13: 文档统一管理**
|
||||||
|
- 所有文档存储在 `MD/` 目录
|
||||||
|
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
||||||
|
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**铁律14: 设计文档必须包含UI设计和调用流程**
|
||||||
|
- 所有新模块/页面的设计文档必须包含:UI布局描述、交互效果清单、前后端调用流程
|
||||||
|
- 没有明确UI设计的模块,禁止直接编码
|
||||||
|
- 详见
|
||||||
|
- 设计文档必须写清楚:系统调用关系、方法函数调用关系、完整业务流程
|
||||||
|
- 设计文档中每个用户操作必须对应:前端事件 → API调用 → 后端处理链路 → 返回数据 → UI渲染
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Karpathy 编码准则
|
||||||
|
|
||||||
|
> 减少 LLM 常见编码错误。偏向谨慎而非速度。
|
||||||
|
|
||||||
|
### 3.1 先想再写
|
||||||
|
- 明确陈述假设,不确定就问
|
||||||
|
- 多种解读时都列出来,不要默默选一种
|
||||||
|
- 有更简单的方案就说出来,该推回就推回
|
||||||
|
- 不清楚的地方停下来,说清楚哪里不清楚
|
||||||
|
|
||||||
|
### 3.2 简洁优先
|
||||||
|
- 不做没要求的功能,不做一次性代码的抽象
|
||||||
|
- 不加没要求的"灵活性"和"可配置性"
|
||||||
|
- 200 行能 50 行搞定就重写
|
||||||
|
- 自问:"高级工程师会不会觉得这过度设计?"
|
||||||
|
|
||||||
|
### 3.3 精准修改
|
||||||
|
- 只改必须改的,不"顺手改进"相邻代码
|
||||||
|
- 匹配现有代码风格,即使你有不同的偏好
|
||||||
|
- 每行改动都能追溯到用户的请求
|
||||||
|
- 只清理你自己改动产生的无用代码
|
||||||
|
|
||||||
|
### 3.4 目标驱动
|
||||||
|
- 把任务转化为可验证目标
|
||||||
|
- 多步任务声明计划:`[步骤] → 验证: [检查]`
|
||||||
|
- 强验收标准让 Agent 能独立循环,弱标准需要持续澄清
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、全链路 6 环分析
|
||||||
|
|
||||||
|
> ⚠️ **涉及数据库字段的 Bug / 需求,必须走完整链路。**
|
||||||
|
|
||||||
|
```
|
||||||
|
前端/页面 → Controller → Service → Mapper → DB/SQL → 关联模块
|
||||||
|
①录入 ②验证 ③业务 ④持久化 ⑤存储 ⑥联动
|
||||||
|
```
|
||||||
|
|
||||||
|
| 环 | 检查内容 |
|
||||||
|
|----|---------|
|
||||||
|
| ① 录入 | 前端有无输入入口(弹窗、表格行编辑、表单) |
|
||||||
|
| ② 验证 | Controller 参数校验、@Valid、权限控制 |
|
||||||
|
| ③ 业务 | Service 业务逻辑、事务边界、多个 Service 实现类入口 |
|
||||||
|
| ④ 持久化 | Mapper XML、DTO 字段映射、类型转换 |
|
||||||
|
| ⑤ 存储 | 数据库表结构、索引、NOT NULL 约束 |
|
||||||
|
| ⑥ 联动 | 上游(医嘱→护士站)、下游(打印、计费、报表)是否同步 |
|
||||||
|
|
||||||
|
**修复后的验证顺序**:
|
||||||
|
1. 数据库:确认状态值已正确写入
|
||||||
|
2. 后端接口:确认返回的状态映射正确
|
||||||
|
3. 前端显示:确认页面显示正确状态文本
|
||||||
|
4. 前端交互:确认按钮/操作基于正确状态启用/禁用
|
||||||
|
5. 统计数据:确认池/报表统计包含新状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、Harness Engineering 方法论
|
||||||
|
|
||||||
|
> Harness = 约束 + 反馈 + 控制平面 + 持久执行
|
||||||
|
|
||||||
|
### 5.1 四层约束金字塔
|
||||||
|
|
||||||
|
| 层级 | 内容 | 落地方式 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **L1 架构约束** | 接口合约、包结构、命名规范、禁止模式 | 本文件铁律 |
|
||||||
|
| **L2 代码质量** | 圈复杂度、代码风格、类型提示 | 编译门禁 + ESLint |
|
||||||
|
| **L3 安全约束** | 敏感信息检测、权限检查、输入验证 | 配置不可硬编码 |
|
||||||
|
| **L4 业务规则** | 领域逻辑、数据一致性、事务边界 | 全链路 6 环验证 |
|
||||||
|
|
||||||
|
**约束设计原则**:
|
||||||
|
- **可验证**:每条约束必须能被自动化检查("覆盖率>90%"✅ "质量要高"❌)
|
||||||
|
- **无歧义**:"每函数不超过50行"✅ "函数不要太长"❌
|
||||||
|
- **优先级**:安全(1) > 架构(2) > 业务(3) > 质量(4) > 性能(5)
|
||||||
|
- **渐进增强**:L1编译通过 → L2+命名规范 → L3+测试覆盖 → L4+安全扫描
|
||||||
|
|
||||||
|
### 5.2 三层反馈系统
|
||||||
|
|
||||||
|
| 层级 | 速度 | 覆盖范围 | 失败处理 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| **L1 编译检查** | <30秒 | 语法、类型、签名 | 立即阻断,自行修复 |
|
||||||
|
| **L2 数据流验证** | <5分钟 | 全链路字段、Mapper XML、DTO | 修复后上报 |
|
||||||
|
| **L3 人工审查** | 10-30分钟 | 架构、设计、业务正确性 | 驳回/指导/批准 |
|
||||||
|
|
||||||
|
**反馈铁律**:
|
||||||
|
- 反馈必须可行动(文件 + 行号 + 错误类型 + 修复方向)
|
||||||
|
- 失败后先回滚到最近检查点,再重试
|
||||||
|
- 持续失败3次 → 上报人类
|
||||||
|
|
||||||
|
### 5.3 控制平面
|
||||||
|
|
||||||
|
```
|
||||||
|
战略层(人类) → 设定目标、审批决策、异常升级
|
||||||
|
战术层(Agent) → 任务分解、update_plan、依赖协调、检查点保存
|
||||||
|
执行层(Agent) → 代码生成、测试执行、错误恢复、幂等重试
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 持久执行
|
||||||
|
|
||||||
|
- 每个关键步骤保存检查点(`update_plan` 进度)
|
||||||
|
- 失败后从最新检查点恢复,不从头开始
|
||||||
|
- 幂等设计:同一操作重复执行结果一致
|
||||||
|
- **三层状态管理**:系统层(工作流ID/超时/重试) → 执行层(当前活动/进度) → 业务层(已完成工作/中间产物)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、五层质量门禁
|
||||||
|
|
||||||
|
| 门禁 | 时间 | 范围 | 失败处理 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| **L1 编译检查** | <30秒 | 语法、类型、导入 | Agent 自行修复 |
|
||||||
|
| **L2 静态分析** | <2分钟 | 代码风格、复杂度、安全 | Agent 修复 |
|
||||||
|
| **L3 单元测试** | <5分钟 | 功能正确性、边界条件 | 自动修复或上报 |
|
||||||
|
| **L4 集成测试** | <15分钟 | 模块间交互、数据流 | 上报人工 |
|
||||||
|
| **L5 生产验证** | 持续 | 监控、告警、性能 | 自动回滚 |
|
||||||
|
|
||||||
|
**提交铁律**:L1-L2 必须通过才能 commit,L3(如有DB变更)必须通过才能 push
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、系统化调试(Systematic Debugging)
|
||||||
|
|
||||||
|
> **铁律:没有根因调查,不能提出修复方案。**
|
||||||
|
|
||||||
|
### 四阶段流程
|
||||||
|
|
||||||
|
**阶段1:根因调查**(修复前必须完成)
|
||||||
|
1. 仔细阅读错误信息(堆栈、行号、错误码)
|
||||||
|
2. 稳定复现(能否可靠触发?步骤?每次?)
|
||||||
|
3. 检查最近变更(git diff、新依赖、配置变更)
|
||||||
|
4. 多组件系统:在每个组件边界加诊断日志,定位哪一层断裂
|
||||||
|
5. 追踪数据流:坏值从哪里来?谁调用的?一直追溯到源头
|
||||||
|
|
||||||
|
**阶段2:模式分析**
|
||||||
|
- 找到同代码库中类似的正常工作代码
|
||||||
|
- 逐项对比差异
|
||||||
|
- 理解依赖关系
|
||||||
|
|
||||||
|
**阶段3:假设与测试**
|
||||||
|
- 形成单一假设:"我认为X是根因,因为Y"
|
||||||
|
- 做最小改动测试
|
||||||
|
- 有效 → 阶段4;无效 → 新假设
|
||||||
|
|
||||||
|
**阶段4:实施**
|
||||||
|
- 创建失败测试用例
|
||||||
|
- 修复根因(不是症状)
|
||||||
|
- 验证修复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、后端开发规范
|
||||||
|
|
||||||
|
### 分层架构
|
||||||
|
```
|
||||||
|
Controller → AppService → Service → Mapper → Entity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
| 类型 | 规则 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| Controller | `XxxController` | `RegistrationController` |
|
||||||
|
| AppService | `IXxxAppService` / `XxxAppServiceImpl` | `IRegistrationAppService` |
|
||||||
|
| Service | `IXxxService` / `XxxServiceImpl` | `IRegistrationService` |
|
||||||
|
| Mapper | `XxxMapper` | `RegistrationMapper` |
|
||||||
|
| Entity | `Xxx` | `Registration` |
|
||||||
|
| DTO | `XxxDto` / `XxxQueryDto` | `RegistrationDto` |
|
||||||
|
|
||||||
|
### 包结构
|
||||||
|
```
|
||||||
|
com.healthlink.his.web.{module}.controller
|
||||||
|
com.healthlink.his.web.{module}.appservice
|
||||||
|
com.healthlink.his.web.{module}.service
|
||||||
|
com.healthlink.his.web.{module}.mapper
|
||||||
|
com.healthlink.his.web.{module}.dto
|
||||||
|
com.healthlink.his.domain.{module}
|
||||||
|
com.healthlink.his.common.enums
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键约束
|
||||||
|
- 所有查询使用 `LambdaQueryWrapper`,禁止字符串拼接 SQL
|
||||||
|
- `@Transactional(rollbackFor = Exception.class)` 管理事务
|
||||||
|
- 所有接口标注 `@PreAuthorize` 权限控制
|
||||||
|
- 患者敏感信息在日志中脱敏
|
||||||
|
- **扩展功能不修改原有函数签名**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、前端开发规范
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- Vue 3 + Vite + Element Plus + Pinia + Axios(基于 RuoYi-Vue3)
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
```
|
||||||
|
src/api/{module}/ # API接口
|
||||||
|
src/views/{module}/ # 页面组件
|
||||||
|
src/store/modules/ # Pinia状态管理
|
||||||
|
src/components/ # 公共组件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键约束
|
||||||
|
- API前缀:`/healthlink-his/api/v1/`
|
||||||
|
- 路由懒加载:`() => import('@/views/xxx/index.vue')`
|
||||||
|
- 页面使用 `<script setup>` 语法
|
||||||
|
- 按钮权限使用 `v-hasPermi` 指令
|
||||||
|
- `onMounted` 中注册的事件在 `onUnmounted` 中移除
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、Agent 体系
|
||||||
|
|
||||||
|
### 角色与路由
|
||||||
|
|
||||||
|
| 代号 | 名称 | 角色 | 路由关键词 |
|
||||||
|
|------|------|------|-----------|
|
||||||
|
| liubei | 刘备 | 项目经理 | 协调、分派、异常升级 |
|
||||||
|
| zhugeliang | 诸葛亮 | 架构师 | 分析、路由、设计 |
|
||||||
|
| guanyu | 关羽 | 后端开发 | java, api, spring, service, controller |
|
||||||
|
| zhaoyun | 赵云 | 前端开发 | vue, 界面, 显示, 弹窗, 按钮 |
|
||||||
|
| xunyu | 荀彧 | DBA | 数据库, sql, 迁移, mapper xml |
|
||||||
|
| zhangfei | 张飞 | 测试 | 测试, QA, 回归 |
|
||||||
|
| huatuo | 华佗 | 验收 | 需求验收、质量确认 |
|
||||||
|
| chenlin | 陈琳 | 文档 | 文档、归档、Git提交 |
|
||||||
|
|
||||||
|
### 协作流水线
|
||||||
|
|
||||||
|
```
|
||||||
|
刘备(协调) → 诸葛亮(分析路由) → {关羽|赵云}(修复) → 荀彧(DB审查) → 张飞(测试) → 华佗(验收) → 陈琳(归档)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bug 修复完整管线(BDT 方法论)
|
||||||
|
|
||||||
|
```
|
||||||
|
获取Bug → 设计测试用例 → 基线测试(应失败) → 全链路修复 → 回归测试(应通过) → 扩展测试(无回归) → 提交
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bug 状态管理铁律
|
||||||
|
- 人类提的 Bug:只加备注,不改状态,不改分配
|
||||||
|
- 智能体提的 Bug:可以改分配和加备注
|
||||||
|
- 已关闭/已解决的 Bug 不再处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、审查与审计
|
||||||
|
|
||||||
|
### 三层审查体系
|
||||||
|
|
||||||
|
| 层级 | 内容 | 时机 |
|
||||||
|
|------|------|------|
|
||||||
|
| **L1 自审** | Agent 对照约束逐条检查 | 每次提交前 |
|
||||||
|
| **L2 配对审查** | Agent 生成变更摘要,人类终审 | PR/提交时 |
|
||||||
|
| **L3 合规审查** | 审计追踪,记录所有 AI 操作 | 持续 |
|
||||||
|
|
||||||
|
### L1 自审清单
|
||||||
|
```yaml
|
||||||
|
self_review:
|
||||||
|
- "所有修改能通过编译?"
|
||||||
|
- "遵守命名规范?"
|
||||||
|
- "测试覆盖达标?"
|
||||||
|
- "没有遗漏的 TODO / DEBUG?"
|
||||||
|
- "变更范围没超出任务边界?"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 评审评分维度
|
||||||
|
| 维度 | 问题 |
|
||||||
|
|------|------|
|
||||||
|
| 正确性 | 行为是否符合目标功能? |
|
||||||
|
| 验证 | 检查是否真的跑过并留下证据? |
|
||||||
|
| 范围纪律 | 是否保持在选定功能范围内? |
|
||||||
|
| 可靠性 | 结果能否在重启后继续工作? |
|
||||||
|
| 可维护性 | 代码和文档是否清楚到可交接? |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、标准工作循环
|
||||||
|
|
||||||
|
```
|
||||||
|
开始会话
|
||||||
|
├→ 1. Init — 读 AGENTS.md + PROGRESS.md + git log
|
||||||
|
├→ 2. Select — 只选一个未完成功能
|
||||||
|
├→ 3. Implement — 一次只做一个,不扩大范围
|
||||||
|
├→ 4. Verify — 运行验证命令,有证据才标记完成
|
||||||
|
└→ 5. Cleanup — 更新进度 + clean-state-checklist + git commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 会话结束前必须运行 Clean State Checklist
|
||||||
|
```
|
||||||
|
□ 标准启动路径仍然可用
|
||||||
|
□ 标准验证路径仍然可运行
|
||||||
|
□ 当前进度已记录到进度日志
|
||||||
|
□ 无半成品步骤处于未记录状态
|
||||||
|
□ 下一轮会话无需人工修复即可继续
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十三、开发流程
|
||||||
|
|
||||||
|
```
|
||||||
|
收到任务
|
||||||
|
├→ ① 分析需求 → 读相关文档(MD/)、读全链路6环
|
||||||
|
├→ ② 制定计划 → update_plan (3-6个阶段)
|
||||||
|
├→ ③ 后端开发 → Controller → AppService → Service → Mapper → Entity → Flyway
|
||||||
|
├→ ④ 后端测试 → mvn test → 接口测试(业务逻辑验证)
|
||||||
|
├→ ⑤ 前端开发 → API接口 → 页面组件 → 路由配置
|
||||||
|
├→ ⑥ 前端测试 → npm run build:dev → 功能验证
|
||||||
|
├→ ⑦ 质量门禁 → L1编译 → L2测试 → L3DB审查 → L4验收 → L5归档
|
||||||
|
└→ ⑧ 提交代码 → git commit(规范格式) → git push → 文档更新
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Commit 格式
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
type: feat|fix|docs|refactor|test|chore
|
||||||
|
scope: 模块名(如 registration, billing, pharmacy)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十四、快速参考命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# === 后端 ===
|
||||||
|
export JAVA_HOME=/opt/jdk-25
|
||||||
|
mvn clean compile -DskipTests # 编译
|
||||||
|
mvn install -DskipTests # 构建
|
||||||
|
mvn test -pl healthlink-his-application -Dtest="XxxTest" -Dsurefire.failIfNoSpecifiedTests=false
|
||||||
|
|
||||||
|
# === 前端 ===
|
||||||
|
cd healthlink-his-ui
|
||||||
|
npm run dev && npm run build:dev && npm run lint && npm run test:run
|
||||||
|
|
||||||
|
# === Git ===
|
||||||
|
git status && git add -A && git commit -m "feat(module): desc" && git push origin develop
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十五、详细规范文档索引
|
||||||
|
|
||||||
|
| 文档 | 路径 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| 执行铁律 | `MD/specs/IRON_RULES.md` | 铁律完整版 |
|
||||||
|
| 后端规范 | `MD/specs/BACKEND_DEVELOPMENT_STANDARD.md` | 后端编码标准 |
|
||||||
|
| 前端规范 | `MD/specs/FRONTEND_DEVELOPMENT_STANDARD.md` | 前端编码标准 |
|
||||||
|
| Harness方法论 | `MD/specs/HARNESS_ENGINEERING.md` | 完整Harness+Agent方法论 |
|
||||||
|
| 文档规范 | `MD/DOCUMENTATION_STANDARD.md` | 文档管理标准 |
|
||||||
|
| 后端清单 | `MD/specs/BACKEND_CHECKLIST.md` | 发布前检查 |
|
||||||
|
| 前端清单 | `MD/specs/FRONTEND_CHECKLIST.md` | 发布前检查 |
|
||||||
|
| 三甲标准 | `MD/standards/GRADE3A_HIS_STANDARD.md` | 三甲医院达标标准 |
|
||||||
|
| Flyway指南 | `MD/guides/FLYWAY_USAGE_GUIDE.md` | 数据库迁移指南 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十六、过往教训
|
||||||
|
|
||||||
|
| 教训 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态链路断裂 | Bug#574: 签到设 BOOKED(1) 而非 CHECKED_IN(3),前端映射缺失 → 必须走完整状态链路 |
|
||||||
|
| 盲删源文件 | AI 看到编译错误直接删文件,没检查 baseline → 必须先确认文件来源 |
|
||||||
|
| 修复方向偏差 | 多次 fallback 改的是错误的 Service → 必须用 rg 搜索所有相关代码路径 |
|
||||||
|
| bug_reports 缺列 | INSERT 静默失败 → 必须检查表结构 |
|
||||||
|
| 禅道 comment API | API 不存在,用 resolve+activate workaround |
|
||||||
|
| SQLite WAL 并发 | 多进程并发写需要 checkpoint |
|
||||||
|
| UTF-8 切片 | 多字节字符不能用 byte index 切片 |
|
||||||
|
| 上下文焦虑 | Agent 感觉上下文快满时会匆忙结束,跳过验证 → 注意 context 40% 阈值 |
|
||||||
|
| 过早宣告胜利 | 自评≠验证,分开"干活"和"检查" |
|
||||||
|
| 覆盖率幻觉 | 覆盖率达标但逻辑没测 → 引入变异测试 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 本文件是 AI 开发规范的唯一信源。各工具配置文件由 `bash scripts/sync-ai-rules.sh` 同步。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ public class TicketAppServiceImpl implements ITicketAppService {
|
|||||||
dto.setIdCard(raw.getIdCard());
|
dto.setIdCard(raw.getIdCard());
|
||||||
dto.setDoctorId(raw.getDoctorId());
|
dto.setDoctorId(raw.getDoctorId());
|
||||||
dto.setDepartmentId(raw.getDepartmentId());
|
dto.setDepartmentId(raw.getDepartmentId());
|
||||||
dto.setRealPatientId(raw.getPatientId());
|
dto.setRealPatientId(raw.getRealPatientId() != null ? raw.getRealPatientId() : raw.getPatientId());
|
||||||
dto.setOrderId(raw.getOrderId());
|
dto.setOrderId(raw.getOrderId());
|
||||||
dto.setOrderNo(raw.getOrderNo());
|
dto.setOrderNo(raw.getOrderNo());
|
||||||
|
|
||||||
|
|||||||
@@ -515,29 +515,28 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
QueryWrapper<CurrentDayEncounterDto> queryWrapper = HisQueryUtils.buildQueryWrapper(null, searchKey,
|
QueryWrapper<CurrentDayEncounterDto> queryWrapper = HisQueryUtils.buildQueryWrapper(null, searchKey,
|
||||||
new HashSet<>(Arrays.asList("patient_name", "organization_name", "practitioner_name", "healthcare_name", "identifier_no")),
|
new HashSet<>(Arrays.asList("patient_name", "organization_name", "practitioner_name", "healthcare_name", "identifier_no")),
|
||||||
request);
|
null); // registerTimeSTime/ETime 已下推到 SQL 内层 WHERE,跳过 buildQueryWrapper 的自动 *STime/*ETime 处理
|
||||||
|
|
||||||
// 手动处理 statusEnum 参数(用于过滤退号记录)
|
// 提取statusEnum参数,下推到内层WHERE(避免外层重复过滤)
|
||||||
|
Integer statusFilter = null;
|
||||||
String statusEnumParam = request.getParameter("statusEnum");
|
String statusEnumParam = request.getParameter("statusEnum");
|
||||||
if (statusEnumParam != null && !statusEnumParam.isEmpty()) {
|
if (statusEnumParam != null && !statusEnumParam.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
Integer statusEnum = Integer.parseInt(statusEnumParam);
|
statusFilter = Integer.parseInt(statusEnumParam);
|
||||||
if (statusEnum == -1) {
|
|
||||||
// -1 表示排除退号记录(正常挂号)
|
|
||||||
queryWrapper.ne("status_enum", 6);
|
|
||||||
} else {
|
|
||||||
// 其他值表示精确匹配
|
|
||||||
queryWrapper.eq("status_enum", statusEnum);
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// 忽略无效的参数值
|
// 忽略无效的参数值
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取日期范围参数,下推到内层WHERE以优化性能(避免全表JOIN后再过滤)
|
||||||
|
String registerTimeSTime = request.getParameter("registerTimeSTime");
|
||||||
|
String registerTimeETime = request.getParameter("registerTimeETime");
|
||||||
|
|
||||||
IPage<CurrentDayEncounterDto> currentDayEncounter = outpatientRegistrationAppMapper.getCurrentDayEncounter(
|
IPage<CurrentDayEncounterDto> currentDayEncounter = outpatientRegistrationAppMapper.getCurrentDayEncounter(
|
||||||
new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(),
|
new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(),
|
||||||
ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper,
|
ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper,
|
||||||
ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue());
|
ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue(),
|
||||||
|
registerTimeSTime, registerTimeETime, statusFilter);
|
||||||
|
|
||||||
// 过滤候选池排除列表
|
// 过滤候选池排除列表
|
||||||
// 仅当调用方显式传 excludeFromCandidatePool=true 时才过滤,避免非分诊场景(挂号/收费)
|
// 仅当调用方显式传 excludeFromCandidatePool=true 时才过滤,避免非分诊场景(挂号/收费)
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ public interface OutpatientRegistrationAppMapper {
|
|||||||
@Param("classEnum") Integer classEnum, @Param("statusEnum") Integer statusEnum,
|
@Param("classEnum") Integer classEnum, @Param("statusEnum") Integer statusEnum,
|
||||||
@Param("participantType1") String participantType1, @Param("participantType2") String participantType2,
|
@Param("participantType1") String participantType1, @Param("participantType2") String participantType2,
|
||||||
@Param(Constants.WRAPPER) QueryWrapper<CurrentDayEncounterDto> queryWrapper,
|
@Param(Constants.WRAPPER) QueryWrapper<CurrentDayEncounterDto> queryWrapper,
|
||||||
@Param("register") Integer register, @Param("paymentStatus") Integer paymentStatus);
|
@Param("register") Integer register, @Param("paymentStatus") Integer paymentStatus,
|
||||||
|
@Param("registerTimeSTime") String registerTimeSTime,
|
||||||
|
@Param("registerTimeETime") String registerTimeETime,
|
||||||
|
@Param("statusFilter") Integer statusFilter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询item绑定的信息(耗材或诊疗)
|
* 查询item绑定的信息(耗材或诊疗)
|
||||||
|
|||||||
@@ -451,10 +451,12 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
|
|||||||
// 删除费用项
|
// 删除费用项
|
||||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.MED_MEDICATION_REQUEST,
|
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.MED_MEDICATION_REQUEST,
|
||||||
adviceSaveDto.getRequestId());
|
adviceSaveDto.getRequestId());
|
||||||
// 删除代煎费
|
// 删除代煎费(按处方号精确清理)
|
||||||
iChargeItemService.remove(new LambdaQueryWrapper<ChargeItem>()
|
if (adviceSaveDto.getPrescriptionNo() != null) {
|
||||||
.eq(ChargeItem::getPrescriptionNo, adviceSaveDto.getPrescriptionNo())
|
iChargeItemService.remove(new LambdaQueryWrapper<ChargeItem>()
|
||||||
.eq(ChargeItem::getProductId, sufferingDefinitionId));
|
.eq(ChargeItem::getPrescriptionNo, adviceSaveDto.getPrescriptionNo())
|
||||||
|
.eq(ChargeItem::getProductId, sufferingDefinitionId));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +616,7 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
|
|||||||
Long encounterDiagnosisId = medicineList.get(0).getEncounterDiagnosisId();
|
Long encounterDiagnosisId = medicineList.get(0).getEncounterDiagnosisId();
|
||||||
// 中药付数
|
// 中药付数
|
||||||
BigDecimal chineseHerbsDoseQuantity = medicineList.get(0).getChineseHerbsDoseQuantity();
|
BigDecimal chineseHerbsDoseQuantity = medicineList.get(0).getChineseHerbsDoseQuantity();
|
||||||
// 🔧 Bug Fix #668: 收集所有处方号(不同分组可能有不同处方号)
|
// 收集所有处方号(不同分组可能有不同处方号)
|
||||||
List<String> prescriptionNos = insertOrUpdateList.stream()
|
List<String> prescriptionNos = insertOrUpdateList.stream()
|
||||||
.map(AdviceSaveDto::getPrescriptionNo)
|
.map(AdviceSaveDto::getPrescriptionNo)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
@@ -628,12 +630,10 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
|
|||||||
AdviceBaseDto adviceBaseDto = new AdviceBaseDto();
|
AdviceBaseDto adviceBaseDto = new AdviceBaseDto();
|
||||||
adviceBaseDto.setAdviceDefinitionId(sufferingDefinitionId); // 医嘱定义id
|
adviceBaseDto.setAdviceDefinitionId(sufferingDefinitionId); // 医嘱定义id
|
||||||
|
|
||||||
// 🔧 Bug Fix #668: 先删除所有处方号关联的中药代煎账单
|
// 先删除该就诊关联的所有中药代煎账单
|
||||||
if (!prescriptionNos.isEmpty()) {
|
iChargeItemService.remove(new LambdaQueryWrapper<ChargeItem>()
|
||||||
iChargeItemService.remove(new LambdaQueryWrapper<ChargeItem>()
|
.eq(ChargeItem::getEncounterId, encounterId)
|
||||||
.in(ChargeItem::getPrescriptionNo, prescriptionNos)
|
.eq(ChargeItem::getProductId, sufferingDefinitionId));
|
||||||
.eq(ChargeItem::getProductId, sufferingDefinitionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对应的诊疗医嘱信息
|
// 对应的诊疗医嘱信息
|
||||||
AdviceBaseDto activityAdviceBaseDto = iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, null,
|
AdviceBaseDto activityAdviceBaseDto = iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, null,
|
||||||
@@ -642,7 +642,7 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
|
|||||||
// 费用定价
|
// 费用定价
|
||||||
AdvicePriceDto advicePriceDto = activityAdviceBaseDto.getPriceList().get(0);
|
AdvicePriceDto advicePriceDto = activityAdviceBaseDto.getPriceList().get(0);
|
||||||
if (advicePriceDto != null) {
|
if (advicePriceDto != null) {
|
||||||
// 🔧 Bug Fix #668: 为每个处方号分别生成代煎账单
|
// 为每个处方号分别生成代煎账单
|
||||||
for (String prescriptionNo : prescriptionNos) {
|
for (String prescriptionNo : prescriptionNos) {
|
||||||
chargeItem = new ChargeItem();
|
chargeItem = new ChargeItem();
|
||||||
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
|
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
|
||||||
@@ -674,12 +674,10 @@ public class DoctorStationChineseMedicalAppServiceImpl implements IDoctorStation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (Whether.NO.getValue().equals(sufferingFlag)) {
|
} else if (Whether.NO.getValue().equals(sufferingFlag)) {
|
||||||
// 🔧 Bug Fix #668: 删除所有处方号关联的中药代煎账单
|
// 删除该就诊关联的所有中药代煎账单
|
||||||
if (!prescriptionNos.isEmpty()) {
|
iChargeItemService.remove(new LambdaQueryWrapper<ChargeItem>()
|
||||||
iChargeItemService.remove(new LambdaQueryWrapper<ChargeItem>()
|
.eq(ChargeItem::getEncounterId, encounterId)
|
||||||
.in(ChargeItem::getPrescriptionNo, prescriptionNos)
|
.eq(ChargeItem::getProductId, sufferingDefinitionId));
|
||||||
.eq(ChargeItem::getProductId, sufferingDefinitionId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 签发时,把草稿状态的账单更新为待收费[中医]
|
// 签发时,把草稿状态的账单更新为待收费[中医]
|
||||||
|
|||||||
@@ -192,6 +192,9 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
|||||||
// 提取requestStatus手动处理,支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
|
// 提取requestStatus手动处理,支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
|
||||||
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
|
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
|
||||||
inpatientAdviceParam.setRequestStatus(null);
|
inpatientAdviceParam.setRequestStatus(null);
|
||||||
|
// Bug #714: 提取deadline手动处理,UNION子查询列名为end_time
|
||||||
|
String deadline = inpatientAdviceParam.getDeadline();
|
||||||
|
inpatientAdviceParam.setDeadline(null);
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
QueryWrapper<InpatientAdviceParam> queryWrapper
|
||||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
||||||
@@ -223,6 +226,16 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
|||||||
if (therapyEnum != null) {
|
if (therapyEnum != null) {
|
||||||
queryWrapper.and(w -> w.eq("therapy_enum", therapyEnum).or().isNull("therapy_enum"));
|
queryWrapper.and(w -> w.eq("therapy_enum", therapyEnum).or().isNull("therapy_enum"));
|
||||||
}
|
}
|
||||||
|
// Bug #714: 手动拼接deadline条件,按医嘱截止时间筛选
|
||||||
|
if (deadline != null && !deadline.isEmpty()) {
|
||||||
|
try {
|
||||||
|
LocalDateTime deadlineTime = LocalDateTime.parse(deadline,
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
queryWrapper.le("end_time", deadlineTime);
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
// 忽略无效的日期格式
|
||||||
|
}
|
||||||
|
}
|
||||||
// 患者医嘱分页列表
|
// 患者医嘱分页列表
|
||||||
Page<InpatientAdviceDto> inpatientAdvicePage
|
Page<InpatientAdviceDto> inpatientAdvicePage
|
||||||
= adviceProcessAppMapper.selectInpatientAdvicePage(new Page<>(pageNo, pageSize), queryWrapper,
|
= adviceProcessAppMapper.selectInpatientAdvicePage(new Page<>(pageNo, pageSize), queryWrapper,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Bug #735: 新医嘱签发后"停嘱医生"字段错误生成数据
|
||||||
|
-- 原因:stopper_name 映射到 update_by 字段,签发时 MyBatis-Plus 自动填充导致错误赋值
|
||||||
|
-- 修复:添加专用 stopper_id 字段,仅在停嘱操作时设置
|
||||||
|
|
||||||
|
-- 药品请求表添加停嘱医生ID字段
|
||||||
|
ALTER TABLE med_medication_request ADD COLUMN IF NOT EXISTS stopper_id BIGINT;
|
||||||
|
|
||||||
|
-- 服务请求表添加停嘱医生ID字段
|
||||||
|
ALTER TABLE wor_service_request ADD COLUMN IF NOT EXISTS stopper_id BIGINT;
|
||||||
@@ -217,6 +217,11 @@
|
|||||||
WHERE T1.delete_flag = '0'
|
WHERE T1.delete_flag = '0'
|
||||||
AND T1.class_enum = #{classEnum}
|
AND T1.class_enum = #{classEnum}
|
||||||
AND T10.context_enum = #{register}
|
AND T10.context_enum = #{register}
|
||||||
|
AND (#{registerTimeSTime} IS NULL OR T1.create_time >= CAST(#{registerTimeSTime} AS TIMESTAMP))
|
||||||
|
AND (#{registerTimeETime} IS NULL OR T1.create_time <= CAST(#{registerTimeETime} AS TIMESTAMP))
|
||||||
|
AND (#{statusFilter} IS NULL
|
||||||
|
OR (#{statusFilter} >= 0 AND T1.status_enum = #{statusFilter})
|
||||||
|
OR (#{statusFilter} = -1 AND T1.status_enum != 6))
|
||||||
) AS T9
|
) AS T9
|
||||||
${ew.customSqlSegment}
|
${ew.customSqlSegment}
|
||||||
ORDER BY T9.register_time DESC
|
ORDER BY T9.register_time DESC
|
||||||
|
|||||||
@@ -108,8 +108,6 @@
|
|||||||
<if test="statusEnum == 18">
|
<if test="statusEnum == 18">
|
||||||
T4.status_enum = #{submitted}
|
T4.status_enum = #{submitted}
|
||||||
</if>
|
</if>
|
||||||
AND T4.summary_no IS NOT NULL
|
|
||||||
AND T4.summary_no != ''
|
|
||||||
) AS ii
|
) AS ii
|
||||||
${ew.customSqlSegment}
|
${ew.customSqlSegment}
|
||||||
GROUP BY ii.encounter_id,
|
GROUP BY ii.encounter_id,
|
||||||
@@ -268,8 +266,6 @@
|
|||||||
AND T15.delete_flag = '0'
|
AND T15.delete_flag = '0'
|
||||||
WHERE T1.delete_flag = '0'
|
WHERE T1.delete_flag = '0'
|
||||||
-- 因发药配药合并,前台只能看到待发药,已发药状态,但是后台配药发药状态都查
|
-- 因发药配药合并,前台只能看到待发药,已发药状态,但是后台配药发药状态都查
|
||||||
AND T1.summary_no IS NOT NULL
|
|
||||||
AND T1.summary_no != ''
|
|
||||||
AND
|
AND
|
||||||
<if test="dispenseStatus == null">
|
<if test="dispenseStatus == null">
|
||||||
T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared},#{summarized})
|
T1.status_enum IN (#{inProgress},#{completed},#{preparation},#{prepared},#{summarized})
|
||||||
|
|||||||
@@ -222,8 +222,8 @@
|
|||||||
T1.based_on_id AS based_on_id,
|
T1.based_on_id AS based_on_id,
|
||||||
T1.medication_id AS advice_definition_id,
|
T1.medication_id AS advice_definition_id,
|
||||||
T1.content_json::jsonb ->> 'remark' AS remark,
|
T1.content_json::jsonb ->> 'remark' AS remark,
|
||||||
T1.effective_dose_end AS stop_time,
|
CASE WHEN T1.status_enum = 6 THEN T1.effective_dose_end ELSE NULL END AS stop_time,
|
||||||
T1.update_by AS stop_user_name
|
CASE WHEN T1.status_enum = 6 THEN T1.update_by ELSE NULL END AS stop_user_name
|
||||||
FROM med_medication_request AS T1
|
FROM med_medication_request AS T1
|
||||||
LEFT JOIN adm_practitioner AS ap ON ap.id = T1.practitioner_id AND ap.delete_flag = '0'
|
LEFT JOIN adm_practitioner AS ap ON ap.id = T1.practitioner_id AND ap.delete_flag = '0'
|
||||||
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
|
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
|
||||||
@@ -339,8 +339,8 @@
|
|||||||
T1.based_on_id AS based_on_id,
|
T1.based_on_id AS based_on_id,
|
||||||
T1.activity_id AS advice_definition_id,
|
T1.activity_id AS advice_definition_id,
|
||||||
T1.remark AS remark,
|
T1.remark AS remark,
|
||||||
T1.occurrence_end_time AS stop_time,
|
CASE WHEN T1.status_enum = 6 THEN T1.occurrence_end_time ELSE NULL END AS stop_time,
|
||||||
T1.update_by AS stop_user_name
|
CASE WHEN T1.status_enum = 6 THEN T1.update_by ELSE NULL END AS stop_user_name
|
||||||
FROM wor_service_request AS T1
|
FROM wor_service_request AS T1
|
||||||
LEFT JOIN adm_practitioner AS ap ON ap.id = T1.requester_id AND ap.delete_flag = '0'
|
LEFT JOIN adm_practitioner AS ap ON ap.id = T1.requester_id AND ap.delete_flag = '0'
|
||||||
LEFT JOIN wor_activity_definition AS T2
|
LEFT JOIN wor_activity_definition AS T2
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class TicketSlotDTO {
|
|||||||
private String patientName;
|
private String patientName;
|
||||||
private String medicalCard;
|
private String medicalCard;
|
||||||
private Long patientId;
|
private Long patientId;
|
||||||
|
private Long realPatientId;
|
||||||
private String phone;
|
private String phone;
|
||||||
private Integer orderStatus;
|
private Integer orderStatus;
|
||||||
private Long orderId;
|
private Long orderId;
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
o.order_no AS orderNo,
|
o.order_no AS orderNo,
|
||||||
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
|
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
|
||||||
pinfo.gender_enum AS genderEnum,
|
pinfo.gender_enum AS genderEnum,
|
||||||
|
pinfo.id AS realPatientId,
|
||||||
pinfo.id_card AS idCard,
|
pinfo.id_card AS idCard,
|
||||||
o.appointment_time AS appointmentTime,
|
o.appointment_time AS appointmentTime,
|
||||||
<include refid="orderStatusNormExpr" /> AS orderStatus,
|
<include refid="orderStatusNormExpr" /> AS orderStatus,
|
||||||
@@ -230,6 +231,7 @@
|
|||||||
o.order_no AS orderNo,
|
o.order_no AS orderNo,
|
||||||
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
|
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
|
||||||
pinfo.gender_enum AS genderEnum,
|
pinfo.gender_enum AS genderEnum,
|
||||||
|
pinfo.id AS realPatientId,
|
||||||
pinfo.id_card AS idCard,
|
pinfo.id_card AS idCard,
|
||||||
o.appointment_time AS appointmentTime,
|
o.appointment_time AS appointmentTime,
|
||||||
<include refid="orderStatusNormExpr" /> AS orderStatus,
|
<include refid="orderStatusNormExpr" /> AS orderStatus,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
ref="patientListRef"
|
ref="patientListRef"
|
||||||
height="620"
|
height="620"
|
||||||
:data="patientList"
|
:data="patientList"
|
||||||
:row-config="{ keyField: 'encounterId', keyField: 'id' }"
|
:row-config="{ keyField: 'encounterId' }"
|
||||||
@cell-click="clickRow"
|
@cell-click="clickRow"
|
||||||
>
|
>
|
||||||
<vxe-column
|
<vxe-column
|
||||||
@@ -447,7 +447,8 @@ function checkSelectable(row, index) {
|
|||||||
/**
|
/**
|
||||||
* 点击患者列表行 获取处方列表
|
* 点击患者列表行 获取处方列表
|
||||||
*/
|
*/
|
||||||
function clickRow(row) {
|
function clickRow(params) {
|
||||||
|
const row = params.row || params;
|
||||||
patientInfo.value = row;
|
patientInfo.value = row;
|
||||||
chargeLoading.value = true;
|
chargeLoading.value = true;
|
||||||
encounterId.value = row.encounterId;
|
encounterId.value = row.encounterId;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-loading="readCardLoading"
|
v-loading="readCardLoading"
|
||||||
class="app-container"
|
class="app-container"
|
||||||
@@ -122,6 +122,7 @@
|
|||||||
<patientList
|
<patientList
|
||||||
:searchkey="patientSearchKey"
|
:searchkey="patientSearchKey"
|
||||||
@selsect-patient="selsectPatient"
|
@selsect-patient="selsectPatient"
|
||||||
|
@mousedown.prevent
|
||||||
/>
|
/>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-input
|
<el-input
|
||||||
@@ -2079,10 +2080,20 @@ async function confirmCheckIn() {
|
|||||||
// 每次开始新的签到流程先清理残留 slotId,避免历史脏值串单
|
// 每次开始新的签到流程先清理残留 slotId,避免历史脏值串单
|
||||||
currentSlotId.value = null;
|
currentSlotId.value = null;
|
||||||
|
|
||||||
|
// 防御性校验:确保关键字段存在
|
||||||
|
if (!patient.departmentId) {
|
||||||
|
ElMessage.error('该号源缺少科室信息,无法完成签到,请联系管理员');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!patient.realPatientId) {
|
||||||
|
ElMessage.error('该号源缺少患者信息,无法完成签到,请联系管理员');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 弹出确认提示
|
// 弹出确认提示
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确认为患者【${patient.patientName}】办理签到挂号?\n` +
|
`确认为患者【${patient.patientName || '未知患者'}】办理签到挂号?\n` +
|
||||||
`科室:${patient.department || '-'}\n` +
|
`科室:${patient.department || '-'}\n` +
|
||||||
`医生:${patient.doctor || '-'}\n` +
|
`医生:${patient.doctor || '-'}\n` +
|
||||||
`费用:¥${patient.fee || '0.00'}`,
|
`费用:¥${patient.fee || '0.00'}`,
|
||||||
@@ -2215,7 +2226,7 @@ async function confirmCheckIn() {
|
|||||||
* 点击患者列表给表单赋值
|
* 点击患者列表给表单赋值
|
||||||
*/
|
*/
|
||||||
function selsectPatient(row) {
|
function selsectPatient(row) {
|
||||||
form.value = { ...form.value, ...row };
|
Object.assign(form.value, row);
|
||||||
form.value.patientId = row.id;
|
form.value.patientId = row.id;
|
||||||
form.value.searchKey = row.name;
|
form.value.searchKey = row.name;
|
||||||
form.value.name = row.name;
|
form.value.name = row.name;
|
||||||
|
|||||||
@@ -636,13 +636,14 @@ const inputRefs = ref({}); // 存储输入框实例
|
|||||||
const requiredProps = ref([]); // 存储必填项 prop 顺序
|
const requiredProps = ref([]); // 存储必填项 prop 顺序
|
||||||
const totalAmount = ref(0);
|
const totalAmount = ref(0);
|
||||||
const tcmDianosis = ref();
|
const tcmDianosis = ref();
|
||||||
const { method_code, unit_code, rate_code, distribution_category_code, dosage_instruction } =
|
const { method_code, unit_code, rate_code, distribution_category_code, dosage_instruction, method_of_decocting_medicine } =
|
||||||
proxy.useDict(
|
proxy.useDict(
|
||||||
'method_code',
|
'method_code',
|
||||||
'unit_code',
|
'unit_code',
|
||||||
'rate_code',
|
'rate_code',
|
||||||
'distribution_category_code',
|
'distribution_category_code',
|
||||||
'dosage_instruction'
|
'dosage_instruction',
|
||||||
|
'method_of_decocting_medicine'
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
v-model="type"
|
v-model="type"
|
||||||
@change="handleRadioChange"
|
@change="handleRadioChange"
|
||||||
>
|
>
|
||||||
<el-radio :value="THERAPY_TYPE_ALL">
|
<el-radio :value="0">
|
||||||
全部
|
全部
|
||||||
</el-radio>
|
</el-radio>
|
||||||
<el-radio :value="1">
|
<el-radio :value="1">
|
||||||
@@ -26,15 +26,13 @@
|
|||||||
临时
|
临时
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<span style="flex-shrink: 0;">截止时间:</span>
|
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="deadline"
|
v-model="deadline"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
placeholder="选择截止时间"
|
placeholder="选择截止时间"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
:clearable="false"
|
style="width: 200px"
|
||||||
style="width: 200px;"
|
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -346,8 +344,7 @@ import {RequestStatus} from '@/utils/medicalConstants';
|
|||||||
const activeNames = ref([]);
|
const activeNames = ref([]);
|
||||||
const prescriptionList = ref([]);
|
const prescriptionList = ref([]);
|
||||||
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
|
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
|
||||||
const THERAPY_TYPE_ALL = 0;
|
const type = ref(0);
|
||||||
const type = ref(THERAPY_TYPE_ALL);
|
|
||||||
const backReasonVisible = ref(false);
|
const backReasonVisible = ref(false);
|
||||||
const backReasonForm = ref({ reason: '' });
|
const backReasonForm = ref({ reason: '' });
|
||||||
const backReasonFormRef = ref(null);
|
const backReasonFormRef = ref(null);
|
||||||
@@ -447,8 +444,8 @@ function handleGetPrescription() {
|
|||||||
getPrescriptionList({
|
getPrescriptionList({
|
||||||
encounterIds: encounterIds,
|
encounterIds: encounterIds,
|
||||||
requestStatus: props.requestStatus,
|
requestStatus: props.requestStatus,
|
||||||
...(type.value !== THERAPY_TYPE_ALL ? { therapyEnum: type.value } : {}),
|
...(type.value !== 0 ? { therapyEnum: type.value } : {}),
|
||||||
deadline: deadline.value,
|
...(deadline.value ? { deadline: deadline.value } : {}),
|
||||||
pageSize: 10000,
|
pageSize: 10000,
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user