feat: 技术债务清理(支付配置动态化、API文档补全、轮询常量提取、废弃代码清理)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s

This commit is contained in:
2026-04-14 11:11:15 +08:00
parent c0b64c9e30
commit 42c5ec912f
63 changed files with 1979 additions and 775 deletions

View File

@@ -797,6 +797,7 @@ func (s *Service) toAccountResponse(acc *model.Account, shopMap map[uint]string,
ShopID: acc.ShopID,
EnterpriseID: acc.EnterpriseID,
Status: acc.Status,
StatusName: constants.GetStatusName(acc.Status),
Creator: acc.Creator,
Updater: acc.Updater,
CreatedAt: acc.CreatedAt.Format("2006-01-02 15:04:05"),

View File

@@ -140,31 +140,30 @@ func (s *Service) Resolve(ctx context.Context, identifier string) (*dto.AssetRes
// buildDeviceResolveResponse 构建设备类型的资产解析响应
func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.Device) (*dto.AssetResolveResponse, error) {
resp := &dto.AssetResolveResponse{
AssetType: "device",
AssetID: device.ID,
VirtualNo: device.VirtualNo,
Status: device.Status,
BatchNo: device.BatchNo,
ShopID: device.ShopID,
SeriesID: device.SeriesID,
FirstCommissionPaid: device.FirstCommissionPaid,
AccumulatedRecharge: device.AccumulatedRecharge,
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
DeviceName: device.DeviceName,
IMEI: device.IMEI,
SN: device.SN,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
OnlineStatus: device.OnlineStatus,
LastOnlineTime: device.LastOnlineTime,
SoftwareVersion: device.SoftwareVersion,
SwitchMode: device.SwitchMode,
LastGatewaySyncAt: device.LastGatewaySyncAt,
EnablePolling: device.EnablePolling,
AssetType: "device",
AssetID: device.ID,
VirtualNo: device.VirtualNo,
Status: device.Status,
StatusName: constants.GetIotCardStatusName(device.Status),
BatchNo: device.BatchNo,
ShopID: device.ShopID,
SeriesID: device.SeriesID,
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
DeviceName: device.DeviceName,
IMEI: device.IMEI,
SN: device.SN,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
OnlineStatus: device.OnlineStatus,
LastOnlineTime: device.LastOnlineTime,
SoftwareVersion: device.SoftwareVersion,
SwitchMode: device.SwitchMode,
LastGatewaySyncAt: device.LastGatewaySyncAt,
EnablePolling: device.EnablePolling,
}
// 查绑定卡
@@ -235,31 +234,30 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.
// buildCardResolveResponse 构建卡类型的资产解析响应
func (s *Service) buildCardResolveResponse(ctx context.Context, card *model.IotCard) (*dto.AssetResolveResponse, error) {
resp := &dto.AssetResolveResponse{
AssetType: "card",
AssetID: card.ID,
VirtualNo: card.VirtualNo,
Status: card.Status,
BatchNo: card.BatchNo,
ShopID: card.ShopID,
SeriesID: card.SeriesID,
FirstCommissionPaid: card.FirstCommissionPaid,
AccumulatedRecharge: card.AccumulatedRecharge,
ActivatedAt: card.ActivatedAt,
CreatedAt: card.CreatedAt,
UpdatedAt: card.UpdatedAt,
RealNameStatus: card.RealNameStatus,
RealNameAt: card.FirstRealnameAt,
NetworkStatus: card.NetworkStatus,
ICCID: card.ICCID,
CarrierID: card.CarrierID,
CarrierType: card.CarrierType,
CarrierName: card.CarrierName,
MSISDN: card.MSISDN,
IMSI: card.IMSI,
CardCategory: card.CardCategory,
Supplier: card.Supplier,
ActivationStatus: card.ActivationStatus,
EnablePolling: card.EnablePolling,
AssetType: "card",
AssetID: card.ID,
VirtualNo: card.VirtualNo,
Status: card.Status,
StatusName: constants.GetIotCardStatusName(card.Status),
BatchNo: card.BatchNo,
ShopID: card.ShopID,
SeriesID: card.SeriesID,
ActivatedAt: card.ActivatedAt,
CreatedAt: card.CreatedAt,
UpdatedAt: card.UpdatedAt,
RealNameStatus: card.RealNameStatus,
RealNameAt: card.FirstRealnameAt,
NetworkStatus: card.NetworkStatus,
ICCID: card.ICCID,
CarrierID: card.CarrierID,
CarrierType: card.CarrierType,
CarrierName: card.CarrierName,
MSISDN: card.MSISDN,
IMSI: card.IMSI,
CardCategory: card.CardCategory,
Supplier: card.Supplier,
ActivationStatus: card.ActivationStatus,
EnablePolling: card.EnablePolling,
}
// 查绑定设备

View File

@@ -137,6 +137,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
return nil, err
}
// [待确认] 业务是否需要实名认证检查?参见 tech-debt-cleanup 提案
// REALNAME-03: 按卡类型判断实名需求,普通卡需要实名,行业卡不需要
// if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
// return nil, errors.New(errors.CodeNeedRealname)

View File

@@ -5,6 +5,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/logger"
"go.uber.org/zap"
@@ -56,8 +57,9 @@ func (s *Service) ListBindings(ctx context.Context, deviceID uint) (*dto.ListDev
IotCardID: binding.IotCardID,
ICCID: card.ICCID,
MSISDN: card.MSISDN,
CarrierName: card.CarrierName, // 直接使用 IotCard 的冗余字段
CarrierName: card.CarrierName,
Status: card.Status,
StatusName: constants.GetIotCardStatusName(card.Status),
BindTime: binding.BindTime,
IsCurrent: binding.IsCurrent,
}

View File

@@ -558,31 +558,29 @@ func (s *Service) extractDeviceIDs(devices []*model.Device) []uint {
func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, seriesMap map[uint]string, bindingCounts map[uint]int64) *dto.DeviceResponse {
resp := &dto.DeviceResponse{
ID: device.ID,
VirtualNo: device.VirtualNo,
IMEI: device.IMEI,
SN: device.SN,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
BatchNo: device.BatchNo,
ShopID: device.ShopID,
Status: device.Status,
StatusName: s.getDeviceStatusName(device.Status),
BoundCardCount: int(bindingCounts[device.ID]),
SeriesID: device.SeriesID,
FirstCommissionPaid: device.FirstCommissionPaid,
AccumulatedRecharge: device.AccumulatedRecharge,
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
OnlineStatus: device.OnlineStatus,
LastOnlineTime: device.LastOnlineTime,
SoftwareVersion: device.SoftwareVersion,
SwitchMode: device.SwitchMode,
LastGatewaySyncAt: device.LastGatewaySyncAt,
ID: device.ID,
VirtualNo: device.VirtualNo,
IMEI: device.IMEI,
SN: device.SN,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
BatchNo: device.BatchNo,
ShopID: device.ShopID,
Status: device.Status,
StatusName: s.getDeviceStatusName(device.Status),
BoundCardCount: int(bindingCounts[device.ID]),
SeriesID: device.SeriesID,
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
OnlineStatus: device.OnlineStatus,
LastOnlineTime: device.LastOnlineTime,
SoftwareVersion: device.SoftwareVersion,
SwitchMode: device.SwitchMode,
LastGatewaySyncAt: device.LastGatewaySyncAt,
}
if device.ShopID != nil && *device.ShopID > 0 {

View File

@@ -178,8 +178,6 @@ func (s *Service) copyAccumulatedFieldsWithTx(tx *gorm.DB, oldAsset, newAsset *r
return errors.New(errors.CodeAssetNotFound)
}
if err := tx.Model(&model.IotCard{}).Where("id = ?", newAsset.AssetID).Updates(map[string]any{
"accumulated_recharge": oldAsset.Card.AccumulatedRecharge,
"first_commission_paid": oldAsset.Card.FirstCommissionPaid,
"accumulated_recharge_by_series": oldAsset.Card.AccumulatedRechargeBySeriesJSON,
"first_recharge_triggered_by_series": oldAsset.Card.FirstRechargeTriggeredBySeriesJSON,
"updated_at": time.Now(),
@@ -193,8 +191,6 @@ func (s *Service) copyAccumulatedFieldsWithTx(tx *gorm.DB, oldAsset, newAsset *r
return errors.New(errors.CodeAssetNotFound)
}
if err := tx.Model(&model.Device{}).Where("id = ?", newAsset.AssetID).Updates(map[string]any{
"accumulated_recharge": oldAsset.Device.AccumulatedRecharge,
"first_commission_paid": oldAsset.Device.FirstCommissionPaid,
"accumulated_recharge_by_series": oldAsset.Device.AccumulatedRechargeBySeriesJSON,
"first_recharge_triggered_by_series": oldAsset.Device.FirstRechargeTriggeredBySeriesJSON,
"updated_at": time.Now(),

View File

@@ -264,11 +264,15 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
BatchNo: card.BatchNo,
Supplier: card.Supplier,
Status: card.Status,
StatusName: constants.GetIotCardStatusName(card.Status),
ShopID: card.ShopID,
ActivatedAt: card.ActivatedAt,
ActivationStatus: card.ActivationStatus,
ActivationStatusName: constants.GetActivationStatusName(card.ActivationStatus),
RealNameStatus: card.RealNameStatus,
RealNameStatusName: constants.GetRealNameStatusName(card.RealNameStatus),
NetworkStatus: card.NetworkStatus,
NetworkStatusName: constants.GetNetworkStatusName(card.NetworkStatus),
DataUsageMB: card.DataUsageMB,
CurrentMonthUsageMB: card.CurrentMonthUsageMB,
CurrentMonthStartDate: card.CurrentMonthStartDate,
@@ -277,8 +281,6 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
LastRealNameCheckAt: card.LastRealNameCheckAt,
EnablePolling: card.EnablePolling,
SeriesID: card.SeriesID,
FirstCommissionPaid: card.FirstCommissionPaid,
AccumulatedRecharge: card.AccumulatedRecharge,
DeviceVirtualNo: card.DeviceVirtualNo,
CreatedAt: card.CreatedAt,
UpdatedAt: card.UpdatedAt,

View File

@@ -18,6 +18,7 @@ import (
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/payment"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
"github.com/redis/go-redis/v9"
@@ -48,6 +49,7 @@ type Service struct {
packageStore *postgres.PackageStore
wechatConfigService WechatConfigServiceInterface
wechatPayment wechat.PaymentServiceInterface
paymentLoader payment.PaymentConfigLoader
queueClient *queue.Client
logger *zap.Logger
resumeCallback packagepkg.ResumeCallback
@@ -73,6 +75,7 @@ func New(
packageStore *postgres.PackageStore,
wechatConfigService WechatConfigServiceInterface,
wechatPayment wechat.PaymentServiceInterface,
paymentLoader payment.PaymentConfigLoader,
queueClient *queue.Client,
logger *zap.Logger,
assetIdentifierStore *postgres.AssetIdentifierStore,
@@ -96,6 +99,7 @@ func New(
packageStore: packageStore,
wechatConfigService: wechatConfigService,
wechatPayment: wechatPayment,
paymentLoader: paymentLoader,
queueClient: queueClient,
logger: logger,
assetIdentifierStore: assetIdentifierStore,
@@ -109,250 +113,6 @@ func (s *Service) SetResumeCallback(callback packagepkg.ResumeCallback) {
s.resumeCallback = callback
}
// CreateLegacy 创建订单(已废弃)
// Deprecated: 使用 CreateAdminOrder 或 CreateH5Order 替代。保留用于回滚。
func (s *Service) CreateLegacy(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
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")
}
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 {
return nil, err
}
// 下单阶段校验混买限制:禁止同一订单同时包含正式套餐和加油包
if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil {
return nil, err
}
// 幂等性检查:防止同一买家对同一载体短时间内重复下单
carrierType, carrierID := resolveCarrierInfo(req)
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
if err != nil {
return nil, err
}
if existingOrderID > 0 {
return s.Get(ctx, existingOrderID)
}
// 获取到分布式锁后,确保无论成功还是失败都释放
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, "首次购买需满足最低充值要求")
}
userID := middleware.GetUserIDFromContext(ctx)
// 提取资源所属店铺ID
var resourceShopID *uint
var seriesID *uint
if validationResult.Card != nil {
resourceShopID = validationResult.Card.ShopID
seriesID = validationResult.Card.SeriesID
} else if validationResult.Device != nil {
resourceShopID = validationResult.Device.ShopID
seriesID = validationResult.Device.SeriesID
}
// 初始化订单字段
orderBuyerType := buyerType
orderBuyerID := buyerID
totalAmount := validationResult.TotalPrice
paymentMethod := req.PaymentMethod
paymentStatus := model.PaymentStatusPending
var paidAt *time.Time
now := time.Now()
isPurchaseOnBehalf := false
var operatorID *uint
operatorType := ""
var actualPaidAmount *int64
purchaseRole := ""
var sellerShopID *uint = resourceShopID
var sellerCostPrice int64
// 场景判断offline平台代购、wallet代理钱包支付、其他待支付
if req.PaymentMethod == model.PaymentMethodOffline {
// ==== 场景 1平台代购offline====
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerCostPrice
// 设置操作者信息(平台代购)
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// ==== 场景 2代理钱包支付wallet====
if buyerType == "" {
if resourceShopID == nil {
return nil, errors.New(errors.CodeInvalidParam, "不支持的钱包支付场景")
}
buyerType = model.BuyerTypeAgent
buyerID = *resourceShopID
}
// 只有代理账号可以使用钱包支付
if buyerType != model.BuyerTypeAgent {
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
}
operatorShopID := buyerID
// 判断资源是否属于操作者
if resourceShopID == nil {
return nil, errors.New(errors.CodeInternalError, "资源店铺ID为空")
}
// 获取第一个套餐ID用于查询成本价
if len(validationResult.Packages) == 0 {
return nil, errors.New(errors.CodeInternalError, "套餐列表为空")
}
firstPackageID := validationResult.Packages[0].ID
if *resourceShopID == operatorShopID {
// ==== 子场景 2.1:代理自购 ====
costPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = operatorShopID
totalAmount = costPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmountVal := costPrice
actualPaidAmount = &actualPaidAmountVal
purchaseRole = model.PurchaseRoleSelfPurchase
sellerCostPrice = costPrice
} else {
// ==== 子场景 2.2:代理代购(给下级购买)====
// 获取买家成本价
buyerCostPrice, err := s.getCostPrice(ctx, *resourceShopID, firstPackageID)
if err != nil {
return nil, err
}
// 获取操作者成本价
operatorCostPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = *resourceShopID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = true
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmount = &operatorCostPrice
purchaseRole = model.PurchaseRolePurchaseForSubordinate
// 代理代购下级时,佣金差额基准应是操作方(代理自己)的成本价,而非下级的成本价
// 使用 buyerCostPrice 会导致差额为 0上级代理无法获得应有佣金
sellerCostPrice = operatorCostPrice
}
}
order := &model.Order{
BaseModel: model.BaseModel{
Creator: userID,
Updater: userID,
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: orderBuyerType,
BuyerID: orderBuyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: totalAmount,
PaymentMethod: paymentMethod,
PaymentStatus: paymentStatus,
PaidAt: paidAt,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
OperatorID: operatorID,
OperatorType: operatorType,
ActualPaidAmount: actualPaidAmount,
PurchaseRole: purchaseRole,
}
items := s.buildOrderItems(userID, validationResult.Packages)
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
// 根据支付方式选择创建订单的方式
if req.PaymentMethod == model.PaymentMethodOffline {
// 平台代购:创建订单并立即激活套餐
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
if operatorID == nil {
return nil, errors.New(errors.CodeInternalError, "钱包支付场景下 operatorID 不能为空")
}
operatorShopID := *operatorID
buyerShopID := orderBuyerID
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else {
// 其他支付方式:创建待支付订单
if err := s.orderStore.Create(ctx, order, items); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
}
}
// CreateAdminOrder 后台订单创建(仅支持 wallet/offline立即扣款或激活
// 与 CreateH5Order 的核心区别后台订单不创建待支付状态wallet 立即扣款offline 立即激活
// POST /api/admin/orders
@@ -2227,6 +1987,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
PaymentVoucherKey: order.PaymentVoucherKey,
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
CommissionStatus: order.CommissionStatus,
CommissionStatusName: constants.GetOrderCommissionStatusName(order.CommissionStatus),
CommissionConfigVersion: order.CommissionConfigVersion,
OperatorID: order.OperatorID,
@@ -2251,13 +2012,8 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
}
// WechatPayJSAPI 发起微信 JSAPI 支付
// TODO: 从 payment_config_id 加载配置动态创建 Payment 实例,替代 s.wechatPayment 单例
// 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景
func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.WechatPayJSAPIResponse, error) {
if s.wechatPayment == nil {
s.logger.Error("微信支付服务未配置")
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
}
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
@@ -2274,6 +2030,16 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
}
// 按 payment_config_id 动态加载支付配置
if order.PaymentConfigID == nil {
s.logger.Error("订单缺少支付配置", zap.Uint("order_id", orderID))
return nil, errors.New(errors.CodeWechatConfigNotFound, "订单缺少支付配置")
}
paymentSvc, err := s.paymentLoader.LoadConfig(ctx, *order.PaymentConfigID)
if err != nil {
return nil, err
}
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
@@ -2283,7 +2049,7 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
description = items[0].PackageName
}
result, err := s.wechatPayment.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
result, err := paymentSvc.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
if err != nil {
s.logger.Error("创建 JSAPI 支付失败",
zap.Uint("order_id", orderID),
@@ -2307,13 +2073,8 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
}
// WechatPayH5 发起微信 H5 支付
// TODO: 从 payment_config_id 加载配置动态创建 Payment 实例,替代 s.wechatPayment 单例
// 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景
func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.WechatH5SceneInfo, buyerType string, buyerID uint) (*dto.WechatPayH5Response, error) {
if s.wechatPayment == nil {
s.logger.Error("微信支付服务未配置")
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
}
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
@@ -2330,6 +2091,16 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
}
// 按 payment_config_id 动态加载支付配置
if order.PaymentConfigID == nil {
s.logger.Error("订单缺少支付配置", zap.Uint("order_id", orderID))
return nil, errors.New(errors.CodeWechatConfigNotFound, "订单缺少支付配置")
}
paymentSvc, err := s.paymentLoader.LoadConfig(ctx, *order.PaymentConfigID)
if err != nil {
return nil, err
}
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
@@ -2344,7 +2115,7 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
H5Type: sceneInfo.H5Info.Type,
}
result, err := s.wechatPayment.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
result, err := paymentSvc.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
if err != nil {
s.logger.Error("创建 H5 支付失败",
zap.Uint("order_id", orderID),

View File

@@ -598,6 +598,8 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
}
}
resp.StatusName = constants.GetStatusName(resp.Status)
resp.ShelfStatusName = constants.GetShelfStatusName(resp.ShelfStatus)
return resp
}

View File

@@ -264,7 +264,7 @@ func (s *AlertService) triggerAlert(ctx context.Context, rule *model.PollingAler
CurrentValue: currentValue,
AlertMessage: alertMessage,
NotificationChannels: rule.NotificationChannels,
NotificationStatus: "pending",
NotificationStatus: constants.PollingManualTriggerStatusPending,
}
if err := s.historyStore.Create(ctx, history); err != nil {

View File

@@ -86,7 +86,7 @@ func (s *ManualTriggerService) TriggerSingle(ctx context.Context, cardID uint, t
TriggerType: "single",
CardIDs: string(cardIDsJSON),
TotalCount: 1,
Status: "processing",
Status: constants.PollingManualTriggerStatusProcessing,
TriggeredBy: triggeredBy,
TriggeredAt: time.Now(),
}
@@ -110,7 +110,7 @@ func (s *ManualTriggerService) TriggerSingle(ctx context.Context, cardID uint, t
// 更新日志状态
_ = s.logStore.UpdateProgress(ctx, triggerLog.ID, 1, 1, 0)
_ = s.logStore.UpdateStatus(ctx, triggerLog.ID, "completed")
_ = s.logStore.UpdateStatus(ctx, triggerLog.ID, constants.PollingManualTriggerStatusCompleted)
s.logger.Info("单卡手动触发成功",
zap.Uint("card_id", cardID),
@@ -153,7 +153,7 @@ func (s *ManualTriggerService) TriggerBatch(ctx context.Context, cardIDs []uint,
TriggerType: "batch",
CardIDs: string(cardIDsJSON),
TotalCount: len(cardIDs),
Status: "processing",
Status: constants.PollingManualTriggerStatusProcessing,
TriggeredBy: triggeredBy,
TriggeredAt: time.Now(),
}
@@ -209,7 +209,7 @@ func (s *ManualTriggerService) processBatchTrigger(ctx context.Context, logID ui
// 更新最终状态
_ = s.logStore.UpdateProgress(ctx, logID, processedCount, successCount, failedCount)
_ = s.logStore.UpdateStatus(ctx, logID, "completed")
_ = s.logStore.UpdateStatus(ctx, logID, constants.PollingManualTriggerStatusCompleted)
s.logger.Info("批量手动触发完成",
zap.Uint("log_id", logID),
@@ -274,7 +274,7 @@ func (s *ManualTriggerService) TriggerByCondition(ctx context.Context, filter *C
CardIDs: string(cardIDsJSON),
ConditionFilter: string(filterJSON),
TotalCount: len(cardIDs),
Status: "processing",
Status: constants.PollingManualTriggerStatusProcessing,
TriggeredBy: triggeredBy,
TriggeredAt: time.Now(),
}
@@ -344,11 +344,11 @@ func (s *ManualTriggerService) CancelTrigger(ctx context.Context, logID uint, tr
return errors.New(errors.CodeForbidden, "无权限取消该任务")
}
if log.Status != "pending" && log.Status != "processing" {
if log.Status != constants.PollingManualTriggerStatusPending && log.Status != constants.PollingManualTriggerStatusProcessing {
return errors.New(errors.CodeInvalidParam, "任务已完成或已取消")
}
return s.logStore.UpdateStatus(ctx, logID, "cancelled")
return s.logStore.UpdateStatus(ctx, logID, constants.PollingManualTriggerStatusCancelled)
}
// GetRunningTasks 获取正在运行的任务

View File

@@ -12,6 +12,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/task"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/payment"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/hibiken/asynq"
"go.uber.org/zap"
@@ -28,7 +29,6 @@ type ForceRechargeRequirement struct {
CurrentAccumulated int64 `json:"current_accumulated"` // 当前累计充值
Threshold int64 `json:"threshold"` // 佣金触发阈值
Message string `json:"message"` // 提示信息
FirstCommissionPaid bool `json:"first_commission_paid"` // 一次性佣金是否已发放
}
// WechatConfigServiceInterface 支付配置服务接口
@@ -49,6 +49,7 @@ type Service struct {
packageSeriesStore *postgres.PackageSeriesStore
commissionRecordStore *postgres.CommissionRecordStore
wechatConfigService WechatConfigServiceInterface
paymentLoader payment.PaymentConfigLoader
queueClient *queue.Client // 任务队列客户端,用于触发自动购包任务
logger *zap.Logger
}
@@ -65,6 +66,7 @@ func New(
packageSeriesStore *postgres.PackageSeriesStore,
commissionRecordStore *postgres.CommissionRecordStore,
wechatConfigService WechatConfigServiceInterface,
paymentLoader payment.PaymentConfigLoader,
logger *zap.Logger,
queueClient *queue.Client,
) *Service {
@@ -79,6 +81,7 @@ func New(
packageSeriesStore: packageSeriesStore,
commissionRecordStore: commissionRecordStore,
wechatConfigService: wechatConfigService,
paymentLoader: paymentLoader,
queueClient: queueClient,
logger: logger,
}
@@ -88,10 +91,10 @@ func New(
// 验证资源、金额范围、强充要求,生成订单号
func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, userID uint) (*dto.RechargeResponse, error) {
// 1. 验证金额范围
if req.Amount < constants.RechargeMinAmount {
if req.Amount < constants.AssetRechargeMinAmount {
return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能低于1元")
}
if req.Amount > constants.RechargeMaxAmount {
if req.Amount > constants.AssetRechargeMaxAmount {
return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能超过100000元")
}
@@ -268,7 +271,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
// HandlePaymentCallback 支付回调处理
// 支持幂等性检查、事务处理、更新余额、触发佣金
// TODO: 按 payment_config_id 加载配置验签(当前留桩,验签由外层处理)
// 验签由外层 Handler 负责;如需按 payment_config_id 加载配置,可通过 s.paymentLoader 获取
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error {
// 1. 查询充值订单
recharge, err := s.assetRechargeStore.GetByRechargeNo(ctx, rechargeNo)
@@ -414,8 +417,8 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceType string, resourceID uint) (*ForceRechargeRequirement, error) {
result := &ForceRechargeRequirement{
NeedForceRecharge: false,
MinAmount: constants.RechargeMinAmount,
MaxAmount: constants.RechargeMaxAmount,
MinAmount: constants.AssetRechargeMinAmount,
MaxAmount: constants.AssetRechargeMaxAmount,
Message: "无强充要求,可自由充值",
}
@@ -455,7 +458,6 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
}
result.CurrentAccumulated = accumulatedRecharge
result.FirstCommissionPaid = firstCommissionPaid
if seriesID == nil || shopID == nil {
return result, nil

View File

@@ -651,6 +651,7 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
ApprovedRefundAmount: r.ApprovedRefundAmount,
RefundReason: r.RefundReason,
Status: r.Status,
StatusName: constants.GetRefundStatusName(r.Status),
ProcessorID: r.ProcessorID,
RejectReason: r.RejectReason,
Remark: r.Remark,

View File

@@ -189,6 +189,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopRequest) (*dto.
District: shop.District,
Address: shop.Address,
Status: shop.Status,
StatusName: constants.GetStatusName(shop.Status),
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
}, nil
@@ -232,6 +233,7 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopReques
District: shop.District,
Address: shop.Address,
Status: shop.Status,
StatusName: constants.GetStatusName(shop.Status),
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
}, nil
@@ -338,6 +340,7 @@ func (s *Service) ListShopResponses(ctx context.Context, req *dto.ShopListReques
District: shop.District,
Address: shop.Address,
Status: shop.Status,
StatusName: constants.GetStatusName(shop.Status),
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
})

View File

@@ -642,7 +642,7 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, shopID uint, req
AgentWalletID: wallet.ID,
ShopID: shopID,
UserID: currentUserID,
TransactionType: constants.TransactionTypeWithdrawal,
TransactionType: constants.AgentTransactionTypeWithdrawal,
Amount: -req.Amount,
BalanceBefore: wallet.Balance,
BalanceAfter: wallet.Balance - req.Amount,