2025-11-27 13:09:54 +08:00
|
|
|
|
package admin_agent
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"database/sql"
|
|
|
|
|
|
"fmt"
|
2026-01-12 16:43:08 +08:00
|
|
|
|
"os"
|
2025-11-27 13:09:54 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
"ycc-server/common/xerr"
|
|
|
|
|
|
"ycc-server/pkg/lzkit/lzUtils"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
|
|
|
|
|
|
|
|
|
|
|
"ycc-server/app/main/api/internal/svc"
|
|
|
|
|
|
"ycc-server/app/main/api/internal/types"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type AdminAuditWithdrawalLogic struct {
|
|
|
|
|
|
logx.Logger
|
|
|
|
|
|
ctx context.Context
|
|
|
|
|
|
svcCtx *svc.ServiceContext
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewAdminAuditWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminAuditWithdrawalLogic {
|
|
|
|
|
|
return &AdminAuditWithdrawalLogic{
|
|
|
|
|
|
Logger: logx.WithContext(ctx),
|
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
|
svcCtx: svcCtx,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 16:43:08 +08:00
|
|
|
|
// parseFloat 解析配置中的浮点数
|
|
|
|
|
|
func (l *AdminAuditWithdrawalLogic) parseFloat(s string) (float64, error) {
|
|
|
|
|
|
var result float64
|
|
|
|
|
|
_, err := fmt.Sscanf(s, "%f", &result)
|
|
|
|
|
|
return result, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 13:09:54 +08:00
|
|
|
|
func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWithdrawalReq) (resp *types.AdminAuditWithdrawalResp, err error) {
|
|
|
|
|
|
// 1. 查询提现记录
|
|
|
|
|
|
withdrawal, err := l.svcCtx.AgentWithdrawalModel.FindOne(l.ctx, req.WithdrawalId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录失败, %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 检查状态(必须是待审核状态)
|
|
|
|
|
|
if withdrawal.Status != 1 {
|
|
|
|
|
|
return nil, errors.Wrapf(xerr.NewErrMsg("该提现记录已处理"), "")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 使用事务处理审核
|
|
|
|
|
|
err = l.svcCtx.AgentWithdrawalModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
|
2026-01-12 18:44:14 +08:00
|
|
|
|
switch req.Status {
|
|
|
|
|
|
case 2: // 审核通过
|
2026-01-12 16:43:08 +08:00
|
|
|
|
// 4.1 根据提现方式处理
|
2026-01-12 18:44:14 +08:00
|
|
|
|
switch withdrawal.WithdrawalType {
|
|
|
|
|
|
case 1:
|
2026-01-12 16:43:08 +08:00
|
|
|
|
// 支付宝提现:审核通过前再次校验月度额度,避免一次性通过多笔超限
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
|
|
|
|
nextMonthStart := monthStart.AddDate(0, 1, 0)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取支付宝月度额度配置(默认 800 元)
|
|
|
|
|
|
alipayQuota := 800.0
|
|
|
|
|
|
if cfg, cfgErr := l.svcCtx.AgentConfigModel.FindOneByConfigKey(transCtx, "alipay_month_quota"); cfgErr == nil {
|
|
|
|
|
|
if parsed, parseErr := l.parseFloat(cfg.ConfigValue); parseErr == nil && parsed > 0 {
|
|
|
|
|
|
alipayQuota = parsed
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-27 13:09:54 +08:00
|
|
|
|
|
2026-01-12 16:43:08 +08:00
|
|
|
|
// 统计本月已成功的支付宝提现金额(status=5)
|
|
|
|
|
|
withdrawBuilder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
|
|
|
|
|
|
Where("agent_id = ? AND withdrawal_type = ? AND status = ? AND create_time >= ? AND create_time < ?",
|
|
|
|
|
|
withdrawal.AgentId, 1, 5, monthStart, nextMonthStart)
|
|
|
|
|
|
usedAmount, sumErr := l.svcCtx.AgentWithdrawalModel.FindSum(transCtx, withdrawBuilder, "amount")
|
|
|
|
|
|
if sumErr != nil {
|
|
|
|
|
|
return errors.Wrapf(sumErr, "查询本月支付宝提现额度使用情况失败")
|
|
|
|
|
|
}
|
2025-11-27 13:09:54 +08:00
|
|
|
|
|
2026-01-12 16:43:08 +08:00
|
|
|
|
if usedAmount+withdrawal.Amount > alipayQuota {
|
|
|
|
|
|
// 超出额度,不允许通过,保持待审核状态并提示原因
|
|
|
|
|
|
withdrawal.Status = 1
|
|
|
|
|
|
withdrawal.Remark = sql.NullString{
|
|
|
|
|
|
String: fmt.Sprintf("超过本月支付宝提现额度(限额:%.2f 元,已用:%.2f 元),请使用银行卡提现或调整金额", alipayQuota, usedAmount),
|
|
|
|
|
|
Valid: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新提现记录失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
return errors.Wrapf(xerr.NewErrMsg("超过本月支付宝提现额度,无法通过该笔提现"), "")
|
2025-11-27 13:09:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 16:43:08 +08:00
|
|
|
|
// 支付宝提现:开发环境下做模拟,不调用真实支付宝转账
|
|
|
|
|
|
mockTransferStatus := "SUCCESS"
|
|
|
|
|
|
var transferResp struct {
|
|
|
|
|
|
Status string
|
|
|
|
|
|
SubCode string
|
|
|
|
|
|
}
|
2025-11-27 13:09:54 +08:00
|
|
|
|
|
2026-01-12 16:43:08 +08:00
|
|
|
|
if os.Getenv("ENV") == "development" {
|
|
|
|
|
|
transferResp.Status = mockTransferStatus
|
|
|
|
|
|
logx.Infof("【DEV】模拟支付宝转账成功,withdrawNo=%s, amount=%.2f, payee=%s",
|
|
|
|
|
|
withdrawal.WithdrawNo, withdrawal.ActualAmount, withdrawal.PayeeAccount)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 生产环境:同步调用支付宝转账接口
|
|
|
|
|
|
outBizNo := withdrawal.WithdrawNo
|
|
|
|
|
|
resp, err := l.svcCtx.AlipayService.AliTransfer(transCtx, withdrawal.PayeeAccount, withdrawal.PayeeName, withdrawal.ActualAmount, "代理提现", outBizNo)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 调用失败:保持状态为待审核(1),只记录备注,方便管理员重试
|
|
|
|
|
|
withdrawal.Status = 1 // 待审核
|
|
|
|
|
|
withdrawal.Remark = sql.NullString{String: fmt.Sprintf("支付宝转账调用失败: %v", err), Valid: true}
|
|
|
|
|
|
_ = l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal)
|
|
|
|
|
|
|
|
|
|
|
|
return errors.Wrapf(err, "支付宝转账失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
transferResp.Status = resp.Status
|
|
|
|
|
|
transferResp.SubCode = resp.SubCode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4.2 根据转账结果更新状态
|
|
|
|
|
|
switch transferResp.Status {
|
|
|
|
|
|
case "SUCCESS":
|
|
|
|
|
|
// 转账成功
|
|
|
|
|
|
withdrawal.Status = 5 // 提现成功
|
|
|
|
|
|
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新提现记录失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新钱包(解冻并扣除)
|
|
|
|
|
|
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "查询钱包失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
wallet.FrozenBalance -= withdrawal.Amount
|
|
|
|
|
|
wallet.WithdrawnAmount += withdrawal.Amount
|
|
|
|
|
|
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新钱包失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新扣税记录状态
|
|
|
|
|
|
taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
|
|
|
|
|
|
Where("withdrawal_id = ?", withdrawal.Id)
|
|
|
|
|
|
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "")
|
|
|
|
|
|
if err == nil && len(taxRecords) > 0 {
|
|
|
|
|
|
taxRecord := taxRecords[0]
|
|
|
|
|
|
taxRecord.TaxStatus = 2 // 已扣税
|
|
|
|
|
|
taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now())
|
|
|
|
|
|
l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case "FAIL":
|
|
|
|
|
|
// 转账失败:保持待审核状态,方便人工处理或重试
|
|
|
|
|
|
withdrawal.Status = 1 // 待审核
|
|
|
|
|
|
errorMsg := l.mapAlipayError(transferResp.SubCode)
|
|
|
|
|
|
withdrawal.Remark = sql.NullString{String: errorMsg, Valid: true}
|
|
|
|
|
|
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新提现记录失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case "DEALING":
|
|
|
|
|
|
// 处理中:同样保持待审核状态(1),由管理员后续确认
|
|
|
|
|
|
withdrawal.Status = 1
|
|
|
|
|
|
withdrawal.Remark = sql.NullString{String: "支付宝处理中,请稍后重试或联系平台", Valid: true}
|
|
|
|
|
|
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新提现记录失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-12 18:44:14 +08:00
|
|
|
|
|
|
|
|
|
|
case 2:
|
2026-01-12 16:43:08 +08:00
|
|
|
|
// 银行卡提现:审核通过即视为提现成功(线下已/将立即打款)
|
2025-11-27 13:09:54 +08:00
|
|
|
|
withdrawal.Status = 5 // 提现成功
|
2026-01-12 16:43:08 +08:00
|
|
|
|
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}
|
2025-11-27 13:09:54 +08:00
|
|
|
|
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新提现记录失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新钱包(解冻并扣除)
|
|
|
|
|
|
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "查询钱包失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
wallet.FrozenBalance -= withdrawal.Amount
|
|
|
|
|
|
wallet.WithdrawnAmount += withdrawal.Amount
|
|
|
|
|
|
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新钱包失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新扣税记录状态
|
|
|
|
|
|
taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
|
2026-01-12 16:43:08 +08:00
|
|
|
|
Where("withdrawal_id = ?", withdrawal.Id)
|
2025-11-27 13:09:54 +08:00
|
|
|
|
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "")
|
|
|
|
|
|
if err == nil && len(taxRecords) > 0 {
|
|
|
|
|
|
taxRecord := taxRecords[0]
|
|
|
|
|
|
taxRecord.TaxStatus = 2 // 已扣税
|
|
|
|
|
|
taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now())
|
|
|
|
|
|
l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 18:44:14 +08:00
|
|
|
|
case 3: // 审核拒绝
|
2025-11-27 13:09:54 +08:00
|
|
|
|
// 4.1 更新提现记录状态为拒绝
|
|
|
|
|
|
withdrawal.Status = 3 // 审核拒绝
|
|
|
|
|
|
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}
|
|
|
|
|
|
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新提现记录失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4.2 解冻余额
|
|
|
|
|
|
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "查询钱包失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
wallet.FrozenBalance -= withdrawal.Amount
|
|
|
|
|
|
wallet.Balance += withdrawal.Amount
|
|
|
|
|
|
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
|
|
|
|
|
|
return errors.Wrapf(err, "更新钱包失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &types.AdminAuditWithdrawalResp{
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// mapAlipayError 映射支付宝错误码
|
|
|
|
|
|
func (l *AdminAuditWithdrawalLogic) mapAlipayError(code string) string {
|
|
|
|
|
|
errorMapping := map[string]string{
|
|
|
|
|
|
"PAYEE_USERINFO_ERROR": "收款方姓名或信息不匹配",
|
|
|
|
|
|
"PAYEE_CARD_INFO_ERROR": "收款支付宝账号及户名不一致",
|
|
|
|
|
|
"PAYEE_IDENTITY_NOT_MATCH": "收款方身份信息不匹配",
|
|
|
|
|
|
"PAYEE_USER_IS_INST": "收款方为金融机构,不能使用提现功能",
|
|
|
|
|
|
"PAYEE_USER_TYPE_ERROR": "该支付宝账号类型不支持提现",
|
|
|
|
|
|
}
|
|
|
|
|
|
if msg, ok := errorMapping[code]; ok {
|
|
|
|
|
|
return msg
|
|
|
|
|
|
}
|
|
|
|
|
|
return "系统错误,请联系客服"
|
|
|
|
|
|
}
|