Files
observer/server/biz/service/order.go
T

162 lines
5.4 KiB
Go

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))
}