All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
1123 lines
37 KiB
Go
1123 lines
37 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)
|
||
}
|
||
|
||
// 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
|
||
rechargeOrderStore *postgres.RechargeOrderStore
|
||
paymentStore *postgres.PaymentStore
|
||
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,
|
||
rechargeOrderStore *postgres.RechargeOrderStore,
|
||
paymentStore *postgres.PaymentStore,
|
||
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,
|
||
rechargeOrderStore: rechargeOrderStore,
|
||
paymentStore: paymentStore,
|
||
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
|
||
}
|
||
|
||
// before_order 策略:必须先实名才能购买套餐
|
||
if assetInfo.RealnamePolicy == constants.RealnamePolicyBeforeOrder && assetInfo.RealNameStatus != 1 {
|
||
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()
|
||
}
|
||
existingValue, err := s.redis.Get(skipCtx, redisKey).Result()
|
||
if err == nil && strings.HasPrefix(existingValue, "CRCH") {
|
||
existingOrder, queryErr := s.rechargeOrderStore.GetByRechargeOrderNo(skipCtx, existingValue)
|
||
if queryErr == nil && existingOrder.Status == model.RechargeOrderStatusPending {
|
||
return &dto.ClientCreateOrderResponse{
|
||
OrderType: "recharge",
|
||
Recharge: &dto.ClientRechargeInfo{
|
||
RechargeID: existingOrder.ID,
|
||
RechargeNo: existingOrder.RechargeOrderNo,
|
||
Amount: existingOrder.Amount,
|
||
Status: rechargeStatusToClientStatus(int(existingOrder.Status)),
|
||
StatusName: clientRechargeStatusName(rechargeStatusToClientStatus(int(existingOrder.Status))),
|
||
AutoPurchaseStatus: existingOrder.AutoPurchaseStatus,
|
||
},
|
||
Idempotent: true,
|
||
}, nil
|
||
}
|
||
if queryErr == nil {
|
||
_ = s.redis.Del(skipCtx, redisKey).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, "序列化关联套餐失败")
|
||
}
|
||
|
||
rechargeOrderNo := generateClientRechargeNo()
|
||
var iotCardID *uint
|
||
var deviceID *uint
|
||
if validationResult.Card != nil {
|
||
iotCardID = &validationResult.Card.ID
|
||
}
|
||
if validationResult.Device != nil {
|
||
deviceID = &validationResult.Device.ID
|
||
}
|
||
|
||
rechargeOrder := &model.RechargeOrder{
|
||
RechargeOrderNo: rechargeOrderNo,
|
||
UserID: customerID,
|
||
AssetWalletID: wallet.ID,
|
||
ResourceType: resourceType,
|
||
ResourceID: resourceID,
|
||
IotCardID: iotCardID,
|
||
DeviceID: deviceID,
|
||
Amount: forceRecharge.ForceRechargeAmount,
|
||
Status: model.RechargeOrderStatusPending,
|
||
ShopIDTag: wallet.ShopIDTag,
|
||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||
OperatorType: "personal_customer",
|
||
Generation: resolveGeneration(validationResult),
|
||
LinkedPackageIDs: datatypes.JSON(linkedPackageIDs),
|
||
LinkedOrderType: resolveOrderType(validationResult),
|
||
LinkedCarrierType: assetInfo.AssetType,
|
||
LinkedCarrierID: &resourceID,
|
||
AutoPurchaseStatus: model.AutoPurchaseStatusPending,
|
||
}
|
||
|
||
if err := s.rechargeOrderStore.Create(ctx, rechargeOrder); err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||
}
|
||
|
||
paymentNo := generateClientRechargeNo()
|
||
payment := &model.Payment{
|
||
PaymentNo: paymentNo,
|
||
OrderID: rechargeOrder.ID,
|
||
OrderType: model.PaymentOrderTypeRecharge,
|
||
PaymentMethod: model.PaymentByWechat,
|
||
Amount: forceRecharge.ForceRechargeAmount,
|
||
Status: model.PaymentRecordStatusPending,
|
||
PaymentConfigID: &activeConfig.ID,
|
||
}
|
||
|
||
if err := s.paymentStore.Create(ctx, payment); err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||
}
|
||
|
||
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, rechargeOrderNo, "余额充值", openID, int(rechargeOrder.Amount))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
s.markClientPurchaseCreated(ctx, redisKey, rechargeOrderNo)
|
||
*created = true
|
||
|
||
clientStatus := rechargeStatusToClientStatus(int(rechargeOrder.Status))
|
||
return &dto.ClientCreateOrderResponse{
|
||
OrderType: "recharge",
|
||
Recharge: &dto.ClientRechargeInfo{
|
||
RechargeID: rechargeOrder.ID,
|
||
RechargeNo: rechargeOrderNo,
|
||
Amount: rechargeOrder.Amount,
|
||
Status: clientStatus,
|
||
StatusName: clientRechargeStatusName(clientStatus),
|
||
AutoPurchaseStatus: rechargeOrder.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) buildPersonalCustomerOperatorSnapshot(ctx context.Context, customerID uint, nickname *string) (*uint, string, string) {
|
||
if customerID == 0 {
|
||
return nil, "", ""
|
||
}
|
||
|
||
name := ""
|
||
if nickname != nil {
|
||
name = strings.TrimSpace(*nickname)
|
||
}
|
||
if name == "" {
|
||
customer, err := s.personalCustomerStore.GetByID(ctx, customerID)
|
||
if err != nil {
|
||
s.logger.Warn("buildPersonalCustomerOperatorSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err))
|
||
} else if customer != nil {
|
||
name = strings.TrimSpace(customer.Nickname)
|
||
}
|
||
}
|
||
|
||
id := customerID
|
||
return &id, model.OperatorAccountTypePersonalCustomer, name
|
||
}
|
||
|
||
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)
|
||
order.OperatorAccountID, order.OperatorAccountType, order.OperatorAccountName = s.buildPersonalCustomerOperatorSnapshot(ctx, customerID, order.BuyerNickname)
|
||
|
||
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 {
|
||
// DB 使用 0-based 常量(0=待支付, 1=已支付, 2=已关闭, 3=已退款)
|
||
// 对外透出 1-based(与 spec 约定一致):1=待支付, 2=已支付, 3=已关闭, 4=已退款
|
||
switch status {
|
||
case model.RechargeOrderStatusPending:
|
||
return 1
|
||
case model.RechargeOrderStatusPaid:
|
||
return 2
|
||
case model.RechargeOrderStatusClosed:
|
||
return 3
|
||
default:
|
||
return 4
|
||
}
|
||
}
|
||
|
||
func clientRechargeStatusName(clientStatus int) string {
|
||
switch clientStatus {
|
||
case 1:
|
||
return "待支付"
|
||
case 2:
|
||
return "已支付"
|
||
case 3:
|
||
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, "订单状态不允许支付")
|
||
}
|
||
|
||
// before_order 策略:必须先实名才能支付订单
|
||
var assetRealnamePolicy string
|
||
var assetRealnameStatus int
|
||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||
card, err := s.iotCardStore.GetByID(skipCtx, *order.IotCardID)
|
||
if err == nil && card != nil {
|
||
assetRealnamePolicy = card.RealnamePolicy
|
||
assetRealnameStatus = card.RealNameStatus
|
||
}
|
||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||
device, err := s.deviceStore.GetByID(skipCtx, *order.DeviceID)
|
||
if err == nil && device != nil {
|
||
assetRealnamePolicy = device.RealnamePolicy
|
||
// 设备的 RealNameStatus 需要通过 Resolve 获取(从当前卡派生)
|
||
resolved, resolveErr := s.assetService.Resolve(skipCtx, device.VirtualNo)
|
||
if resolveErr == nil && resolved != nil {
|
||
assetRealnameStatus = resolved.RealNameStatus
|
||
}
|
||
}
|
||
}
|
||
if assetRealnamePolicy == constants.RealnamePolicyBeforeOrder && assetRealnameStatus != 1 {
|
||
return nil, errors.New(errors.CodeNeedRealname)
|
||
}
|
||
|
||
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
|
||
}
|