Merge branch 'develop' of http://192.168.110.253:3000/wangyizhe/his into develop
This commit is contained in:
134
docs/specs/cicd-gatekeeper.md
Normal file
134
docs/specs/cicd-gatekeeper.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# CI/CD构建门禁规范
|
||||
|
||||
## 🎯 规范目标
|
||||
|
||||
建立自动化质量门禁,确保每次代码提交都经过严格验证,防止低质量代码进入主干分支,提升系统稳定性和开发效率。
|
||||
|
||||
## 🔒 门禁层级
|
||||
|
||||
### 1. 提交前门禁(Pre-commit)
|
||||
**触发时机**:`git commit` 执行前
|
||||
**验证内容**:
|
||||
- ESLint 代码规范检查
|
||||
- Prettier 代码格式化
|
||||
- 简单的单元测试(快速执行)
|
||||
|
||||
**工具配置**:
|
||||
- Husky + lint-staged
|
||||
- 配置文件:`.husky/pre-commit`
|
||||
|
||||
### 2. 推送前门禁(Pre-push)
|
||||
**触发时机**:`git push` 执行前
|
||||
**验证内容**:
|
||||
- 完整的单元测试套件
|
||||
- 构建验证(`npm run build`)
|
||||
- 集成测试(核心流程)
|
||||
|
||||
**工具配置**:
|
||||
- Husky pre-push hook
|
||||
- 配置文件:`.husky/pre-push`
|
||||
|
||||
### 3. CI流水线门禁(CI Pipeline)
|
||||
**触发时机**:代码推送到远程仓库后
|
||||
**验证内容**:
|
||||
- 完整的测试套件(单元+集成+端到端)
|
||||
- 代码覆盖率检查(≥80%)
|
||||
- 安全扫描(SAST)
|
||||
- 构建产物验证
|
||||
- 部署到测试环境
|
||||
|
||||
**工具配置**:
|
||||
- Spug CI/CD 流水线
|
||||
- Gitea Webhook 触发
|
||||
|
||||
### 4. 发布前门禁(Release Gate)
|
||||
**触发时机**:准备发布到生产环境前
|
||||
**验证内容**:
|
||||
- 生产环境冒烟测试
|
||||
- 性能基准测试
|
||||
- 安全合规检查
|
||||
- 回滚预案验证
|
||||
|
||||
## ⚙️ 具体配置要求
|
||||
|
||||
### ESLint 配置
|
||||
```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
|
||||
```
|
||||
|
||||
### 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%
|
||||
|
||||
### 报告机制
|
||||
- 每日门禁失败统计
|
||||
- 周度质量趋势报告
|
||||
- 月度规则优化建议
|
||||
|
||||
## 🔄 持续改进
|
||||
|
||||
### 规则演进
|
||||
- 每月评审门禁规则有效性
|
||||
- 根据项目需求调整检查强度
|
||||
- 引入新的质量检查工具
|
||||
|
||||
### 团队培训
|
||||
- 新成员入职培训包含门禁规范
|
||||
- 定期分享最佳实践案例
|
||||
- 建立常见问题解决方案库
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2026年4月24日
|
||||
**负责人**:陈琳(文档专家)
|
||||
**技术方案**:诸葛亮(架构师)
|
||||
**适用范围**:HIS 系统所有项目
|
||||
129
docs/specs/commit-template.md
Normal file
129
docs/specs/commit-template.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 代码提交变更说明模板
|
||||
|
||||
## 📝 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` 命令执行过程
|
||||
2. **成功标识**:明确显示构建成功的提示信息
|
||||
3. **时间戳**:截图包含当前时间,证明是最新构建
|
||||
4. **分支信息**:显示当前工作分支名称
|
||||
|
||||
### 截图示例
|
||||
```
|
||||
$ git checkout feature/patient-edit
|
||||
$ npm run build
|
||||
|
||||
> 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 系统所有开发人员
|
||||
82
docs/specs/frontend-checklist.md
Normal file
82
docs/specs/frontend-checklist.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 前端发布前检查清单
|
||||
|
||||
## 📋 基础检查项
|
||||
|
||||
### 代码质量
|
||||
- [ ] 代码已通过 ESLint 检查,无警告和错误
|
||||
- [ ] 代码已通过 Prettier 格式化
|
||||
- [ ] 无 console.log() 等调试代码残留
|
||||
- [ ] 变量命名符合规范,语义清晰
|
||||
- [ ] 函数职责单一,复杂度适中
|
||||
|
||||
### 构建验证
|
||||
- [ ] 本地执行 `npm run build` 成功完成
|
||||
- [ ] 构建产物无报错,体积合理
|
||||
- [ ] 静态资源路径正确,无404错误
|
||||
- [ ] 环境变量配置正确(开发/测试/生产)
|
||||
|
||||
### 功能验证
|
||||
- [ ] 核心功能流程完整测试通过
|
||||
- [ ] 边界条件和异常场景已覆盖
|
||||
- [ ] 表单验证逻辑正确
|
||||
- [ ] API 接口调用正常,错误处理完善
|
||||
- [ ] 路由跳转逻辑正确
|
||||
|
||||
## 🔧 技术检查项
|
||||
|
||||
### 模块导入检查
|
||||
- [ ] 所有 import 语句引用的模块实际存在
|
||||
- [ ] 无未使用的 import 导入
|
||||
- [ ] 路径别名(@/)配置正确
|
||||
- [ ] 第三方库版本兼容性确认
|
||||
|
||||
### 性能优化
|
||||
- [ ] 组件按需加载(懒加载)已配置
|
||||
- [ ] 大数据列表已实现虚拟滚动或分页
|
||||
- [ ] 图片资源已压缩,格式合适
|
||||
- [ ] 无内存泄漏风险(事件监听器、定时器等)
|
||||
|
||||
### 安全检查
|
||||
- [ ] 用户输入已做 XSS 防护
|
||||
- [ ] 敏感信息不在前端硬编码
|
||||
- [ ] API 请求已做 CSRF 防护
|
||||
- [ ] 权限控制逻辑正确
|
||||
|
||||
## 🌐 兼容性检查
|
||||
|
||||
### 浏览器兼容
|
||||
- [ ] 主流浏览器(Chrome、Firefox、Safari、Edge)显示正常
|
||||
- [ ] 移动端适配良好(如适用)
|
||||
- [ ] 分辨率适配(1366x768、1920x1080等)
|
||||
|
||||
### 设备兼容
|
||||
- [ ] 触摸设备操作体验良好
|
||||
- [ ] 键盘导航支持完整
|
||||
- [ ] 屏幕阅读器兼容性(无障碍)
|
||||
|
||||
## 📱 发布准备
|
||||
|
||||
### 文档更新
|
||||
- [ ] 相关 API 文档已同步更新
|
||||
- [ ] 用户操作手册已更新(如适用)
|
||||
- [ ] 变更日志已记录
|
||||
|
||||
### 回滚预案
|
||||
- [ ] 回滚方案已准备
|
||||
- [ ] 数据兼容性已确认
|
||||
- [ ] 紧急联系人已明确
|
||||
|
||||
## ✅ 最终确认
|
||||
|
||||
### 发布前最后检查
|
||||
- [ ] 本地构建截图已附在 PR 中
|
||||
- [ ] 测试环境部署验证通过
|
||||
- [ ] Code Review 已完成并获得批准
|
||||
- [ ] 相关 Bug 已关闭或延期说明
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2026年4月24日
|
||||
**负责人**:陈琳(文档专家)
|
||||
**适用范围**:HIS 系统所有前端项目
|
||||
@@ -165,4 +165,9 @@ public class CurrentDayEncounterDto {
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long poolId;
|
||||
|
||||
/**
|
||||
* 诊室名称(Bug #410:分诊队列需显示诊室而非科室)
|
||||
*/
|
||||
private String clinicRoom;
|
||||
|
||||
}
|
||||
|
||||
@@ -136,9 +136,11 @@ public class SurgicalScheduleAppServiceImpl implements ISurgicalScheduleAppServi
|
||||
}
|
||||
}
|
||||
|
||||
LoginUser loginUser = new LoginUser();
|
||||
//获取当前登录用户信息
|
||||
loginUser = SecurityUtils.getLoginUser();
|
||||
// Bug #432 修复:获取当前登录用户信息,增加null校验防止NPE
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return R.fail("用户未登录或登录已过期");
|
||||
}
|
||||
// 当前登录用户ID
|
||||
Long userId = loginUser.getUserId();
|
||||
|
||||
|
||||
@@ -1395,9 +1395,17 @@ public class ConsultationAppServiceImpl implements IConsultationAppService {
|
||||
}
|
||||
|
||||
// 4. 更新邀请记录(存储会诊意见)
|
||||
// 直接存储用户输入的原始意见内容,不添加医师姓名前缀
|
||||
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode()); // 已确认
|
||||
invited.setConfirmOpinion(dto.getConsultationOpinion()); // 直接存储原始意见,不添加前缀
|
||||
// Bug #388:格式化存储,确保回显时参加医师和意见完整
|
||||
invited.setInvitedStatus(ConsultationStatusEnum.CONFIRMED.getCode());
|
||||
|
||||
String deptName = StringUtils.hasText(dto.getConfirmingDeptName())
|
||||
? dto.getConfirmingDeptName() : invited.getInvitedDepartmentName();
|
||||
String physician = StringUtils.hasText(dto.getConfirmingPhysician())
|
||||
? dto.getConfirmingPhysician() : currentPhysicianName;
|
||||
|
||||
// 格式:科室-参加医师:意见内容
|
||||
String formattedOpinion = String.format("%s-%s:%s", deptName, physician, dto.getConsultationOpinion());
|
||||
invited.setConfirmOpinion(formattedOpinion);
|
||||
invited.setConfirmTime(new Date());
|
||||
consultationInvitedMapper.updateById(invited);
|
||||
|
||||
|
||||
@@ -1144,7 +1144,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
|
||||
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
||||
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
||||
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
|
||||
// #415 价格非负验证
|
||||
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
|
||||
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
|
||||
unitPrice = unitPrice.abs(); // 负数取绝对值
|
||||
}
|
||||
chargeItem.setUnitPrice(unitPrice); // 单价
|
||||
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
||||
|
||||
// 显式设置tenantId、createBy和createTime字段,防止自动填充机制失效
|
||||
@@ -1616,7 +1621,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
|
||||
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
||||
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
||||
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
|
||||
// #415 价格非负验证
|
||||
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
|
||||
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
|
||||
unitPrice = unitPrice.abs(); // 负数取绝对值
|
||||
}
|
||||
chargeItem.setUnitPrice(unitPrice); // 单价
|
||||
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
||||
|
||||
// 显式设置审计字段,防止自动填充机制失效
|
||||
@@ -1842,7 +1852,12 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id
|
||||
chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量
|
||||
chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位
|
||||
chargeItem.setUnitPrice(adviceSaveDto.getUnitPrice()); // 单价
|
||||
// #415 价格非负验证
|
||||
BigDecimal unitPrice = adviceSaveDto.getUnitPrice();
|
||||
if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) {
|
||||
unitPrice = unitPrice.abs(); // 负数取绝对值
|
||||
}
|
||||
chargeItem.setUnitPrice(unitPrice); // 单价
|
||||
chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价
|
||||
|
||||
iChargeItemService.saveOrUpdate(chargeItem);
|
||||
|
||||
@@ -59,7 +59,9 @@ import com.openhis.web.personalization.dto.ActivityDeviceDto;
|
||||
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||
import com.openhis.appointmentmanage.domain.SchedulePool;
|
||||
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
|
||||
import com.openhis.clinical.domain.Order;
|
||||
import com.openhis.clinical.service.IOrderService;
|
||||
import com.openhis.workflow.domain.ServiceRequest;
|
||||
@@ -196,6 +198,8 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
||||
private IOrderService iOrderService;
|
||||
@Autowired
|
||||
private ScheduleSlotMapper scheduleSlotMapper;
|
||||
@Autowired
|
||||
private SchedulePoolMapper schedulePoolMapper;
|
||||
|
||||
/**
|
||||
* 【门诊预结算】
|
||||
@@ -1982,6 +1986,60 @@ public class PaymentRecServiceImpl implements IPaymentRecService {
|
||||
|
||||
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||
|
||||
// Bug #409:预约签到挂号时,修正 serviceTypeId 为预约类型而非挂号类型
|
||||
// 数据链:Order → ScheduleSlot → SchedulePool → HealthcareService
|
||||
try {
|
||||
Order appointmentOrder = iOrderService.getOne(
|
||||
new LambdaQueryWrapper<Order>()
|
||||
.eq(Order::getPatientId, encounterFormData.getPatientId())
|
||||
.eq(Order::getStatus, CommonConstants.AppointmentOrderStatus.CHECKED_IN)
|
||||
.eq(Order::getDeleteFlag, "0")
|
||||
.orderByDesc(Order::getCreateTime)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
if (appointmentOrder != null && appointmentOrder.getSlotId() != null) {
|
||||
ScheduleSlot slot = scheduleSlotMapper.selectById(appointmentOrder.getSlotId());
|
||||
if (slot != null && slot.getPoolId() != null) {
|
||||
SchedulePool pool = schedulePoolMapper.selectById(slot.getPoolId());
|
||||
if (pool != null && pool.getRegType() != null) {
|
||||
// pool.getRegType() 存储号别名称如"普通号-预约",精确匹配 HealthcareService
|
||||
HealthcareService appointmentHealthcareService = healthcareServiceService.getOne(
|
||||
new LambdaQueryWrapper<HealthcareService>()
|
||||
.eq(HealthcareService::getOfferedOrgId, encounterFormData.getOrganizationId())
|
||||
.eq(HealthcareService::getDeleteFlag, "0")
|
||||
.eq(HealthcareService::getName, pool.getRegType())
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
if (appointmentHealthcareService != null) {
|
||||
encounterFormData.setServiceTypeId(appointmentHealthcareService.getId());
|
||||
encounterFormData.setOrderId(appointmentOrder.getId());
|
||||
logger.info("预约签到挂号修正serviceTypeId={},号别={}",
|
||||
appointmentHealthcareService.getId(), pool.getRegType());
|
||||
} else {
|
||||
// 精确匹配失败,尝试 LIKE 匹配
|
||||
appointmentHealthcareService = healthcareServiceService.getOne(
|
||||
new LambdaQueryWrapper<HealthcareService>()
|
||||
.eq(HealthcareService::getOfferedOrgId, encounterFormData.getOrganizationId())
|
||||
.eq(HealthcareService::getDeleteFlag, "0")
|
||||
.like(HealthcareService::getName, pool.getRegType())
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (appointmentHealthcareService != null) {
|
||||
encounterFormData.setServiceTypeId(appointmentHealthcareService.getId());
|
||||
encounterFormData.setOrderId(appointmentOrder.getId());
|
||||
logger.info("预约签到挂号(LIKE)serviceTypeId={},号别={}",
|
||||
appointmentHealthcareService.getId(), pool.getRegType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("修正serviceTypeId失败,不影响挂号流程", e);
|
||||
}
|
||||
|
||||
// 保存就诊信息
|
||||
Encounter encounter = new Encounter();
|
||||
BeanUtils.copyProperties(encounterFormData, encounter);
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
T9.organization_id AS organizationId,
|
||||
T9.organization_name AS organizationName,
|
||||
T9.healthcare_name AS healthcareName,
|
||||
T9.clinic_room AS clinicRoom, -- Bug #410:诊室名称
|
||||
T9.practitioner_user_id AS practitionerUserId,
|
||||
T9.practitioner_name AS practitionerName,
|
||||
T9.contract_name AS contractName,
|
||||
@@ -99,10 +100,12 @@
|
||||
T18.identifier_no AS identifier_no,
|
||||
T1.order_id AS order_id,
|
||||
om.slot_id AS slot_id,
|
||||
ss.pool_id AS pool_id
|
||||
ss.pool_id AS pool_id,
|
||||
sp.clinic_room AS clinic_room -- Bug #410:从号源池获取诊室
|
||||
FROM adm_encounter AS T1
|
||||
LEFT JOIN order_main AS om ON T1.order_id = om.id AND om.delete_flag = '0'
|
||||
LEFT JOIN adm_schedule_slot AS ss ON om.slot_id = ss.id AND ss.delete_flag = '0'
|
||||
LEFT JOIN adm_schedule_pool AS sp ON ss.pool_id = sp.id AND sp.delete_flag = '0' -- Bug #410
|
||||
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
|
||||
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
|
||||
LEFT JOIN (
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
AND T2.delete_flag = '0'
|
||||
WHERE
|
||||
T1.delete_flag = '0'
|
||||
<if test="chargeItemContext == 1 ">
|
||||
<if test="chargeItemContext == 1">
|
||||
AND T1.instance_table = #{MED_MEDICATION_DEFINITION}
|
||||
</if>
|
||||
<if test="chargeItemContext == 2 ">
|
||||
<if test="chargeItemContext == 2">
|
||||
AND T1.instance_table = #{ADM_DEVICE_DEFINITION}
|
||||
</if>
|
||||
<if test="chargeItemContext == 3 ">
|
||||
<if test="chargeItemContext == 3">
|
||||
AND (T1.instance_table = #{WOR_ACTIVITY_DEFINITION} OR T1.instance_table = #{ADM_HEALTHCARE_SERVICE})
|
||||
</if>
|
||||
GROUP BY T1.tenant_id,
|
||||
|
||||
@@ -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) {
|
||||
return request({
|
||||
url: '/app-common/department-list',
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
<el-tag v-else type="warning" size="small">已确认</el-tag>
|
||||
</div>
|
||||
<div class="opinion-content">
|
||||
{{ opinion.opinion }}
|
||||
{{ formatOpinionContent(opinion.opinion) }}
|
||||
</div>
|
||||
<div v-if="opinion.signatureTime" class="opinion-footer">
|
||||
签名时间:{{ formatDateTime(opinion.signatureTime) }}
|
||||
@@ -301,6 +301,16 @@ const formatDateTime = (date) => {
|
||||
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 getDoctorDept = () => userStore.orgName || '当前科室'
|
||||
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
v-for="cat in filteredCategoryList"
|
||||
:key="cat.typeId"
|
||||
:name="cat.typeId"
|
||||
@click="handleCategoryExpand(cat)"
|
||||
>
|
||||
<template #title>
|
||||
<span class="cat-title">{{ cat.categoryName }}</span>
|
||||
@@ -596,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) => {
|
||||
if (form.inspectionMethod && !newMethods.find(m => m.name === form.inspectionMethod)) {
|
||||
form.inspectionMethod = '';
|
||||
@@ -832,11 +860,6 @@ function handleSave() {
|
||||
const firstCheckType = selectedItems.value[0]?.checkType || 'unknown';
|
||||
form.examTypeCode = firstCheckType;
|
||||
|
||||
// 如果有选中的检查方法,更新表单中的检查方法字段(取第一个选中项目的检查方法)
|
||||
const firstItemWithMethod = selectedItems.value.find(item => item.selectedMethod);
|
||||
if (firstItemWithMethod?.selectedMethod) {
|
||||
form.inspectionMethod = firstItemWithMethod.selectedMethod.name;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...form,
|
||||
@@ -1050,6 +1073,11 @@ function selectMethodCheckbox(checked, item, method) {
|
||||
}
|
||||
// 联动更新表单检查方法显示字段
|
||||
updateMethodDisplay();
|
||||
|
||||
// #430: 套餐金额实时同步到申请单
|
||||
nextTick(() => {
|
||||
form.totalAmount = totalAmountCalc.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Bug #384修复: 更新检查方法显示字段(联动)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -80,11 +80,23 @@
|
||||
<script setup name="BloodTransfusion">
|
||||
import {getCurrentInstance, onBeforeMount, onMounted, reactive, ref} from 'vue';
|
||||
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 {getApplicationList, saveBloodTransfusio} from './api';
|
||||
|
||||
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 props = defineProps({});
|
||||
const state = reactive({});
|
||||
@@ -175,9 +187,7 @@ const projectWithDepartment = (selectProjectIds, type) => {
|
||||
isRelease = false;
|
||||
}
|
||||
// 选中项目中的执行科室id与全部科室数据做匹配
|
||||
const findItem = orgOptions.value.find((item) => {
|
||||
return item.id == obj.orgId;
|
||||
});
|
||||
const findItem = findTreeItem(orgOptions.value, obj.orgId);
|
||||
|
||||
if (!findItem) {
|
||||
isRelease = false;
|
||||
@@ -249,8 +259,8 @@ const submit = () => {
|
||||
};
|
||||
/** 查询科室 */
|
||||
const getLocationInfo = () => {
|
||||
getOrgList().then((res) => {
|
||||
orgOptions.value = res.data?.records[0]?.children;
|
||||
getDepartmentList().then((res) => {
|
||||
orgOptions.value = res.data || [];
|
||||
});
|
||||
};
|
||||
// 获取诊断目录
|
||||
|
||||
@@ -81,11 +81,23 @@
|
||||
import {getCurrentInstance, onBeforeMount, onMounted, reactive, watch} from 'vue';
|
||||
import {patientInfo} from '../../../store/patient.js';
|
||||
import {getApplicationList, saveInspection} from './api';
|
||||
import {getOrgList} from '../../../../../basicmanage/ward/components/api.js';
|
||||
import {getDepartmentList} from '@/api/public.js';
|
||||
import {getEncounterDiagnosis} from '../../api.js';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
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 props = defineProps({});
|
||||
const state = reactive({});
|
||||
@@ -100,7 +112,7 @@ const getList = () => {
|
||||
}
|
||||
loading.value = true;
|
||||
getApplicationList({
|
||||
pageSize: 10000,
|
||||
pageSize: 500,
|
||||
pageNum: 1,
|
||||
categoryCode: '22',
|
||||
organizationId: patientInfo.value.inHospitalOrgId,
|
||||
@@ -177,9 +189,7 @@ const projectWithDepartment = (selectProjectIds, type) => {
|
||||
isRelease = false;
|
||||
}
|
||||
// 选中项目中的执行科室id与全部科室数据做匹配
|
||||
const findItem = orgOptions.value.find((item) => {
|
||||
return item.id == obj.orgId;
|
||||
});
|
||||
const findItem = findTreeItem(orgOptions.value, obj.orgId);
|
||||
if (!findItem) {
|
||||
isRelease = false;
|
||||
ElMessage({
|
||||
@@ -251,8 +261,8 @@ const submit = () => {
|
||||
};
|
||||
/** 查询科室 */
|
||||
const getLocationInfo = () => {
|
||||
getOrgList().then((res) => {
|
||||
orgOptions.value = res.data?.records[0]?.children;
|
||||
getDepartmentList().then((res) => {
|
||||
orgOptions.value = res.data || [];
|
||||
console.log('科室========>', JSON.stringify(orgOptions.value));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -80,12 +80,24 @@
|
||||
<script setup name="MedicalExaminations">
|
||||
import {getCurrentInstance, onBeforeMount, onMounted, reactive, watch} from 'vue';
|
||||
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 {getApplicationList, saveCheckd} from './api';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
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 props = defineProps({});
|
||||
const orgOptions = ref([]); // 科室选项
|
||||
@@ -100,7 +112,7 @@ const getList = () => {
|
||||
}
|
||||
loading.value = true;
|
||||
getApplicationList({
|
||||
pageSize: 10000,
|
||||
pageSize: 500,
|
||||
pageNum: 1,
|
||||
categoryCode: '23',
|
||||
organizationId: patientInfo.value.inHospitalOrgId,
|
||||
@@ -176,9 +188,7 @@ const projectWithDepartment = (selectProjectIds, type) => {
|
||||
isRelease = false;
|
||||
}
|
||||
// 选中项目中的执行科室id与全部科室数据做匹配
|
||||
const findItem = orgOptions.value.find((item) => {
|
||||
return item.id == obj.orgId;
|
||||
});
|
||||
const findItem = findTreeItem(orgOptions.value, obj.orgId);
|
||||
|
||||
if (!findItem) {
|
||||
isRelease = false;
|
||||
@@ -250,8 +260,8 @@ const submit = () => {
|
||||
};
|
||||
/** 查询科室 */
|
||||
const getLocationInfo = () => {
|
||||
getOrgList().then((res) => {
|
||||
orgOptions.value = res.data?.records[0]?.children;
|
||||
getDepartmentList().then((res) => {
|
||||
orgOptions.value = res.data || [];
|
||||
});
|
||||
};
|
||||
// 获取诊断目录
|
||||
|
||||
@@ -80,12 +80,24 @@
|
||||
<script setup name="Surgery">
|
||||
import {getCurrentInstance, onBeforeMount, onMounted, reactive} from 'vue';
|
||||
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 {getApplicationList, saveSurgery} from './api';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
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 props = defineProps({});
|
||||
const state = reactive({});
|
||||
@@ -176,9 +188,7 @@ const projectWithDepartment = (selectProjectIds, type) => {
|
||||
isRelease = false;
|
||||
}
|
||||
// 选中项目中的执行科室id与全部科室数据做匹配
|
||||
const findItem = orgOptions.value.find((item) => {
|
||||
return item.id == obj.orgId;
|
||||
});
|
||||
const findItem = findTreeItem(orgOptions.value, obj.orgId);
|
||||
|
||||
if (!findItem) {
|
||||
isRelease = false;
|
||||
@@ -251,8 +261,8 @@ const submit = () => {
|
||||
};
|
||||
/** 查询科室 */
|
||||
const getLocationInfo = () => {
|
||||
getOrgList().then((res) => {
|
||||
orgOptions.value = res.data?.records[0]?.children;
|
||||
getDepartmentList().then((res) => {
|
||||
orgOptions.value = res.data || [];
|
||||
});
|
||||
};
|
||||
// 获取诊断目录
|
||||
|
||||
@@ -988,10 +988,13 @@ function selectRow(rowValue, index) {
|
||||
form.purchaseinventoryList[index].unitList = rowValue.unitList[0];
|
||||
form.purchaseinventoryList[index].lotNumber = rowValue.lotNumber;
|
||||
form.purchaseinventoryList[index].ybNo = rowValue.ybNo;
|
||||
// #439 fix: 不清空sourceLocationId,保留handleAddRow设置的仓库ID
|
||||
if (!form.purchaseinventoryList[index].sourceLocationId) {
|
||||
form.purchaseinventoryList[index].sourceLocationId = '';
|
||||
}
|
||||
getPharmacyCabinetList().then((res) => {
|
||||
purposeTypeListOptions.value = res.data;
|
||||
// handleLocationClick(1, row, index)
|
||||
handleLocationClick(1, rowValue, index)
|
||||
});
|
||||
form.purchaseinventoryList[index].itemQuantity = 0;
|
||||
form.purchaseinventoryList[index].totalPrice = 0;
|
||||
|
||||
@@ -710,8 +710,10 @@ const loadQueueFromDb = async () => {
|
||||
id: it.id,
|
||||
queueOrder: it.queueOrder,
|
||||
patientName: it.patientName ?? '-',
|
||||
appointmentType: it.healthcareName ?? '普通',
|
||||
room: it.organizationName ?? '-',
|
||||
// 队列数据已从入队时存储的 healthcareName 读取(包含"预约"或"挂号"后缀)
|
||||
appointmentType: it.healthcareName ?? '普通号挂号',
|
||||
// Bug #410:使用 roomNo(诊室)而非 organizationName(科室)
|
||||
room: it.roomNo ?? it.organizationName ?? '-',
|
||||
doctor: it.practitionerName ?? '-',
|
||||
waitingTime: waitingTime,
|
||||
createTime: it.createTime, // 保存创建时间,用于定时器计算
|
||||
@@ -820,15 +822,18 @@ const loadDataFromApi = async () => {
|
||||
organizationName: item.organizationName,
|
||||
patientName: item.patientName ?? '-',
|
||||
age: parseAge(item.age),
|
||||
appointmentType: item.healthcareName ?? '普通',
|
||||
room: item.organizationName ? `${item.organizationName}` : '-',
|
||||
// Bug #409/410:根据 isFromAppointment 区分预约/挂号,使用 clinicRoom 诊室
|
||||
appointmentType: (item.healthcareName || '').replace('挂号', '') + (item.isFromAppointment ? '预约' : '挂号'),
|
||||
room: item.clinicRoom ?? item.organizationName ?? '-',
|
||||
doctor: item.practitionerName ?? '-',
|
||||
// 当前接口返回的是 practitionerUserId,保存为 practitionerId 供入队使用
|
||||
practitionerId: item.practitionerUserId ?? null,
|
||||
matchingRule: '-', // 这里先不做智能规则匹配
|
||||
// 号源池和槽位信息(用于分诊队列)
|
||||
poolId: item.poolId ?? null,
|
||||
slotId: item.slotId ?? null
|
||||
slotId: item.slotId ?? null,
|
||||
// 保存原始 isFromAppointment 用于调试
|
||||
isFromAppointment: item.isFromAppointment ?? false
|
||||
}))
|
||||
console.log('【心内科】候选池已加载', originalCandidatePoolList.value.length, '条今天的数据')
|
||||
} else {
|
||||
@@ -1046,7 +1051,8 @@ const handleAddToQueue = async () => {
|
||||
healthcareName: c.appointmentType,
|
||||
practitionerName: c.doctor,
|
||||
practitionerId: c.practitionerId ?? null,
|
||||
roomNo: c.roomNo ?? c.room ?? null,
|
||||
// Bug #410:c.room 已是诊室名称
|
||||
roomNo: c.room ?? null,
|
||||
poolId: c.poolId ?? null,
|
||||
slotId: c.slotId ?? null
|
||||
})
|
||||
@@ -1163,7 +1169,8 @@ const handleAddAllToQueue = async () => {
|
||||
healthcareName: c.appointmentType,
|
||||
practitionerName: c.doctor,
|
||||
practitionerId: c.practitionerId ?? null,
|
||||
roomNo: c.roomNo ?? c.room ?? null,
|
||||
// Bug #410:c.room 已是诊室名称
|
||||
roomNo: c.room ?? null,
|
||||
poolId: c.poolId ?? null,
|
||||
slotId: c.slotId ?? null
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user