162 lines
5.4 KiB
Go
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))
|
|
}
|