be09a115ec
接口侧: - /v1/public/user/withdrawal_log 响应新增 summary 字段, 包含: - commission_balance (当前余额) - locked_by_pending (待审批占用) - available_to_withdraw (可提现) - total_historical_amount (已通过提现总额) - total_refunded_amount (退款回扣总额) - total_income_amount (收入总额) - 前端可据此自洽展示账目对账, 用户能在一个接口里看清整笔账 代码守护: - updateUserBasicInfoLogic 拒绝任何 change<0 的 commission 修改 - 扣减必须走 approveWithdrawal 写 type=334 日志 - 防止未来 admin 误操作再次造成 user.commission 与 system_logs 失衡 时间戳修复: - queryWithdrawalLogLogic 和 queryCommissionReturnLogLogic 的时间戳 从 UnixMilli 改回 Unix (秒级), 符合项目"后端统一秒级"约定 测试: - 补 buildSummary 的 4 个 mock 查询期望 - 加 summary 字段值正确性断言
172 lines
6.1 KiB
Go
172 lines
6.1 KiB
Go
package user
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/perfect-panel/server/internal/model/log"
|
||
"github.com/perfect-panel/server/internal/model/user"
|
||
"github.com/perfect-panel/server/internal/svc"
|
||
"github.com/perfect-panel/server/internal/types"
|
||
"github.com/perfect-panel/server/pkg/constant"
|
||
"github.com/perfect-panel/server/pkg/logger"
|
||
"github.com/perfect-panel/server/pkg/xerr"
|
||
"github.com/pkg/errors"
|
||
)
|
||
|
||
const (
|
||
withdrawalLogBizTypeWithdrawal = "withdrawal"
|
||
withdrawalLogBizTypeCommissionRefund = "commission_refund"
|
||
)
|
||
|
||
type QueryWithdrawalLogLogic struct {
|
||
logger.Logger
|
||
ctx context.Context
|
||
svcCtx *svc.ServiceContext
|
||
}
|
||
|
||
// NewQueryWithdrawalLogLogic Query Withdrawal Log
|
||
func NewQueryWithdrawalLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryWithdrawalLogLogic {
|
||
return &QueryWithdrawalLogLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalLogListRequest) (resp *types.QueryWithdrawalLogListResponse, err error) {
|
||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||
if !ok {
|
||
l.Error("current user is not found in context")
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||
}
|
||
|
||
page, size := normalizePagination(req.Page, req.Size)
|
||
|
||
switch req.BizType {
|
||
case "", withdrawalLogBizTypeWithdrawal:
|
||
return l.queryWithdrawalLogs(u.Id, page, size)
|
||
case withdrawalLogBizTypeCommissionRefund:
|
||
return l.queryCommissionRefundLogs(u.Id, page, size)
|
||
default:
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid biz_type: %s", req.BizType)
|
||
}
|
||
}
|
||
|
||
func (l *QueryWithdrawalLogLogic) queryWithdrawalLogs(userID int64, page, size int) (*types.QueryWithdrawalLogListResponse, error) {
|
||
query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Withdrawal{}).Where("user_id = ?", userID)
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count withdrawal logs failed: %v", err)
|
||
}
|
||
|
||
var rows []user.Withdrawal
|
||
if err := query.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&rows).Error; err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query withdrawal logs failed: %v", err)
|
||
}
|
||
|
||
list := make([]types.WithdrawalLog, 0, len(rows))
|
||
for _, row := range rows {
|
||
list = append(list, types.WithdrawalLog{
|
||
Id: row.Id,
|
||
BizType: withdrawalLogBizTypeWithdrawal,
|
||
UserId: row.UserId,
|
||
Amount: row.Amount,
|
||
Content: row.Content,
|
||
Status: row.Status,
|
||
Reason: row.Reason,
|
||
Method: row.Method,
|
||
Account: row.Account,
|
||
QrCodeUrl: row.QrCodeUrl,
|
||
CreatedAt: row.CreatedAt.Unix(),
|
||
UpdatedAt: row.UpdatedAt.Unix(),
|
||
})
|
||
}
|
||
|
||
summary, err := l.buildSummary(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &types.QueryWithdrawalLogListResponse{
|
||
List: list,
|
||
Total: total,
|
||
Summary: summary,
|
||
}, nil
|
||
}
|
||
|
||
// buildSummary 聚合用户佣金账目快照,供前端展示闭环对账
|
||
//
|
||
// commission_balance = user.commission(当前余额,由 system_logs 累计而来)
|
||
// locked_by_pending = 待审批提现占用
|
||
// available_to_withdraw = balance - locked
|
||
// total_historical_amount = 已通过提现累计(status=1)
|
||
// total_refunded_amount = 佣金回扣累计(type=333/337/338)
|
||
// total_income_amount = 佣金收入累计(type=331/332)
|
||
func (l *QueryWithdrawalLogLogic) buildSummary(userID int64) (*types.WithdrawalLogSummary, error) {
|
||
db := l.svcCtx.DB.WithContext(l.ctx)
|
||
var summary types.WithdrawalLogSummary
|
||
|
||
if err := db.Model(&user.User{}).Where("id = ?", userID).
|
||
Select("COALESCE(commission, 0)").Scan(&summary.CommissionBalance).Error; err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "load commission balance failed: %v", err)
|
||
}
|
||
|
||
if err := db.Model(&user.Withdrawal{}).
|
||
Where("user_id = ? AND status = ?", userID, user.WithdrawalStatusPending).
|
||
Select("COALESCE(SUM(amount), 0)").Scan(&summary.LockedByPending).Error; err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "sum pending withdrawals failed: %v", err)
|
||
}
|
||
|
||
if err := db.Model(&user.Withdrawal{}).
|
||
Where("user_id = ? AND status = ?", userID, user.WithdrawalStatusApproved).
|
||
Select("COALESCE(SUM(amount), 0)").Scan(&summary.TotalHistoricalAmount).Error; err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "sum approved withdrawals failed: %v", err)
|
||
}
|
||
|
||
row := db.Raw(`
|
||
SELECT
|
||
COALESCE(SUM(CASE WHEN CAST(JSON_EXTRACT(content,'$.type') AS UNSIGNED) IN (?,?)
|
||
THEN CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED) ELSE 0 END), 0) AS income,
|
||
COALESCE(SUM(CASE WHEN CAST(JSON_EXTRACT(content,'$.type') AS UNSIGNED) IN (?,?,?)
|
||
THEN CAST(JSON_EXTRACT(content,'$.amount') AS SIGNED) ELSE 0 END), 0) AS refund
|
||
FROM system_logs
|
||
WHERE type = ? AND object_id = ?`,
|
||
log.CommissionTypePurchase, log.CommissionTypeRenewal,
|
||
log.CommissionTypeRefund, log.CommissionTypeWithdrawReject, log.CommissionTypeWithdrawCancel,
|
||
log.TypeCommission.Uint8(), userID,
|
||
).Row()
|
||
if err := row.Scan(&summary.TotalIncomeAmount, &summary.TotalRefundedAmount); err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "aggregate commission logs failed: %v", err)
|
||
}
|
||
|
||
summary.AvailableToWithdraw = summary.CommissionBalance - summary.LockedByPending
|
||
return &summary, nil
|
||
}
|
||
|
||
func (l *QueryWithdrawalLogLogic) queryCommissionRefundLogs(userID int64, page, size int) (*types.QueryWithdrawalLogListResponse, error) {
|
||
queryLogic := NewQueryCommissionReturnLogLogic(l.ctx, l.svcCtx)
|
||
rows, total, err := queryLogic.queryCommissionReturnLogRecordsByEventTypes(userID, page, size, log.CommissionTypeRefund)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
list := make([]types.WithdrawalLog, 0, len(rows))
|
||
for _, row := range rows {
|
||
list = append(list, types.WithdrawalLog{
|
||
Id: row.LogID,
|
||
BizType: withdrawalLogBizTypeCommissionRefund,
|
||
UserId: row.UserID,
|
||
Amount: row.Amount,
|
||
Content: row.Content,
|
||
CreatedAt: row.CreatedAt,
|
||
UpdatedAt: row.UpdatedAt,
|
||
})
|
||
}
|
||
|
||
return &types.QueryWithdrawalLogListResponse{
|
||
List: list,
|
||
Total: total,
|
||
}, nil
|
||
}
|