t
This commit is contained in:
@@ -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"` // 是否成功
|
||||
}
|
||||
)
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
198
app/main/api/internal/logic/agent/bankcardwithdrawallogic.go
Normal file
198
app/main/api/internal/logic/agent/bankcardwithdrawallogic.go
Normal 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
|
||||
}
|
||||
87
app/main/api/internal/logic/agent/getbankcardinfologic.go
Normal file
87
app/main/api/internal/logic/agent/getbankcardinfologic.go
Normal 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
|
||||
}
|
||||
@@ -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"` // 每页数据量
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
33
deploy/sql/bank_card_withdrawal.sql
Normal file
33
deploy/sql/bank_card_withdrawal.sql
Normal 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 必须与实名认证的姓名一致
|
||||
|
||||
439
银行卡提现功能实施计划.md
Normal file
439
银行卡提现功能实施计划.md
Normal 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个工作日**
|
||||
|
||||
Reference in New Issue
Block a user