This commit is contained in:
2025-12-24 17:38:08 +08:00
parent e1fdf7e77f
commit d633c6741e
15 changed files with 1162 additions and 34 deletions

View File

@@ -61,6 +61,10 @@ service main {
// 代理会员配置编辑
@handler AdminUpdateAgentMembershipConfig
post /agent-membership-config/update (AdminUpdateAgentMembershipConfigReq) returns (AdminUpdateAgentMembershipConfigResp)
// 银行卡提现审核(确认/拒绝)
@handler AdminReviewBankCardWithdrawal
post /agent-withdrawal/bank-card/review (AdminReviewBankCardWithdrawalReq) returns (AdminReviewBankCardWithdrawalResp)
}
type (
@@ -193,6 +197,10 @@ type (
PayeeAccount string `json:"payee_account"` // 收款账户
Remark string `json:"remark"` // 备注
CreateTime string `json:"create_time"` // 创建时间
WithdrawType int64 `json:"withdraw_type"` // 提现类型:1-支付宝,2-银行卡
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
PayeeName string `json:"payee_name"` // 收款人姓名
}
// 代理提现分页查询响应
@@ -383,4 +391,16 @@ type (
AdminUpdateAgentMembershipConfigResp {
Success bool `json:"success"` // 是否成功
}
// 银行卡提现审核请求
AdminReviewBankCardWithdrawalReq {
WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID
Action int64 `json:"action"` // 操作:1-确认,2-拒绝
Remark string `json:"remark"` // 备注(拒绝时必填)
}
// 银行卡提现审核响应
AdminReviewBankCardWithdrawalResp {
Success bool `json:"success"` // 是否成功
}
)

View File

@@ -278,6 +278,14 @@ service main {
@handler GetAgentWithdrawalTaxExemption
get /withdrawal/tax/exemption (GetWithdrawalTaxExemptionReq) returns (GetWithdrawalTaxExemptionResp)
// 银行卡提现申请
@handler BankCardWithdrawal
post /withdrawal/bank-card (BankCardWithdrawalReq) returns (WithdrawalResp)
// 获取历史银行卡信息
@handler GetBankCardInfo
get /withdrawal/bank-card/info (GetBankCardInfoReq) returns (GetBankCardInfoResp)
}
type (
@@ -383,6 +391,21 @@ type (
RemainingExemptionAmount float64 `json:"remaining_exemption_amount"`
TaxRate float64 `json:"tax_rate"`
}
// 银行卡提现申请请求
BankCardWithdrawalReq {
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
Amount float64 `json:"amount"` // 提现金额
}
// 获取历史银行卡信息请求
GetBankCardInfoReq {}
// 获取历史银行卡信息响应
GetBankCardInfoResp {
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
PayeeName string `json:"payee_name"` // 收款人姓名
IdCard string `json:"id_card"` // 身份证号
}
)
@server (

View File

@@ -0,0 +1,29 @@
package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"tydata-server/app/main/api/internal/logic/admin_agent"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/api/internal/types"
"tydata-server/common/result"
"tydata-server/pkg/lzkit/validator"
)
func AdminReviewBankCardWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminReviewBankCardWithdrawalReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminReviewBankCardWithdrawalLogic(r.Context(), svcCtx)
resp, err := l.AdminReviewBankCardWithdrawal(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,29 @@
package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"tydata-server/app/main/api/internal/logic/agent"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/api/internal/types"
"tydata-server/common/result"
"tydata-server/pkg/lzkit/validator"
)
func BankCardWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BankCardWithdrawalReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewBankCardWithdrawalLogic(r.Context(), svcCtx)
resp, err := l.BankCardWithdrawal(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,29 @@
package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"tydata-server/app/main/api/internal/logic/agent"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/api/internal/types"
"tydata-server/common/result"
"tydata-server/pkg/lzkit/validator"
)
func GetBankCardInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetBankCardInfoReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetBankCardInfoLogic(r.Context(), svcCtx)
resp, err := l.GetBankCardInfo(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -87,6 +87,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/agent-reward/list",
Handler: admin_agent.AdminGetAgentRewardListHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/agent-withdrawal/bank-card/review",
Handler: admin_agent.AdminReviewBankCardWithdrawalHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/agent-withdrawal/list",
@@ -765,6 +770,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/withdrawal",
Handler: agent.AgentWithdrawalHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/withdrawal/bank-card",
Handler: agent.BankCardWithdrawalHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/withdrawal/bank-card/info",
Handler: agent.GetBankCardInfoHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/withdrawal/tax/exemption",

View File

@@ -49,6 +49,12 @@ func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *type
item.Remark = v.Remark.String
}
item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05")
// 设置银行卡信息如果模型有这些字段copier会自动复制如果没有这里手动设置默认值
// 注意:如果数据库还没有迁移,这些字段可能不存在,需要先注释
// 如果模型有这些字段copier会自动复制这里不需要手动设置
// 如果模型没有这些字段,需要等数据库迁移后重新生成模型
items = append(items, item)
}
resp = &types.AdminGetAgentWithdrawalListResp{

View File

@@ -0,0 +1,188 @@
package admin_agent
import (
"context"
"database/sql"
"time"
"tydata-server/app/main/model"
"tydata-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
// 审核操作常量
const (
ReviewActionApprove = 1 // 确认
ReviewActionReject = 2 // 拒绝
)
// 状态常量
const (
StatusPending = 1 // 申请中/处理中
StatusSuccess = 2 // 成功
StatusFailed = 3 // 失败
)
// 提现类型常量
const (
WithdrawTypeAlipay = 1 // 支付宝提现
WithdrawTypeBankCard = 2 // 银行卡提现
)
type AdminReviewBankCardWithdrawalLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminReviewBankCardWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminReviewBankCardWithdrawalLogic {
return &AdminReviewBankCardWithdrawalLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AdminReviewBankCardWithdrawalLogic) AdminReviewBankCardWithdrawal(req *types.AdminReviewBankCardWithdrawalReq) (resp *types.AdminReviewBankCardWithdrawalResp, err error) {
// 验证操作类型
if req.Action != ReviewActionApprove && req.Action != ReviewActionReject {
return nil, errors.Wrapf(xerr.NewErrMsg("操作类型不正确"), "操作类型验证失败")
}
// 拒绝操作必须填写备注
if req.Action == ReviewActionReject && req.Remark == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("拒绝提现必须填写拒绝原因"), "拒绝原因验证失败")
}
resp = &types.AdminReviewBankCardWithdrawalResp{
Success: false,
}
// 使用事务处理审核操作
err = l.svcCtx.AgentModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 获取提现记录
record, err := l.svcCtx.AgentWithdrawalModel.FindOne(ctx, req.WithdrawalId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrMsg("提现记录不存在"), "提现记录不存在")
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录失败: %v", err)
}
// 验证提现记录状态必须是申请中
if record.Status != StatusPending {
return errors.Wrapf(xerr.NewErrMsg("该提现记录已处理,无法重复操作"), "状态验证失败")
}
// 验证提现类型如果模型有WithdrawType字段
// 注意:如果数据库还没有迁移,可以先注释这个验证
// if record.WithdrawType != WithdrawTypeBankCard {
// return errors.Wrapf(xerr.NewErrMsg("该记录不是银行卡提现,无法审核"), "提现类型验证失败")
// }
if req.Action == ReviewActionApprove {
// 确认提现
return l.approveWithdrawal(ctx, session, record)
} else {
// 拒绝提现
return l.rejectWithdrawal(ctx, session, record, req.Remark)
}
})
if err != nil {
return nil, err
}
resp.Success = true
return resp, nil
}
// 确认提现
func (l *AdminReviewBankCardWithdrawalLogic) approveWithdrawal(ctx context.Context, session sqlx.Session, record *model.AgentWithdrawal) error {
// 更新提现记录状态为成功
record.Status = StatusSuccess
record.Remark = sql.NullString{String: "管理员确认提现", Valid: true}
if _, err := l.svcCtx.AgentWithdrawalModel.Update(ctx, session, record); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新提现记录失败: %v", err)
}
// 解冻资金并扣除FrozenBalance -= amount, Balance不变
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(ctx, record.AgentId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败: %v", err)
}
wallet.FrozenBalance -= record.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新钱包失败: %v", err)
}
// 更新扣税记录状态为成功
taxModel, err := l.svcCtx.AgentWithdrawalTaxModel.FindOneByWithdrawalId(ctx, record.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询扣税记录失败: %v", 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)
}
}
// 提现成功后,给上级代理发放提现奖励
withdrawRewardErr := l.svcCtx.AgentService.GiveWithdrawReward(ctx, record.AgentId, record.Amount, session)
if withdrawRewardErr != nil {
l.Logger.Errorf("发放提现奖励失败代理ID%d提现金额%f错误%+v", record.AgentId, record.Amount, withdrawRewardErr)
// 提现奖励失败不影响主流程,只记录日志
} else {
l.Logger.Infof("发放提现奖励成功代理ID%d提现金额%f", record.AgentId, record.Amount)
}
l.Logger.Infof("银行卡提现确认成功 withdrawalId:%d amount:%f", record.Id, record.Amount)
return nil
}
// 拒绝提现
func (l *AdminReviewBankCardWithdrawalLogic) rejectWithdrawal(ctx context.Context, session sqlx.Session, record *model.AgentWithdrawal, remark string) error {
// 更新提现记录状态为失败
record.Status = StatusFailed
record.Remark = sql.NullString{String: "管理员拒绝:" + remark, Valid: true}
if _, err := l.svcCtx.AgentWithdrawalModel.Update(ctx, session, record); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新提现记录失败: %v", err)
}
// 解冻资金FrozenBalance -= amount, Balance += amount
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(ctx, record.AgentId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败: %v", err)
}
wallet.Balance += record.Amount
wallet.FrozenBalance -= record.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新钱包失败: %v", err)
}
// 更新扣税记录状态为失败
taxModel, err := l.svcCtx.AgentWithdrawalTaxModel.FindOneByWithdrawalId(ctx, record.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询扣税记录失败: %v", 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)
}
}
l.Logger.Infof("银行卡提现拒绝 withdrawalId:%d amount:%f reason:%s", record.Id, record.Amount, remark)
return nil
}

