七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user