revert: restore develop to clean baseline 5132de36 (remove all AI changes)

This commit is contained in:
2026-05-28 09:43:49 +08:00
parent bdec44d6c5
commit 913a971ce4
481 changed files with 3036 additions and 26749 deletions

View File

@@ -1,122 +0,0 @@
/**
* 订单录入页面脚本
* 负责医嘱录入后显示总量单位
* 修复 Bug #561医嘱录入后总量单位显示为 “null”
* 原因是从诊疗目录获取的单位字段在某些情况下返回 null页面直接使用该值导致显示异常。
* 现在在取值时加入容错处理,若返回值为 null、undefined、空字符串则使用诊疗目录配置的默认单位。
*/
import { getOrderConfig } from '@/services/orderConfig';
import { getCatalogItem } from '@/services/catalog';
// 记录当前医嘱的总量单位
let currentTotalUnit = '';
/**
* 初始化医嘱录入页面
*/
export function initOrderEntry() {
// 绑定录入完成事件
document
.getElementById('orderSubmitBtn')
.addEventListener('click', handleOrderSubmit);
}
/**
* 处理医嘱提交
*/
async function handleOrderSubmit(event) {
event.preventDefault();
const orderData = collectOrderFormData();
try {
// 保存医嘱
const savedOrder = await saveOrder(orderData);
// 更新页面显示的总量单位
await updateTotalUnitDisplay(savedOrder);
} catch (err) {
console.error('保存医嘱失败:', err);
alert('医嘱保存失败,请稍后重试。');
}
}
/**
* 收集表单数据
*/
function collectOrderFormData() {
const form = document.getElementById('orderForm');
const formData = new FormData(form);
const data = {};
for (const [key, value] of formData.entries()) {
data[key] = value;
}
return data;
}
/**
* 保存医嘱(调用后端接口)
*/
async function saveOrder(order) {
// 这里使用 fetch 示例,实际项目请使用统一的 ajax 封装
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(order),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
/**
* 更新页面上显示的总量单位
* @param {Object} savedOrder 后端返回的已保存医嘱对象
*/
async function updateTotalUnitDisplay(savedOrder) {
// 1. 先尝试从后端返回的医嘱对象中获取 totalUnit
// 有些业务场景后端会直接返回配置好的单位,此时直接使用即可
let totalUnit = savedOrder.totalUnit;
// 2. 若后端未返回null、undefined、空字符串则需要从诊疗目录中读取配置的默认单位
if (!totalUnit) {
try {
const catalogItem = await getCatalogItem(savedOrder.catalogId);
// catalogItem.unit 为诊疗目录配置的单位字段
totalUnit = catalogItem && catalogItem.unit ? catalogItem.unit : '';
} catch (e) {
console.warn('获取诊疗目录单位失败,使用空字符串作为默认单位', e);
totalUnit = '';
}
}
// 3. 再次做容错处理,防止出现 null/undefined
if (!totalUnit) {
totalUnit = '';
}
// 4. 更新全局变量并渲染到页面
currentTotalUnit = totalUnit;
const unitSpan = document.getElementById('totalUnitDisplay');
if (unitSpan) {
unitSpan.textContent = totalUnit;
}
}
/**
* 对外暴露的获取当前总量单位的接口,供其他模块使用
*/
export function getCurrentTotalUnit() {
return currentTotalUnit;
}
// 页面初始化
document.addEventListener('DOMContentLoaded', initOrderEntry);

View File

@@ -1,64 +0,0 @@
package com.his.pharmacy.dao;
import com.his.pharmacy.model.DispenseDetail;
import com.his.pharmacy.model.DispenseSummary;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 发药汇总 DAO
*
* 关键改动:实现 upsertSummaries 与 decrementSummaries 为原子 UPSERT 操作,确保在同一事务内
* 汇总数据的写入时机与明细保持一致,避免业务脱节。
*/
@Mapper
public interface DispenseSummaryDao {
/**
* 批量 UPSERT 汇总记录。
* 对每条明细,按照 hospitalization_id、drug_id 进行唯一键匹配,
* 若不存在则 INSERT若已存在则 UPDATE 累加数量和金额。
*
* 采用 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 语法(若使用 PostgreSQL请改为 ON CONFLICT
*/
@Insert({
"<script>",
"INSERT INTO dispense_summary (hospitalization_id, drug_id, total_quantity, total_amount, version)",
"VALUES",
"<foreach collection='details' item='d' separator=','>",
"(#{d.hospitalizationId}, #{d.drugId}, #{d.quantity}, #{d.amount}, 0)",
"</foreach>",
"ON DUPLICATE KEY UPDATE",
"total_quantity = total_quantity + VALUES(total_quantity),",
"total_amount = total_amount + VALUES(total_amount),",
"version = version + 1",
"</script>"
})
void upsertSummaries(@Param("details") List<DispenseDetail> details);
/**
* 批量扣减汇总记录(退药场景)。
* 同样使用 UPSERT 语法,只是将数量和金额减去对应值。
*/
@Insert({
"<script>",
"INSERT INTO dispense_summary (hospitalization_id, drug_id, total_quantity, total_amount, version)",
"VALUES",
"<foreach collection='details' item='d' separator=','>",
"(#{d.hospitalizationId}, #{d.drugId}, -#{d.quantity}, -#{d.amount}, 0)",
"</foreach>",
"ON DUPLICATE KEY UPDATE",
"total_quantity = total_quantity + VALUES(total_quantity),",
"total_amount = total_amount + VALUES(total_amount),",
"version = version + 1",
"</script>"
})
void decrementSummaries(@Param("details") List<DispenseDetail> details);
/**
* 查询汇总(供业务或报表使用)
*/
@Select("SELECT * FROM dispense_summary WHERE hospitalization_id = #{hospitalizationId}")
List<DispenseSummary> findByHospitalizationId(@Param("hospitalizationId") Long hospitalizationId);
}

View File

@@ -1,72 +0,0 @@
package com.his.pharmacy.service;
import com.his.pharmacy.dao.DispenseDetailDao;
import com.his.pharmacy.dao.DispenseSummaryDao;
import com.his.pharmacy.model.DispenseDetail;
import com.his.pharmacy.model.DispenseSummary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 住院发退药业务服务
*
* 修复 Bug #503
* 原因发药明细DispenseDetail在保存后立即提交事务而发药汇总DispenseSummary在同一事务的后置提交阶段才写入
* 导致两者的触发时机不一致,外部系统或后续业务读取汇总数据时可能出现明细已存在而汇总未更新的情况,产生业务脱节风险。
*
* 解决方案:
* 1. 将明细和汇总的保存统一放在同一个事务中,确保两者要么同时成功,要么同时回滚。
* 2. 在保存明细后不立即刷新/提交,而是延迟到事务结束时统一写入汇总。
* 3. 为防止并发导致的汇总重复计算使用乐观锁version或数据库行级锁保证汇总的唯一性。
*
* 实现细节:
* - 使用 Spring 的 @Transactional 将整个发药流程包装为一个事务。
* - 在保存明细后不调用 flush而是直接返回等到事务提交时统一调用 summaryDao.upsertSummaries。
* - 为汇总表添加唯一约束 (hospitalization_id, drug_id) 并在更新时使用 “INSERT … ON DUPLICATE KEY UPDATE” 语句MySQL或等价的 UPSERTPostgreSQL
* - 增加日志记录,便于后续审计。
*/
@Service
public class DrugDispenseService {
private static final Logger logger = LoggerFactory.getLogger(DrugDispenseService.class);
private final DispenseDetailDao detailDao;
private final DispenseSummaryDao summaryDao;
public DrugDispenseService(DispenseDetailDao detailDao, DispenseSummaryDao summaryDao) {
this.detailDao = detailDao;
this.summaryDao = summaryDao;
}
/**
* 发药(包括新增明细和更新汇总)
*
* @param details 发药明细列表
*/
@Transactional(rollbackFor = Exception.class)
public void dispenseDrugs(List<DispenseDetail> details) {
// 1. 保存所有明细
for (DispenseDetail detail : details) {
// 这里不调用 flush交由事务统一提交
detailDao.insert(detail);
}
// 2. 同步更新/插入汇总数据
// 使用 upsert 确保在同一事务内完成,避免明细先提交而汇总延迟的问题
try {
summaryDao.upsertSummaries(details);
} catch (Exception e) {
logger.error("Failed to upsert dispense summaries for details: {}", details, e);
// 抛出异常让事务回滚,保持明细与汇总的一致性
throw e;
}
// 3. 业务结束,事务提交后明细与汇总同时持久化
}
// 其他业务方法保持不变...
}

View File

@@ -1,116 +0,0 @@
package com.openhis.application.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.openhis.application.domain.entity.AdmSchedulePool;
import com.openhis.application.domain.entity.AdmScheduleSlot;
import com.openhis.application.domain.entity.OrderMain;
import com.openhis.application.exception.BusinessException;
import com.openhis.application.mapper.AdmSchedulePoolMapper;
import com.openhis.application.mapper.AdmScheduleSlotMapper;
import com.openhis.application.mapper.OrderMainMapper;
import com.openhis.application.service.AppointmentService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 预约挂号业务实现
*
* 修复 Bug #574预约签到缴费成功后数据库 adm_schedule_slot.status 状态未及时流转为 “3”已取号
* 修复 Bug #575预约成功后数据库表 adm_schedule_pool 中的 booked_num 字段未实时累加。
*
* 业务说明:
* 1. 当患者完成预约挂号的缴费并完成签到取号系统需要将对应的排班号槽adm_schedule_slot状态从
* “2”已预约更新为 “3”已取号以便后续排队、叫号等业务能够正确识别该号已被使用。
* 2. 该状态更新必须在同一个事务内完成,确保支付成功后状态一定会被持久化,避免出现“已支付但号槽仍显示为未取号”的不一致情况。
*
* 实现思路:
* - 在支付成功的业务方法payAndCheckIn先完成支付相关的业务处理如更新订单状态、生成缴费记录等
* 随后调用 AdmScheduleSlotMapper.updateStatus 将对应的 slotId 状态更新为 3。
* - 使用 Spring 的 @Transactional 注解保证事务原子性;若更新失败则抛出 BusinessException事务回滚。
*/
@Service
public class AppointmentServiceImpl implements AppointmentService {
private final AdmScheduleSlotMapper admScheduleSlotMapper;
private final OrderMainMapper orderMainMapper;
private final AdmSchedulePoolMapper admSchedulePoolMapper;
public AppointmentServiceImpl(AdmScheduleSlotMapper admScheduleSlotMapper,
OrderMainMapper orderMainMapper,
AdmSchedulePoolMapper admSchedulePoolMapper) {
this.admScheduleSlotMapper = admScheduleSlotMapper;
this.orderMainMapper = orderMainMapper;
this.admSchedulePoolMapper = admSchedulePoolMapper;
}
/**
* 支付并签到(取号)业务。
*
* @param orderId 预约订单主键
* @param slotId 对应的排班号槽主键
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void payAndCheckIn(Long orderId, Long slotId) {
// 1. 校验订单是否存在且状态为“待支付”(0)
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("预约订单不存在");
}
if (order.getStatus() != 0) {
throw new BusinessException("订单状态不允许支付或签到");
}
// 2. 更新订单状态为“已支付”(1) 并记录支付时间
OrderMain updatedOrder = new OrderMain();
updatedOrder.setId(orderId);
updatedOrder.setStatus(1); // 已支付
updatedOrder.setPayTime(java.time.LocalDateTime.now());
int orderUpdateCnt = orderMainMapper.updateById(updatedOrder);
if (orderUpdateCnt != 1) {
throw new BusinessException("订单支付状态更新失败");
}
// 3. 将对应的号槽状态从“已预约”(2) 更新为“已取号”(3)
LambdaUpdateWrapper<AdmScheduleSlot> slotUpdate = new LambdaUpdateWrapper<>();
slotUpdate.eq(AdmScheduleSlot::getId, slotId)
.eq(AdmScheduleSlot::getStatus, 2) // 只在已预约状态下更新
.set(AdmScheduleSlot::getStatus, 3) // 已取号
.set(AdmScheduleSlot::getCheckInTime, java.time.LocalDateTime.now());
int slotUpdateCnt = admScheduleSlotMapper.update(null, slotUpdate);
if (slotUpdateCnt != 1) {
throw new BusinessException("号槽状态更新失败,可能已被其他操作修改");
}
// 4. 更新对应排班池的已预约数量booked_num+1
// 这里假设 slot 表中有 poolId 字段指向所属的 adm_schedule_pool
AdmScheduleSlot slot = admScheduleSlotMapper.selectById(slotId);
if (slot == null) {
throw new BusinessException("号槽信息获取失败");
}
LambdaUpdateWrapper<AdmSchedulePool> poolUpdate = new LambdaUpdateWrapper<>();
poolUpdate.eq(AdmSchedulePool::getId, slot.getPoolId())
.setSql("booked_num = booked_num + 1");
int poolUpdateCnt = admSchedulePoolMapper.update(null, poolUpdate);
if (poolUpdateCnt != 1) {
throw new BusinessException("排班池已预约数量更新失败");
}
}
/**
* 仅预约成功(未支付)时调用,用于累计 pool 的 booked_num。
*
* @param poolId 所属排班池主键
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void incrementBookedNum(Long poolId) {
LambdaUpdateWrapper<AdmSchedulePool> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AdmSchedulePool::getId, poolId)
.setSql("booked_num = booked_num + 1");
int cnt = admSchedulePoolMapper.update(null, wrapper);
if (cnt != 1) {
throw new BusinessException("预约成功后更新排班池 booked_num 失败");
}
}
}

View File

@@ -1,77 +0,0 @@
package com.openhis.application.service.impl;
import com.openhis.application.mapper.OrderDetailMapper;
import com.openhis.application.mapper.OrderMainMapper;
import com.openhis.application.domain.entity.OrderMain;
import com.openhis.application.domain.entity.OrderDetail;
import com.openhis.application.exception.BusinessException;
import com.openhis.application.dto.OrderVerificationDTO;
import com.openhis.application.mapper.OrderVerificationMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 医嘱校对业务实现
*
* 修复 Bug #505药品医嘱已由药房发药护士仍能在“医嘱校对”模块执行“退回”操作。
* 修复 Bug #506门诊诊前退号后确保相关表的状态值与生产环境定义保持一致。
* 修复 Bug #595医嘱校对模块列表字段缺失严重与医生站医嘱要素不一致。
* 通过结构化查询与DTO映射确保返回字段包含开始时间、单次剂量、总量、总金额、频次/用法、
* 开嘱医生、停嘱时间、停嘱医生、注射药品、皮试状态、诊断等,并标记皮试高亮标识。
*/
@Service
public class OrderVerificationServiceImpl implements OrderVerificationService {
private final OrderMainMapper orderMainMapper;
private final OrderDetailMapper orderDetailMapper;
private final OrderVerificationMapper orderVerificationMapper;
public OrderVerificationServiceImpl(OrderMainMapper orderMainMapper,
OrderDetailMapper orderDetailMapper,
OrderVerificationMapper orderVerificationMapper) {
this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper;
this.orderVerificationMapper = orderVerificationMapper;
}
/**
* 医嘱退回(撤销)操作
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void returnOrder(Long orderId, String reason) {
// 1. 校验医嘱是否存在
OrderMain order = orderMainMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("医嘱不存在");
}
// 2. 检查是否已发药(药房已发药的医嘱不能退回)
if ("DISPATCHED".equals(order.getDispenseStatus())) {
throw new BusinessException("药品已发药,不能退回");
}
// 3. 更新医嘱主表状态为“已取消”(与生产环境定义保持一致)
// 生产环境约定的取消状态值为 "CANCELLED"
order.setStatus("CANCELLED");
order.setCancelReason(reason);
orderMainMapper.updateById(order);
// 4. 更新医嘱明细表对应的状态为“已取消”
List<OrderDetail> details = orderDetailMapper.selectListByOrderId(orderId);
if (details != null && !details.isEmpty()) {
for (OrderDetail detail : details) {
detail.setStatus("CANCELLED");
orderDetailMapper.updateById(detail);
}
}
// 5. 记录日志(可选)
log.info("Order {} has been returned/cancelled. Reason: {}", orderId, reason);
}
// 其余业务方法保持不变...
}

View File

@@ -1,120 +0,0 @@
package com.openhis.application.service.impl;
import com.openhis.application.domain.entity.Registration;
import com.openhis.application.domain.entity.RegistrationDetail;
import com.openhis.application.domain.entity.ScheduleSlot;
import com.openhis.application.mapper.RegistrationMapper;
import com.openhis.application.mapper.RegistrationDetailMapper;
import com.openhis.application.mapper.ScheduleSlotMapper; // ← 修正错误的包路径
import com.openhis.application.exception.BusinessException;
import com.openhis.application.service.RegistrationService;
import com.openhis.application.constants.RegistrationStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 门诊挂号业务实现
*
* 修复 Bug #506门诊诊前退号后数据库多表状态值变更与 PRD 定义不符。
*
* 退号业务需要同时更新以下表的状态:
* 1. registration_main → status = "CANCELLED"
* 2. registration_detail → status = "CANCELLED"
*
* 之前的实现只更新了 registration_main 表,导致 registration_detail
* 仍保持原来的 “REGISTERED” 状态,与产品需求不一致,进而在后续查询、统计
* 以及对账时出现数据不一致的问题。
*
* 本次修复在同一事务内统一更新两张表,并使用统一的状态常量
* {@link RegistrationStatus#CANCELLED},确保所有相关记录的状态保持同步。
*
* 另外,修复 Bug #575预约成功后adm_schedule_pool 表中的 booked_num
* 未实时累加。新增对 ScheduleSlot对应 adm_schedule_pool的已预约数
* 增量更新,确保前端查询可立即得到最新的可预约余量。
*
* 以及 Bug #574预约签到缴费成功后adm_schedule_slot.status 未及时流转为 “3”(已取)。
* 在缴费成功后同步更新对应的 ScheduleSlot 状态为 3。
*/
@Service
public class RegistrationServiceImpl implements RegistrationService {
private static final Logger log = LoggerFactory.getLogger(RegistrationServiceImpl.class);
private final RegistrationMapper registrationMapper;
private final RegistrationDetailMapper registrationDetailMapper;
private final ScheduleSlotMapper scheduleSlotMapper; // 新增成员变量
public RegistrationServiceImpl(RegistrationMapper registrationMapper,
RegistrationDetailMapper registrationDetailMapper,
ScheduleSlotMapper scheduleSlotMapper) {
this.registrationMapper = registrationMapper;
this.registrationDetailMapper = registrationDetailMapper;
this.scheduleSlotMapper = scheduleSlotMapper;
}
/**
* 预约成功后,更新号源已预约数。
*
* @param slotId 号源主键
*/
private void incrementBookedNum(Long slotId) {
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId);
if (slot == null) {
throw new BusinessException("号源不存在slotId=" + slotId);
}
slot.setBookedNum(slot.getBookedNum() + 1);
scheduleSlotMapper.updateByPrimaryKeySelective(slot);
}
/**
* 预约签到缴费成功后,将对应的号源状态流转为 “3”(已取)。
*
* @param slotId 号源主键
*/
private void markSlotAsTaken(Long slotId) {
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(slotId);
if (slot == null) {
throw new BusinessException("号源不存在slotId=" + slotId);
}
// 状态 3 表示 “已取”
slot.setStatus(3);
scheduleSlotMapper.updateByPrimaryKeySelective(slot);
}
/**
* 示例:完成挂号(包括预约、支付、签到)业务的核心实现。
* 真实项目中该方法会被更细粒度的业务拆分,此处仅演示关键状态更新。
*
* @param registration 挂号主记录
* @param details 挂号明细列表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void completeRegistration(Registration registration,
List<RegistrationDetail> details) {
// 1. 保存挂号主表
registrationMapper.insertSelective(registration);
// 2. 保存挂号明细
for (RegistrationDetail detail : details) {
detail.setRegistrationId(registration.getId());
registrationDetailMapper.insertSelective(detail);
}
// 3. 预约成功后,累计已预约数
if (registration.getScheduleSlotId() != null) {
incrementBookedNum(registration.getScheduleSlotId());
}
// 4. 支付成功后(此处假设已经完成支付),将号源状态置为已取
if (registration.getScheduleSlotId() != null && registration.getStatus() == RegistrationStatus.PAID) {
markSlotAsTaken(registration.getScheduleSlotId());
}
}
// 其它业务方法(如退号)保持不变,已在其他提交中实现...
}

View File

@@ -1,38 +0,0 @@
package com.openhis.web.outpatient.controller;
import com.openhis.web.outpatient.service.CheckRequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 检查申请接口
*
* 修复 Bug #550
* 1. 前端已改为手动控制勾选,后端需要确保一次只能提交同一检查项目的唯一记录。
* 2. 防止重复提交导致明细耦合,新增校验逻辑。
*/
@RestController
@RequestMapping("/outpatient/check-requests")
public class CheckRequestController {
@Autowired
private CheckRequestService checkRequestService;
@GetMapping
public List<Map<String, Object>> list() {
return checkRequestService.listPendingRequests();
}
@PostMapping("/submit")
public Map<String, Object> submit(@RequestBody List<Map<String, Object>> selected) {
try {
checkRequestService.validateAndSubmit(selected);
return Map.of("code", 200, "msg", "提交成功");
} catch (IllegalArgumentException e) {
return Map.of("code", 400, "msg", e.getMessage());
}
}
}

View File

@@ -1,75 +0,0 @@
package com.openhis.web.outpatient.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 门诊医嘱返回的 DTO
*
* 关键修复Bug #561
* 1. 之前返回的“总量单位”字段仅是字典表的主键 ID前端直接展示导致出现 “null” 或者数字 ID。
* 2. 新增 `totalUnitName` 字段用于返回字典中文名称,并在 JSON 序列化时保持向后兼容。
* - 旧字段 `totalUnitId`ID仍保留供老接口使用。
* - 前端页面改为使用 `totalUnitName`,即可得到正确的中文单位显示。
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OrderDTO {
private Long id;
private String itemName;
private Double price;
/** 原始的单位 ID字典表主键保留兼容老接口 */
@JsonProperty("totalUnitId")
private Integer totalUnitId;
/** 新增:总量单位的中文名称,前端展示使用 */
@JsonProperty("totalUnitName")
private String totalUnitName;
// 其它已有字段省略 ...
// ------------------- Getter / Setter -------------------
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getTotalUnitId() {
return totalUnitId;
}
public void setTotalUnitId(Integer totalUnitId) {
this.totalUnitId = totalUnitId;
}
public String getTotalUnitName() {
return totalUnitName;
}
public void setTotalUnitName(String totalUnitName) {
this.totalUnitName = totalUnitName;
}
// 其它 getter / setter 省略 ...
}

View File

@@ -1,68 +0,0 @@
package com.openhis.web.outpatient.mapper;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
/**
* 检查申请数据访问层
*
* 新增:
* 1. selectPendingItemCodes(List<String>) 根据项目代码查询仍在“待处理”状态的记录,用于提交前的唯一性校验。
* 2. batchInsertCheckRequests(List<Map<String,Object>>) 一次性批量插入检查申请明细,避免前端明细与后端耦合。
*/
@Mapper
public interface CheckRequestMapper {
@Select("SELECT * FROM outpatient_check_request WHERE status = 0") // 0 表示待处理
List<Map<String, Object>> selectPendingRequests();
/**
* 查询给定项目代码列表中,仍处于待处理状态的项目代码。
*
* @param itemCodes 项目代码集合
* @return 已存在待处理状态的项目代码集合
*/
@Select({
"<script>",
"SELECT DISTINCT item_code FROM outpatient_check_request",
"WHERE status = 0",
"AND item_code IN",
"<foreach item='code' collection='itemCodes' open='(' separator=',' close=')'>",
"#{code}",
"</foreach>",
"</script>"
})
List<String> selectPendingItemCodes(@Param("itemCodes") List<String> itemCodes);
/**
* 批量插入检查申请记录。
*
* 前端传入的每条记录必须包含以下键:
* - itemCode : 检查项目代码
* - patientId : 患者 ID
* - doctorId : 医生 ID
* - requestTime: 申请时间(若未提供则使用 NOW()
*
* 其它业务字段(如 status在 SQL 中统一写死为待处理状态0
*/
@Insert({
"<script>",
"INSERT INTO outpatient_check_request (item_code, patient_id, doctor_id, request_time, status)",
"VALUES",
"<foreach collection='list' item='item' separator=','>",
"(",
"#{item.itemCode},",
"#{item.patientId},",
"#{item.doctorId},",
"<choose>",
" <when test='item.requestTime != null'>#{item.requestTime}</when>",
" <otherwise>NOW()</otherwise>",
"</choose>,",
"0",
")",
"</foreach>",
"</script>"
})
int batchInsertCheckRequests(@Param("list") List<Map<String, Object>> list);
}

View File

@@ -1,94 +0,0 @@
package com.openhis.web.outpatient.mapper;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
/**
* 门诊医嘱相关数据访问层
*
* 修复 Bug #561
* 医嘱录入后,总量单位显示为 “null”。根因是查询医嘱时未把诊疗目录中配置的
* “total_unit” 字段取出来,导致前端取到的值为 null。此处在查询医嘱列表
* 时通过 LEFT JOIN 诊疗目录表treatment_catalog并将 total_unit
* 映射为 totalUnit前端使用的属性名从而保证即使医嘱本身没有该字段
* 也能得到正确的单位值。
*
* 新增:
* - updatePayStatus更新预约订单的支付状态。
* - listOrdersByPatientPaged为门诊医生工作站的“待写病历”列表提供分页查询避免一次性加载大量数据导致页面卡顿。
*/
@Mapper
public interface OrderMapper {
/**
* 查询门诊医嘱列表(含总量单位)。
*
* @param patientId 患者 ID
* @return 医嘱列表,每条记录包含 totalUnit 字段
*/
@Select({
"<script>",
"SELECT o.id,",
" o.patient_id AS patientId,",
" o.doctor_id AS doctorId,",
" o.item_code AS itemCode,",
" o.item_name AS itemName,",
" o.quantity,",
" o.dosage,",
" o.frequency,",
" o.route,",
" o.start_date AS startDate,",
" o.end_date AS endDate,",
" /* 从诊疗目录获取配置的总量单位 */",
" tc.total_unit AS totalUnit",
"FROM outpatient_order o",
"LEFT JOIN treatment_catalog tc ON o.item_code = tc.item_code",
"WHERE o.patient_id = #{patientId}",
"</script>"
})
List<Map<String, Object>> listOrdersByPatient(@Param("patientId") Long patientId);
/**
* 分页查询门诊医嘱列表(含总量单位),用于“待写病历”页面。
*
* @param patientId 患者 ID
* @param offset 数据偏移量从0开始
* @param limit 每页记录数
* @return 医嘱列表,每条记录包含 totalUnit 字段
*/
@Select({
"<script>",
"SELECT o.id,",
" o.patient_id AS patientId,",
" o.doctor_id AS doctorId,",
" o.item_code AS itemCode,",
" o.item_name AS itemName,",
" o.quantity,",
" o.dosage,",
" o.frequency,",
" o.route,",
" o.start_date AS startDate,",
" o.end_date AS endDate,",
" tc.total_unit AS totalUnit",
"FROM outpatient_order o",
"LEFT JOIN treatment_catalog tc ON o.item_code = tc.item_code",
"WHERE o.patient_id = #{patientId}",
"ORDER BY o.create_time DESC",
"LIMIT #{limit} OFFSET #{offset}",
"</script>"
})
List<Map<String, Object>> listOrdersByPatientPaged(@Param("patientId") Long patientId,
@Param("offset") int offset,
@Param("limit") int limit);
/**
* 更新预约订单的支付状态。
*
* @param orderId 订单ID
* @param status 支付状态码
* @return 受影响的行数
*/
@Update("UPDATE outpatient_order SET pay_status = #{status} WHERE id = #{orderId}")
int updatePayStatus(@Param("orderId") Long orderId, @Param("status") int status);
}

View File

@@ -1,34 +0,0 @@
package com.openhis.web.outpatient.service;
import com.openhis.web.outpatient.dto.OrderDTO;
import com.openhis.web.outpatient.mapper.OrderMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 门诊医嘱服务实现
*
* 关键修复Bug #561
* 1. 在业务层返回的 DTO 中已经包含 totalUnitName前端直接使用即可。
*/
@Service
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
public OrderServiceImpl(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
@Override
public List<OrderDTO> getOrdersByDoctor(Long doctorId) {
if (doctorId == null) {
throw new IllegalArgumentException("医生ID不能为空");
}
// 直接返回 Mapper 已经填充好的 totalUnitName
return orderMapper.selectOrderListByDoctor(doctorId);
}
// 其它业务方法保持不变 ...
}

View File

@@ -1,67 +0,0 @@
package com.openhis.web.outpatient.service.impl;
import com.openhis.web.outpatient.mapper.CheckRequestMapper;
import com.openhis.web.outpatient.service.CheckRequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/**
* 检查申请业务实现
*
* 修复 Bug #550
* - 在提交前校验同一检查项目item_code是否已存在未完成的申请防止自动勾选冲突。
* - 通过一次 INSERT 完成明细保存,避免前端明细与后端耦合。
*/
@Service
public class CheckRequestServiceImpl implements CheckRequestService {
@Autowired
private CheckRequestMapper checkRequestMapper;
@Override
public List<Map<String, Object>> listPendingRequests() {
return checkRequestMapper.selectPendingRequests();
}
/**
* 校验并提交检查申请
*
* @param selected 前端选中的检查项目列表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void validateAndSubmit(List<Map<String, Object>> selected) {
if (selected == null || selected.isEmpty()) {
return;
}
// 1. 检查前端是否传入了重复的 item_code
List<String> duplicateCodes = selected.stream()
.collect(Collectors.groupingBy(item -> (String) item.get("itemCode")))
.entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!duplicateCodes.isEmpty()) {
throw new IllegalArgumentException("请求中包含重复的检查项目代码: " + duplicateCodes);
}
// 2. 检查数据库中是否已经存在未完成的相同项目
List<String> itemCodes = selected.stream()
.map(item -> (String) item.get("itemCode"))
.collect(Collectors.toList());
List<String> alreadyPending = checkRequestMapper.selectPendingItemCodes(itemCodes);
if (!alreadyPending.isEmpty()) {
throw new IllegalArgumentException("以下项目已存在待处理申请,请勿重复提交: " + alreadyPending);
}
// 3. 批量插入申请记录解耦明细仅保存主项与关联方法ID
checkRequestMapper.batchInsertCheckRequests(selected);
}
}

View File

@@ -1,54 +0,0 @@
package com.openhis.web.outpatient.mapper;
import com.openhis.web.outpatient.dto.OrderDTO;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 门诊医嘱相关数据库操作 Mapper
*
* 关键修复Bug #561
* 1. 在查询医嘱列表时联表查询字典表his_dict获取“总量单位”的中文名称。
* 之前仅返回 total_unit_id导致前端展示为 null 或者数字 ID。
* 现在返回 total_unit_name并映射到 DTO 的 totalUnitName 字段。
*
* 关键修复Bug #562
* 2. 为防止一次性查询全部医嘱导致页面加载卡顿,新增默认分页限制(前端可自行扩展)。
* 当前查询仅返回前 200 条记录,足以满足“待写病历”列表的快速展示需求。
* 如需完整数据,可在业务层自行实现分页参数。
*/
@Mapper
public interface OrderMapper {
// 其它已有方法省略 ...
/**
* 查询门诊医嘱列表(含总量单位中文名称)。
*
* @param doctorId 医生 ID
* @return 包含总量单位中文名称的医嘱 DTO 列表(默认限制前 200 条)
*/
@Select({
"<script>",
"SELECT",
" o.id,",
" o.item_name AS itemName,",
" o.price,",
" o.total_unit_id AS totalUnitId,",
// 通过字典表获取中文名称字典表约定type='unit', id=total_unit_id, name=中文名称
" (SELECT d.name FROM his_dict d WHERE d.type = 'unit' AND d.id = o.total_unit_id) AS totalUnitName,",
" o.quantity,",
" o.frequency,",
" o.route,",
" o.skin_test_status AS skinTestStatus",
"FROM his_outpatient_order o",
"WHERE o.doctor_id = #{doctorId}",
// 默认限制返回记录数,避免一次性加载过多导致前端卡顿
"LIMIT 200",
"</script>"
})
List<OrderDTO> selectOrderListByDoctor(@Param("doctorId") Long doctorId);
// 其它已有方法保持不变 ...
}

View File

@@ -1,12 +0,0 @@
-- 创建发药汇总表,加入唯一约束以支持 UPSERT
CREATE TABLE IF NOT EXISTS dispense_summary (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
hospitalization_id BIGINT NOT NULL,
drug_id BIGINT NOT NULL,
total_quantity DECIMAL(12,2) NOT NULL DEFAULT 0,
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_hosp_drug (hospitalization_id, drug_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='住院发药汇总表';

View File

@@ -1,22 +0,0 @@
<template>
<el-table :data="orderList" style="width: 100%">
<el-table-column prop="itemName" label="项目名称" />
<el-table-column prop="price" label="单价" width="80" />
<!-- 使用 totalUnitName 替代原来的 totalUnitId数值 -->
<el-table-column prop="totalUnitName" label="总量单位" width="80" />
<!-- 其它列保持不变 -->
</el-table>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getDoctorOrders } from '@/api/outpatient';
const orderList = ref([]);
onMounted(async () => {
const doctorId = /* 当前医生 ID */;
const { data } = await getDoctorOrders({ doctorId });
orderList.value = data;
});
</script>

View File

@@ -1,100 +0,0 @@
/**
* 预约挂号业务服务
* 包含预约、签到、缴费等核心流程的实现
*/
const db = require('../models');
const { Transaction } = require('sequelize');
/**
* 处理预约缴费成功后的后置业务
*
* 业务说明:
* 1. 缴费成功后需要把对应的号源槽adm_schedule_slot状态从 “2”(已预约) 改为 “3”(已取号)
* 2. 同时需要记录实际取号时间,以便后续统计和对账;
* 3. 该操作必须在同一个事务中完成,防止出现“缴费成功但号源状态未更新”的不一致情况。
*
* 之前的实现只在业务层返回了成功信息,忘记了对 adm_schedule_slot 表进行状态更新,
* 导致前端在查询号源时仍然显示为 “已预约”,从而出现 Bug #574。
*
* 下面的实现补足了状态流转的缺失,并确保在异常情况下事务回滚。
*
* @param {Object} paymentInfo 缴费返回的业务数据,必须包含:
* - scheduleSlotId: 对应的号源槽主键
* - paymentId: 支付单号(用于日志记录)
* @param {Object} userContext 当前操作用户的上下文(如 userId、operatorName 等)
* @returns {Promise<Object>} 返回更新后的号源槽信息
*/
async function handlePaymentSuccess(paymentInfo, userContext) {
const { scheduleSlotId, paymentId } = paymentInfo;
if (!scheduleSlotId) {
throw new Error('scheduleSlotId is required for payment success handling');
}
// 使用事务确保原子性
const transaction = await db.sequelize.transaction();
try {
// 1⃣ 读取当前号源槽,确保它仍然处于“已预约”(status = 2) 状态
const slot = await db.adm_schedule_slot.findOne({
where: { id: scheduleSlotId },
transaction,
lock: transaction.LOCK.UPDATE, // 防止并发修改
});
if (!slot) {
throw new Error(`Schedule slot not found, id=${scheduleSlotId}`);
}
// 只在状态为“已预约”时才允许流转到“已取号”
if (slot.status !== 2) {
// 若已经是“已取号”或其他状态,直接返回当前记录,避免重复更新
await transaction.commit();
return slot;
}
// 2⃣ 更新号源槽状态为 “3”(已取号) 并记录取号时间
await slot.update(
{
status: 3, // 已取号
taken_at: new Date(), // 实际取号时间
payment_id: paymentId, // 关联支付单号,便于追溯
updated_by: userContext.userId, // 操作人
updated_at: new Date(),
},
{ transaction }
);
// 3⃣ 如有需要,可在此处写入审计日志(示例)
await db.audit_log.create(
{
action: 'SLOT_STATUS_CHANGED',
description: `Schedule slot ${scheduleSlotId} status changed from 2 to 3 after payment ${paymentId}`,
operator_id: userContext.userId,
operator_name: userContext.operatorName,
target_table: 'adm_schedule_slot',
target_id: scheduleSlotId,
before_status: 2,
after_status: 3,
created_at: new Date(),
},
{ transaction }
);
// 提交事务
await transaction.commit();
// 返回最新的号源槽对象
return await db.adm_schedule_slot.findByPk(scheduleSlotId);
} catch (err) {
// 发生异常时回滚事务,确保数据不出现半更新状态
await transaction.rollback();
// 重新抛出异常,让上层统一处理
throw err;
}
}
module.exports = {
handlePaymentSuccess,
// 其它预约相关的业务方法...
};