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 而非省略
510 lines
15 KiB
Go
510 lines
15 KiB
Go
package shop_commission
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"time"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||
"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 {
|
||
shopStore *postgres.ShopStore
|
||
accountStore *postgres.AccountStore
|
||
agentWalletStore *postgres.AgentWalletStore
|
||
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore
|
||
commissionRecordStore *postgres.CommissionRecordStore
|
||
db *gorm.DB
|
||
logger *zap.Logger
|
||
}
|
||
|
||
func New(
|
||
shopStore *postgres.ShopStore,
|
||
accountStore *postgres.AccountStore,
|
||
agentWalletStore *postgres.AgentWalletStore,
|
||
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore,
|
||
commissionRecordStore *postgres.CommissionRecordStore,
|
||
db *gorm.DB,
|
||
logger *zap.Logger,
|
||
) *Service {
|
||
return &Service{
|
||
shopStore: shopStore,
|
||
accountStore: accountStore,
|
||
agentWalletStore: agentWalletStore,
|
||
commissionWithdrawalReqStore: commissionWithdrawalReqStore,
|
||
commissionRecordStore: commissionRecordStore,
|
||
db: db,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
func (s *Service) ListShopCommissionSummary(ctx context.Context, req *dto.ShopCommissionSummaryListReq) (*dto.ShopCommissionSummaryPageResult, error) {
|
||
opts := &store.QueryOptions{
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
OrderBy: "created_at DESC",
|
||
}
|
||
if opts.Page == 0 {
|
||
opts.Page = 1
|
||
}
|
||
if opts.PageSize == 0 {
|
||
opts.PageSize = constants.DefaultPageSize
|
||
}
|
||
|
||
filters := make(map[string]interface{})
|
||
if req.ShopName != "" {
|
||
filters["shop_name"] = req.ShopName
|
||
}
|
||
|
||
shops, total, err := s.shopStore.List(ctx, opts, filters)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺列表失败")
|
||
}
|
||
|
||
if len(shops) == 0 {
|
||
return &dto.ShopCommissionSummaryPageResult{
|
||
Items: []dto.ShopCommissionSummaryItem{},
|
||
Total: 0,
|
||
Page: opts.Page,
|
||
Size: opts.PageSize,
|
||
}, nil
|
||
}
|
||
|
||
shopIDs := make([]uint, 0, len(shops))
|
||
for _, shop := range shops {
|
||
shopIDs = append(shopIDs, shop.ID)
|
||
}
|
||
|
||
walletSummaries, err := s.agentWalletStore.GetShopCommissionSummaryBatch(ctx, shopIDs)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺钱包汇总失败")
|
||
}
|
||
|
||
withdrawnAmounts, err := s.commissionWithdrawalReqStore.SumAmountByShopIDsAndStatus(ctx, shopIDs, constants.WithdrawalStatusApproved)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询已提现金额失败")
|
||
}
|
||
|
||
withdrawingAmounts, err := s.commissionWithdrawalReqStore.SumAmountByShopIDsAndStatus(ctx, shopIDs, constants.WithdrawalStatusPending)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询提现中金额失败")
|
||
}
|
||
|
||
primaryAccounts, err := s.accountStore.GetPrimaryAccountsByShopIDs(ctx, shopIDs)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主账号失败")
|
||
}
|
||
|
||
accountMap := make(map[uint]*model.Account)
|
||
for _, acc := range primaryAccounts {
|
||
if acc.ShopID != nil {
|
||
accountMap[*acc.ShopID] = acc
|
||
}
|
||
}
|
||
|
||
items := make([]dto.ShopCommissionSummaryItem, 0, len(shops))
|
||
for _, shop := range shops {
|
||
if req.Username != "" {
|
||
acc := accountMap[shop.ID]
|
||
if acc == nil || !containsSubstring(acc.Username, req.Username) {
|
||
total--
|
||
continue
|
||
}
|
||
}
|
||
|
||
item := s.buildCommissionSummaryItem(shop, walletSummaries[shop.ID], withdrawnAmounts[shop.ID], withdrawingAmounts[shop.ID], accountMap[shop.ID])
|
||
items = append(items, item)
|
||
}
|
||
|
||
return &dto.ShopCommissionSummaryPageResult{
|
||
Items: items,
|
||
Total: total,
|
||
Page: opts.Page,
|
||
Size: opts.PageSize,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) buildCommissionSummaryItem(shop *model.Shop, walletSummary *model.AgentWallet, withdrawnAmount, withdrawingAmount int64, account *model.Account) dto.ShopCommissionSummaryItem {
|
||
var balance, frozenBalance int64
|
||
if walletSummary != nil {
|
||
balance = walletSummary.Balance
|
||
frozenBalance = walletSummary.FrozenBalance
|
||
}
|
||
|
||
totalCommission := balance + frozenBalance + withdrawnAmount
|
||
unwithdrawCommission := totalCommission - withdrawnAmount
|
||
availableCommission := balance - withdrawingAmount
|
||
if availableCommission < 0 {
|
||
availableCommission = 0
|
||
}
|
||
|
||
var username, phone string
|
||
if account != nil {
|
||
username = account.Username
|
||
phone = account.Phone
|
||
}
|
||
|
||
return dto.ShopCommissionSummaryItem{
|
||
ShopID: shop.ID,
|
||
ShopName: shop.ShopName,
|
||
ShopCode: shop.ShopCode,
|
||
Username: username,
|
||
Phone: phone,
|
||
TotalCommission: totalCommission,
|
||
WithdrawnCommission: withdrawnAmount,
|
||
UnwithdrawCommission: unwithdrawCommission,
|
||
FrozenCommission: frozenBalance,
|
||
WithdrawingCommission: withdrawingAmount,
|
||
AvailableCommission: availableCommission,
|
||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
}
|
||
|
||
func (s *Service) ListShopWithdrawalRequests(ctx context.Context, shopID uint, req *dto.ShopWithdrawalRequestListReq) (*dto.ShopWithdrawalRequestPageResult, error) {
|
||
_, err := s.shopStore.GetByID(ctx, shopID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||
}
|
||
|
||
opts := &store.QueryOptions{
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
OrderBy: "created_at DESC",
|
||
}
|
||
if opts.Page == 0 {
|
||
opts.Page = 1
|
||
}
|
||
if opts.PageSize == 0 {
|
||
opts.PageSize = constants.DefaultPageSize
|
||
}
|
||
|
||
filters := &postgres.WithdrawalRequestListFilters{
|
||
ShopID: shopID,
|
||
WithdrawalNo: req.WithdrawalNo,
|
||
}
|
||
|
||
if req.StartTime != "" {
|
||
t, err := time.Parse("2006-01-02 15:04:05", req.StartTime)
|
||
if err == nil {
|
||
filters.StartTime = &t
|
||
}
|
||
}
|
||
if req.EndTime != "" {
|
||
t, err := time.Parse("2006-01-02 15:04:05", req.EndTime)
|
||
if err == nil {
|
||
filters.EndTime = &t
|
||
}
|
||
}
|
||
|
||
requests, total, err := s.commissionWithdrawalReqStore.ListByShopID(ctx, opts, filters)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询提现记录失败")
|
||
}
|
||
|
||
shop, _ := s.shopStore.GetByID(ctx, shopID)
|
||
shopHierarchy := s.buildShopHierarchyPath(ctx, shop)
|
||
|
||
applicantIDs := make([]uint, 0)
|
||
processorIDs := make([]uint, 0)
|
||
for _, r := range requests {
|
||
if r.ApplicantID > 0 {
|
||
applicantIDs = append(applicantIDs, r.ApplicantID)
|
||
}
|
||
if r.ProcessorID > 0 {
|
||
processorIDs = append(processorIDs, r.ProcessorID)
|
||
}
|
||
}
|
||
|
||
applicantMap := make(map[uint]string)
|
||
processorMap := make(map[uint]string)
|
||
|
||
if len(applicantIDs) > 0 {
|
||
accounts, _ := s.accountStore.GetByIDs(ctx, applicantIDs)
|
||
for _, acc := range accounts {
|
||
applicantMap[acc.ID] = acc.Username
|
||
}
|
||
}
|
||
if len(processorIDs) > 0 {
|
||
accounts, _ := s.accountStore.GetByIDs(ctx, processorIDs)
|
||
for _, acc := range accounts {
|
||
processorMap[acc.ID] = acc.Username
|
||
}
|
||
}
|
||
|
||
items := make([]dto.ShopWithdrawalRequestItem, 0, len(requests))
|
||
for _, r := range requests {
|
||
item := s.buildWithdrawalRequestItem(r, shop.ShopName, shopHierarchy, applicantMap, processorMap)
|
||
items = append(items, item)
|
||
}
|
||
|
||
return &dto.ShopWithdrawalRequestPageResult{
|
||
Items: items,
|
||
Total: total,
|
||
Page: opts.Page,
|
||
Size: opts.PageSize,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) buildWithdrawalRequestItem(r *model.CommissionWithdrawalRequest, shopName, shopHierarchy string, applicantMap, processorMap map[uint]string) dto.ShopWithdrawalRequestItem {
|
||
var processorID *uint
|
||
if r.ProcessorID > 0 {
|
||
processorID = &r.ProcessorID
|
||
}
|
||
|
||
var accountName, accountNumber, bankName string
|
||
if len(r.AccountInfo) > 0 {
|
||
var info map[string]interface{}
|
||
if err := json.Unmarshal(r.AccountInfo, &info); err == nil {
|
||
if v, ok := info["account_name"].(string); ok {
|
||
accountName = v
|
||
}
|
||
if v, ok := info["account_number"].(string); ok {
|
||
accountNumber = v
|
||
}
|
||
if v, ok := info["bank_name"].(string); ok {
|
||
bankName = v
|
||
}
|
||
}
|
||
}
|
||
|
||
var processedAt, paidAt string
|
||
if r.ProcessedAt != nil {
|
||
processedAt = r.ProcessedAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
if r.PaidAt != nil {
|
||
paidAt = r.PaidAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
return dto.ShopWithdrawalRequestItem{
|
||
ID: r.ID,
|
||
WithdrawalNo: r.WithdrawalNo,
|
||
Amount: r.Amount,
|
||
FeeRate: r.FeeRate,
|
||
Fee: r.Fee,
|
||
ActualAmount: r.ActualAmount,
|
||
Status: r.Status,
|
||
StatusName: getWithdrawalStatusName(r.Status),
|
||
ShopID: r.ShopID,
|
||
ShopName: shopName,
|
||
ShopHierarchy: shopHierarchy,
|
||
ApplicantID: r.ApplicantID,
|
||
ApplicantName: applicantMap[r.ApplicantID],
|
||
ProcessorID: processorID,
|
||
ProcessorName: processorMap[r.ProcessorID],
|
||
WithdrawalMethod: r.WithdrawalMethod,
|
||
PaymentType: r.PaymentType,
|
||
AccountName: accountName,
|
||
AccountNumber: accountNumber,
|
||
BankName: bankName,
|
||
RejectReason: r.RejectReason,
|
||
Remark: r.Remark,
|
||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
ProcessedAt: processedAt,
|
||
PaidAt: paidAt,
|
||
}
|
||
}
|
||
|
||
func (s *Service) buildShopHierarchyPath(ctx context.Context, shop *model.Shop) string {
|
||
if shop == nil {
|
||
return ""
|
||
}
|
||
|
||
path := shop.ShopName
|
||
current := shop
|
||
depth := 0
|
||
|
||
for current.ParentID != nil && depth < 2 {
|
||
parent, err := s.shopStore.GetByID(ctx, *current.ParentID)
|
||
if err != nil {
|
||
break
|
||
}
|
||
path = parent.ShopName + "_" + path
|
||
current = parent
|
||
depth++
|
||
}
|
||
|
||
return path
|
||
}
|
||
|
||
func (s *Service) ListShopCommissionRecords(ctx context.Context, shopID uint, req *dto.ShopCommissionRecordListReq) (*dto.ShopCommissionRecordPageResult, error) {
|
||
_, err := s.shopStore.GetByID(ctx, shopID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||
}
|
||
|
||
opts := &store.QueryOptions{
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
OrderBy: "created_at DESC",
|
||
}
|
||
if opts.Page == 0 {
|
||
opts.Page = 1
|
||
}
|
||
if opts.PageSize == 0 {
|
||
opts.PageSize = constants.DefaultPageSize
|
||
}
|
||
|
||
filters := &postgres.CommissionRecordListFilters{
|
||
ShopID: shopID,
|
||
CommissionSource: req.CommissionSource,
|
||
ICCID: req.ICCID,
|
||
DeviceNo: req.VirtualNo,
|
||
OrderNo: req.OrderNo,
|
||
}
|
||
|
||
records, total, err := s.commissionRecordStore.ListByShopID(ctx, opts, filters)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询佣金明细失败")
|
||
}
|
||
|
||
items := make([]dto.ShopCommissionRecordItem, 0, len(records))
|
||
for _, r := range records {
|
||
item := dto.ShopCommissionRecordItem{
|
||
ID: r.ID,
|
||
Amount: r.Amount,
|
||
BalanceAfter: r.BalanceAfter,
|
||
CommissionSource: r.CommissionSource,
|
||
Status: r.Status,
|
||
StatusName: getCommissionStatusName(r.Status),
|
||
OrderID: r.OrderID,
|
||
OrderNo: "",
|
||
VirtualNo: "",
|
||
ICCID: "",
|
||
OrderCreatedAt: "",
|
||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
|
||
return &dto.ShopCommissionRecordPageResult{
|
||
Items: items,
|
||
Total: total,
|
||
Page: opts.Page,
|
||
Size: opts.PageSize,
|
||
}, nil
|
||
}
|
||
|
||
func getWithdrawalStatusName(status int) string {
|
||
switch status {
|
||
case constants.WithdrawalStatusPending:
|
||
return "待审核"
|
||
case constants.WithdrawalStatusApproved:
|
||
return "已通过"
|
||
case constants.WithdrawalStatusRejected:
|
||
return "已拒绝"
|
||
case constants.WithdrawalStatusPaid:
|
||
return "已到账"
|
||
default:
|
||
return "未知"
|
||
}
|
||
}
|
||
|
||
func getCommissionStatusName(status int) string {
|
||
switch status {
|
||
case constants.CommissionStatusFrozen:
|
||
return "已冻结"
|
||
case constants.CommissionStatusUnfreezing:
|
||
return "解冻中"
|
||
case constants.CommissionStatusReleased:
|
||
return "已发放"
|
||
case constants.CommissionStatusInvalid:
|
||
return "已失效"
|
||
default:
|
||
return "未知"
|
||
}
|
||
}
|
||
|
||
func containsSubstring(s, substr string) bool {
|
||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && len(substr) > 0 && contains(s, substr)))
|
||
}
|
||
|
||
func contains(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
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
|
||
})
|
||
}
|