package agent import ( "context" "database/sql" "fmt" "qnc-server/app/main/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/main/api/internal/svc" "qnc-server/app/main/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{} ) var finalWithdrawAmount float64 // 实际到账金额 // 使用事务处理核心操作 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.DB_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.freezeFunds(session, agentWallet, req.Amount); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "资金冻结失败: %v", err) } yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month())) // 计算税务额度 taxExemption, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.FindOneByAgentIdYearMonth(l.ctx, agentModel.Id, yearMonth) if err != nil { if errors.Is(err, model.ErrNotFound) { taxExemption, err = l.createMonthlyExemption(session, agentModel.Id, yearMonth) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建代理税务额度失败: %v", err) } } else { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理税务额度失败: %v", err) } } var taxAmount float64 // 应缴税费 var taxDeductionPart float64 // 应税金额 var TaxStatus int64 // 扣税状态 var exemptionAmount float64 // 免税金额 taxRate := l.svcCtx.Config.TaxConfig.TaxRate if taxExemption.RemainingExemptionAmount < req.Amount { // 超过免税额度需要扣税 exemptionAmount = taxExemption.RemainingExemptionAmount // 免税金额 = 剩余免税额度 TaxStatus = model.TaxStatusPending // 扣税状态 = 待扣税 taxDeductionPart = req.Amount - taxExemption.RemainingExemptionAmount // 应税金额 = 提现金额 - 剩余免税额度 taxAmount = taxDeductionPart * taxRate // 应缴税费 = 应税金额 * 税率 finalWithdrawAmount = req.Amount - taxAmount // 实际到账金额 = 提现金额 - 应缴税费 taxExemption.UsedExemptionAmount += exemptionAmount // 已使用免税额度 = 已使用免税额度 + 免税金额 taxExemption.RemainingExemptionAmount -= exemptionAmount // 剩余免税额度 = 剩余免税额度 - 免税金额 err = l.svcCtx.AgentWithdrawalTaxExemptionModel.UpdateWithVersion(l.ctx, session, taxExemption) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新代理税务额度失败: %v", err) } } else { // 未超过免税额度,免税 exemptionAmount = req.Amount // 免税金额 = 提现金额 TaxStatus = model.TaxStatusExempt // 扣税状态 = 免税 taxDeductionPart = 0 // 应税金额 = 0 finalWithdrawAmount = req.Amount // 实际到账金额 = 提现金额 taxAmount = 0 // 应缴税费 = 0 taxExemption.UsedExemptionAmount += exemptionAmount // 已使用免税额度 = 已使用免税额度 + 免税金额 taxExemption.RemainingExemptionAmount -= exemptionAmount // 剩余免税额度 = 剩余免税额度 - 免税金额 err = l.svcCtx.AgentWithdrawalTaxExemptionModel.UpdateWithVersion(l.ctx, session, taxExemption) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新代理税务额度失败: %v", err) } } // 创建提现记录(初始状态为处理中) withdrawalID, err := l.createWithdrawalRecord(session, agentModel.Id, req.PayeeAccount, req.Amount, finalWithdrawAmount, taxAmount, outBizNo) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建提现记录失败: %v", err) } // 扣税记录 taxModel := &model.AgentWithdrawalTax{ AgentId: agentModel.Id, YearMonth: yearMonth, WithdrawalId: withdrawalID, WithdrawalAmount: req.Amount, ExemptionAmount: exemptionAmount, TaxableAmount: taxDeductionPart, TaxRate: taxRate, TaxAmount: taxAmount, ActualAmount: finalWithdrawAmount, TaxStatus: TaxStatus, Remark: sql.NullString{String: "提现成功自动扣税", Valid: true}, ExemptionRecordId: taxExemption.Id, } _, err = l.svcCtx.AgentWithdrawalTaxModel.Insert(ctx, session, taxModel) if 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, finalWithdrawAmount, "代理提现", outBizNo) if err != nil { l.Logger.Errorf("【支付宝转账失败】outBizNo:%s error:%v", outBizNo, err) 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, payeeAccount string, amount float64, finalWithdrawAmount float64, taxAmount float64, outBizNo string) (int64, error) { record := &model.AgentWithdrawal{ AgentId: agentID, WithdrawNo: outBizNo, PayeeAccount: payeeAccount, Amount: amount, ActualAmount: finalWithdrawAmount, TaxAmount: taxAmount, Status: StatusProcessing, } result, err := l.svcCtx.AgentWithdrawalModel.Insert(l.ctx, session, record) if err != nil { return 0, err } return result.LastInsertId() } // 冻结资金(事务内操作) 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 } taxModel, err := l.svcCtx.AgentWithdrawalTaxModel.FindOneByWithdrawalId(ctx, record.Id) if err != nil { return err } if taxModel.TaxStatus == model.TaxStatusPending { taxModel.TaxStatus = model.TaxStatusFailed // 扣税状态 = 失败 taxModel.TaxTime = sql.NullTime{Time: time.Now(), Valid: true} if err := l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(ctx, session, taxModel); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新扣税记录失败: %v", err) } } taxExemption, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.FindOne(ctx, taxModel.ExemptionRecordId) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理税务额度失败: %v", err) } taxExemption.UsedExemptionAmount -= taxModel.ExemptionAmount taxExemption.RemainingExemptionAmount += taxModel.ExemptionAmount if err := l.svcCtx.AgentWithdrawalTaxExemptionModel.UpdateWithVersion(ctx, session, taxExemption); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新代理税务额度失败: %v", 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 } taxModel, err := l.svcCtx.AgentWithdrawalTaxModel.FindOneByWithdrawalId(ctx, record.Id) if err != nil { return err } if taxModel.TaxStatus == model.TaxStatusPending { taxModel.TaxStatus = model.TaxStatusSuccess // 扣税状态 = 成功 taxModel.TaxTime = sql.NullTime{Time: time.Now(), Valid: true} if err := l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(ctx, session, taxModel); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新扣税记录失败: %v", 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) } // 超时处理 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) } func (l *AgentWithdrawalLogic) createMonthlyExemption(session sqlx.Session, agentId int64, yearMonth int64) (*model.AgentWithdrawalTaxExemption, error) { exemption := &model.AgentWithdrawalTaxExemption{ AgentId: agentId, YearMonth: yearMonth, TotalExemptionAmount: l.svcCtx.Config.TaxConfig.TaxExemptionAmount, UsedExemptionAmount: 0.00, RemainingExemptionAmount: l.svcCtx.Config.TaxConfig.TaxExemptionAmount, } result, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.Insert(l.ctx, session, exemption) if err != nil { return nil, err } id, _ := result.LastInsertId() exemption.Id = id return exemption, nil }