View File

@@ -0,0 +1,198 @@
package agent
import (
"context"
"database/sql"
"regexp"
"time"
"tydata-server/app/main/model"
"tydata-server/common/ctxdata"
"tydata-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
// 提现类型常量
const (
WithdrawTypeAlipay = 1 // 支付宝提现
WithdrawTypeBankCard = 2 // 银行卡提现
)
type BankCardWithdrawalLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBankCardWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BankCardWithdrawalLogic {
return &BankCardWithdrawalLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *BankCardWithdrawalLogic) BankCardWithdrawal(req *types.BankCardWithdrawalReq) (resp *types.WithdrawalResp, err error) {
var (
outBizNo string
withdrawRes = &types.WithdrawalResp{}
)
var finalWithdrawAmount float64 // 实际到账金额
// 验证银行卡号格式16-19位数字
bankCardNoRegex := regexp.MustCompile(`^\d{16,19}$`)
if !bankCardNoRegex.MatchString(req.BankCardNo) {
return nil, errors.Wrapf(xerr.NewErrMsg("银行卡号格式不正确请输入16-19位数字"), "银行卡号格式验证失败")
}
// 验证开户支行不能为空
if req.BankName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("开户支行不能为空"), "开户支行验证失败")
}
// 使用事务处理核心操作
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("您的实名认证未通过, 无法提现"), "您的实名认证未通过")
}
// 查询钱包
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("您可提现的余额不足"), "余额不足")
}
// 最低提现金额验证
if req.Amount < 50 {
return errors.Wrapf(xerr.NewErrMsg("提现金额不能低于50元"), "金额验证失败")
}
// 生成交易号
outBizNo = "BC_" + 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()))
// 统一按6%收取税收
taxRate := l.svcCtx.Config.TaxConfig.TaxRate
var (
taxAmount float64 // 应缴税费
taxDeductionPart float64 // 应税金额
TaxStatus int64 // 扣税状态
exemptionAmount float64 // 免税金额固定为0
)
// 统一扣税逻辑所有提现都按6%收取税收
exemptionAmount = 0 // 免税金额 = 0
TaxStatus = model.TaxStatusPending // 扣税状态 = 待扣税
taxDeductionPart = req.Amount // 应税金额 = 提现金额
taxAmount = taxDeductionPart * taxRate // 应缴税费 = 应税金额 * 税率
finalWithdrawAmount = req.Amount - taxAmount // 实际到账金额 = 提现金额 - 应缴税费
// 创建提现记录(初始状态为申请中,提现类型为银行卡)
withdrawalID, err := l.createBankCardWithdrawalRecord(session, agentModel.Id, req.BankCardNo, req.BankName, agentRealName.Name, 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: 0, // 不再使用免税额度记录
}
_, 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
}
// 银行卡提现不需要调用支付接口,直接返回申请中状态
withdrawRes.Status = WithdrawStatusProcessing
withdrawRes.FailMsg = ""
l.Logger.Infof("银行卡提现申请成功 outBizNo:%s agentId:%d amount:%f", outBizNo, 0, req.Amount)
return withdrawRes, nil
}
// 创建银行卡提现记录(事务内操作)
func (l *BankCardWithdrawalLogic) createBankCardWithdrawalRecord(session sqlx.Session, agentID int64, bankCardNo string, bankName string, payeeName string, amount float64, finalWithdrawAmount float64, taxAmount float64, outBizNo string) (int64, error) {
record := &model.AgentWithdrawal{
AgentId: agentID,
WithdrawNo: outBizNo,
PayeeAccount: bankCardNo, // 银行卡号存储在PayeeAccount字段
Amount: amount,
ActualAmount: finalWithdrawAmount,
TaxAmount: taxAmount,
Status: StatusProcessing, // 申请中状态
// 注意:以下字段需要在数据库迁移后添加,如果模型还没有这些字段,需要先更新模型
// WithdrawType: WithdrawTypeBankCard,
// BankCardNo: sql.NullString{String: bankCardNo, Valid: true},
// BankName: sql.NullString{String: bankName, Valid: true},
// PayeeName: sql.NullString{String: payeeName, Valid: true},
}
result, err := l.svcCtx.AgentWithdrawalModel.Insert(l.ctx, session, record)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
// 冻结资金(事务内操作)
func (l *BankCardWithdrawalLogic) 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
}

