package mysqldao import ( "context" "errors" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" "gorm.io/gorm" "gorm.io/gorm/clause" ) // OrderDao 订单数据访问 type OrderDao struct { db *gorm.DB } // NewOrderDao 创建订单 DAO func NewOrderDao() *OrderDao { return &OrderDao{db: global.MysqlEngine} } // OrderCreate 创建订单 func (d *OrderDao) OrderCreate(ctx context.Context, order *mysqlmodel.Order) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderCreate") defer span.End() if err := d.db.WithContext(ctx).Create(order).Error; err != nil { span.RecordError(err) return err } return nil } // OrderUpdateByOrderNo 按订单号更新订单字段 func (d *OrderDao) OrderUpdateByOrderNo(ctx context.Context, orderNo string, updates map[string]any) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderUpdateByOrderNo") defer span.End() if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).Where("order_no = ?", orderNo).Updates(updates).Error; err != nil { span.RecordError(err) return err } return nil } // OrderUpdateStatusByOrderNo 条件流转订单状态(防重复回调),返回是否更新成功 func (d *OrderDao) OrderUpdateStatusByOrderNo(ctx context.Context, orderNo string, fromStatus, toStatus int8) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderUpdateStatusByOrderNo") defer span.End() res := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Where("order_no = ? AND order_status = ?", orderNo, fromStatus). Update("order_status", toStatus) if res.Error != nil { span.RecordError(res.Error) return false, res.Error } return res.RowsAffected > 0, nil } // OrderExpireByTime 将超过支付有效期的待支付订单批量置为已过期,返回更新条数。 // 状态字面量:1=待支付 4=已过期(与 service 层 OrderStatusPending/OrderStatusExpired 一致)。 func (d *OrderDao) OrderExpireByTime(ctx context.Context, now int64) (int64, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderExpireByTime") defer span.End() res := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Where("order_status = ? AND expire_time < ?", 1, now). Update("order_status", 4) if res.Error != nil { span.RecordError(res.Error) return 0, res.Error } return res.RowsAffected, nil } // OrderListVirtualUnconfirmed 查询需主动查单确认的虚拟支付订单: // 支付渠道为虚拟支付(2)且状态为待支付(1)/已过期(4),创建时间落在 [since, until) 内。 // 用于「发货推送丢失」的兜底轮询(官方建议推送与轮询同时实现),按 id 升序限量返回。 func (d *OrderDao) OrderListVirtualUnconfirmed(ctx context.Context, since, until int64, limit int) ([]mysqlmodel.Order, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderListVirtualUnconfirmed") defer span.End() var list []mysqlmodel.Order if err := d.db.WithContext(ctx). Where("pay_channel = ? AND order_status IN ? AND created_at >= ? AND created_at < ?", 2, []int8{1, 4}, since, until). Order("id ASC").Limit(limit).Find(&list).Error; err != nil { span.RecordError(err) return nil, err } return list, nil } // OrderListVirtualRefunding 查询停留在「申请退款中」的虚拟支付订单(updated_at 早于水位), // 供退款结果查单兜底任务反查微信侧真实退款结果:推送丢失/未识别时避免订单永久卡单。 // 状态字面量:2=虚拟支付 6=申请退款中(与 service 层 PayChannelVirtual/OrderStatusRefunding 一致)。 func (d *OrderDao) OrderListVirtualRefunding(ctx context.Context, beforeUpdatedAt int64, limit int) ([]mysqlmodel.Order, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderListVirtualRefunding") defer span.End() var list []mysqlmodel.Order if err := d.db.WithContext(ctx). Where("pay_channel = ? AND order_status = ? AND updated_at < ?", 2, 6, beforeUpdatedAt). Order("id ASC").Limit(limit).Find(&list).Error; err != nil { span.RecordError(err) return nil, err } return list, nil } // OrderListPaidMissingBenefitTasks 查询存在未建权益任务的已支付订单,修复支付回调与任务落库之间的崩溃窗口。 // 除「任务不存在」外,也把「任务被取消(status=4)」视为待修复:订单曾进入退款流程时任务被取消, // 退款失败回退为已支付后需要重新发放,避免「已付款却无权益」(已退款订单状态为 5,不会被本查询命中)。 func (d *OrderDao) OrderListPaidMissingBenefitTasks(ctx context.Context, limit int) ([]mysqlmodel.Order, error) { var list []mysqlmodel.Order err := d.db.WithContext(ctx).Where("order_status = ?", 2). Where(`(member_days > 0 AND NOT EXISTS (SELECT 1 FROM benefit_grant_tasks t WHERE t.order_no=orders.order_no AND t.benefit_type=1 AND t.status<>4)) OR (cos_quota > 0 AND NOT EXISTS (SELECT 1 FROM benefit_grant_tasks t WHERE t.order_no=orders.order_no AND t.benefit_type=2 AND t.status<>4)) OR (ai_recommend_quota > 0 AND NOT EXISTS (SELECT 1 FROM benefit_grant_tasks t WHERE t.order_no=orders.order_no AND t.benefit_type=3 AND t.status<>4)) OR (closet_share_quota > 0 AND NOT EXISTS (SELECT 1 FROM benefit_grant_tasks t WHERE t.order_no=orders.order_no AND t.benefit_type=4 AND t.status<>4)) OR (outfit_share_quota > 0 AND NOT EXISTS (SELECT 1 FROM benefit_grant_tasks t WHERE t.order_no=orders.order_no AND t.benefit_type=5 AND t.status<>4))`). Order("id ASC").Limit(limit).Find(&list).Error return list, err } // OrderGetByID 按主键 ID 查询订单 func (d *OrderDao) OrderGetByID(ctx context.Context, id int64) (*mysqlmodel.Order, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderGetByID") defer span.End() var order mysqlmodel.Order if err := d.db.WithContext(ctx).First(&order, id).Error; err != nil { span.RecordError(err) return nil, err } return &order, nil } // OrderGetByOrderNo 按商户订单号查询订单 func (d *OrderDao) OrderGetByOrderNo(ctx context.Context, orderNo string) (*mysqlmodel.Order, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderGetByOrderNo") defer span.End() var order mysqlmodel.Order if err := d.db.WithContext(ctx).Where("order_no = ?", orderNo).First(&order).Error; err != nil { span.RecordError(err) return nil, err } return &order, nil } // OrderListByUserID 分页查询用户订单(status 为 0 不筛选) func (d *OrderDao) OrderListByUserID(ctx context.Context, userID int64, status int8, page, pageSize int) ([]mysqlmodel.Order, int64, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderListByUserID") defer span.End() var list []mysqlmodel.Order var total int64 q := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).Where("user_id = ?", userID) if status != 0 { q = q.Where("order_status = ?", status) } if err := q.Count(&total).Error; err != nil { span.RecordError(err) return nil, 0, err } if err := q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&list).Error; err != nil { span.RecordError(err) return nil, 0, err } return list, total, nil } // RefundFlowLogDao 退款流水数据访问 type RefundFlowLogDao struct { db *gorm.DB } // NewRefundFlowLogDao 创建退款流水 DAO func NewRefundFlowLogDao() *RefundFlowLogDao { return &RefundFlowLogDao{db: global.MysqlEngine} } // RefundApplyWithOrderStatus 原子受理退款申请:锁定订单行并校验「2 已支付」, // 将订单流转为「6 申请退款中」,同时新建退款流水或复用上次失败的流水(换新退款单号并重置为待处理)。 // allowed=false 表示订单状态不满足(非已支付/已被并发受理)或已有进行中的退款流水, // 此时不做任何写入,调用方应返回「当前状态不允许退款」。 // 状态字面量:0=待处理(流水) 2=已支付 3=退款失败(流水) 6=申请退款中(与 service 层常量一致)。 func (d *RefundFlowLogDao) RefundApplyWithOrderStatus(ctx context.Context, orderNo, refundNo, reason string, applyTime int64) (*mysqlmodel.RefundFlowLog, bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundApplyWithOrderStatus") defer span.End() var flow *mysqlmodel.RefundFlowLog allowed := false // 订单行锁串行化同一订单的并发申请:仅首个请求能把 2 流转为 6,其余请求读到 6 后直接拒绝。 err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var order mysqlmodel.Order if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("order_no = ?", orderNo).First(&order).Error; err != nil { return err } if order.OrderStatus != 2 { return nil } var existing mysqlmodel.RefundFlowLog gerr := tx.Where("order_no = ?", orderNo).First(&existing).Error switch { case gerr != nil && !errors.Is(gerr, gorm.ErrRecordNotFound): return gerr case gerr == nil && existing.Status != 3: // 已有待处理 / 退款中 / 已成功的退款流水:不允许重复申请。 return nil case gerr == nil: // 失败重试:复用旧记录,换新退款单号并重置为待处理。 if err := tx.Model(&mysqlmodel.RefundFlowLog{}).Where("id = ?", existing.ID).Updates(map[string]any{ "refund_no": refundNo, "refund_amount": order.PayAmount, "refund_reason": reason, "status": 0, "apply_time": applyTime, "fail_reason": "", }).Error; err != nil { return err } existing.RefundNo = refundNo existing.RefundAmount = order.PayAmount existing.RefundReason = reason existing.Status = 0 existing.ApplyTime = applyTime existing.FailReason = "" flow = &existing default: created := &mysqlmodel.RefundFlowLog{ OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.UserID, RefundNo: refundNo, RefundAmount: order.PayAmount, RefundReason: reason, Status: 0, ApplyTime: applyTime, } if err := tx.Create(created).Error; err != nil { return err } flow = created } if err := tx.Model(&mysqlmodel.Order{}).Where("id = ?", order.ID).Update("order_status", 6).Error; err != nil { return err } allowed = true return nil }) if err != nil { span.RecordError(err) return nil, false, err } return flow, allowed, nil } // RefundFlowLogCreate 创建退款流水 func (d *RefundFlowLogDao) RefundFlowLogCreate(ctx context.Context, log *mysqlmodel.RefundFlowLog) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundFlowLogCreate") defer span.End() if err := d.db.WithContext(ctx).Create(log).Error; err != nil { span.RecordError(err) return err } return nil } // RefundFlowLogUpdateStatus 按退款单号更新退款流水字段 func (d *RefundFlowLogDao) RefundFlowLogUpdateStatus(ctx context.Context, refundNo string, updates map[string]any) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundFlowLogUpdateStatus") defer span.End() if err := d.db.WithContext(ctx).Model(&mysqlmodel.RefundFlowLog{}).Where("refund_no = ?", refundNo).Updates(updates).Error; err != nil { span.RecordError(err) return err } return nil } // RefundFlowLogUpdateStatusByFrom 条件流转退款流水状态(防并发/乱序回调覆盖),返回是否更新成功。 func (d *RefundFlowLogDao) RefundFlowLogUpdateStatusByFrom(ctx context.Context, refundNo string, fromStatus int8, updates map[string]any) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundFlowLogUpdateStatusByFrom") defer span.End() res := d.db.WithContext(ctx).Model(&mysqlmodel.RefundFlowLog{}). Where("refund_no = ? AND status = ?", refundNo, fromStatus). Updates(updates) if res.Error != nil { span.RecordError(res.Error) return false, res.Error } return res.RowsAffected > 0, nil } // RefundFlowLogGetByRefundNo 按退款单号查询退款流水 func (d *RefundFlowLogDao) RefundFlowLogGetByRefundNo(ctx context.Context, refundNo string) (*mysqlmodel.RefundFlowLog, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundFlowLogGetByRefundNo") defer span.End() var log mysqlmodel.RefundFlowLog if err := d.db.WithContext(ctx).Where("refund_no = ?", refundNo).First(&log).Error; err != nil { span.RecordError(err) return nil, err } return &log, nil } // RefundFlowLogGetByOrderNo 按商户订单号查询退款流水 func (d *RefundFlowLogDao) RefundFlowLogGetByOrderNo(ctx context.Context, orderNo string) (*mysqlmodel.RefundFlowLog, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundFlowLogGetByOrderNo") defer span.End() var log mysqlmodel.RefundFlowLog if err := d.db.WithContext(ctx).Where("order_no = ?", orderNo).First(&log).Error; err != nil { span.RecordError(err) return nil, err } return &log, nil }