package agent import ( "context" "fmt" "time" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/lzUtils" "github.com/google/uuid" "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 ApplyWithdrawalLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewApplyWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyWithdrawalLogic { return &ApplyWithdrawalLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (resp *types.ApplyWithdrawalResp, err error) { userID, err := ctxdata.GetUidFromCtx(l.ctx) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) } // 1. 获取代理信息 agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) if err != nil { if errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) } // 2. 验证实名认证 realName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id) if err != nil { if errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "") } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询实名认证失败, %v", err) } // 检查是否已通过三要素核验(verify_time不为空表示已通过) if !realName.VerifyTime.Valid { return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "") } // 3. 验证提现方式 if req.WithdrawalType != 1 && req.WithdrawalType != 2 { return nil, errors.Wrapf(xerr.NewErrMsg("提现方式无效"), "") } // 4. 验证提现信息 if req.WithdrawalType == 1 { // 支付宝提现:验证支付宝账号和姓名 if req.PayeeAccount == "" { return nil, errors.Wrapf(xerr.NewErrMsg("请输入支付宝账号"), "") } if req.PayeeName == "" { return nil, errors.Wrapf(xerr.NewErrMsg("请输入收款人姓名"), "") } } else if req.WithdrawalType == 2 { // 银行卡提现:验证银行卡号、开户行和姓名 if req.BankCardNo == "" { return nil, errors.Wrapf(xerr.NewErrMsg("请输入银行卡号"), "") } if req.BankName == "" { return nil, errors.Wrapf(xerr.NewErrMsg("请输入开户行名称"), "") } if req.PayeeName == "" { return nil, errors.Wrapf(xerr.NewErrMsg("请输入收款人姓名"), "") } } // 5. 验证提现金额 if req.Amount <= 0 { return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "") } // 6. 获取钱包信息 wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败, %v", err) } // 7. 验证余额(包括检查是否为负数) if wallet.Balance < 0 { return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("账户存在欠款,请先补足欠款后再申请提现,当前余额:%.2f", wallet.Balance)), "") } if wallet.Balance < req.Amount { return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "") } // 8. 支付宝月度提现额度校验(仅针对支付宝提现) if req.WithdrawalType == 1 { now := time.Now() monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) nextMonthStart := monthStart.AddDate(0, 1, 0) // 8.1 获取支付宝月度额度配置(默认 800 元) alipayQuota := 800.0 if cfg, cfgErr := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, "alipay_month_quota"); cfgErr == nil { if parsed, parseErr := l.parseFloat(cfg.ConfigValue); parseErr == nil && parsed > 0 { alipayQuota = parsed } } // 8.2 统计本月已申请/成功的支付宝提现金额(status IN (1,5)),避免多次申请占用超额 withdrawBuilder := l.svcCtx.AgentWithdrawalModel.SelectBuilder(). Where("agent_id = ? AND withdrawal_type = ? AND status IN (1,5) AND create_time >= ? AND create_time < ?", agent.Id, 1, monthStart, nextMonthStart) usedAmount, sumErr := l.svcCtx.AgentWithdrawalModel.FindSum(l.ctx, withdrawBuilder, "amount") if sumErr != nil { return nil, errors.Wrapf(sumErr, "查询本月支付宝提现额度使用情况失败") } remainQuota := alipayQuota - usedAmount if remainQuota <= 0 { return nil, errors.Wrapf( xerr.NewErrMsg(fmt.Sprintf("本月支付宝提现额度已用完(额度:%.2f 元),请使用银行卡提现", alipayQuota)), "", ) } if req.Amount > remainQuota { return nil, errors.Wrapf( xerr.NewErrMsg(fmt.Sprintf("本月支付宝最高可提现 %.2f 元,请调整提现金额或使用银行卡提现", remainQuota)), "", ) } } // 9. 计算税费 yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month())) taxInfo, err := l.calculateTax(l.ctx, agent.Id, req.Amount, yearMonth) if err != nil { return nil, errors.Wrapf(err, "计算税费失败") } // 10. 生成提现单号(WD开头 + GenerateOutTradeNo生成的订单号,确保总长度不超过32个字符) orderNo := l.svcCtx.AlipayService.GenerateOutTradeNo() withdrawNo := "WD" + orderNo // 确保总长度不超过32个字符 if len(withdrawNo) > 32 { withdrawNo = withdrawNo[:32] } // 11. 使用事务处理提现申请 var withdrawalId string err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { // 11.1 冻结余额 wallet.FrozenBalance += req.Amount wallet.Balance -= req.Amount if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil { return errors.Wrapf(err, "冻结余额失败") } // 11.2 创建提现记录 withdrawal := &model.AgentWithdrawal{ Id: uuid.New().String(), AgentId: agent.Id, WithdrawNo: withdrawNo, WithdrawalType: req.WithdrawalType, PayeeAccount: req.PayeeAccount, PayeeName: req.PayeeName, Amount: req.Amount, ActualAmount: taxInfo.ActualAmount, TaxAmount: taxInfo.TaxAmount, Status: 1, // 待审核 } // 如果是银行卡提现,设置银行卡相关字段 if req.WithdrawalType == 2 { withdrawal.BankCardNo = lzUtils.StringToNullString(req.BankCardNo) withdrawal.BankName = lzUtils.StringToNullString(req.BankName) // 银行卡提现时,payee_account 可以存储银行卡号(便于查询),也可以留空 if req.PayeeAccount == "" { withdrawal.PayeeAccount = req.BankCardNo } } _, err := l.svcCtx.AgentWithdrawalModel.Insert(transCtx, session, withdrawal) if err != nil { return errors.Wrapf(err, "创建提现记录失败") } withdrawalId = withdrawal.Id // 11.3 创建扣税记录 taxRecord := &model.AgentWithdrawalTax{ AgentId: agent.Id, WithdrawalId: withdrawalId, YearMonth: yearMonth, WithdrawalAmount: req.Amount, TaxableAmount: taxInfo.TaxableAmount, TaxRate: taxInfo.TaxRate, TaxAmount: taxInfo.TaxAmount, ActualAmount: taxInfo.ActualAmount, TaxStatus: 1, // 待扣税 Remark: lzUtils.StringToNullString("提现申请"), } if _, err := l.svcCtx.AgentWithdrawalTaxModel.Insert(transCtx, session, taxRecord); err != nil { return errors.Wrapf(err, "创建扣税记录失败") } return nil }) if err != nil { return nil, err } return &types.ApplyWithdrawalResp{ WithdrawalId: withdrawalId, WithdrawalNo: withdrawNo, }, nil } // TaxInfo 税费信息 type TaxInfo struct { TaxableAmount float64 // 应税金额 TaxRate float64 // 税率 TaxAmount float64 // 税费金额 ActualAmount float64 // 实际到账金额 } // calculateTax 计算税费 func (l *ApplyWithdrawalLogic) calculateTax(ctx context.Context, agentId string, amount float64, yearMonth int64) (*TaxInfo, error) { // 获取税率配置(默认6%) taxRate := 0.06 config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(ctx, "tax_rate") if err == nil { if parsedRate, parseErr := l.parseFloat(config.ConfigValue); parseErr == nil { taxRate = parsedRate } } // 查询本月已提现金额 // 注意:FindAll 方法会自动添加 del_state = ? 条件,所以这里不需要手动添加 // 这里对 year_month 使用反引号包裹,避免与某些数据库版本/SQL 模式下的关键字冲突 builder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder(). Where("agent_id = ? AND `year_month` = ?", agentId, yearMonth) taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(ctx, builder, "") if err != nil { return nil, errors.Wrapf(err, "查询月度提现记录失败") } // 计算本月累计提现金额 monthlyTotal := 0.0 for _, record := range taxRecords { monthlyTotal += record.WithdrawalAmount } // 获取免税额度配置(默认0,即无免税额度) exemptionAmount := 0.0 exemptionConfig, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(ctx, "tax_exemption_amount") if err == nil { if parsedAmount, parseErr := l.parseFloat(exemptionConfig.ConfigValue); parseErr == nil { exemptionAmount = parsedAmount } } // 计算应税金额 // 如果本月累计 + 本次提现金额 <= 免税额度,则本次提现免税 // 否则,应税金额 = 本次提现金额 - (免税额度 - 本月累计)(如果免税额度 > 本月累计) taxableAmount := amount if exemptionAmount > 0 { remainingExemption := exemptionAmount - monthlyTotal if remainingExemption > 0 { if amount <= remainingExemption { // 本次提现完全免税 taxableAmount = 0 } else { // 部分免税 taxableAmount = amount - remainingExemption } } } // 计算税费 taxAmount := taxableAmount * taxRate actualAmount := amount - taxAmount return &TaxInfo{ TaxableAmount: taxableAmount, TaxRate: taxRate, TaxAmount: taxAmount, ActualAmount: actualAmount, }, nil } // parseFloat 解析浮点数 func (l *ApplyWithdrawalLogic) parseFloat(s string) (float64, error) { var result float64 _, err := fmt.Sscanf(s, "%f", &result) return result, err }