All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
主要变更: 1. OpenAPI 文档契约对齐 - 统一错误响应字段名为 msg(非 message) - 规范 envelope 响应结构(code, msg, data, timestamp) - 个人客户路由纳入文档体系(使用 Register 机制) - 新增 BuildDocHandlers() 统一管理 handler 构造 - 确保文档生成的幂等性 2. Service 层错误处理统一 - 全面替换 fmt.Errorf 为 errors.New/Wrap - 统一错误码使用规范 - Handler 层参数校验不泄露底层细节 - 新增错误码验证集成测试 3. 代码质量提升 - 删除未使用的 Task handler 和路由 - 新增代码规范检查脚本(check-service-errors.sh) - 新增注释路径一致性检查(check-comment-paths.sh) - 更新 API 文档生成指南 4. OpenSpec 归档 - 归档 openapi-contract-alignment 变更(63 tasks) - 归档 service-error-unify-core 变更 - 归档 service-error-unify-support 变更 - 归档 code-cleanup-docs-update 变更 - 归档 handler-validation-security 变更 - 同步 delta specs 到主规范文件 影响范围: - pkg/openapi: 新增 handlers.go,优化 generator.go - internal/service/*: 48 个 service 文件错误处理统一 - internal/handler/admin: 优化参数校验错误提示 - internal/routes: 个人客户路由改造,删除 task 路由 - scripts: 新增 3 个代码检查脚本 - docs: 更新 OpenAPI 文档(15750+ 行) - openspec/specs: 同步 3 个主规范文件 破坏性变更:无 向后兼容:是
428 lines
12 KiB
Go
428 lines
12 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"
|
|
)
|
|
|
|
type Service struct {
|
|
shopStore *postgres.ShopStore
|
|
accountStore *postgres.AccountStore
|
|
walletStore *postgres.WalletStore
|
|
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore
|
|
commissionRecordStore *postgres.CommissionRecordStore
|
|
}
|
|
|
|
func New(
|
|
shopStore *postgres.ShopStore,
|
|
accountStore *postgres.AccountStore,
|
|
walletStore *postgres.WalletStore,
|
|
commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore,
|
|
commissionRecordStore *postgres.CommissionRecordStore,
|
|
) *Service {
|
|
return &Service{
|
|
shopStore: shopStore,
|
|
accountStore: accountStore,
|
|
walletStore: walletStore,
|
|
commissionWithdrawalReqStore: commissionWithdrawalReqStore,
|
|
commissionRecordStore: commissionRecordStore,
|
|
}
|
|
}
|
|
|
|
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.walletStore.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 *postgres.ShopCommissionSummary, 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.DeviceNo,
|
|
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: "",
|
|
DeviceNo: "",
|
|
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
|
|
}
|