313 lines
10 KiB
Go
313 lines
10 KiB
Go
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
|
||
}
|