// Package client_order 提供 C 端订单下单服务。 package client_order import ( "context" "fmt" "math/rand" "slices" "strconv" "strings" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" asset "github.com/break/junhong_cmp_fiber/internal/service/asset" "github.com/break/junhong_cmp_fiber/internal/service/purchase_validation" "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/bytedance/sonic" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/datatypes" "gorm.io/gorm" ) const ( clientPurchaseIdempotencyTTL = 5 * time.Minute clientPurchaseLockTTL = 10 * time.Second ) // WechatConfigServiceInterface 微信配置服务接口。 type WechatConfigServiceInterface interface { GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) } // OrderWalletPayServiceInterface 订单钱包支付服务接口。 // 用于将钱包扣款、套餐激活、佣金计算等核心逻辑委托给 B 端 order.Service 处理。 type OrderWalletPayServiceInterface interface { WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error } // ForceRechargeRequirement 强充要求。 type ForceRechargeRequirement struct { NeedForceRecharge bool ForceRechargeAmount int64 } // Service 客户端订单服务。 type Service struct { assetService *asset.Service purchaseValidationService *purchase_validation.Service orderStore *postgres.OrderStore rechargeRecordStore *postgres.AssetRechargeStore walletStore *postgres.AssetWalletStore personalDeviceStore *postgres.PersonalCustomerDeviceStore openIDStore *postgres.PersonalCustomerOpenIDStore personalCustomerStore *postgres.PersonalCustomerStore personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore wechatConfigService WechatConfigServiceInterface orderPaymentService OrderWalletPayServiceInterface packageSeriesStore *postgres.PackageSeriesStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore iotCardStore *postgres.IotCardStore deviceStore *postgres.DeviceStore db *gorm.DB redis *redis.Client logger *zap.Logger } // New 创建客户端订单服务。 func New( assetService *asset.Service, purchaseValidationService *purchase_validation.Service, orderStore *postgres.OrderStore, rechargeRecordStore *postgres.AssetRechargeStore, walletStore *postgres.AssetWalletStore, personalDeviceStore *postgres.PersonalCustomerDeviceStore, openIDStore *postgres.PersonalCustomerOpenIDStore, personalCustomerStore *postgres.PersonalCustomerStore, personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore, wechatConfigService WechatConfigServiceInterface, orderPaymentService OrderWalletPayServiceInterface, packageSeriesStore *postgres.PackageSeriesStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, db *gorm.DB, redisClient *redis.Client, logger *zap.Logger, ) *Service { return &Service{ assetService: assetService, purchaseValidationService: purchaseValidationService, orderStore: orderStore, rechargeRecordStore: rechargeRecordStore, walletStore: walletStore, personalDeviceStore: personalDeviceStore, openIDStore: openIDStore, personalCustomerStore: personalCustomerStore, personalCustomerPhoneStore: personalCustomerPhoneStore, wechatConfigService: wechatConfigService, orderPaymentService: orderPaymentService, packageSeriesStore: packageSeriesStore, shopSeriesAllocationStore: shopSeriesAllocationStore, iotCardStore: iotCardStore, deviceStore: deviceStore, db: db, redis: redisClient, logger: logger, } } // CreateOrder 创建客户端订单。 // 普通套餐下单:仅创建待支付订单,不发起支付,需后续调用 POST /orders/:id/pay 支付。 // 强充场景:检测到需要强充时,直接创建充值单并发起微信支付(一步完成),此时 app_type 必传。 func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.ClientCreateOrderRequest) (*dto.ClientCreateOrderResponse, error) { if req == nil { return nil, errors.New(errors.CodeInvalidParam) } if s.redis == nil { return nil, errors.New(errors.CodeInternalError, "Redis 服务未配置") } skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{}) assetInfo, err := s.assetService.Resolve(skipCtx, strings.TrimSpace(req.Identifier)) if err != nil { return nil, err } if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil { return nil, err } validationResult, err := s.validatePurchase(skipCtx, assetInfo, req.PackageIDs) if err != nil { return nil, err } // 暂不做实名认证拦截,由网关侧处理 // REALNAME-03: 普通卡需要实名,行业卡不需要,待后续业务明确后按卡类型分支启用 // if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified { // return nil, errors.New(errors.CodeNeedRealname) // } // 先判断是否需要强充,避免普通下单时不必要地校验微信配置 forceRecharge := s.checkForceRechargeRequirement(skipCtx, validationResult) businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req.PackageIDs) redisKey := constants.RedisClientPurchaseIdempotencyKey(businessKey) lockKey := constants.RedisClientPurchaseLockKey(assetInfo.AssetType, assetInfo.AssetID) lockAcquired, err := s.redis.SetNX(skipCtx, lockKey, time.Now().String(), clientPurchaseLockTTL).Result() if err != nil { s.logger.Warn("获取客户端购买分布式锁失败,继续尝试幂等标记", zap.Error(err), zap.String("lock_key", lockKey), ) } if err == nil && !lockAcquired { return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交") } claimed, err := s.redis.SetNX(skipCtx, redisKey, "processing", clientPurchaseIdempotencyTTL).Result() if err != nil { if lockAcquired { _ = s.redis.Del(skipCtx, lockKey).Err() } return nil, errors.Wrap(errors.CodeInternalError, err, "设置客户端购买幂等标记失败") } if !claimed { if lockAcquired { _ = s.redis.Del(skipCtx, lockKey).Err() } return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交") } created := false defer func() { if lockAcquired { _ = s.redis.Del(skipCtx, lockKey).Err() } if !created { _ = s.redis.Del(skipCtx, redisKey).Err() } }() if forceRecharge.NeedForceRecharge { // 强充场景需要微信支付,此时 app_type 必须传入 activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType) if err != nil { return nil, err } openID, err := s.resolveCustomerOpenID(skipCtx, customerID, appID) if err != nil { return nil, err } paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType) if err != nil { return nil, err } return s.createForceRechargeOrder(skipCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created) } return s.createPackageOrder(skipCtx, customerID, validationResult, redisKey, &created) } func (s *Service) checkAssetOwnership(ctx context.Context, customerID uint, virtualNo string) error { owned, err := s.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, virtualNo) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败") } if owned { return nil } records, err := s.personalDeviceStore.GetByCustomerID(ctx, customerID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败") } for _, record := range records { if record == nil { continue } if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo { return nil } } return errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在") } 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) case constants.ResourceTypeDevice: return s.purchaseValidationService.ValidateDevicePurchase(ctx, assetInfo.AssetID, packageIDs) default: return nil, errors.New(errors.CodeInvalidParam) } } func (s *Service) resolveWechatConfig(ctx context.Context, appType string) (*model.WechatConfig, string, error) { activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx) if err != nil { return nil, "", errors.Wrap(errors.CodeDatabaseError, err, "查询微信配置失败") } if activeConfig == nil { return nil, "", errors.New(errors.CodeWechatPayFailed, "未找到生效的微信支付配置") } switch appType { case "official_account": if activeConfig.OaAppID == "" { return nil, "", errors.New(errors.CodeWechatPayFailed, "公众号支付配置不完整") } return activeConfig, activeConfig.OaAppID, nil case "miniapp": if activeConfig.MiniappAppID == "" { return nil, "", errors.New(errors.CodeWechatPayFailed, "小程序支付配置不完整") } return activeConfig, activeConfig.MiniappAppID, nil default: return nil, "", errors.New(errors.CodeInvalidParam) } } func (s *Service) resolveCustomerOpenID(ctx context.Context, customerID uint, appID string) (string, error) { records, err := s.openIDStore.ListByCustomerID(ctx, customerID) if err != nil { return "", errors.Wrap(errors.CodeDatabaseError, err, "查询微信授权信息失败") } for _, record := range records { if record == nil { continue } if record.AppID == appID && strings.TrimSpace(record.OpenID) != "" { return record.OpenID, nil } } return "", errors.New(errors.CodeNotFound, "未找到当前应用的微信授权信息") } func (s *Service) createPackageOrder( ctx context.Context, customerID uint, validationResult *purchase_validation.PurchaseValidationResult, redisKey string, created *bool, ) (*dto.ClientCreateOrderResponse, error) { // 查询卖家成本价,用于差价佣金链式计算的起点 // 平台直销(sellerShopID == 0)时成本价为 0,CalculateCostDiffCommission 会直接跳过 var sellerCostPrice int64 sellerShopID := resolveSellerShopID(validationResult) if sellerShopID > 0 && len(validationResult.Packages) > 0 { costPrice, err := s.purchaseValidationService.GetCostPrice(ctx, validationResult.Packages[0], sellerShopID) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询卖家成本价失败") } sellerCostPrice = costPrice } order, err := s.buildPendingOrder(ctx, customerID, validationResult, sellerCostPrice) if err != nil { return nil, err } items, err := s.buildOrderItems(ctx, customerID, validationResult) if err != nil { return nil, err } if err := s.orderStore.Create(ctx, order, items); err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败") } s.markClientPurchaseCreated(ctx, redisKey, order.OrderNo) *created = true return &dto.ClientCreateOrderResponse{ OrderType: "package", Order: &dto.ClientOrderInfo{ OrderID: order.ID, OrderNo: order.OrderNo, TotalAmount: order.TotalAmount, PaymentStatus: order.PaymentStatus, PaymentStatusName: constants.GetOrderPaymentStatusName(order.PaymentStatus), CreatedAt: formatClientServiceTime(order.CreatedAt), }, }, nil } func (s *Service) createForceRechargeOrder( ctx context.Context, customerID uint, openID string, assetInfo *dto.AssetResolveResponse, validationResult *purchase_validation.PurchaseValidationResult, activeConfig *model.WechatConfig, forceRecharge *ForceRechargeRequirement, redisKey string, paymentProvider PaymentProvider, created *bool, ) (*dto.ClientCreateOrderResponse, error) { resourceType, resourceID, err := resolveWalletResource(validationResult) if err != nil { return nil, err } wallet, err := s.getOrCreateWallet(ctx, resourceType, resourceID, resolveSellerShopID(validationResult)) if err != nil { return nil, err } linkedPackageIDs, err := sonic.Marshal(extractPackageIDs(validationResult.Packages)) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "序列化关联套餐失败") } carrierID := resourceID recharge := &model.AssetRechargeRecord{ UserID: customerID, AssetWalletID: wallet.ID, ResourceType: resourceType, ResourceID: resourceID, RechargeNo: generateClientRechargeNo(), Amount: forceRecharge.ForceRechargeAmount, PaymentMethod: model.PaymentMethodWechat, PaymentConfigID: &activeConfig.ID, Status: 1, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, OperatorType: "personal_customer", Generation: resolveGeneration(validationResult), LinkedPackageIDs: datatypes.JSON(linkedPackageIDs), LinkedOrderType: resolveOrderType(validationResult), LinkedCarrierType: assetInfo.AssetType, LinkedCarrierID: &carrierID, AutoPurchaseStatus: constants.AutoPurchaseStatusPending, } if err := s.rechargeRecordStore.Create(ctx, recharge); err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值记录失败") } paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, recharge.RechargeNo, "余额充值", openID, int(recharge.Amount)) if err != nil { return nil, err } // 微信支付创建成功后才标记幂等 key,确保支付失败时 defer 能正确清除 key s.markClientPurchaseCreated(ctx, redisKey, recharge.RechargeNo) *created = true clientStatus := rechargeStatusToClientStatus(recharge.Status) return &dto.ClientCreateOrderResponse{ OrderType: "recharge", Recharge: &dto.ClientRechargeInfo{ RechargeID: recharge.ID, RechargeNo: recharge.RechargeNo, Amount: recharge.Amount, Status: clientStatus, StatusName: clientRechargeStatusName(clientStatus), AutoPurchaseStatus: recharge.AutoPurchaseStatus, }, PayConfig: buildClientPayConfigFromResult(paymentResult), LinkedPackageInfo: buildLinkedPackageInfo(validationResult, forceRecharge), }, nil } func (s *Service) fetchBuyerSnapshot(ctx context.Context, customerID uint) (phone, nickname *string) { if customerID == 0 { return nil, nil } customer, err := s.personalCustomerStore.GetByID(ctx, customerID) if err != nil { s.logger.Warn("fetchBuyerSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err)) } else if customer != nil && customer.Nickname != "" { nickname = &customer.Nickname } phoneRecord, err := s.personalCustomerPhoneStore.GetPrimaryPhone(ctx, customerID) if err != nil { s.logger.Warn("fetchBuyerSnapshot: 查询买家手机号失败", zap.Uint("customer_id", customerID), zap.Error(err)) } else if phoneRecord != nil && phoneRecord.Phone != "" { phone = &phoneRecord.Phone } return phone, nickname } func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result *purchase_validation.PurchaseValidationResult, sellerCostPrice int64) (*model.Order, error) { orderType := resolveOrderType(result) if orderType == "" { return nil, errors.New(errors.CodeInvalidParam) } now := time.Now() expiresAt := now.Add(constants.OrderExpireTimeout) order := &model.Order{ BaseModel: model.BaseModel{ Creator: customerID, Updater: customerID, }, OrderNo: s.orderStore.GenerateOrderNo(), OrderType: orderType, BuyerType: model.BuyerTypePersonal, BuyerID: customerID, TotalAmount: result.TotalPrice, PaymentStatus: model.PaymentStatusPending, CommissionStatus: model.CommissionStatusPending, CommissionConfigVersion: 0, Source: constants.OrderSourceClient, Generation: resolveGeneration(result), ExpiresAt: &expiresAt, SellerCostPrice: sellerCostPrice, } if result.Card != nil { order.IotCardID = &result.Card.ID order.SeriesID = result.Card.SeriesID order.SellerShopID = result.Card.ShopID } else if result.Device != nil { order.DeviceID = &result.Device.ID order.SeriesID = result.Device.SeriesID order.SellerShopID = result.Device.ShopID } order.BuyerPhone, order.BuyerNickname = s.fetchBuyerSnapshot(ctx, customerID) return order, nil } 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)) for _, pkg := range result.Packages { if pkg == nil { continue } unitPrice, err := s.purchaseValidationService.GetPurchasePrice(ctx, pkg, sellerShopID) if err != nil { return nil, err } items = append(items, &model.OrderItem{ BaseModel: model.BaseModel{ Creator: customerID, Updater: customerID, }, PackageID: pkg.ID, PackageName: pkg.PackageName, PackageType: &pkg.PackageType, Quantity: 1, UnitPrice: unitPrice, Amount: unitPrice, }) } return items, nil } func (s *Service) checkForceRechargeRequirement(ctx context.Context, result *purchase_validation.PurchaseValidationResult) *ForceRechargeRequirement { defaultResult := &ForceRechargeRequirement{NeedForceRecharge: false} var seriesID *uint var sellerShopID uint if result.Card != nil { seriesID = result.Card.SeriesID if result.Card.ShopID != nil { sellerShopID = *result.Card.ShopID } } else if result.Device != nil { seriesID = result.Device.SeriesID if result.Device.ShopID != nil { sellerShopID = *result.Device.ShopID } } if seriesID == nil || *seriesID == 0 { return defaultResult } series, err := s.packageSeriesStore.GetByID(ctx, *seriesID) if err != nil { s.logger.Warn("查询套餐系列失败", zap.Uint("series_id", *seriesID), zap.Error(err)) return defaultResult } config, err := series.GetOneTimeCommissionConfig() if err != nil || config == nil || !config.Enable { return defaultResult } if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge { return &ForceRechargeRequirement{ NeedForceRecharge: true, ForceRechargeAmount: config.Threshold, } } if config.EnableForceRecharge { amount := config.ForceAmount if amount == 0 { amount = config.Threshold } return &ForceRechargeRequirement{ NeedForceRecharge: true, ForceRechargeAmount: amount, } } if sellerShopID > 0 { allocation, allocErr := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, sellerShopID, *seriesID) if allocErr == nil && allocation.EnableForceRecharge { amount := allocation.ForceRechargeAmount if amount == 0 { amount = config.Threshold } return &ForceRechargeRequirement{ NeedForceRecharge: true, ForceRechargeAmount: amount, } } } return defaultResult } func (s *Service) markClientPurchaseCreated(ctx context.Context, redisKey string, value string) { if err := s.redis.Set(ctx, redisKey, value, clientPurchaseIdempotencyTTL).Err(); err != nil { s.logger.Warn("设置客户端购买幂等标记失败", zap.String("redis_key", redisKey), zap.Error(err), ) } } func buildLinkedPackageInfo(result *purchase_validation.PurchaseValidationResult, forceRecharge *ForceRechargeRequirement) *dto.LinkedPackageInfo { packageNames := make([]string, 0, len(result.Packages)) for _, pkg := range result.Packages { if pkg == nil || pkg.PackageName == "" { continue } packageNames = append(packageNames, pkg.PackageName) } return &dto.LinkedPackageInfo{ PackageNames: packageNames, TotalPackageAmount: result.TotalPrice, ForceRechargeAmount: forceRecharge.ForceRechargeAmount, WalletCredit: forceRecharge.ForceRechargeAmount, } } // getOrCreateWallet 获取或自动创建资产钱包 // 兜底机制:当钱包不存在时自动创建,避免因钱包未提前创建导致业务中断 func (s *Service) getOrCreateWallet(ctx context.Context, resourceType string, resourceID uint, shopIDTag uint) (*model.AssetWallet, error) { wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) if err == nil { return wallet, nil } if err != gorm.ErrRecordNotFound { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败") } // 钱包不存在,自动创建空钱包 s.logger.Info("资产钱包不存在,自动创建", zap.String("resource_type", resourceType), zap.Uint("resource_id", resourceID), zap.Uint("shop_id_tag", shopIDTag), ) newWallet := &model.AssetWallet{ ResourceType: resourceType, ResourceID: resourceID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopIDTag, } if createErr := s.walletStore.Create(ctx, newWallet); createErr != nil { return nil, errors.Wrap(errors.CodeDatabaseError, createErr, "自动创建资产钱包失败") } // 重新查询获取完整数据(含数据库生成的字段) wallet, err = s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败") } return wallet, nil } func resolveWalletResource(result *purchase_validation.PurchaseValidationResult) (string, uint, error) { if result.Card != nil { return constants.AssetWalletResourceTypeIotCard, result.Card.ID, nil } if result.Device != nil { return constants.AssetWalletResourceTypeDevice, result.Device.ID, nil } return "", 0, errors.New(errors.CodeInvalidParam) } func resolveOrderType(result *purchase_validation.PurchaseValidationResult) string { if result.Card != nil { return model.OrderTypeSingleCard } if result.Device != nil { return model.OrderTypeDevice } return "" } func resolveGeneration(result *purchase_validation.PurchaseValidationResult) int { if result.Card != nil && result.Card.Generation > 0 { return result.Card.Generation } if result.Device != nil && result.Device.Generation > 0 { return result.Device.Generation } return 1 } func resolveSellerShopID(result *purchase_validation.PurchaseValidationResult) uint { if result.Card != nil && result.Card.ShopID != nil { return *result.Card.ShopID } if result.Device != nil && result.Device.ShopID != nil { return *result.Device.ShopID } return 0 } func extractPackageIDs(packages []*model.Package) []uint { ids := make([]uint, 0, len(packages)) for _, pkg := range packages { if pkg == nil { continue } ids = append(ids, pkg.ID) } return ids } func buildClientPurchaseBusinessKey(customerID uint, assetInfo *dto.AssetResolveResponse, packageIDs []uint) string { sorted := make([]uint, len(packageIDs)) copy(sorted, packageIDs) slices.Sort(sorted) parts := make([]string, 0, len(sorted)) for _, id := range sorted { parts = append(parts, strconv.FormatUint(uint64(id), 10)) } return fmt.Sprintf("%d:%s:%d:%s", customerID, assetInfo.AssetType, assetInfo.AssetID, strings.Join(parts, ",")) } func rechargeStatusToClientStatus(status int) int { switch status { case 1: return 0 case 2, 3: return 1 default: return 2 } } func clientRechargeStatusName(clientStatus int) string { switch clientStatus { case 0: return "待支付" case 1: return "已支付" default: return "已关闭" } } func formatClientServiceTime(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) } func generateClientRechargeNo() string { return fmt.Sprintf("CRCH%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000)) } // ListOrders 查询 C 端订单列表。 func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.ClientOrderListRequest) ([]dto.ClientOrderListItem, int64, error) { skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{}) assetInfo, err := s.assetService.Resolve(skipCtx, strings.TrimSpace(req.Identifier)) if err != nil { return nil, 0, err } if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil { return nil, 0, err } generation, err := s.getAssetGeneration(skipCtx, assetInfo.AssetType, assetInfo.AssetID) if err != nil { return nil, 0, err } query := s.db.WithContext(skipCtx). Model(&model.Order{}). Where("generation = ?", generation) if assetInfo.AssetType == constants.ResourceTypeDevice { query = query.Where("order_type = ? AND device_id = ?", model.OrderTypeDevice, assetInfo.AssetID) } else { query = query.Where("order_type = ? AND iot_card_id = ?", model.OrderTypeSingleCard, assetInfo.AssetID) } if req.PaymentStatus != nil { query = query.Where("payment_status = ?", *req.PaymentStatus) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询订单总数失败") } var orders []*model.Order offset := (req.Page - 1) * req.PageSize if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&orders).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询订单列表失败") } orderIDs := make([]uint, 0, len(orders)) for _, order := range orders { if order != nil { orderIDs = append(orderIDs, order.ID) } } itemMap, err := s.loadOrderItemMap(skipCtx, orderIDs) if err != nil { return nil, 0, err } list := make([]dto.ClientOrderListItem, 0, len(orders)) for _, order := range orders { if order == nil { continue } packageNames := make([]string, 0) for _, item := range itemMap[order.ID] { if item != nil && item.PackageName != "" { packageNames = append(packageNames, item.PackageName) } } list = append(list, dto.ClientOrderListItem{ OrderID: order.ID, OrderNo: order.OrderNo, TotalAmount: order.TotalAmount, PaymentStatus: order.PaymentStatus, PaymentStatusName: constants.GetOrderPaymentStatusName(order.PaymentStatus), CreatedAt: formatClientServiceTime(order.CreatedAt), PackageNames: packageNames, }) } return list, total, nil } // GetOrderDetail 查询 C 端订单详情。 func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID uint) (*dto.ClientOrderDetailResponse, error) { order, items, err := s.orderStore.GetByIDWithItems(ctx, orderID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "订单不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单详情失败") } skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{}) virtualNo, err := s.getOrderVirtualNo(skipCtx, order) if err != nil { return nil, err } if err := s.checkAssetOwnership(skipCtx, customerID, virtualNo); err != nil { return nil, err } packages := make([]dto.ClientOrderPackageItem, 0, len(items)) for _, item := range items { if item == nil { continue } packages = append(packages, dto.ClientOrderPackageItem{ PackageID: item.PackageID, PackageName: item.PackageName, Price: item.UnitPrice, Quantity: item.Quantity, }) } return &dto.ClientOrderDetailResponse{ OrderID: order.ID, OrderNo: order.OrderNo, TotalAmount: order.TotalAmount, PaymentStatus: order.PaymentStatus, PaymentStatusName: constants.GetOrderPaymentStatusName(order.PaymentStatus), PaymentMethod: order.PaymentMethod, CreatedAt: formatClientServiceTime(order.CreatedAt), PaidAt: formatClientServiceTimePtr(order.PaidAt), CompletedAt: nil, Packages: packages, }, nil } // PayOrder D4 对待支付的 C 端订单发起支付。 // 支持 wallet(钱包扣款)和 wechat(微信 JSAPI)两种支付方式。 // 先校验订单归属和状态,再按 payment_method 路由到对应支付逻辑: // - wallet:委托 B 端 WalletPay(乐观锁扣款 + 激活套餐 + 佣金入队) // - wechat:查微信配置 + OpenID → 预下单 → 更新 payment_config_id → 返回 JS 支付参数 func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, req *dto.ClientPayOrderRequest) (*dto.ClientPayOrderResponse, error) { if req == nil { return nil, errors.New(errors.CodeInvalidParam) } // 跳过 GORM 店铺过滤,C 端订单查询不依赖 seller_shop_id 过滤 skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{}) order, err := s.orderStore.GetByID(skipCtx, orderID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "订单不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败") } if order.BuyerType != model.BuyerTypePersonal || order.BuyerID != customerID { return nil, errors.New(errors.CodeForbidden, "无权操作此订单") } if order.PaymentStatus != model.PaymentStatusPending { return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付") } switch req.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 case model.PaymentMethodWechat: activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType) if err != nil { return nil, err } openID, err := s.resolveCustomerOpenID(skipCtx, customerID, appID) if err != nil { return nil, err } paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType) if err != nil { return nil, err } // 记录本次支付使用的微信配置,供支付回调验签使用 if err := s.db.WithContext(skipCtx).Model(&model.Order{}). Where("id = ?", orderID). Update("payment_config_id", activeConfig.ID).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新订单支付配置失败") } paymentResult, err := paymentProvider.CreateJSAPIPayment(skipCtx, order.OrderNo, "套餐购买", openID, int(order.TotalAmount)) if err != nil { return nil, err } return &dto.ClientPayOrderResponse{ PaymentMethod: model.PaymentMethodWechat, PayConfig: buildClientPayConfigFromResult(paymentResult), }, nil default: return nil, errors.New(errors.CodeInvalidParam, "不支持的支付方式") } } func (s *Service) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) { switch assetType { case "card": card, err := s.iotCardStore.GetByID(ctx, assetID) if err != nil { if err == gorm.ErrRecordNotFound { return 0, errors.New(errors.CodeAssetNotFound) } return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败") } return card.Generation, nil case constants.ResourceTypeDevice: device, err := s.deviceStore.GetByID(ctx, assetID) if err != nil { if err == gorm.ErrRecordNotFound { return 0, errors.New(errors.CodeAssetNotFound) } return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败") } return device.Generation, nil default: return 0, errors.New(errors.CodeInvalidParam) } } func (s *Service) loadOrderItemMap(ctx context.Context, orderIDs []uint) (map[uint][]*model.OrderItem, error) { result := make(map[uint][]*model.OrderItem) if len(orderIDs) == 0 { return result, nil } var items []*model.OrderItem if err := s.db.WithContext(ctx).Where("order_id IN ?", orderIDs).Order("id ASC").Find(&items).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单明细失败") } for _, item := range items { if item != nil { result[item.OrderID] = append(result[item.OrderID], item) } } return result, nil } func (s *Service) getOrderVirtualNo(ctx context.Context, order *model.Order) (string, error) { if order == nil { return "", errors.New(errors.CodeNotFound, "订单不存在") } switch order.OrderType { case model.OrderTypeSingleCard: if order.IotCardID == nil || *order.IotCardID == 0 { return "", errors.New(errors.CodeInvalidParam) } card, err := s.iotCardStore.GetByID(ctx, *order.IotCardID) if err != nil { if err == gorm.ErrRecordNotFound { return "", errors.New(errors.CodeAssetNotFound) } return "", errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败") } return card.VirtualNo, nil case model.OrderTypeDevice: if order.DeviceID == nil || *order.DeviceID == 0 { return "", errors.New(errors.CodeInvalidParam) } device, err := s.deviceStore.GetByID(ctx, *order.DeviceID) if err != nil { if err == gorm.ErrRecordNotFound { return "", errors.New(errors.CodeAssetNotFound) } return "", errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败") } return device.VirtualNo, nil default: return "", errors.New(errors.CodeInvalidParam) } } func formatClientServiceTimePtr(t *time.Time) *string { if t == nil || t.IsZero() { return nil } formatted := formatClientServiceTime(*t) return &formatted }