新增后台管理
This commit is contained in:
		
							
								
								
									
										327
									
								
								app/main/api/internal/logic/agent/agentwithdrawallogic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								app/main/api/internal/logic/agent/agentwithdrawallogic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,327 @@ | ||||
| package agent | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 	"tydata-server/app/user/model" | ||||
| 	"tydata-server/common/ctxdata" | ||||
| 	"tydata-server/common/xerr" | ||||
| 	"tydata-server/pkg/lzkit/lzUtils" | ||||
|  | ||||
| 	"github.com/cenkalti/backoff/v4" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/smartwalle/alipay/v3" | ||||
| 	"github.com/zeromicro/go-zero/core/stores/sqlx" | ||||
|  | ||||
| 	"tydata-server/app/user/cmd/api/internal/svc" | ||||
| 	"tydata-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.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.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) | ||||
| } | ||||
|  | ||||
| // 超时处理 | ||||
| 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) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user