From 2248558bd3728a29f70971d2c7c50fdbf5c17393 Mon Sep 17 00:00:00 2001 From: huang Date: Mon, 16 Mar 2026 15:43:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=80=82=E9=85=8D=20asset=5Fwallet?= =?UTF-8?q?=20=E6=9B=B4=E5=90=8D=EF=BC=8C=E6=9B=B4=E6=96=B0=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E3=80=81=E5=85=85=E5=80=BC=E5=92=8C=E8=B4=AD=E4=B9=B0?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- internal/service/order/service.go | 90 +++++++++++++----- .../service/purchase_validation/service.go | 53 +++++++++++ internal/service/recharge/service.go | 94 +++++++++---------- 3 files changed, 167 insertions(+), 70 deletions(-) diff --git a/internal/service/order/service.go b/internal/service/order/service.go index 1d45fd6..c1dbf9e 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -31,7 +31,7 @@ type Service struct { orderStore *postgres.OrderStore orderItemStore *postgres.OrderItemStore agentWalletStore *postgres.AgentWalletStore - cardWalletStore *postgres.CardWalletStore + assetWalletStore *postgres.AssetWalletStore purchaseValidationService *purchase_validation.Service shopPackageAllocationStore *postgres.ShopPackageAllocationStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore @@ -51,7 +51,7 @@ func New( orderStore *postgres.OrderStore, orderItemStore *postgres.OrderItemStore, agentWalletStore *postgres.AgentWalletStore, - cardWalletStore *postgres.CardWalletStore, + assetWalletStore *postgres.AssetWalletStore, purchaseValidationService *purchase_validation.Service, shopPackageAllocationStore *postgres.ShopPackageAllocationStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, @@ -70,7 +70,7 @@ func New( orderStore: orderStore, orderItemStore: orderItemStore, agentWalletStore: agentWalletStore, - cardWalletStore: cardWalletStore, + assetWalletStore: assetWalletStore, purchaseValidationService: purchaseValidationService, shopPackageAllocationStore: shopPackageAllocationStore, shopSeriesAllocationStore: shopSeriesAllocationStore, @@ -326,18 +326,35 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde var validationResult *purchase_validation.PurchaseValidationResult var err error - if req.OrderType == model.OrderTypeSingleCard { - if req.IotCardID == nil { - return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID") + if req.PaymentMethod == model.PaymentMethodOffline { + // offline 订单:绕过代理 Allocation 上架检查,仅验证套餐全局状态 + if req.OrderType == model.OrderTypeSingleCard { + if req.IotCardID == nil { + return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID") + } + validationResult, err = s.purchaseValidationService.ValidateAdminOfflineCardPurchase(ctx, *req.IotCardID, req.PackageIDs) + } else if req.OrderType == model.OrderTypeDevice { + if req.DeviceID == nil { + return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID") + } + validationResult, err = s.purchaseValidationService.ValidateAdminOfflineDevicePurchase(ctx, *req.DeviceID, req.PackageIDs) + } else { + return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型") } - validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs) - } else if req.OrderType == model.OrderTypeDevice { - if req.DeviceID == nil { - return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID") - } - validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs) } else { - return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型") + if req.OrderType == model.OrderTypeSingleCard { + if req.IotCardID == nil { + return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID") + } + validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs) + } else if req.OrderType == model.OrderTypeDevice { + if req.DeviceID == nil { + return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID") + } + validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs) + } else { + return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型") + } } if err != nil { @@ -362,9 +379,12 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID) defer s.redis.Del(ctx, lockKey) - forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult) - if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount { - return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求") + // offline 订单不检查强充:平台直接操作,不涉及消费者支付门槛,不产生一次性佣金触发条件 + if req.PaymentMethod != model.PaymentMethodOffline { + forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult) + if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount { + return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求") + } } userID := middleware.GetUserIDFromContext(ctx) @@ -1271,12 +1291,12 @@ func (s *Service) unfreezeWalletForCancel(ctx context.Context, tx *gorm.DB, orde } else { return errors.New(errors.CodeInternalError, "无法确定钱包归属") } - wallet, err := s.cardWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) + wallet, err := s.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) if err != nil { - return errors.Wrap(errors.CodeWalletNotFound, err, "查询卡钱包失败") + return errors.Wrap(errors.CodeWalletNotFound, err, "查询资产钱包失败") } - // 卡钱包解冻:直接减少冻结余额 - result := tx.Model(&model.CardWallet{}). + // 资产钱包解冻:直接减少冻结余额 + result := tx.Model(&model.AssetWallet{}). Where("id = ? AND frozen_balance >= ?", wallet.ID, order.TotalAmount). Updates(map[string]any{ "frozen_balance": gorm.Expr("frozen_balance - ?", order.TotalAmount), @@ -1393,8 +1413,8 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, return s.activatePackage(ctx, tx, order) }) } else { - // 卡钱包系统(iot_card 或 device) - wallet, err := s.cardWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) + // 资产钱包系统(iot_card 或 device) + wallet, err := s.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeWalletNotFound, "钱包不存在") @@ -1437,7 +1457,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, } } - walletResult := tx.Model(&model.CardWallet{}). + // 扣款前记录余额快照,用于写入流水 + balanceBefore := wallet.Balance + + walletResult := tx.Model(&model.AssetWallet{}). Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version). Updates(map[string]any{ "balance": gorm.Expr("balance - ?", order.TotalAmount), @@ -1450,6 +1473,27 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突") } + // 扣款成功后补写扣款流水,填补流水表中扣款记录缺失的问题 + deductTx := &model.AssetWalletTransaction{ + AssetWalletID: wallet.ID, + ResourceType: resourceType, + ResourceID: resourceID, + UserID: buyerID, + TransactionType: "deduct", + Amount: -order.TotalAmount, + BalanceBefore: balanceBefore, + BalanceAfter: balanceBefore - order.TotalAmount, + Status: 1, + ReferenceType: strPtr("order"), + ReferenceNo: &order.OrderNo, + Remark: strPtr("钱包支付套餐"), + ShopIDTag: wallet.ShopIDTag, + EnterpriseIDTag: wallet.EnterpriseIDTag, + } + if err := tx.Create(deductTx).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "创建扣款流水失败") + } + return s.activatePackage(ctx, tx, order) }) } diff --git a/internal/service/purchase_validation/service.go b/internal/service/purchase_validation/service.go index c3231ab..2131ae6 100644 --- a/internal/service/purchase_validation/service.go +++ b/internal/service/purchase_validation/service.go @@ -173,3 +173,56 @@ func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, buye return pkg.SuggestedRetailPrice } +// ValidateAdminOfflineCardPurchase 后台 offline 订单专用卡验证 +// 绕过代理 Allocation 上架检查,仅验证套餐全局状态 +func (s *Service) ValidateAdminOfflineCardPurchase(ctx context.Context, cardID uint, packageIDs []uint) (*PurchaseValidationResult, error) { + card, err := s.iotCardStore.GetByID(ctx, cardID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在") + } + return nil, err + } + + if card.SeriesID == nil || *card.SeriesID == 0 { + return nil, errors.New(errors.CodeInvalidParam, "该卡未关联套餐系列,无法购买套餐") + } + + packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *card.SeriesID, 0) + if err != nil { + return nil, err + } + + return &PurchaseValidationResult{ + Card: card, + Packages: packages, + TotalPrice: totalPrice, + }, nil +} + +// ValidateAdminOfflineDevicePurchase 后台 offline 订单专用设备验证 +// 绕过代理 Allocation 上架检查,仅验证套餐全局状态 +func (s *Service) ValidateAdminOfflineDevicePurchase(ctx context.Context, deviceID uint, packageIDs []uint) (*PurchaseValidationResult, error) { + device, err := s.deviceStore.GetByID(ctx, deviceID) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, errors.New(errors.CodeNotFound, "设备不存在") + } + return nil, err + } + + if device.SeriesID == nil || *device.SeriesID == 0 { + return nil, errors.New(errors.CodeInvalidParam, "该设备未关联套餐系列,无法购买套餐") + } + + packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *device.SeriesID, 0) + if err != nil { + return nil, err + } + + return &PurchaseValidationResult{ + Device: device, + Packages: packages, + TotalPrice: totalPrice, + }, nil +} diff --git a/internal/service/recharge/service.go b/internal/service/recharge/service.go index a546920..c17efd5 100644 --- a/internal/service/recharge/service.go +++ b/internal/service/recharge/service.go @@ -29,26 +29,26 @@ type ForceRechargeRequirement struct { } // Service 充值服务 -// 负责卡钱包(IoT卡/设备)的充值订单创建、预检、支付回调处理等业务逻辑 +// 负责资产钱包(IoT卡/设备)的充值订单创建、预检、支付回调处理等业务逻辑 type Service struct { - db *gorm.DB - cardRechargeStore *postgres.CardRechargeStore - cardWalletStore *postgres.CardWalletStore - cardWalletTransactionStore *postgres.CardWalletTransactionStore - iotCardStore *postgres.IotCardStore - deviceStore *postgres.DeviceStore - shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore - packageSeriesStore *postgres.PackageSeriesStore - commissionRecordStore *postgres.CommissionRecordStore - logger *zap.Logger + db *gorm.DB + assetRechargeStore *postgres.AssetRechargeStore + assetWalletStore *postgres.AssetWalletStore + assetWalletTransactionStore *postgres.AssetWalletTransactionStore + iotCardStore *postgres.IotCardStore + deviceStore *postgres.DeviceStore + shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore + packageSeriesStore *postgres.PackageSeriesStore + commissionRecordStore *postgres.CommissionRecordStore + logger *zap.Logger } // New 创建充值服务实例 func New( db *gorm.DB, - cardRechargeStore *postgres.CardRechargeStore, - cardWalletStore *postgres.CardWalletStore, - cardWalletTransactionStore *postgres.CardWalletTransactionStore, + assetRechargeStore *postgres.AssetRechargeStore, + assetWalletStore *postgres.AssetWalletStore, + assetWalletTransactionStore *postgres.AssetWalletTransactionStore, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, @@ -57,16 +57,16 @@ func New( logger *zap.Logger, ) *Service { return &Service{ - db: db, - cardRechargeStore: cardRechargeStore, - cardWalletStore: cardWalletStore, - cardWalletTransactionStore: cardWalletTransactionStore, - iotCardStore: iotCardStore, - deviceStore: deviceStore, - shopSeriesAllocationStore: shopSeriesAllocationStore, - packageSeriesStore: packageSeriesStore, - commissionRecordStore: commissionRecordStore, - logger: logger, + db: db, + assetRechargeStore: assetRechargeStore, + assetWalletStore: assetWalletStore, + assetWalletTransactionStore: assetWalletTransactionStore, + iotCardStore: iotCardStore, + deviceStore: deviceStore, + shopSeriesAllocationStore: shopSeriesAllocationStore, + packageSeriesStore: packageSeriesStore, + commissionRecordStore: commissionRecordStore, + logger: logger, } } @@ -82,13 +82,13 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, us } // 2. 获取资源(卡或设备)钱包 - var wallet *model.CardWallet + var wallet *model.AssetWallet var err error if req.ResourceType == "iot_card" { - wallet, err = s.cardWalletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID) + wallet, err = s.assetWalletStore.GetByResourceTypeAndID(ctx, "iot_card", req.ResourceID) } else if req.ResourceType == "device" { - wallet, err = s.cardWalletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID) + wallet, err = s.assetWalletStore.GetByResourceTypeAndID(ctx, "device", req.ResourceID) } else { return nil, errors.New(errors.CodeInvalidParam, "无效的资源类型") } @@ -115,9 +115,9 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, us rechargeNo := s.generateRechargeNo() // 5. 创建充值订单 - recharge := &model.CardRechargeRecord{ + recharge := &model.AssetRechargeRecord{ UserID: userID, - CardWalletID: wallet.ID, + AssetWalletID: wallet.ID, ResourceType: req.ResourceType, ResourceID: req.ResourceID, RechargeNo: rechargeNo, @@ -128,7 +128,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, us EnterpriseIDTag: wallet.EnterpriseIDTag, } - if err := s.cardRechargeStore.Create(ctx, recharge); err != nil { + if err := s.assetRechargeStore.Create(ctx, recharge); err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") } @@ -173,7 +173,7 @@ func (s *Service) GetRechargeCheck(ctx context.Context, resourceType string, res // GetByID 根据ID查询充值订单详情 // 支持数据权限过滤 func (s *Service) GetByID(ctx context.Context, id uint, userID uint) (*dto.RechargeResponse, error) { - recharge, err := s.cardRechargeStore.GetByID(ctx, id) + recharge, err := s.assetRechargeStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeRechargeNotFound, "充值订单不存在") @@ -201,10 +201,10 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID pageSize = constants.DefaultPageSize } - params := &postgres.ListCardRechargeParams{ + params := &postgres.ListAssetRechargeParams{ Page: page, PageSize: pageSize, - UserID: &userID, // 数据权限:只能查看自己的 + UserID: &userID, } if req.Status != nil { @@ -212,7 +212,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID } if req.WalletID != nil { walletID := *req.WalletID - params.CardWalletID = &walletID + params.AssetWalletID = &walletID } if req.StartTime != nil { params.StartTime = req.StartTime @@ -221,7 +221,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID params.EndTime = req.EndTime } - recharges, total, err := s.cardRechargeStore.List(ctx, params) + recharges, total, err := s.assetRechargeStore.List(ctx, params) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单列表失败") } @@ -249,7 +249,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID // 支持幂等性检查、事务处理、更新余额、触发佣金 func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error { // 1. 查询充值订单 - recharge, err := s.cardRechargeStore.GetByRechargeNo(ctx, rechargeNo) + recharge, err := s.assetRechargeStore.GetByRechargeNo(ctx, rechargeNo) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeRechargeNotFound, "充值订单不存在") @@ -272,7 +272,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, } // 4. 获取钱包信息 - wallet, err := s.cardWalletStore.GetByID(ctx, recharge.CardWalletID) + wallet, err := s.assetWalletStore.GetByID(ctx, recharge.AssetWalletID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") } @@ -286,7 +286,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, err = s.db.Transaction(func(tx *gorm.DB) error { // 6.1 更新充值订单状态(带状态检查,实现乐观锁) oldStatus := constants.RechargeStatusPending - if err := s.cardRechargeStore.UpdateStatusWithOptimisticLock(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil { + if err := s.assetRechargeStore.UpdateStatusWithOptimisticLock(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil { if err == gorm.ErrRecordNotFound { // 状态已变更,幂等处理 return nil @@ -295,13 +295,13 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, } // 6.2 更新支付信息 - if err := s.cardRechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil { + if err := s.assetRechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败") } // 6.3 增加钱包余额(使用乐观锁) balanceBefore := wallet.Balance - result := tx.Model(&model.CardWallet{}). + result := tx.Model(&model.AssetWallet{}). Where("id = ? AND version = ?", wallet.ID, wallet.Version). Updates(map[string]any{ "balance": gorm.Expr("balance + ?", recharge.Amount), @@ -314,11 +314,11 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, return errors.New(errors.CodeInternalError, "钱包版本冲突,请重试") } - // 6.4 创建钱包交易记录 + // 6.4 创建钱包交易记录(reference_no 存储充值单号,便于前端跳转) remark := "钱包充值" refType := "recharge" - transaction := &model.CardWalletTransaction{ - CardWalletID: wallet.ID, + transaction := &model.AssetWalletTransaction{ + AssetWalletID: wallet.ID, ResourceType: resourceType, ResourceID: resourceID, UserID: recharge.UserID, @@ -328,7 +328,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, BalanceAfter: balanceBefore + recharge.Amount, Status: 1, ReferenceType: &refType, - ReferenceID: &recharge.ID, + ReferenceNo: &recharge.RechargeNo, Remark: &remark, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, @@ -348,7 +348,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, } // 6.7 更新充值订单状态为已完成 - if err := tx.Model(&model.CardRechargeRecord{}). + if err := tx.Model(&model.AssetRechargeRecord{}). Where("id = ?", recharge.ID). Update("status", constants.RechargeStatusCompleted).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单完成状态失败") @@ -729,7 +729,7 @@ func (s *Service) generateRechargeNo() string { } // buildRechargeResponse 构建充值订单响应 -func (s *Service) buildRechargeResponse(recharge *model.CardRechargeRecord) *dto.RechargeResponse { +func (s *Service) buildRechargeResponse(recharge *model.AssetRechargeRecord) *dto.RechargeResponse { statusText := "" switch recharge.Status { case constants.RechargeStatusPending: @@ -748,7 +748,7 @@ func (s *Service) buildRechargeResponse(recharge *model.CardRechargeRecord) *dto ID: recharge.ID, RechargeNo: recharge.RechargeNo, UserID: recharge.UserID, - WalletID: recharge.CardWalletID, + WalletID: recharge.AssetWalletID, Amount: recharge.Amount, PaymentMethod: recharge.PaymentMethod, PaymentChannel: recharge.PaymentChannel,