Files
junhong_cmp_fiber/internal/service/client_order/service.go
huang a97462dd69
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m9s
fix: 支付失败时幂等标记未清理导致用户无法重试 — *created=true移到支付成功后
2026-03-30 13:06:45 +08:00

900 lines
28 KiB
Go

// 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)
}
// 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
wechatConfigService WechatConfigServiceInterface
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,
wechatConfigService WechatConfigServiceInterface,
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,
wechatConfigService: wechatConfigService,
packageSeriesStore: packageSeriesStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
db: db,
redis: redisClient,
logger: logger,
}
}
// CreateOrder 创建客户端订单。
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 服务未配置")
}
skipPermissionCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
assetInfo, err := s.assetService.Resolve(skipPermissionCtx, strings.TrimSpace(req.Identifier))
if err != nil {
return nil, err
}
if err := s.checkAssetOwnership(skipPermissionCtx, customerID, assetInfo.VirtualNo); err != nil {
return nil, err
}
validationResult, err := s.validatePurchase(skipPermissionCtx, 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)
}
activeConfig, appID, err := s.resolveWechatConfig(skipPermissionCtx, req.AppType)
if err != nil {
return nil, err
}
openID, err := s.resolveCustomerOpenID(skipPermissionCtx, customerID, appID)
if err != nil {
return nil, err
}
businessKey := buildClientPurchaseBusinessKey(customerID, assetInfo, req)
redisKey := constants.RedisClientPurchaseIdempotencyKey(businessKey)
lockKey := constants.RedisClientPurchaseLockKey(assetInfo.AssetType, assetInfo.AssetID)
lockAcquired, err := s.redis.SetNX(skipPermissionCtx, 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(skipPermissionCtx, redisKey, "processing", clientPurchaseIdempotencyTTL).Result()
if err != nil {
if lockAcquired {
_ = s.redis.Del(skipPermissionCtx, lockKey).Err()
}
return nil, errors.Wrap(errors.CodeInternalError, err, "设置客户端购买幂等标记失败")
}
if !claimed {
if lockAcquired {
_ = s.redis.Del(skipPermissionCtx, lockKey).Err()
}
return nil, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
}
created := false
defer func() {
if lockAcquired {
_ = s.redis.Del(skipPermissionCtx, lockKey).Err()
}
if !created {
_ = s.redis.Del(skipPermissionCtx, redisKey).Err()
}
}()
paymentProvider, err := s.newPaymentProvider(activeConfig, appID, req.AppType)
if err != nil {
return nil, err
}
forceRecharge := s.checkForceRechargeRequirement(skipPermissionCtx, validationResult)
if forceRecharge.NeedForceRecharge {
return s.createForceRechargeOrder(skipPermissionCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created)
}
return s.createPackageOrder(skipPermissionCtx, customerID, openID, validationResult, activeConfig, redisKey, paymentProvider, &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,
openID string,
validationResult *purchase_validation.PurchaseValidationResult,
activeConfig *model.WechatConfig,
redisKey string,
paymentProvider PaymentProvider,
created *bool,
) (*dto.ClientCreateOrderResponse, error) {
order, err := s.buildPendingOrder(customerID, validationResult, activeConfig)
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)
description := "套餐购买"
if len(items) > 0 && items[0] != nil && items[0].PackageName != "" {
description = items[0].PackageName
}
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
if err != nil {
return nil, err
}
*created = true
return &dto.ClientCreateOrderResponse{
OrderType: "package",
Order: &dto.ClientOrderInfo{
OrderID: order.ID,
OrderNo: order.OrderNo,
TotalAmount: order.TotalAmount,
PaymentStatus: order.PaymentStatus,
CreatedAt: formatClientServiceTime(order.CreatedAt),
},
PayConfig: buildClientPayConfigFromResult(paymentResult),
}, 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, "创建充值记录失败")
}
s.markClientPurchaseCreated(ctx, redisKey, recharge.RechargeNo)
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, recharge.RechargeNo, "余额充值", openID, int(recharge.Amount))
if err != nil {
return nil, err
}
*created = true
return &dto.ClientCreateOrderResponse{
OrderType: "recharge",
Recharge: &dto.ClientRechargeInfo{
RechargeID: recharge.ID,
RechargeNo: recharge.RechargeNo,
Amount: recharge.Amount,
Status: rechargeStatusToClientStatus(recharge.Status),
AutoPurchaseStatus: recharge.AutoPurchaseStatus,
},
PayConfig: buildClientPayConfigFromResult(paymentResult),
LinkedPackageInfo: buildLinkedPackageInfo(validationResult, forceRecharge),
}, nil
}
func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation.PurchaseValidationResult, activeConfig *model.WechatConfig) (*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,
PaymentMethod: model.PaymentMethodWechat,
PaymentStatus: model.PaymentStatusPending,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0,
Source: constants.OrderSourceClient,
Generation: resolveGeneration(result),
ExpiresAt: &expiresAt,
PaymentConfigID: &activeConfig.ID,
}
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
}
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,
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, req *dto.ClientCreateOrderRequest) string {
packageIDs := make([]uint, 0, len(req.PackageIDs))
packageIDs = append(packageIDs, req.PackageIDs...)
slices.Sort(packageIDs)
parts := make([]string, 0, len(packageIDs))
for _, packageID := range packageIDs {
parts = append(parts, strconv.FormatUint(uint64(packageID), 10))
}
return fmt.Sprintf("%d:%s:%d:%s:%s", customerID, assetInfo.AssetType, assetInfo.AssetID, req.AppType, strings.Join(parts, ","))
}
func rechargeStatusToClientStatus(status int) int {
switch status {
case 1:
return 0
case 2, 3:
return 1
default:
return 2
}
}
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,
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,
PaymentMethod: order.PaymentMethod,
CreatedAt: formatClientServiceTime(order.CreatedAt),
PaidAt: formatClientServiceTimePtr(order.PaidAt),
CompletedAt: nil,
Packages: packages,
}, nil
}
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
}