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 }