feat: 业务逻辑补全 — 佣金待审记录、C端订单重构、支付抽象、富友支付、卡设备状态联动
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
F-1: 佣金链断裂时创建 status=99 零额待审记录,新增修正接口 POST /commission-records/:id/resolve F-4: C端订单查询从 Handler 迁移至 Service 层,移除 SkipPermissionCtx J-1: 富友支付 JSAPI/MiniApp 预下单实现,回调补全签名验证 J-2: 平台钱包代购支持(buyerType 为空时使用资产所属代理钱包) J-3: 套餐激活后自动更新卡/设备 status=3,最后套餐过期后更新 status=4 支付抽象: 引入 PaymentProvider 接口 + 微信/富友适配器,CreateOrder 支持多支付渠道 修复: 设备/IoT卡响应 DTO 移除 omitempty,空值字段返回 null 而非省略
This commit is contained in:
@@ -311,28 +311,8 @@ func (s *Service) GetRealtimeStatus(ctx context.Context, assetType string, id ui
|
||||
resp.DeviceProtectStatus = s.getDeviceProtectStatus(ctx, id)
|
||||
|
||||
// 实时查询 Gateway 设备状态(不缓存,per D-05)
|
||||
// Gateway 失败不阻断主流程,DeviceRealtime 返回 null(per D-06)
|
||||
if s.gatewayClient != nil {
|
||||
device, devErr := s.deviceStore.GetByID(ctx, id)
|
||||
if devErr == nil {
|
||||
// IMEI 优先,无则用 SN(与 Refresh 中 updateDeviceFromSyncInfo 保持一致)
|
||||
cardNo := device.IMEI
|
||||
if cardNo == "" {
|
||||
cardNo = device.SN
|
||||
}
|
||||
if cardNo != "" {
|
||||
if syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{
|
||||
CardNo: cardNo,
|
||||
}); syncErr == nil {
|
||||
resp.DeviceRealtime = mapSyncRespToDeviceGatewayInfo(syncResp)
|
||||
} else {
|
||||
logger.GetAppLogger().Warn("GetRealtimeStatus: sync-info 调用失败,DeviceRealtime 返回 null",
|
||||
zap.Uint("device_id", id),
|
||||
zap.Error(syncErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Gateway 失败不阻断主流程,DeviceRealtime 始终非 nil,失败时仅填 GatewayMsg(per D-06)
|
||||
resp.DeviceRealtime = s.fetchDeviceGatewayInfo(ctx, id)
|
||||
|
||||
default:
|
||||
return nil, errors.New(errors.CodeInvalidParam, "不支持的资产类型,仅支持 card 或 device")
|
||||
@@ -341,6 +321,44 @@ func (s *Service) GetRealtimeStatus(ctx context.Context, assetType string, id ui
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// fetchDeviceGatewayInfo 查询设备 Gateway 实时状态,始终返回非 nil 对象
|
||||
// Gateway 成功时填充全量字段,失败时仅填充 GatewayMsg 供前端展示错误原因
|
||||
func (s *Service) fetchDeviceGatewayInfo(ctx context.Context, deviceID uint) *dto.DeviceGatewayInfo {
|
||||
if s.gatewayClient == nil {
|
||||
msg := "Gateway 客户端未配置"
|
||||
return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
|
||||
}
|
||||
|
||||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
msg := "查询设备信息失败: " + err.Error()
|
||||
return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
|
||||
}
|
||||
|
||||
// IMEI 优先,无则用 SN(与 Refresh 中 updateDeviceFromSyncInfo 保持一致)
|
||||
cardNo := device.IMEI
|
||||
if cardNo == "" {
|
||||
cardNo = device.SN
|
||||
}
|
||||
if cardNo == "" {
|
||||
msg := "设备缺少 IMEI 和 SN,无法查询 Gateway 实时状态"
|
||||
return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
|
||||
}
|
||||
|
||||
syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{
|
||||
CardNo: cardNo,
|
||||
})
|
||||
if syncErr != nil {
|
||||
msg := syncErr.Error()
|
||||
logger.GetAppLogger().Warn("fetchDeviceGatewayInfo: sync-info 调用失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Error(syncErr))
|
||||
return &dto.DeviceGatewayInfo{GatewayMsg: &msg}
|
||||
}
|
||||
|
||||
return mapSyncRespToDeviceGatewayInfo(syncResp)
|
||||
}
|
||||
|
||||
// mapSyncRespToDeviceGatewayInfo 将 Gateway SyncDeviceInfoResp 映射为 B 端 DeviceGatewayInfo DTO
|
||||
// 使用指针字段,未赋值的字段保持 nil(omitempty)
|
||||
func mapSyncRespToDeviceGatewayInfo(r *gateway.SyncDeviceInfoResp) *dto.DeviceGatewayInfo {
|
||||
|
||||
146
internal/service/client_order/payment_provider.go
Normal file
146
internal/service/client_order/payment_provider.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package client_order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
)
|
||||
|
||||
// PaymentProvider C 端支付提供者接口。
|
||||
// 抽象不同支付渠道(微信、富友等)的预下单能力。
|
||||
type PaymentProvider interface {
|
||||
// CreateJSAPIPayment 创建 JSAPI 预下单,返回前端调起支付的参数。
|
||||
CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error)
|
||||
}
|
||||
|
||||
// PaymentResult 预下单结果,前端用这些参数调起支付。
|
||||
type PaymentResult struct {
|
||||
AppID string
|
||||
TimeStamp string
|
||||
NonceStr string
|
||||
Package string
|
||||
SignType string
|
||||
PaySign string
|
||||
}
|
||||
|
||||
// wechatPaymentProvider 微信支付适配器。
|
||||
type wechatPaymentProvider struct {
|
||||
paymentService *wechat.PaymentService
|
||||
}
|
||||
|
||||
// CreateJSAPIPayment 创建微信 JSAPI 预下单。
|
||||
func (w *wechatPaymentProvider) CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error) {
|
||||
result, err := w.paymentService.CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractPaymentResult(result.PayConfig), nil
|
||||
}
|
||||
|
||||
// fuiouPaymentProvider 富友支付适配器。
|
||||
type fuiouPaymentProvider struct {
|
||||
client *fuiou.Client
|
||||
appID string
|
||||
}
|
||||
|
||||
// CreateJSAPIPayment 创建富友 JSAPI 预下单。
|
||||
func (f *fuiouPaymentProvider) CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error) {
|
||||
amountStr := strconv.Itoa(amount)
|
||||
resp, err := f.client.WxPreCreate(orderNo, amountStr, description, "", "JSAPI", f.appID, openID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败")
|
||||
}
|
||||
|
||||
return &PaymentResult{
|
||||
AppID: resp.SdkAppid,
|
||||
TimeStamp: resp.SdkTimestamp,
|
||||
NonceStr: resp.SdkNoncestr,
|
||||
Package: resp.SdkPackage,
|
||||
SignType: resp.SdkSigntype,
|
||||
PaySign: resp.SdkPaysign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newPaymentProvider 根据支付配置创建支付提供者。
|
||||
func (s *Service) newPaymentProvider(config *model.WechatConfig, appID string) (PaymentProvider, error) {
|
||||
switch config.ProviderType {
|
||||
case model.ProviderTypeFuiou:
|
||||
client, err := fuiou.NewClient(
|
||||
config.FyInsCd,
|
||||
config.FyMchntCd,
|
||||
config.FyTermID,
|
||||
config.FyAPIURL,
|
||||
config.FyNotifyURL,
|
||||
config.FyPrivateKey,
|
||||
config.FyPublicKey,
|
||||
s.logger,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "创建富友客户端失败")
|
||||
}
|
||||
return &fuiouPaymentProvider{client: client, appID: appID}, nil
|
||||
default:
|
||||
cache := wechat.NewRedisCache(s.redis)
|
||||
paymentApp, err := wechat.NewPaymentAppFromConfig(config, appID, cache, s.logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "创建微信支付应用失败")
|
||||
}
|
||||
return &wechatPaymentProvider{paymentService: wechat.NewPaymentService(paymentApp, s.logger)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// buildClientPayConfigFromResult 将预下单结果转换为客户端支付参数。
|
||||
func buildClientPayConfigFromResult(result *PaymentResult) *dto.ClientPayConfig {
|
||||
if result == nil {
|
||||
result = &PaymentResult{}
|
||||
}
|
||||
|
||||
return &dto.ClientPayConfig{
|
||||
AppID: result.AppID,
|
||||
Timestamp: result.TimeStamp,
|
||||
NonceStr: result.NonceStr,
|
||||
PackageVal: result.Package,
|
||||
SignType: result.SignType,
|
||||
PaySign: result.PaySign,
|
||||
}
|
||||
}
|
||||
|
||||
// extractPaymentResult 从微信 SDK 的 map 配置中提取统一支付参数。
|
||||
func extractPaymentResult(payConfig any) *PaymentResult {
|
||||
configMap, _ := payConfig.(map[string]any)
|
||||
if configMap == nil {
|
||||
configMap = map[string]any{}
|
||||
}
|
||||
|
||||
return &PaymentResult{
|
||||
AppID: payFirstNonEmpty(payStringFromAny(configMap["appId"])),
|
||||
TimeStamp: payFirstNonEmpty(payStringFromAny(configMap["timeStamp"]), payStringFromAny(configMap["timestamp"])),
|
||||
NonceStr: payStringFromAny(configMap["nonceStr"]),
|
||||
Package: payStringFromAny(configMap["package"]),
|
||||
SignType: payStringFromAny(configMap["signType"]),
|
||||
PaySign: payStringFromAny(configMap["paySign"]),
|
||||
}
|
||||
}
|
||||
|
||||
func payStringFromAny(value any) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
|
||||
func payFirstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"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/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
@@ -53,6 +52,9 @@ type Service struct {
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
@@ -69,6 +71,9 @@ func New(
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
db *gorm.DB,
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
@@ -83,6 +88,9 @@ func New(
|
||||
wechatConfigService: wechatConfigService,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
db: db,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -166,17 +174,17 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
}
|
||||
}()
|
||||
|
||||
paymentService, err := s.newPaymentService(activeConfig, appID)
|
||||
paymentProvider, err := s.newPaymentProvider(activeConfig, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceRecharge := s.checkForceRechargeRequirement(skipPermissionCtx, validationResult)
|
||||
if forceRecharge.NeedForceRecharge {
|
||||
return s.createForceRechargeOrder(skipPermissionCtx, customerID, appID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentService, &created)
|
||||
return s.createForceRechargeOrder(skipPermissionCtx, customerID, openID, assetInfo, validationResult, activeConfig, forceRecharge, redisKey, paymentProvider, &created)
|
||||
}
|
||||
|
||||
return s.createPackageOrder(skipPermissionCtx, customerID, appID, openID, validationResult, activeConfig, redisKey, paymentService, &created)
|
||||
return s.createPackageOrder(skipPermissionCtx, customerID, openID, validationResult, activeConfig, redisKey, paymentProvider, &created)
|
||||
}
|
||||
|
||||
func (s *Service) checkAssetOwnership(ctx context.Context, customerID uint, virtualNo string) error {
|
||||
@@ -258,24 +266,14 @@ func (s *Service) resolveCustomerOpenID(ctx context.Context, customerID uint, ap
|
||||
return "", errors.New(errors.CodeNotFound, "未找到当前应用的微信授权信息")
|
||||
}
|
||||
|
||||
func (s *Service) newPaymentService(wechatConfig *model.WechatConfig, appID string) (*wechat.PaymentService, error) {
|
||||
cache := wechat.NewRedisCache(s.redis)
|
||||
paymentApp, err := wechat.NewPaymentAppFromConfig(wechatConfig, appID, cache, s.logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "创建微信支付应用失败")
|
||||
}
|
||||
return wechat.NewPaymentService(paymentApp, s.logger), nil
|
||||
}
|
||||
|
||||
func (s *Service) createPackageOrder(
|
||||
ctx context.Context,
|
||||
customerID uint,
|
||||
appID string,
|
||||
openID string,
|
||||
validationResult *purchase_validation.PurchaseValidationResult,
|
||||
activeConfig *model.WechatConfig,
|
||||
redisKey string,
|
||||
paymentService *wechat.PaymentService,
|
||||
paymentProvider PaymentProvider,
|
||||
created *bool,
|
||||
) (*dto.ClientCreateOrderResponse, error) {
|
||||
order, err := s.buildPendingOrder(customerID, validationResult, activeConfig)
|
||||
@@ -300,7 +298,7 @@ func (s *Service) createPackageOrder(
|
||||
description = items[0].PackageName
|
||||
}
|
||||
|
||||
payResult, err := paymentService.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
|
||||
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -314,21 +312,20 @@ func (s *Service) createPackageOrder(
|
||||
PaymentStatus: order.PaymentStatus,
|
||||
CreatedAt: formatClientServiceTime(order.CreatedAt),
|
||||
},
|
||||
PayConfig: buildClientPayConfig(appID, payResult.PayConfig),
|
||||
PayConfig: buildClientPayConfigFromResult(paymentResult),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) createForceRechargeOrder(
|
||||
ctx context.Context,
|
||||
customerID uint,
|
||||
appID string,
|
||||
openID string,
|
||||
assetInfo *dto.AssetResolveResponse,
|
||||
validationResult *purchase_validation.PurchaseValidationResult,
|
||||
activeConfig *model.WechatConfig,
|
||||
forceRecharge *ForceRechargeRequirement,
|
||||
redisKey string,
|
||||
paymentService *wechat.PaymentService,
|
||||
paymentProvider PaymentProvider,
|
||||
created *bool,
|
||||
) (*dto.ClientCreateOrderResponse, error) {
|
||||
resourceType, resourceID, err := resolveWalletResource(validationResult)
|
||||
@@ -378,7 +375,7 @@ func (s *Service) createForceRechargeOrder(
|
||||
s.markClientPurchaseCreated(ctx, redisKey, recharge.RechargeNo)
|
||||
*created = true
|
||||
|
||||
payResult, err := paymentService.CreateJSAPIOrder(ctx, recharge.RechargeNo, "余额充值", openID, int(recharge.Amount))
|
||||
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, recharge.RechargeNo, "余额充值", openID, int(recharge.Amount))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -392,7 +389,7 @@ func (s *Service) createForceRechargeOrder(
|
||||
Status: rechargeStatusToClientStatus(recharge.Status),
|
||||
AutoPurchaseStatus: recharge.AutoPurchaseStatus,
|
||||
},
|
||||
PayConfig: buildClientPayConfig(appID, payResult.PayConfig),
|
||||
PayConfig: buildClientPayConfigFromResult(paymentResult),
|
||||
LinkedPackageInfo: buildLinkedPackageInfo(validationResult, forceRecharge),
|
||||
}, nil
|
||||
}
|
||||
@@ -561,22 +558,6 @@ func buildLinkedPackageInfo(result *purchase_validation.PurchaseValidationResult
|
||||
}
|
||||
}
|
||||
|
||||
func buildClientPayConfig(appID string, payConfig any) *dto.ClientPayConfig {
|
||||
configMap, _ := payConfig.(map[string]any)
|
||||
if configMap == nil {
|
||||
configMap = map[string]any{}
|
||||
}
|
||||
|
||||
return &dto.ClientPayConfig{
|
||||
AppID: firstNonEmpty(stringFromAny(configMap["appId"]), appID),
|
||||
Timestamp: firstNonEmpty(stringFromAny(configMap["timeStamp"]), stringFromAny(configMap["timestamp"])),
|
||||
NonceStr: stringFromAny(configMap["nonceStr"]),
|
||||
PackageVal: stringFromAny(configMap["package"]),
|
||||
SignType: stringFromAny(configMap["signType"]),
|
||||
PaySign: stringFromAny(configMap["paySign"]),
|
||||
}
|
||||
}
|
||||
|
||||
func resolveWalletResource(result *purchase_validation.PurchaseValidationResult) (string, uint, error) {
|
||||
if result.Card != nil {
|
||||
return constants.AssetWalletResourceTypeIotCard, result.Card.ID, nil
|
||||
@@ -663,18 +644,217 @@ func generateClientRechargeNo() string {
|
||||
return fmt.Sprintf("CRCH%d%06d", time.Now().UnixNano()/1e6, rand.Intn(1000000))
|
||||
}
|
||||
|
||||
func stringFromAny(value any) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
// 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{})
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
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)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -172,7 +172,30 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
|
||||
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, currentShop.ID, packageID)
|
||||
if err != nil {
|
||||
s.logger.Warn("上级店铺未分配该套餐,跳过", zap.Uint("shop_id", currentShop.ID), zap.Uint("package_id", packageID))
|
||||
s.logger.Warn("上级店铺未分配该套餐,佣金链断裂",
|
||||
zap.Uint("shop_id", currentShop.ID),
|
||||
zap.Uint("package_id", packageID),
|
||||
zap.Uint("order_id", order.ID))
|
||||
pendingRecord := &model.CommissionRecord{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Updater,
|
||||
},
|
||||
ShopID: currentShop.ID,
|
||||
OrderID: order.ID,
|
||||
IotCardID: order.IotCardID,
|
||||
DeviceID: order.DeviceID,
|
||||
CommissionSource: model.CommissionSourceCostDiff,
|
||||
Amount: 0,
|
||||
Status: constants.CommissionStatusPendingReview,
|
||||
Remark: fmt.Sprintf("套餐系列[%d]未分配给该代理(shop_id=%d),成本价差链路断裂,请人工核查", *order.SeriesID, currentShop.ID),
|
||||
}
|
||||
if createErr := s.commissionRecordStore.Create(ctx, pendingRecord); createErr != nil {
|
||||
s.logger.Warn("创建待审佣金记录失败",
|
||||
zap.Uint("shop_id", currentShop.ID),
|
||||
zap.Uint("order_id", order.ID),
|
||||
zap.Error(createErr))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -503,9 +526,30 @@ func (s *Service) calculateChainOneTimeCommission(ctx context.Context, bottomSho
|
||||
parentShopID := *currentShop.ParentID
|
||||
parentSeriesAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, parentShopID, seriesID)
|
||||
if err != nil {
|
||||
s.logger.Warn("上级店铺未分配该系列,停止链式计算",
|
||||
s.logger.Warn("上级店铺未分配该系列,佣金链断裂",
|
||||
zap.Uint("parent_shop_id", parentShopID),
|
||||
zap.Uint("series_id", seriesID))
|
||||
zap.Uint("series_id", seriesID),
|
||||
zap.Uint("order_id", order.ID))
|
||||
pendingRecord := &model.CommissionRecord{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Updater,
|
||||
},
|
||||
ShopID: parentShopID,
|
||||
OrderID: order.ID,
|
||||
IotCardID: cardID,
|
||||
DeviceID: deviceID,
|
||||
CommissionSource: model.CommissionSourceOneTime,
|
||||
Amount: 0,
|
||||
Status: constants.CommissionStatusPendingReview,
|
||||
Remark: fmt.Sprintf("套餐系列[%d]未分配给该代理(shop_id=%d),一次性佣金链路断裂,请人工核查", seriesID, parentShopID),
|
||||
}
|
||||
if createErr := s.commissionRecordStore.Create(ctx, pendingRecord); createErr != nil {
|
||||
s.logger.Warn("创建待审佣金记录失败",
|
||||
zap.Uint("shop_id", parentShopID),
|
||||
zap.Uint("order_id", order.ID),
|
||||
zap.Error(createErr))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"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/break/junhong_cmp_fiber/pkg/fuiou"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
@@ -194,6 +195,14 @@ func (s *Service) CreateLegacy(ctx context.Context, req *dto.CreateOrderRequest,
|
||||
|
||||
} 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, "只有代理账号可以使用钱包支付")
|
||||
@@ -2379,14 +2388,126 @@ func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRe
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// FuiouPayJSAPI 富友公众号 JSAPI 支付发起(留桩)
|
||||
// TODO: 实现富友支付发起逻辑
|
||||
func (s *Service) FuiouPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) error {
|
||||
return errors.New(errors.CodeFuiouPayFailed, "富友支付发起暂未实现")
|
||||
func (s *Service) FuiouPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.FuiouPayJSAPIResponse, error) {
|
||||
return s.fuiouPreCreate(ctx, orderID, openID, buyerType, buyerID, "JSAPI", false)
|
||||
}
|
||||
|
||||
// FuiouPayMiniApp 富友小程序支付发起(留桩)
|
||||
// TODO: 实现富友小程序支付发起逻辑
|
||||
func (s *Service) FuiouPayMiniApp(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) error {
|
||||
return errors.New(errors.CodeFuiouPayFailed, "富友小程序支付发起暂未实现")
|
||||
func (s *Service) FuiouPayMiniApp(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.FuiouPayJSAPIResponse, error) {
|
||||
return s.fuiouPreCreate(ctx, orderID, openID, buyerType, buyerID, "LETPAY", true)
|
||||
}
|
||||
|
||||
func (s *Service) fuiouPreCreate(
|
||||
ctx context.Context,
|
||||
orderID uint,
|
||||
openID string,
|
||||
buyerType string,
|
||||
buyerID uint,
|
||||
tradeType string,
|
||||
useMiniappAppID bool,
|
||||
) (*dto.FuiouPayJSAPIResponse, error) {
|
||||
if strings.TrimSpace(openID) == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "openID 不能为空")
|
||||
}
|
||||
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单失败")
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "查询生效支付配置失败")
|
||||
}
|
||||
if activeConfig == nil || activeConfig.ProviderType != model.ProviderTypeFuiou {
|
||||
return nil, errors.New(errors.CodeFuiouPayFailed, "未找到生效的富友支付配置")
|
||||
}
|
||||
|
||||
subAppID := activeConfig.OaAppID
|
||||
if useMiniappAppID {
|
||||
subAppID = activeConfig.MiniappAppID
|
||||
}
|
||||
|
||||
if strings.TrimSpace(activeConfig.FyInsCd) == "" ||
|
||||
strings.TrimSpace(activeConfig.FyMchntCd) == "" ||
|
||||
strings.TrimSpace(activeConfig.FyTermID) == "" ||
|
||||
strings.TrimSpace(activeConfig.FyPrivateKey) == "" ||
|
||||
strings.TrimSpace(activeConfig.FyPublicKey) == "" ||
|
||||
strings.TrimSpace(activeConfig.FyAPIURL) == "" ||
|
||||
strings.TrimSpace(activeConfig.FyNotifyURL) == "" ||
|
||||
strings.TrimSpace(subAppID) == "" {
|
||||
return nil, errors.New(errors.CodeFuiouPayFailed, "富友支付配置不完整")
|
||||
}
|
||||
|
||||
client, err := fuiou.NewClient(
|
||||
activeConfig.FyInsCd,
|
||||
activeConfig.FyMchntCd,
|
||||
activeConfig.FyTermID,
|
||||
activeConfig.FyAPIURL,
|
||||
activeConfig.FyNotifyURL,
|
||||
activeConfig.FyPrivateKey,
|
||||
activeConfig.FyPublicKey,
|
||||
s.logger,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "创建富友支付客户端失败")
|
||||
}
|
||||
|
||||
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
|
||||
}
|
||||
description := "套餐购买"
|
||||
if len(items) > 0 {
|
||||
description = items[0].PackageName
|
||||
}
|
||||
|
||||
termIP := "127.0.0.1"
|
||||
if ip := middleware.GetIPFromContext(ctx); ip != nil && strings.TrimSpace(*ip) != "" {
|
||||
termIP = *ip
|
||||
}
|
||||
|
||||
resp, err := client.WxPreCreate(
|
||||
order.OrderNo,
|
||||
strconv.FormatInt(order.TotalAmount, 10),
|
||||
description,
|
||||
termIP,
|
||||
tradeType,
|
||||
subAppID,
|
||||
openID,
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.Error("富友预下单失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.String("trade_type", tradeType),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败")
|
||||
}
|
||||
|
||||
s.logger.Info("富友预下单成功",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.String("trade_type", tradeType),
|
||||
)
|
||||
|
||||
return &dto.FuiouPayJSAPIResponse{
|
||||
AppID: resp.SdkAppid,
|
||||
TimeStamp: resp.SdkTimestamp,
|
||||
NonceStr: resp.SdkNoncestr,
|
||||
Package: resp.SdkPackage,
|
||||
SignType: resp.SdkSigntype,
|
||||
PaySign: resp.SdkPaysign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ func (s *ActivationService) ActivateByRealname(ctx context.Context, carrierType
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活套餐失败")
|
||||
}
|
||||
|
||||
s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("套餐已激活",
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Uint("package_id", usage.PackageID),
|
||||
@@ -300,6 +302,8 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活排队主套餐失败")
|
||||
}
|
||||
|
||||
s.syncCarrierStatusActivated(ctx, tx, &nextMain, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("排队主套餐已激活",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.Uint("package_id", nextMain.PackageID),
|
||||
@@ -320,3 +324,48 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, carrierType string, carrierID uint) {
|
||||
if usage == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if usage.UsageType == constants.PackageUsageTypeDevice || usage.DeviceID > 0 || carrierType == constants.AssetTypeDevice {
|
||||
deviceID := usage.DeviceID
|
||||
if deviceID == 0 {
|
||||
deviceID = carrierID
|
||||
}
|
||||
if deviceID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.Device{}).
|
||||
Where("id = ? AND status < ?", deviceID, constants.DeviceStatusActivated).
|
||||
Update("status", constants.DeviceStatusActivated).Error; err != nil {
|
||||
s.logger.Warn("套餐激活后更新设备状态失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cardID := usage.IotCardID
|
||||
if cardID == 0 {
|
||||
cardID = carrierID
|
||||
}
|
||||
if cardID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.IotCard{}).
|
||||
Where("id = ? AND status < ?", cardID, constants.IotCardStatusActivated).
|
||||
Update("status", constants.IotCardStatusActivated).Error; err != nil {
|
||||
s.logger.Warn("套餐激活后更新卡状态失败",
|
||||
zap.Uint("iot_card_id", cardID),
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -19,6 +21,8 @@ type Service struct {
|
||||
agentWalletStore *postgres.AgentWalletStore
|
||||
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -27,6 +31,8 @@ func New(
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore,
|
||||
commissionRecordStore *postgres.CommissionRecordStore,
|
||||
db *gorm.DB,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
shopStore: shopStore,
|
||||
@@ -34,6 +40,8 @@ func New(
|
||||
agentWalletStore: agentWalletStore,
|
||||
commissionWithdrawalReqStore: commissionWithdrawalReqStore,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,3 +433,77 @@ func contains(s, substr string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResolveCommissionRecord 修正待审佣金记录(status=99)
|
||||
// release: 填入金额并入账到代理佣金钱包
|
||||
// invalidate: 标记为已失效
|
||||
func (s *Service) ResolveCommissionRecord(ctx context.Context, recordID uint, req *dto.CommissionRecordResolveRequest) error {
|
||||
record, err := s.commissionRecordStore.GetByID(ctx, recordID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeNotFound, err, "佣金记录不存在")
|
||||
}
|
||||
|
||||
if record.Status != constants.CommissionStatusPendingReview {
|
||||
return errors.New(errors.CodeInvalidParam, "该记录不是待修正状态")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
resolveRemark := record.Remark
|
||||
if req.Remark != "" {
|
||||
resolveRemark += " | 处理备注: " + req.Remark
|
||||
}
|
||||
|
||||
if req.Action == "invalidate" {
|
||||
return s.commissionRecordStore.UpdateByID(ctx, nil, recordID, map[string]any{
|
||||
"status": model.CommissionStatusInvalid,
|
||||
"remark": resolveRemark,
|
||||
})
|
||||
}
|
||||
|
||||
// release 入账
|
||||
if req.Amount == nil || *req.Amount <= 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "入账操作必须指定金额")
|
||||
}
|
||||
amount := *req.Amount
|
||||
|
||||
wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, record.ShopID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeNotFound, err, "店铺佣金钱包不存在")
|
||||
}
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 更新佣金记录
|
||||
if err := s.commissionRecordStore.UpdateByID(ctx, tx, recordID, map[string]any{
|
||||
"status": model.CommissionStatusReleased,
|
||||
"amount": amount,
|
||||
"released_at": now,
|
||||
"remark": resolveRemark,
|
||||
}); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败")
|
||||
}
|
||||
|
||||
// 入账到佣金钱包(乐观锁)
|
||||
balanceBefore := wallet.Balance
|
||||
result := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance + ?", amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新佣金钱包余额失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInternalError, "佣金钱包版本冲突,请重试")
|
||||
}
|
||||
|
||||
// 回写入账后余额
|
||||
if err := s.commissionRecordStore.UpdateByID(ctx, tx, recordID, map[string]any{
|
||||
"balance_after": balanceBefore + amount,
|
||||
}); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新入账后余额失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user