七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s

This commit is contained in:
2026-07-25 17:06:58 +08:00
parent ad9f613dd6
commit 73f5125d3d
249 changed files with 17137 additions and 877 deletions

View File

@@ -42,6 +42,12 @@ type OrderWalletPayServiceInterface interface {
WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error
}
// PaymentMethodPolicy 提供按资产类型校验支付方式的能力。
type PaymentMethodPolicy interface {
AllowedMethods(ctx context.Context, assetType string) ([]string, error)
EnsureAllowed(ctx context.Context, assetType, paymentMethod string) error
}
// ForceRechargeRequirement 强充要求。
type ForceRechargeRequirement struct {
NeedForceRecharge bool
@@ -69,6 +75,12 @@ type Service struct {
db *gorm.DB
redis *redis.Client
logger *zap.Logger
paymentMethodPolicy PaymentMethodPolicy
}
// SetPaymentMethodPolicy 注入 C 端支付方式策略。
func (s *Service) SetPaymentMethodPolicy(policy PaymentMethodPolicy) {
s.paymentMethodPolicy = policy
}
// New 创建客户端订单服务。
@@ -132,12 +144,20 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
if err != nil {
return nil, err
}
if owned, err := s.customerBinding.OwnsAsset(skipCtx, customerID, assetInfo.AssetType, assetInfo.AssetID); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
} else if !owned {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
}
if s.paymentMethodPolicy == nil {
return nil, errors.New(errors.CodeNoPaymentConfig)
}
if req.PaymentMethod == "" {
return nil, errors.New(errors.CodeInvalidParam, "请选择支付方式")
}
if err := s.paymentMethodPolicy.EnsureAllowed(skipCtx, assetInfo.AssetType, req.PaymentMethod); err != nil {
return nil, err
}
validationResult, err := s.validatePurchase(skipCtx, assetInfo, req.PackageIDs)
if err != nil {
@@ -152,7 +172,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
// 先判断是否需要强充,避免普通下单时不必要地校验微信配置
forceRecharge := s.checkForceRechargeRequirement(skipCtx, validationResult)
businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req.PackageIDs)
businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req.PackageIDs, req.PaymentMethod)
redisKey := constants.RedisClientPurchaseIdempotencyKey(businessKey)
lockKey := constants.RedisClientPurchaseLockKey(assetInfo.AssetType, assetInfo.AssetID)
@@ -229,6 +249,15 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
}()
if forceRecharge.NeedForceRecharge {
if s.paymentMethodPolicy == nil {
return nil, errors.New(errors.CodeNoPaymentConfig)
}
if err := s.paymentMethodPolicy.EnsureAllowed(skipCtx, assetInfo.AssetType, req.PaymentMethod); err != nil {
return nil, err
}
if req.PaymentMethod == model.PaymentMethodWallet {
return nil, errors.New(errors.CodePaymentMethodUnavailable, "该套餐需要先充值,请选择当前资产允许的微信或支付宝方式")
}
// 强充第三方支付方式与资产类型必须匹配:单卡只允许支付宝,设备只允许微信(已暂时注释)
// switch assetInfo.AssetType {
// case "card":
@@ -268,7 +297,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
return s.createForceRechargeOrder(skipCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created)
}
return s.createPackageOrder(skipCtx, customerID, validationResult, redisKey, &created)
return s.createPackageOrder(skipCtx, customerID, validationResult, req.PaymentMethod, redisKey, &created)
}
// getOrderAssetInfo 从订单中提取资产类型和 ID无需额外 DB 查询
@@ -295,9 +324,9 @@ func getOrderAssetInfo(order *model.Order) (string, uint, error) {
func (s *Service) validatePurchase(ctx context.Context, assetInfo *dto.AssetResolveResponse, packageIDs []uint) (*purchase_validation.PurchaseValidationResult, error) {
switch assetInfo.AssetType {
case "card":
return s.purchaseValidationService.ValidateCardPurchase(ctx, assetInfo.AssetID, packageIDs)
return s.purchaseValidationService.ValidatePersonalCardPurchase(ctx, assetInfo.AssetID, packageIDs)
case constants.ResourceTypeDevice:
return s.purchaseValidationService.ValidateDevicePurchase(ctx, assetInfo.AssetID, packageIDs)
return s.purchaseValidationService.ValidatePersonalDevicePurchase(ctx, assetInfo.AssetID, packageIDs)
default:
return nil, errors.New(errors.CodeInvalidParam)
}
@@ -350,6 +379,7 @@ func (s *Service) createPackageOrder(
ctx context.Context,
customerID uint,
validationResult *purchase_validation.PurchaseValidationResult,
paymentMethod string,
redisKey string,
created *bool,
) (*dto.ClientCreateOrderResponse, error) {
@@ -365,7 +395,7 @@ func (s *Service) createPackageOrder(
sellerCostPrice = costPrice
}
order, err := s.buildPendingOrder(ctx, customerID, validationResult, sellerCostPrice)
order, err := s.buildPendingOrder(ctx, customerID, validationResult, sellerCostPrice, paymentMethod)
if err != nil {
return nil, err
}
@@ -388,6 +418,7 @@ func (s *Service) createPackageOrder(
OrderID: order.ID,
OrderNo: order.OrderNo,
TotalAmount: order.TotalAmount,
PaymentMethod: order.PaymentMethod,
PaymentStatus: order.PaymentStatus,
PaymentStatusName: constants.GetOrderPaymentStatusName(order.PaymentStatus),
CreatedAt: formatClientServiceTime(order.CreatedAt),
@@ -538,7 +569,7 @@ func (s *Service) buildPersonalCustomerOperatorSnapshot(ctx context.Context, cus
return &id, model.OperatorAccountTypePersonalCustomer, name
}
func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result *purchase_validation.PurchaseValidationResult, sellerCostPrice int64) (*model.Order, error) {
func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result *purchase_validation.PurchaseValidationResult, sellerCostPrice int64, paymentMethod string) (*model.Order, error) {
orderType := resolveOrderType(result)
if orderType == "" {
return nil, errors.New(errors.CodeInvalidParam)
@@ -556,10 +587,12 @@ func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result
BuyerType: model.BuyerTypePersonal,
BuyerID: customerID,
TotalAmount: result.TotalPrice,
PaymentMethod: paymentMethod,
PaymentStatus: model.PaymentStatusPending,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0,
Source: constants.OrderSourceClient,
PurchaseRole: model.PurchaseRoleSelfPurchase,
Generation: resolveGeneration(result),
ExpiresAt: &expiresAt,
SellerCostPrice: sellerCostPrice,
@@ -569,10 +602,12 @@ func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result
order.IotCardID = &result.Card.ID
order.SeriesID = result.Card.SeriesID
order.SellerShopID = result.Card.ShopID
order.AssetIdentifier = result.Card.ICCID
} else if result.Device != nil {
order.DeviceID = &result.Device.ID
order.SeriesID = result.Device.SeriesID
order.SellerShopID = result.Device.ShopID
order.AssetIdentifier = firstNonEmptyClientOrderIdentifier(result.Device.VirtualNo, result.Device.IMEI)
}
order.BuyerPhone, order.BuyerNickname = s.fetchBuyerSnapshot(ctx, customerID)
@@ -581,6 +616,16 @@ func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result
return order, nil
}
// firstNonEmptyClientOrderIdentifier 返回 C 端订单可用的第一个资产标识快照。
func firstNonEmptyClientOrderIdentifier(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func (s *Service) buildOrderItems(ctx context.Context, customerID uint, result *purchase_validation.PurchaseValidationResult) ([]*model.OrderItem, error) {
sellerShopID := resolveSellerShopID(result)
items := make([]*model.OrderItem, 0, len(result.Packages))
@@ -617,13 +662,20 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, result *pur
var seriesID *uint
var sellerShopID uint
var firstCommissionTriggered bool
if result.Card != nil {
seriesID = result.Card.SeriesID
if seriesID != nil {
firstCommissionTriggered = result.Card.IsFirstRechargeTriggeredBySeries(*seriesID)
}
if result.Card.ShopID != nil {
sellerShopID = *result.Card.ShopID
}
} else if result.Device != nil {
seriesID = result.Device.SeriesID
if seriesID != nil {
firstCommissionTriggered = result.Device.IsFirstRechargeTriggeredBySeries(*seriesID)
}
if result.Device.ShopID != nil {
sellerShopID = *result.Device.ShopID
}
@@ -643,6 +695,9 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, result *pur
if err != nil || config == nil || !config.Enable {
return defaultResult
}
if firstCommissionTriggered {
return defaultResult
}
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
return &ForceRechargeRequirement{
@@ -796,7 +851,7 @@ func extractPackageIDs(packages []*model.Package) []uint {
return ids
}
func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, packageIDs []uint) string {
func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, packageIDs []uint, paymentMethod string) string {
sorted := make([]uint, len(packageIDs))
copy(sorted, packageIDs)
slices.Sort(sorted)
@@ -806,7 +861,7 @@ func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolve
parts = append(parts, strconv.FormatUint(uint64(id), 10))
}
return fmt.Sprintf("%d:%s:%d:%s", customerID, assetInfo.AssetType, assetInfo.AssetID, strings.Join(parts, ","))
return fmt.Sprintf("%d:%s:%d:%s:%s", customerID, assetInfo.AssetType, assetInfo.AssetID, strings.Join(parts, ","), paymentMethod)
}
func rechargeStatusToClientStatus(status int) int {
@@ -1090,7 +1145,7 @@ func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.Clie
query := s.db.WithContext(skipCtx).
Model(&model.Order{}).
Where("generation = ?", generation)
Where("generation = ? AND buyer_type = ? AND buyer_id = ?", generation, model.BuyerTypePersonal, customerID)
if assetInfo.AssetType == constants.ResourceTypeDevice {
query = query.Where("order_type = ? AND device_id = ?", model.OrderTypeDevice, assetInfo.AssetID)
@@ -1132,19 +1187,29 @@ func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.Clie
}
packageNames := make([]string, 0)
packageIDs := make([]uint, 0)
for _, item := range itemMap[order.ID] {
if item != nil && item.PackageName != "" {
packageNames = append(packageNames, item.PackageName)
if item != nil {
packageIDs = append(packageIDs, item.PackageID)
if item.PackageName != "" {
packageNames = append(packageNames, item.PackageName)
}
}
}
assetType, assetID := clientOrderAssetReference(order)
list = append(list, dto.ClientOrderListItem{
OrderID: order.ID,
OrderNo: order.OrderNo,
AssetType: assetType,
AssetID: assetID,
AssetIdentifier: order.AssetIdentifier,
TotalAmount: order.TotalAmount,
PaymentMethod: order.PaymentMethod,
PaymentStatus: order.PaymentStatus,
PaymentStatusName: constants.GetOrderPaymentStatusName(order.PaymentStatus),
CreatedAt: formatClientServiceTime(order.CreatedAt),
PackageIDs: packageIDs,
PackageNames: packageNames,
})
}
@@ -1161,6 +1226,9 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单详情失败")
}
if order.BuyerType != model.BuyerTypePersonal || order.BuyerID != customerID {
return nil, errors.New(errors.CodeForbidden, "无权查看此订单")
}
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
orderAssetType, orderAssetID, err := getOrderAssetInfo(order)
@@ -1185,10 +1253,14 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
Quantity: item.Quantity,
})
}
assetType, assetID := clientOrderAssetReference(order)
return &dto.ClientOrderDetailResponse{
OrderID: order.ID,
OrderNo: order.OrderNo,
AssetType: assetType,
AssetID: assetID,
AssetIdentifier: order.AssetIdentifier,
TotalAmount: order.TotalAmount,
PaymentStatus: order.PaymentStatus,
PaymentStatusName: constants.GetOrderPaymentStatusName(order.PaymentStatus),
@@ -1200,6 +1272,19 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
}, nil
}
func clientOrderAssetReference(order *model.Order) (string, uint) {
if order == nil {
return "", 0
}
if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
return constants.ResourceTypeDevice, *order.DeviceID
}
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
return "card", *order.IotCardID
}
return "", 0
}
// PayOrder D4 对待支付的 C 端订单发起支付。
// 支持 wallet钱包扣款和 wechat微信 JSAPI两种支付方式。
// 先校验订单归属和状态,再按 payment_method 路由到对应支付逻辑:
@@ -1228,6 +1313,19 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
if order.PaymentStatus != model.PaymentStatusPending {
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
}
paymentMethod := order.PaymentMethod
if paymentMethod == "" {
return nil, errors.New(errors.CodePaymentMethodUnavailable, "历史订单缺少支付方式,请重新下单")
}
if req.PaymentMethod != "" && req.PaymentMethod != paymentMethod {
return nil, errors.New(errors.CodeConflict, "支付方式与订单创建时选择不一致")
}
if s.paymentMethodPolicy == nil {
return nil, errors.New(errors.CodeNoPaymentConfig)
}
if err := s.paymentMethodPolicy.EnsureAllowed(skipCtx, order.OrderType, paymentMethod); err != nil {
return nil, err
}
// before_order 策略:必须先实名才能支付订单
var assetRealnamePolicy string
@@ -1267,12 +1365,12 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
// }
// }
switch req.PaymentMethod {
switch paymentMethod {
case model.PaymentMethodWallet:
if err := s.orderPaymentService.WalletPay(skipCtx, orderID, model.BuyerTypePersonal, customerID); err != nil {
return nil, err
}
return &dto.ClientPayOrderResponse{PaymentMethod: model.PaymentMethodWallet}, nil
return &dto.ClientPayOrderResponse{PaymentMethod: paymentMethod}, nil
case model.PaymentMethodWechat:
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
@@ -1326,7 +1424,7 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
return nil, err
}
return &dto.ClientPayOrderResponse{
PaymentMethod: model.PaymentMethodWechat,
PaymentMethod: paymentMethod,
PayConfig: buildClientPayConfigFromResult(paymentResult),
}, nil
@@ -1356,7 +1454,7 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
)
}
return &dto.ClientPayOrderResponse{
PaymentMethod: model.PaymentMethodAlipay,
PaymentMethod: paymentMethod,
PaymentLink: paymentLink,
}, nil