328 lines
11 KiB
Go
328 lines
11 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"qnc-server/app/user/model"
|
|
"qnc-server/common/ctxdata"
|
|
"qnc-server/common/xerr"
|
|
"qnc-server/pkg/lzkit/lzUtils"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/pkg/errors"
|
|
"github.com/smartwalle/alipay/v3"
|
|
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
|
|
|
"qnc-server/app/user/cmd/api/internal/svc"
|
|
"qnc-server/app/user/cmd/api/internal/types"
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
)
|
|
|
|
// 状态常量
|
|
const (
|
|
StatusProcessing = 1 // 处理中
|
|
StatusSuccess = 2 // 成功
|
|
StatusFailed = 3 // 失败
|
|
)
|
|
|
|
// 前端响应状态
|
|
const (
|
|
WithdrawStatusProcessing = 1
|
|
WithdrawStatusSuccess = 2
|
|
WithdrawStatusFailed = 3
|
|
)
|
|
|
|
type AgentWithdrawalLogic struct {
|
|
logx.Logger
|
|
ctx context.Context
|
|
svcCtx *svc.ServiceContext
|
|
}
|
|
|
|
func NewAgentWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgentWithdrawalLogic {
|
|
return &AgentWithdrawalLogic{
|
|
Logger: logx.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: svcCtx,
|
|
}
|
|
}
|
|
|
|
func (l *AgentWithdrawalLogic) AgentWithdrawal(req *types.WithdrawalReq) (*types.WithdrawalResp, error) {
|
|
var (
|
|
outBizNo string
|
|
withdrawRes = &types.WithdrawalResp{}
|
|
)
|
|
|
|
// 使用事务处理核心操作
|
|
err := l.svcCtx.AgentModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
|
|
userID, err := ctxdata.GetUidFromCtx(l.ctx)
|
|
if err != nil {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %v", err)
|
|
}
|
|
agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
|
|
if err != nil {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理信息失败: %v", err)
|
|
}
|
|
agentRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agentModel.Id)
|
|
if err != nil {
|
|
if errors.Is(err, model.ErrNotFound) {
|
|
return errors.Wrapf(xerr.NewErrMsg("您未进行实名认证, 无法提现"), "您未进行实名认证")
|
|
}
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理实名信息失败: %v", err)
|
|
}
|
|
if agentRealName.Status != model.AgentRealNameStatusApproved {
|
|
return errors.Wrapf(xerr.NewErrMsg("您的实名认证未通过, 无法提现"), "您的实名认证未通过")
|
|
}
|
|
if agentRealName.Name != req.PayeeName {
|
|
return errors.Wrapf(xerr.NewErrMsg("您的实名认证信息不匹配, 无法提现"), "您的实名认证信息不匹配")
|
|
}
|
|
|
|
// 查询钱包
|
|
agentWallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agentModel.Id)
|
|
if err != nil {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理钱包失败: %v", err)
|
|
}
|
|
|
|
// 校验可提现金额
|
|
if req.Amount > agentWallet.Balance {
|
|
return errors.Wrapf(xerr.NewErrMsg("您可提现的余额不足"), "获取用户ID失败")
|
|
}
|
|
|
|
// 生成交易号
|
|
outBizNo = "W_" + l.svcCtx.AlipayService.GenerateOutTradeNo()
|
|
|
|
// 创建提现记录(初始状态为处理中)
|
|
if err = l.createWithdrawalRecord(session, agentModel.Id, req, outBizNo); err != nil {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建提现记录失败: %v", err)
|
|
}
|
|
|
|
// 冻结资金(事务内操作)
|
|
if err = l.freezeFunds(session, agentWallet, req.Amount); err != nil {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "资金冻结失败: %v", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 同步调用支付宝转账
|
|
transferResp, err := l.svcCtx.AlipayService.AliTransfer(l.ctx, req.PayeeAccount, req.PayeeName, req.Amount, "代理提现", outBizNo)
|
|
if err != nil {
|
|
l.handleTransferError(outBizNo, err, "支付宝接口调用失败")
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "支付宝接口调用失败: %v", err)
|
|
}
|
|
|
|
switch {
|
|
case transferResp.Status == "SUCCESS":
|
|
// 立即处理成功状态
|
|
l.handleTransferSuccess(outBizNo, transferResp)
|
|
withdrawRes.Status = WithdrawStatusSuccess
|
|
case transferResp.Status == "FAIL" || transferResp.SubCode != "":
|
|
// 处理明确失败
|
|
errorMsg := l.mapAlipayError(transferResp.SubCode)
|
|
l.handleTransferFailure(outBizNo, transferResp)
|
|
withdrawRes.Status = WithdrawStatusFailed
|
|
withdrawRes.FailMsg = errorMsg
|
|
case transferResp.Status == "DEALING":
|
|
// 处理中状态,启动异步轮询
|
|
go l.startAsyncPolling(outBizNo)
|
|
withdrawRes.Status = WithdrawStatusProcessing
|
|
default:
|
|
// 未知状态按失败处理
|
|
l.handleTransferError(outBizNo, fmt.Errorf("未知状态:%s", transferResp.Status), "支付宝返回未知状态")
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "支付宝接口调用失败: %v", err)
|
|
}
|
|
return withdrawRes, nil
|
|
}
|
|
|
|
// 错误类型映射
|
|
func (l *AgentWithdrawalLogic) mapAlipayError(code string) string {
|
|
errorMapping := map[string]string{
|
|
// 账户存在性错误
|
|
"PAYEE_ACCOUNT_NOT_EXSIT": "收款账户不存在,请检查账号是否正确",
|
|
"PAYEE_NOT_EXIST": "收款账户不存在或姓名有误,请核实信息",
|
|
"PAYEE_ACC_OCUPIED": "收款账号存在多个账户,无法确认唯一性",
|
|
"PAYEE_MID_CANNOT_SAME": "收款方和中间方不能是同一个人,请修改收款方或者中间方信息",
|
|
|
|
// 实名认证问题
|
|
"PAYEE_CERTIFY_LEVEL_LIMIT": "收款方未完成实名认证",
|
|
"PAYEE_NOT_RELNAME_CERTIFY": "收款方未完成实名认证",
|
|
"PAYEE_CERT_INFO_ERROR": "收款方证件信息不匹配",
|
|
|
|
// 账户状态异常
|
|
"PAYEE_ACCOUNT_STATUS_ERROR": "收款账户状态异常,请更换账号",
|
|
"PAYEE_USERINFO_STATUS_ERROR": "收款账户状态异常,无法收款",
|
|
"PERMIT_LIMIT_PAYEE": "收款账户异常,请更换账号",
|
|
"BLOCK_USER_FORBBIDEN_RECIEVE": "账户冻结无法收款",
|
|
"PAYEE_TRUSTEESHIP_ACC_OVER_LIMIT": "收款方托管子户累计收款金额超限",
|
|
|
|
// 账户信息错误
|
|
"PAYEE_USERINFO_ERROR": "收款方姓名或信息不匹配",
|
|
"PAYEE_CARD_INFO_ERROR": "收款支付宝账号及户名不一致",
|
|
"PAYEE_IDENTITY_NOT_MATCH": "收款方身份信息不匹配",
|
|
"PAYEE_USER_IS_INST": "收款方为金融机构,不能使用提现功能,请更换收款账号",
|
|
"PAYEE_USER_TYPE_ERROR": "该支付宝账号类型不支持提现,请更换收款账号",
|
|
|
|
// 权限与限制
|
|
"PAYEE_RECEIVE_COUNT_EXCEED_LIMIT": "收款次数超限,请明日再试",
|
|
"PAYEE_OUT_PERMLIMIT_CHECK_FAILURE": "收款方权限校验不通过",
|
|
"PERMIT_NON_BANK_LIMIT_PAYEE": "收款方未完善身份信息,无法收款",
|
|
}
|
|
if msg, ok := errorMapping[code]; ok {
|
|
return msg
|
|
}
|
|
return "系统错误,请联系客服"
|
|
}
|
|
|
|
// 创建提现记录(事务内操作)
|
|
func (l *AgentWithdrawalLogic) createWithdrawalRecord(session sqlx.Session, agentID int64, req *types.WithdrawalReq, outBizNo string) error {
|
|
record := &model.AgentWithdrawal{
|
|
AgentId: agentID,
|
|
WithdrawNo: outBizNo,
|
|
PayeeAccount: req.PayeeAccount,
|
|
Amount: req.Amount,
|
|
Status: StatusProcessing,
|
|
}
|
|
|
|
_, err := l.svcCtx.AgentWithdrawalModel.Insert(l.ctx, session, record)
|
|
return err
|
|
}
|
|
|
|
// 冻结资金(事务内操作)
|
|
func (l *AgentWithdrawalLogic) freezeFunds(session sqlx.Session, wallet *model.AgentWallet, amount float64) error {
|
|
wallet.Balance -= amount
|
|
wallet.FrozenBalance += amount
|
|
err := l.svcCtx.AgentWalletModel.UpdateWithVersion(l.ctx, session, wallet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 处理异步轮询
|
|
func (l *AgentWithdrawalLogic) startAsyncPolling(outBizNo string) {
|
|
go func() {
|
|
detachedCtx := context.WithoutCancel(l.ctx)
|
|
retryConfig := &backoff.ExponentialBackOff{
|
|
InitialInterval: 10 * time.Second,
|
|
RandomizationFactor: 0.5, // 增加随机因子防止惊群
|
|
Multiplier: 2,
|
|
MaxInterval: 30 * time.Second,
|
|
MaxElapsedTime: 5 * time.Minute, // 缩短总超时
|
|
Clock: backoff.SystemClock,
|
|
}
|
|
retryConfig.Reset()
|
|
operation := func() error {
|
|
statusRsp, err := l.svcCtx.AlipayService.QueryTransferStatus(detachedCtx, outBizNo)
|
|
if err != nil {
|
|
return err // 触发重试
|
|
}
|
|
|
|
switch statusRsp.Status {
|
|
case "SUCCESS":
|
|
l.handleTransferSuccess(outBizNo, statusRsp)
|
|
return nil
|
|
case "FAIL":
|
|
l.handleTransferFailure(outBizNo, statusRsp)
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("转账处理中")
|
|
}
|
|
}
|
|
|
|
err := backoff.RetryNotify(operation,
|
|
backoff.WithContext(retryConfig, detachedCtx),
|
|
func(err error, duration time.Duration) {
|
|
l.Logger.Infof("轮询延迟 outBizNo:%s 等待:%v", outBizNo, duration)
|
|
})
|
|
|
|
if err != nil {
|
|
l.handleTransferTimeout(outBizNo)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// 统一状态更新
|
|
func (l *AgentWithdrawalLogic) updateWithdrawalStatus(outBizNo string, status int64, errorMsg string) {
|
|
detachedCtx := context.WithoutCancel(l.ctx)
|
|
|
|
err := l.svcCtx.AgentModel.Trans(detachedCtx, func(ctx context.Context, session sqlx.Session) error {
|
|
// 获取提现记录
|
|
record, err := l.svcCtx.AgentWithdrawalModel.FindOneByWithdrawNo(l.ctx, outBizNo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 更新状态
|
|
record.Status = status
|
|
record.Remark = lzUtils.StringToNullString(errorMsg)
|
|
if _, err = l.svcCtx.AgentWithdrawalModel.Update(ctx, session, record); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 失败时解冻资金
|
|
if status == StatusFailed {
|
|
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(ctx, record.AgentId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wallet.Balance += record.Amount
|
|
wallet.FrozenBalance -= record.Amount
|
|
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if status == StatusSuccess {
|
|
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(ctx, record.AgentId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wallet.FrozenBalance -= record.Amount
|
|
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
l.Logger.Errorf("状态更新失败 outBizNo:%s error:%v", outBizNo, err)
|
|
}
|
|
}
|
|
|
|
// 成功处理
|
|
func (l *AgentWithdrawalLogic) handleTransferSuccess(outBizNo string, rsp interface{}) {
|
|
l.updateWithdrawalStatus(outBizNo, StatusSuccess, "")
|
|
l.Logger.Infof("提现成功 outBizNo:%s", outBizNo)
|
|
}
|
|
|
|
// 失败处理
|
|
func (l *AgentWithdrawalLogic) handleTransferFailure(outBizNo string, rsp interface{}) {
|
|
var errorMsg string
|
|
if resp, ok := rsp.(*alipay.FundTransUniTransferRsp); ok {
|
|
errorMsg = l.mapAlipayError(resp.SubCode)
|
|
}
|
|
l.updateWithdrawalStatus(outBizNo, StatusFailed, errorMsg)
|
|
l.Logger.Errorf("提现失败 outBizNo:%s reason:%s", outBizNo, errorMsg)
|
|
l.Logger.Errorf("错误响应 rsp:%+v", rsp)
|
|
}
|
|
|
|
// 超时处理
|
|
func (l *AgentWithdrawalLogic) handleTransferTimeout(outBizNo string) {
|
|
l.updateWithdrawalStatus(outBizNo, StatusFailed, "系统处理超时")
|
|
l.Logger.Errorf("轮询超时 outBizNo:%s", outBizNo)
|
|
}
|
|
|
|
// 错误处理
|
|
func (l *AgentWithdrawalLogic) handleTransferError(outBizNo string, err error, contextMsg string) {
|
|
l.updateWithdrawalStatus(outBizNo, StatusFailed, "系统处理异常")
|
|
l.Logger.Errorf("%s outBizNo:%s error:%v", contextMsg, outBizNo, err)
|
|
}
|