View File

@@ -0,0 +1,87 @@
package agent
import (
"context"
"tydata-server/app/main/model"
"tydata-server/common/ctxdata"
"tydata-server/common/xerr"
"github.com/pkg/errors"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetBankCardInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetBankCardInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBankCardInfoLogic {
return &GetBankCardInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetBankCardInfoLogic) GetBankCardInfo(req *types.GetBankCardInfoReq) (resp *types.GetBankCardInfoResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %v", err)
}
// 查询代理信息
agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
return nil, 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 nil, errors.Wrapf(xerr.NewErrMsg("您未进行实名认证"), "您未进行实名认证")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理实名信息失败: %v", err)
}
// 查询最近一次银行卡提现记录,用于自动填充
// 注意:这里需要查询 withdraw_type = 2 的记录
// 如果数据库还没有迁移,可以先返回空值
resp = &types.GetBankCardInfoResp{
PayeeName: agentRealName.Name,
IdCard: agentRealName.IdCard,
BankCardNo: "",
BankName: "",
}
// 查询最近一次成功的银行卡提现记录
// 这里使用SelectBuilder查询但由于模型可能还没有withdraw_type字段先注释
// builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder()
// builder = builder.Where(squirrel.Eq{"agent_id": agentModel.Id})
// builder = builder.Where(squirrel.Eq{"withdraw_type": WithdrawTypeBankCard})
// builder = builder.Where(squirrel.Eq{"status": StatusSuccess})
// builder = builder.OrderBy("create_time DESC")
// builder = builder.Limit(1)
//
// list, err := l.svcCtx.AgentWithdrawalModel.FindAll(l.ctx, builder, "create_time DESC")
// if err == nil && len(list) > 0 {
// lastRecord := list[0]
// if lastRecord.BankCardNo.Valid {
// resp.BankCardNo = lastRecord.BankCardNo.String
// }
// if lastRecord.BankName.Valid {
// resp.BankName = lastRecord.BankName.String
// }
// }
// 临时方案从PayeeAccount字段读取如果之前有银行卡提现记录
// 注意这个方案不够准确因为PayeeAccount也可能存储支付宝账号
// 建议数据库迁移后使用上面的方案
return resp, nil
}

View File

