package service import ( "context" "crypto/rand" "encoding/hex" "fmt" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "observer-server/biz/consts" "observer-server/biz/dao" "observer-server/biz/model/dto" "observer-server/biz/model/entity" "observer-server/common" ) // orderService 订单业务:创建(惰性回收+复用+渠道下单)、确认(幂等,回调为唯一授权来源) type orderService struct{} var Order = &orderService{} // ListPlans 套餐列表(config.yml plans 静态定价,客户端不硬编码价格) func (s *orderService) ListPlans(ctx context.Context, req *dto.ListPlansReq) (*dto.ListPlansRes, error) { plans, err := common.ListPlans(ctx) if err != nil { return nil, err } items := make([]*dto.PlanItem, 0, len(plans)) for _, p := range plans { items = append(items, &dto.PlanItem{ PlanId: p.Id, Days: p.Days, PriceCents: p.PriceCents, }) } return &dto.ListPlansRes{List: items}, nil } // Create 创建支付订单: // 1. 校验套餐(服务端锁价,金额不由客户端传入)与渠道配置 // 2. 单写者串行事务:惰性关闭超时未支付订单 → 2 小时内复用 created 订单(防重复扣款)→ 新建订单 // 3. 渠道统一下单(外部 IO,串行外执行),失败回滚关闭订单 func (s *orderService) Create(ctx context.Context, req *dto.CreateOrderReq) (*dto.CreateOrderRes, error) { plan, err := common.GetPlan(ctx, req.PlanId) if err != nil { return nil, err } if plan == nil { return nil, gerror.NewCode(common.CodePlanNotConfigured, "套餐不存在") } if err := Payment.ensureChannelConfigured(ctx, req.Channel); err != nil { return nil, err } phoneNum := common.PhoneFromCtx(ctx) var order *entity.PaymentOrder err = common.Serial().Submit(ctx, func() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 惰性关闭超时未支付订单(对齐微信支付 2 小时有效期,保证支付成功后回调必然可落授权) if err := dao.PaymentOrder.CloseStaleCreatedInTx(ctx, tx, phoneNum, gtime.Now().Add(-consts.PendingOrderWindowSeconds*time.Second)); err != nil { return err } // 同账号窗口内 created 订单直接复用,禁止重复创建 recent, err := dao.PaymentOrder.GetRecentCreatedInTx(ctx, tx, phoneNum, gtime.Now().Add(-consts.PendingOrderWindowSeconds*time.Second)) if err != nil { return err } if recent != nil { order = recent return nil } order = &entity.PaymentOrder{ OrderId: genOrderId(), PhoneNum: phoneNum, PlanId: req.PlanId, Channel: req.Channel, AmountCents: plan.PriceCents, Status: consts.OrderStatusCreated, CreatedAt: gtime.Now(), } return dao.PaymentOrder.InsertOneInTx(ctx, tx, order) }) }) if err != nil { return nil, err } // 渠道统一下单(外部 IO 不占串行写者) payParams, err := Payment.CreatePayParams(ctx, order, fmt.Sprintf("%d天", plan.Days), req.Channel) if err != nil { if closeErr := common.Serial().Submit(ctx, func() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { return dao.PaymentOrder.UpdateClosedInTx(ctx, tx, order.OrderId) }) }); closeErr != nil { g.Log().Errorf(ctx, "渠道下单失败回滚关闭订单 %s 失败: %+v", order.OrderId, closeErr) } return nil, err } return &dto.CreateOrderRes{OrderId: order.OrderId, PayParams: payParams}, nil } // Confirm 确认支付结果(幂等):只读订单状态,回调未到账返回 created; // 与回调写链路同经单写者串行,避免读到写中脏状态。 func (s *orderService) Confirm(ctx context.Context, req *dto.ConfirmOrderReq) (*dto.ConfirmOrderRes, error) { var status string err := common.Serial().Submit(ctx, func() error { o, err := dao.PaymentOrder.GetByPk(ctx, req.OrderId) if err != nil { return err } if o == nil { return gerror.NewCode(common.CodeOrderNotFound, "订单不存在") } switch o.Status { case consts.OrderStatusPaid: status = "paid" case consts.OrderStatusCreated: status = "created" default: status = "closed" } return nil }) if err != nil { return nil, err } return &dto.ConfirmOrderRes{Status: status}, nil } // AdminListOrders 管理端订单分页查询:金额原样整数分返回(展示转换在前端) func (s *orderService) AdminListOrders(ctx context.Context, req *dto.AdminOrderListReq) (*dto.AdminOrderListRes, error) { page, size := common.NormalizePage(req.Page, req.Size) list, total, err := dao.PaymentOrder.PageByFilter(ctx, req.PhoneNum, req.Status, req.StartAt, req.EndAt, page, size) if err != nil { return nil, err } items := make([]*dto.AdminOrderItem, 0, len(list)) for _, o := range list { items = append(items, &dto.AdminOrderItem{ OrderId: o.OrderId, PhoneNum: o.PhoneNum, PlanId: o.PlanId, Channel: o.Channel, AmountCents: o.AmountCents, Status: o.Status, CreatedAt: o.CreatedAt, PaidAt: o.PaidAt, }) } return &dto.AdminOrderListRes{Total: total, List: items}, nil } // genOrderId 订单号:O + 时间戳 + 随机后缀,长度 ≥ 20 不可枚举 func genOrderId() string { buf := make([]byte, 8) if _, err := rand.Read(buf); err != nil { return fmt.Sprintf("O%d", time.Now().UnixNano()) } return fmt.Sprintf("O%s%s", time.Now().Format("20060102150405"), hex.EncodeToString(buf)) }