feat: 实现订单支付功能模块
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
- 新增订单管理、支付回调、购买验证等核心服务 - 实现订单、订单项目的数据存储层和 API 接口 - 添加订单数据库迁移和 DTO 定义 - 更新 API 文档和路由配置 - 同步 3 个新规范到主规范库(订单管理、订单支付、套餐购买验证) - 完成 OpenSpec 变更归档 Ultraworked with Sisyphus
This commit is contained in:
420
internal/service/order/service.go
Normal file
420
internal/service/order/service.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
orderStore *postgres.OrderStore
|
||||
orderItemStore *postgres.OrderItemStore
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
orderStore *postgres.OrderStore,
|
||||
orderItemStore *postgres.OrderItemStore,
|
||||
walletStore *postgres.WalletStore,
|
||||
purchaseValidationService *purchase_validation.Service,
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
orderStore: orderStore,
|
||||
orderItemStore: orderItemStore,
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
allocationConfigStore: allocationConfigStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
|
||||
var validationResult *purchase_validation.PurchaseValidationResult
|
||||
var err error
|
||||
|
||||
if req.OrderType == model.OrderTypeSingleCard {
|
||||
if req.IotCardID == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
|
||||
}
|
||||
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
|
||||
} else if req.OrderType == model.OrderTypeDevice {
|
||||
if req.DeviceID == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
|
||||
}
|
||||
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
|
||||
} else {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
Updater: userID,
|
||||
},
|
||||
OrderNo: s.orderStore.GenerateOrderNo(),
|
||||
OrderType: req.OrderType,
|
||||
BuyerType: buyerType,
|
||||
BuyerID: buyerID,
|
||||
IotCardID: req.IotCardID,
|
||||
DeviceID: req.DeviceID,
|
||||
TotalAmount: validationResult.TotalPrice,
|
||||
PaymentStatus: model.PaymentStatusPending,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
CommissionConfigVersion: configVersion,
|
||||
}
|
||||
|
||||
var items []*model.OrderItem
|
||||
for _, pkg := range validationResult.Packages {
|
||||
item := &model.OrderItem{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
Updater: userID,
|
||||
},
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
Quantity: 1,
|
||||
UnitPrice: pkg.SuggestedRetailPrice,
|
||||
Amount: pkg.SuggestedRetailPrice,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := s.orderStore.Create(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.OrderResponse, error) {
|
||||
order, items, err := s.orderStore.GetByIDWithItems(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType string, buyerID uint) (*dto.OrderListResponse, error) {
|
||||
page := req.Page
|
||||
pageSize := req.PageSize
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
opts := &store.QueryOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
filters := map[string]any{
|
||||
"buyer_type": buyerType,
|
||||
"buyer_id": buyerID,
|
||||
}
|
||||
if req.PaymentStatus != nil {
|
||||
filters["payment_status"] = *req.PaymentStatus
|
||||
}
|
||||
if req.OrderType != "" {
|
||||
filters["order_type"] = req.OrderType
|
||||
}
|
||||
if req.OrderNo != "" {
|
||||
filters["order_no"] = req.OrderNo
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
filters["start_time"] = req.StartTime
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
filters["end_time"] = req.EndTime
|
||||
}
|
||||
|
||||
orders, total, err := s.orderStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var orderIDs []uint
|
||||
for _, o := range orders {
|
||||
orderIDs = append(orderIDs, o.ID)
|
||||
}
|
||||
|
||||
itemsMap := make(map[uint][]*model.OrderItem)
|
||||
if len(orderIDs) > 0 {
|
||||
allItems, err := s.orderItemStore.ListByOrderIDs(ctx, orderIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range allItems {
|
||||
itemsMap[item.OrderID] = append(itemsMap[item.OrderID], item)
|
||||
}
|
||||
}
|
||||
|
||||
var list []*dto.OrderResponse
|
||||
for _, o := range orders {
|
||||
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &dto.OrderListResponse{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Cancel(ctx context.Context, id uint, buyerType string, buyerID uint) error {
|
||||
order, err := s.orderStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "只能取消待支付的订单")
|
||||
}
|
||||
|
||||
return s.orderStore.UpdatePaymentStatus(ctx, id, model.PaymentStatusCancelled, nil)
|
||||
}
|
||||
|
||||
func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error {
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
var resourceType string
|
||||
var resourceID uint
|
||||
|
||||
if buyerType == model.BuyerTypePersonal {
|
||||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||||
resourceType = "iot_card"
|
||||
resourceID = *order.IotCardID
|
||||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||||
resourceType = "device"
|
||||
resourceID = *order.DeviceID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "无法确定钱包归属")
|
||||
}
|
||||
} else if buyerType == model.BuyerTypeAgent {
|
||||
resourceType = "shop"
|
||||
resourceID = buyerID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "不支持的买家类型")
|
||||
}
|
||||
|
||||
wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID, "main")
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if wallet.Balance < order.TotalAmount {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.Wallet{}).
|
||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", order.TotalAmount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
|
||||
}
|
||||
|
||||
if err := tx.Model(&model.Order{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"payment_status": model.PaymentStatusPaid,
|
||||
"payment_method": model.PaymentMethodWallet,
|
||||
"paid_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, paymentMethod string) error {
|
||||
order, err := s.orderStore.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if order.PaymentStatus == model.PaymentStatusPaid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Order{}).Where("id = ?", order.ID).Updates(map[string]any{
|
||||
"payment_status": model.PaymentStatusPaid,
|
||||
"payment_method": paymentMethod,
|
||||
"paid_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model.Order) error {
|
||||
items, err := s.orderItemStore.ListByOrderID(ctx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, item := range items {
|
||||
var pkg model.Package
|
||||
if err := tx.First(&pkg, item.PackageID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usage := &model.PackageUsage{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Creator,
|
||||
},
|
||||
OrderID: order.ID,
|
||||
PackageID: item.PackageID,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.DataAmountMB,
|
||||
ActivatedAt: now,
|
||||
ExpiresAt: now.AddDate(0, pkg.DurationMonths, 0),
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||||
usage.IotCardID = *order.IotCardID
|
||||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||||
usage.DeviceID = *order.DeviceID
|
||||
}
|
||||
|
||||
if err := tx.Create(usage).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) snapshotCommissionConfig(ctx context.Context, allocationID uint) int {
|
||||
if s.allocationConfigStore == nil {
|
||||
return 0
|
||||
}
|
||||
config, err := s.allocationConfigStore.GetEffective(ctx, allocationID, time.Now())
|
||||
if err != nil || config == nil {
|
||||
return 0
|
||||
}
|
||||
return config.Version
|
||||
}
|
||||
|
||||
func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
|
||||
var itemResponses []*dto.OrderItemResponse
|
||||
for _, item := range items {
|
||||
itemResponses = append(itemResponses, &dto.OrderItemResponse{
|
||||
ID: item.ID,
|
||||
PackageID: item.PackageID,
|
||||
PackageName: item.PackageName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPrice,
|
||||
Amount: item.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
statusText := ""
|
||||
switch order.PaymentStatus {
|
||||
case model.PaymentStatusPending:
|
||||
statusText = "待支付"
|
||||
case model.PaymentStatusPaid:
|
||||
statusText = "已支付"
|
||||
case model.PaymentStatusCancelled:
|
||||
statusText = "已取消"
|
||||
case model.PaymentStatusRefunded:
|
||||
statusText = "已退款"
|
||||
}
|
||||
|
||||
return &dto.OrderResponse{
|
||||
ID: order.ID,
|
||||
OrderNo: order.OrderNo,
|
||||
OrderType: order.OrderType,
|
||||
BuyerType: order.BuyerType,
|
||||
BuyerID: order.BuyerID,
|
||||
IotCardID: order.IotCardID,
|
||||
DeviceID: order.DeviceID,
|
||||
TotalAmount: order.TotalAmount,
|
||||
PaymentMethod: order.PaymentMethod,
|
||||
PaymentStatus: order.PaymentStatus,
|
||||
PaymentStatusText: statusText,
|
||||
PaidAt: order.PaidAt,
|
||||
CommissionStatus: order.CommissionStatus,
|
||||
CommissionConfigVersion: order.CommissionConfigVersion,
|
||||
Items: itemResponses,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user