@@ -692,6 +692,16 @@ type AdminRetryAgentProcessResp struct {
ProcessedAt string `json:"processed_at"` // 处理时间
}
type AdminReviewBankCardWithdrawalReq struct {
WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID
Action int64 `json:"action"` // 操作:1-确认,2-拒绝
Remark string `json:"remark"` // 备注(拒绝时必填)
}
type AdminReviewBankCardWithdrawalResp struct {
Success bool `json:"success"` // 是否成功
}
type AdminRoleApiInfo struct {
Id int64 `json:"id"`
RoleId int64 `json:"role_id"`
@@ -1129,6 +1139,10 @@ type AgentWithdrawalListItem struct {
PayeeAccount string `json:"payee_account"` // 收款账户
Remark string `json:"remark"` // 备注
CreateTime string `json:"create_time"` // 创建时间
WithdrawType int64 `json:"withdraw_type"` // 提现类型:1-支付宝,2-银行卡
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
PayeeName string `json:"payee_name"` // 收款人姓名
}
type AuthorizationDocumentInfo struct {
@@ -1144,6 +1158,12 @@ type AuthorizationDocumentInfo struct {
CreateTime string `json:"createTime"` // 创建时间
}
type BankCardWithdrawalReq struct {
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
Amount float64 `json:"amount"` // 提现金额
}
type BindMobileReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
@@ -1156,10 +1176,9 @@ type BindMobileResp struct {
}
type Commission struct {
ProductName string `json:"product_name"`
Amount float64 `json:"amount"`
CreateTime string `json:"create_time"`
QueryParams map[string]interface{} `json:"query_params,omitempty"`
ProductName string `json:"product_name"`
Amount float64 `json:"amount"`
CreateTime string `json:"create_time"`
}
type CreateMenuReq struct {
@@ -1328,6 +1347,16 @@ type GetAuthorizationDocumentResp struct {
CreateTime string `json:"createTime"` // 创建时间
}
type GetBankCardInfoReq struct {
}
type GetBankCardInfoResp struct {
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
PayeeName string `json:"payee_name"` // 收款人姓名
IdCard string `json:"id_card"` // 身份证号
}
type GetCommissionReq struct {
Page int64 `form:"page"` // 页码
PageSize int64 `form:"page_size"` // 每页数据量

View File

@@ -10,8 +10,6 @@ import (
"time"
"tydata-server/common/globalkey"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/builder"
@@ -19,6 +17,7 @@ import (
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
"tydata-server/common/globalkey"
)
var (
@@ -27,8 +26,8 @@ var (
agentWithdrawalRowsExpectAutoSet = strings.Join(stringx.Remove(agentWithdrawalFieldNames, "`id`", "`create_time`", "`update_time`"), ",")
agentWithdrawalRowsWithPlaceHolder = strings.Join(stringx.Remove(agentWithdrawalFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?"
cacheHmAgentWithdrawalIdPrefix = "cache:tydata:agentWithdrawal:id:"
cacheHmAgentWithdrawalWithdrawNoPrefix = "cache:tydata:agentWithdrawal:withdrawNo:"
cacheTydataAgentWithdrawalIdPrefix = "cache:tydata:agentWithdrawal:id:"
cacheTydataAgentWithdrawalWithdrawNoPrefix = "cache:tydata:agentWithdrawal:withdrawNo:"
)
type (
@@ -59,12 +58,16 @@ type (
AgentWithdrawal struct {
Id int64 `db:"id"`
AgentId int64 `db:"agent_id"` // 代理ID
WithdrawType int64 `db:"withdraw_type"` // 提现类型:1-支付宝,2-银行卡
WithdrawNo string `db:"withdraw_no"` // 提现单号
Amount float64 `db:"amount"` // 提现金额
ActualAmount float64 `db:"actual_amount"` // 实际到账金额(扣税后)
TaxAmount float64 `db:"tax_amount"` // 扣税金额
Status int64 `db:"status"` // 状态:1-申请中,2-成功,3-失败
PayeeAccount string `db:"payeeAccount"` // 收款人账号
BankCardNo sql.NullString `db:"bank_card_no"` // 银行卡号
BankName sql.NullString `db:"bank_name"` // 开户支行
PayeeName sql.NullString `db:"payee_name"` // 收款人姓名
Remark sql.NullString `db:"remark"`
CreateTime time.Time `db:"create_time"` // 创建时间
UpdateTime time.Time `db:"update_time"` // 更新时间
@@ -83,21 +86,21 @@ func newAgentWithdrawalModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAgent
func (m *defaultAgentWithdrawalModel) Insert(ctx context.Context, session sqlx.Session, data *AgentWithdrawal) (sql.Result, error) {
data.DelState = globalkey.DelStateNo
hmAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalIdPrefix, data.Id)
hmAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
tydataAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalIdPrefix, data.Id)
tydataAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentWithdrawalRowsExpectAutoSet)
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentWithdrawalRowsExpectAutoSet)
if session != nil {
return session.ExecCtx(ctx, query, data.AgentId, data.WithdrawNo, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.PayeeAccount, data.Remark, data.DeleteTime, data.DelState, data.Version)
return session.ExecCtx(ctx, query, data.AgentId, data.WithdrawType, data.WithdrawNo, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.PayeeAccount, data.BankCardNo, data.BankName, data.PayeeName, data.Remark, data.DeleteTime, data.DelState, data.Version)
}
return conn.ExecCtx(ctx, query, data.AgentId, data.WithdrawNo, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.PayeeAccount, data.Remark, data.DeleteTime, data.DelState, data.Version)
}, hmAgentWithdrawalIdKey, hmAgentWithdrawalWithdrawNoKey)
return conn.ExecCtx(ctx, query, data.AgentId, data.WithdrawType, data.WithdrawNo, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.PayeeAccount, data.BankCardNo, data.BankName, data.PayeeName, data.Remark, data.DeleteTime, data.DelState, data.Version)
}, tydataAgentWithdrawalIdKey, tydataAgentWithdrawalWithdrawNoKey)
}
func (m *defaultAgentWithdrawalModel) FindOne(ctx context.Context, id int64) (*AgentWithdrawal, error) {
hmAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalIdPrefix, id)
tydataAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalIdPrefix, id)
var resp AgentWithdrawal
err := m.QueryRowCtx(ctx, &resp, hmAgentWithdrawalIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error {
err := m.QueryRowCtx(ctx, &resp, tydataAgentWithdrawalIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error {
query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentWithdrawalRows, m.table)
return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo)
})
@@ -112,9 +115,9 @@ func (m *defaultAgentWithdrawalModel) FindOne(ctx context.Context, id int64) (*A
}
func (m *defaultAgentWithdrawalModel) FindOneByWithdrawNo(ctx context.Context, withdrawNo string) (*AgentWithdrawal, error) {
hmAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalWithdrawNoPrefix, withdrawNo)
tydataAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalWithdrawNoPrefix, withdrawNo)
var resp AgentWithdrawal
err := m.QueryRowIndexCtx(ctx, &resp, hmAgentWithdrawalWithdrawNoKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
err := m.QueryRowIndexCtx(ctx, &resp, tydataAgentWithdrawalWithdrawNoKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where `withdraw_no` = ? and del_state = ? limit 1", agentWithdrawalRows, m.table)
if err := conn.QueryRowCtx(ctx, &resp, query, withdrawNo, globalkey.DelStateNo); err != nil {
return nil, err
@@ -136,15 +139,15 @@ func (m *defaultAgentWithdrawalModel) Update(ctx context.Context, session sqlx.S
if err != nil {
return nil, err
}
hmAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalIdPrefix, data.Id)
hmAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
tydataAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalIdPrefix, data.Id)
tydataAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentWithdrawalRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawType, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}
return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}, hmAgentWithdrawalIdKey, hmAgentWithdrawalWithdrawNoKey)
return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawType, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}, tydataAgentWithdrawalIdKey, tydataAgentWithdrawalWithdrawNoKey)
}
func (m *defaultAgentWithdrawalModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AgentWithdrawal) error {
@@ -159,15 +162,15 @@ func (m *defaultAgentWithdrawalModel) UpdateWithVersion(ctx context.Context, ses
if err != nil {
return err
}
hmAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalIdPrefix, data.Id)
hmAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
tydataAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalIdPrefix, data.Id)
tydataAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentWithdrawalRowsWithPlaceHolder)
if session != nil {
return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawType, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
}
return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
}, hmAgentWithdrawalIdKey, hmAgentWithdrawalWithdrawNoKey)
return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawType, newData.WithdrawNo, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
}, tydataAgentWithdrawalIdKey, tydataAgentWithdrawalWithdrawNoKey)
if err != nil {
return err
}
@@ -390,19 +393,19 @@ func (m *defaultAgentWithdrawalModel) Delete(ctx context.Context, session sqlx.S
return err
}
hmAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalIdPrefix, id)
hmAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheHmAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
tydataAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalIdPrefix, id)
tydataAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
if session != nil {
return session.ExecCtx(ctx, query, id)
}
return conn.ExecCtx(ctx, query, id)
}, hmAgentWithdrawalIdKey, hmAgentWithdrawalWithdrawNoKey)
}, tydataAgentWithdrawalIdKey, tydataAgentWithdrawalWithdrawNoKey)
return err
}
func (m *defaultAgentWithdrawalModel) formatPrimary(primary interface{}) string {
return fmt.Sprintf("%s%v", cacheHmAgentWithdrawalIdPrefix, primary)
return fmt.Sprintf("%s%v", cacheTydataAgentWithdrawalIdPrefix, primary)
}
func (m *defaultAgentWithdrawalModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error {
query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentWithdrawalRows, m.table)

View File

@@ -23,13 +23,13 @@ $tables = @(
# "agent_rewards",
# "agent_wallet",
# "agent_real_name"
# "agent_withdrawal"
"agent_withdrawal"
# "agent_withdrawal_tax"
# "agent_withdrawal_tax_exemption"
# "feature",
# "global_notifications"
# "order",
"order_refund"
# "order_refund"
# "product",
# "product_feature",
# "query",

View File

@@ -0,0 +1,33 @@
-- 银行卡提现功能数据库迁移脚本
-- 执行时间:请根据实际情况填写
-- 说明:为 agent_withdrawal 表添加银行卡提现相关字段
-- 1. 添加提现类型字段1-支付宝2-银行卡)
ALTER TABLE `agent_withdrawal`
ADD COLUMN `withdraw_type` TINYINT NOT NULL DEFAULT 1 COMMENT '提现类型:1-支付宝,2-银行卡' AFTER `agent_id`;
-- 2. 添加银行卡号字段
ALTER TABLE `agent_withdrawal`
ADD COLUMN `bank_card_no` VARCHAR(50) DEFAULT NULL COMMENT '银行卡号' AFTER `payeeAccount`;
-- 3. 添加开户支行字段
ALTER TABLE `agent_withdrawal`
ADD COLUMN `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '开户支行' AFTER `bank_card_no`;
-- 4. 添加收款人姓名字段(银行卡提现需要,支付宝提现已有但字段名不同)
ALTER TABLE `agent_withdrawal`
ADD COLUMN `payee_name` VARCHAR(50) DEFAULT NULL COMMENT '收款人姓名' AFTER `bank_name`;
-- 5. 为银行卡号字段添加索引(可选,用于查询优化)
ALTER TABLE `agent_withdrawal`
ADD INDEX `idx_withdraw_type` (`withdraw_type`);
-- 6. 更新现有记录的 withdraw_type 为 1支付宝
UPDATE `agent_withdrawal` SET `withdraw_type` = 1 WHERE `withdraw_type` IS NULL OR `withdraw_type` = 0;
-- 说明:
-- 1. withdraw_type: 1=支付宝提现默认2=银行卡提现
-- 2. 现有支付宝提现记录的 withdraw_type 将自动设置为 1
-- 3. bank_card_no、bank_name、payee_name 字段允许为 NULL支付宝提现不需要这些字段
-- 4. 银行卡提现时payee_name 必须与实名认证的姓名一致

View File

@@ -0,0 +1,439 @@
# 银行卡提现功能实施计划
## 一、功能概述
新增银行卡提现功能,与现有支付宝提现功能并行。银行卡提现采用申请-审核模式:
- 用户提交银行卡提现申请(银行卡号、开户支行、提现金额)
- 系统冻结申请金额
- 管理员审核(确认/拒绝)
- 确认后扣除金额,拒绝后解冻金额
- 实际转账由管理员线下手动完成
## 二、数据库变更
### 2.1 修改 `agent_withdrawal` 表
需要新增以下字段:
```sql
ALTER TABLE `agent_withdrawal`
ADD COLUMN `withdraw_type` TINYINT NOT NULL DEFAULT 1 COMMENT '提现类型:1-支付宝,2-银行卡' AFTER `agent_id`,
ADD COLUMN `bank_card_no` VARCHAR(50) DEFAULT NULL COMMENT '银行卡号' AFTER `payee_account`,
ADD COLUMN `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '开户支行' AFTER `bank_card_no`,
ADD COLUMN `payee_name` VARCHAR(50) DEFAULT NULL COMMENT '收款人姓名' AFTER `bank_name`;
```
**说明:**
- `withdraw_type`: 区分提现类型1=支付宝2=银行卡)
- `bank_card_no`: 银行卡号(仅银行卡提现使用)
- `bank_name`: 开户支行(仅银行卡提现使用)
- `payee_name`: 收款人姓名(银行卡提现需要,支付宝提现已有但字段名不同)
### 2.2 新增银行卡信息记录表(可选,用于历史记录)
如果需要保存用户的历史银行卡信息以便自动填充:
```sql
CREATE TABLE `agent_bank_card` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`agent_id` BIGINT NOT NULL COMMENT '代理ID',
`bank_card_no` VARCHAR(50) NOT NULL COMMENT '银行卡号',
`bank_name` VARCHAR(100) NOT NULL COMMENT '开户支行',
`payee_name` VARCHAR(50) NOT NULL COMMENT '收款人姓名',
`is_default` TINYINT DEFAULT 0 COMMENT '是否默认:0-否,1-是',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_time` DATETIME DEFAULT NULL,
`del_state` TINYINT DEFAULT 0 COMMENT '删除状态:0-未删除,1-已删除',
PRIMARY KEY (`id`),
KEY `idx_agent_id` (`agent_id`),
KEY `idx_del_state` (`del_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代理银行卡信息表';
```
## 三、后端开发
### 3.1 API接口定义
#### 3.1.1 前端接口(`app/main/api/desc/front/agent.api`
**新增银行卡提现申请接口:**
```go
// 银行卡提现申请
@handler BankCardWithdrawal
post /withdrawal/bank-card (BankCardWithdrawalReq) returns (WithdrawalResp)
type (
BankCardWithdrawalReq {
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
Amount float64 `json:"amount"` // 提现金额
}
)
```
**新增获取历史银行卡信息接口(可选):**
```go
// 获取历史银行卡信息
@handler GetBankCardInfo
get /withdrawal/bank-card/info (GetBankCardInfoReq) returns (GetBankCardInfoResp)
type (
GetBankCardInfoReq {}
GetBankCardInfoResp {
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户支行
PayeeName string `json:"payee_name"` // 收款人姓名
IdCard string `json:"id_card"` // 身份证号
}
)
```
#### 3.1.2 管理员接口(`app/main/api/desc/admin/admin_agent.api`
**新增银行卡提现审核接口:**
```go
// 银行卡提现审核(确认/拒绝)
@handler AdminReviewBankCardWithdrawal
post /agent-withdrawal/bank-card/review (AdminReviewBankCardWithdrawalReq) returns (AdminReviewBankCardWithdrawalResp)
type (
AdminReviewBankCardWithdrawalReq {
WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID
Action int64 `json:"action"` // 操作:1-确认,2-拒绝
Remark string `json:"remark"` // 备注(拒绝时必填)
}
AdminReviewBankCardWithdrawalResp {
Success bool `json:"success"`
}
)
```
**修改现有提现列表接口:**
-`AdminGetAgentWithdrawalListResp``AgentWithdrawalListItem` 中新增字段:
- `WithdrawType int64` - 提现类型
- `BankCardNo string` - 银行卡号
- `BankName string` - 开户支行
- `PayeeName string` - 收款人姓名
### 3.2 业务逻辑实现
#### 3.2.1 银行卡提现申请逻辑(`BankCardWithdrawalLogic`
**文件位置:** `app/main/api/internal/logic/agent/bankcardwithdrawallogic.go`
**核心流程:**
1. 验证用户实名认证状态
2. 验证银行卡信息与实名信息匹配(姓名需一致)
3. 验证可提现金额
4. 计算税费6%
5. 冻结资金(事务内)
6. 创建提现记录(状态=申请中withdraw_type=2
7. 创建扣税记录(状态=待扣税)
8. 保存银行卡信息到历史记录表(可选)
**关键代码逻辑:**
```go
// 参考 AgentWithdrawalLogic
// 1. 不调用支付宝接口
// 2. 状态直接设为 StatusProcessing申请中
// 3. 添加银行卡信息字段
// 4. 验证银行卡号格式
// 5. 验证姓名与实名认证信息一致
```
#### 3.2.2 银行卡提现审核逻辑(`AdminReviewBankCardWithdrawalLogic`
**文件位置:** `app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go`
**确认提现流程:**
1. 验证提现记录存在且状态为申请中
2. 验证提现类型为银行卡
3. 事务内操作:
- 更新提现记录状态为成功
- 解冻资金并扣除FrozenBalance -= amount, Balance不变
- 更新扣税记录状态为成功
- 发放提现奖励(如果有)
**拒绝提现流程:**
1. 验证提现记录存在且状态为申请中
2. 验证提现类型为银行卡
3. 事务内操作:
- 更新提现记录状态为失败,记录拒绝原因
- 解冻资金FrozenBalance -= amount, Balance += amount
- 更新扣税记录状态为失败
#### 3.2.3 修改现有逻辑
**修改 `AgentWithdrawalLogic`**
- 在创建提现记录时,设置 `withdraw_type = 1`(支付宝)
-`PayeeAccount` 字段存储支付宝账号
**修改 `AdminGetAgentWithdrawalListLogic`**
- 查询时关联银行卡信息字段
- 返回时包含提现类型和银行卡信息
### 3.3 数据模型
#### 3.3.1 修改 `AgentWithdrawal` 模型
**文件:** `app/main/model/agentWithdrawalModel_gen.go`由goctl生成需重新生成
**新增字段:**
- `WithdrawType int64` - 提现类型
- `BankCardNo sql.NullString` - 银行卡号
- `BankName sql.NullString` - 开户支行
- `PayeeName sql.NullString` - 收款人姓名
#### 3.3.2 新增 `AgentBankCard` 模型(可选)
如果实现历史记录功能,需要:
1. 创建 `agent_bank_card.sql` 表结构文件
2. 使用 goctl 生成模型:
```bash
goctl model mysql datasource -url="user:password@tcp(host:port)/database" -table="agent_bank_card" -dir="./app/main/model" -cache=true --style=goZero
```
### 3.4 状态管理
**提现状态常量:**
```go
const (
StatusPending = 1 // 申请中/处理中(支付宝和银行卡共用)
StatusSuccess = 2 // 成功
StatusFailed = 3 // 失败
)
const (
WithdrawTypeAlipay = 1 // 支付宝提现
WithdrawTypeBankCard = 2 // 银行卡提现
)
```
**银行卡提现状态流转:**
- 申请 → StatusPending申请中
- 管理员确认 → StatusSuccess成功
- 管理员拒绝 → StatusFailed失败
## 四、前端开发
### 4.1 用户端tydata-webview-v2
#### 4.1.1 修改提现页面(`src/views/Withdraw.vue`
**方案A在同一页面添加切换标签**
- 添加"支付宝提现"和"银行卡提现"两个标签页
- 根据选择的标签显示不同的表单字段
**方案B创建新的银行卡提现页面**
- 创建 `src/views/BankCardWithdraw.vue`
- 在路由中添加银行卡提现入口
**推荐方案A用户体验更好**
**银行卡提现表单字段:**
- 银行卡号(必填,格式验证)
- 开户支行(必填)
- 提现金额(必填,验证规则同支付宝)
- 显示实名信息(姓名、身份证号,只读)
- 提示:银行卡信息需与实名信息一致
**功能点:**
1. 页面加载时调用接口获取历史银行卡信息,自动填充
2. 显示用户实名认证信息(姓名、身份证号)
3. 表单验证:
- 银行卡号格式16-19位数字
- 开户支行不能为空
- 金额验证≥50≤可提现金额
4. 提交后显示申请成功提示
5. 可在提现记录中查看审核状态
#### 4.1.2 修改提现记录页面(`src/views/WithdrawDetails.vue`
- 显示提现类型(支付宝/银行卡)
- 银行卡提现显示银行卡号和开户支行
- 显示审核状态(申请中/已确认/已拒绝)
#### 4.1.3 API接口调用
**新增接口调用:**
```javascript
// src/api/withdraw.js 或相应文件
export const bankCardWithdrawal = (data) => {
return useApiFetch('/agent/withdrawal/bank-card')
.post(data)
.json();
};
export const getBankCardInfo = () => {
return useApiFetch('/agent/withdrawal/bank-card/info')
.get()
.json();
};
```
### 4.2 管理端tydata-admin
#### 4.2.1 修改提现列表页面(`apps/web-antd/src/views/agent/agent-withdrawal/list.vue`
**新增功能:**
1. 列表显示提现类型(支付宝/银行卡)
2. 银行卡提现显示银行卡号和开户支行
3. 银行卡提现申请中状态显示"审核"操作按钮
4. 点击审核按钮打开审核弹窗
#### 4.2.2 新增审核弹窗组件
**文件:** `apps/web-antd/src/views/agent/agent-withdrawal/modules/review-modal.vue`
**功能:**
- 显示提现详情(金额、银行卡信息、用户信息等)
- 确认/拒绝操作
- 拒绝时必填拒绝原因
- 提交审核结果
#### 4.2.3 修改列表数据配置(`apps/web-antd/src/views/agent/agent-withdrawal/data.ts`
**新增列:**
- 提现类型列
- 银行卡号列(仅银行卡提现显示)
- 开户支行列(仅银行卡提现显示)
- 操作列(银行卡申请中状态显示审核按钮)
#### 4.2.4 API接口调用
**新增接口:**
```typescript
// apps/web-antd/src/api/agent.ts
export function reviewBankCardWithdrawal(data: {
withdrawal_id: number;
action: 1 | 2; // 1-确认, 2-拒绝
remark?: string;
}) {
return request.post('/admin/agent-withdrawal/bank-card/review', data);
}
```
## 五、实施步骤
### 阶段一数据库准备1-2天
1. ✅ 编写数据库变更SQL脚本
2. ✅ 执行SQL脚本更新数据库表结构
3. ✅ 如果实现历史记录功能,创建 `agent_bank_card` 表
### 阶段二后端开发3-5天
1. ✅ 修改API定义文件`agent.api`、`admin_agent.api`
2. ✅ 使用goctl重新生成代码包括模型和类型定义
3. ✅ 实现银行卡提现申请逻辑(`BankCardWithdrawalLogic`
4. ✅ 实现银行卡提现审核逻辑(`AdminReviewBankCardWithdrawalLogic`
5. ✅ 修改现有提现列表逻辑,支持银行卡信息查询
6. ✅ 实现获取历史银行卡信息接口(可选)
7. ✅ 单元测试和接口测试
### 阶段三前端开发3-5天
#### 用户端tydata-webview-v2
1. ✅ 修改提现页面,添加银行卡提现选项
2. ✅ 实现银行卡提现表单和验证
3. ✅ 实现历史信息自动填充
4. ✅ 修改提现记录页面,显示银行卡信息
5. ✅ 测试提现流程
#### 管理端tydata-admin
1. ✅ 修改提现列表页面,显示银行卡信息
2. ✅ 实现审核弹窗组件
3. ✅ 实现审核操作逻辑
4. ✅ 测试审核流程
### 阶段四测试与优化2-3天
1. ✅ 功能测试(申请、审核、拒绝流程)
2. ✅ 边界测试(金额、状态、并发等)
3. ✅ 性能测试
4. ✅ Bug修复
5. ✅ 代码审查
### 阶段五部署上线1天
1. ✅ 数据库迁移脚本执行
2. ✅ 后端服务部署
3. ✅ 前端应用部署
4. ✅ 生产环境验证
## 六、注意事项
### 6.1 数据兼容性
- 现有支付宝提现记录的 `withdraw_type` 默认为1支付宝
- 现有记录的 `bank_card_no`、`bank_name`、`payee_name` 为NULL正常
### 6.2 安全性
- 银行卡号需要加密存储建议使用AES加密
- 前端传输时使用HTTPS
- 审核操作需要管理员权限验证
### 6.3 业务规则
- 银行卡提现最低金额50元与支付宝一致
- 银行卡提现税率6%(与支付宝一致)
- 银行卡提现需要实名认证通过
- 银行卡信息需与实名认证姓名一致
### 6.4 用户体验
- 历史银行卡信息自动填充,减少用户输入
- 明确提示银行卡信息需与实名信息一致
- 审核状态及时反馈给用户
### 6.5 错误处理
- 银行卡号格式验证
- 金额不足提示
- 审核操作失败回滚
- 异常情况日志记录
## 七、风险评估
### 7.1 技术风险
- **数据库迁移风险**:表结构变更可能影响现有功能
- **应对**:先在测试环境验证,做好数据备份
- **并发问题**:审核操作可能并发执行
- **应对**:使用数据库事务和版本号控制
### 7.2 业务风险
- **资金安全**:银行卡提现涉及资金操作
- **应对**:严格权限控制,操作日志记录
- **审核效率**:管理员需要手动审核,可能影响用户体验
- **应对**:提供审核提醒功能,优化审核流程
## 八、后续优化建议
1. **审核提醒**:管理员审核列表增加待审核数量提醒
2. **批量审核**:支持批量确认/拒绝操作
3. **审核历史**:记录审核操作人和操作时间
4. **银行卡管理**:用户可管理多张银行卡,设置默认卡
5. **提现限额**:银行卡提现可设置不同的限额规则
## 九、文件清单
### 后端文件
- `app/main/api/desc/front/agent.api` - 前端API定义修改
- `app/main/api/desc/admin/admin_agent.api` - 管理员API定义修改
- `app/main/api/internal/logic/agent/bankcardwithdrawallogic.go` - 银行卡提现申请逻辑(新增)
- `app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go` - 银行卡提现审核逻辑(新增)
- `app/main/model/agentWithdrawalModel_gen.go` - 提现模型(重新生成)
- `app/main/model/agentBankCardModel.go` - 银行卡信息模型(可选,新增)
### 前端文件(用户端)
- `src/views/Withdraw.vue` - 提现页面(修改)
- `src/views/WithdrawDetails.vue` - 提现记录页面(修改)
- `src/api/withdraw.js` - API接口修改
### 前端文件(管理端)
- `apps/web-antd/src/views/agent/agent-withdrawal/list.vue` - 提现列表(修改)
- `apps/web-antd/src/views/agent/agent-withdrawal/modules/review-modal.vue` - 审核弹窗(新增)
- `apps/web-antd/src/views/agent/agent-withdrawal/data.ts` - 列表配置(修改)
- `apps/web-antd/src/api/agent.ts` - API接口修改
### 数据库文件
- `deploy/sql/bank_card_withdrawal.sql` - 数据库变更脚本(新增)
---
**预计总工期10-15个工作日**