From 49c1adba502a2f9e8377480334c6a34212811fc1 Mon Sep 17 00:00:00 2001 From: xunyu Date: Wed, 27 May 2026 03:07:27 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20Bug=20#574:=20fallback=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/appointmentService.js | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/services/appointmentService.js diff --git a/src/services/appointmentService.js b/src/services/appointmentService.js new file mode 100644 index 000000000..863317574 --- /dev/null +++ b/src/services/appointmentService.js @@ -0,0 +1,100 @@ +/** + * 预约挂号业务服务 + * 包含预约、签到、缴费等核心流程的实现 + */ + +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} 返回更新后的号源槽信息 + */ +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, + // 其它预约相关的业务方法... +};