From d633c6741ed885b6c2e88a83723b032a3f890aab Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 24 Dec 2025 17:38:08 +0800 Subject: [PATCH 1/3] t --- app/main/api/desc/admin/admin_agent.api | 20 + app/main/api/desc/front/agent.api | 23 + .../adminreviewbankcardwithdrawalhandler.go | 29 ++ .../agent/bankcardwithdrawalhandler.go | 29 ++ .../handler/agent/getbankcardinfohandler.go | 29 ++ app/main/api/internal/handler/routes.go | 15 + .../admingetagentwithdrawallistlogic.go | 6 + .../adminreviewbankcardwithdrawallogic.go | 188 ++++++++ .../logic/agent/bankcardwithdrawallogic.go | 198 ++++++++ .../logic/agent/getbankcardinfologic.go | 87 ++++ app/main/api/internal/types/types.go | 37 +- app/main/model/agentWithdrawalModel_gen.go | 59 +-- deploy/script/gen_models.ps1 | 4 +- deploy/sql/bank_card_withdrawal.sql | 33 ++ 银行卡提现功能实施计划.md | 439 ++++++++++++++++++ 15 files changed, 1162 insertions(+), 34 deletions(-) create mode 100644 app/main/api/internal/handler/admin_agent/adminreviewbankcardwithdrawalhandler.go create mode 100644 app/main/api/internal/handler/agent/bankcardwithdrawalhandler.go create mode 100644 app/main/api/internal/handler/agent/getbankcardinfohandler.go create mode 100644 app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go create mode 100644 app/main/api/internal/logic/agent/bankcardwithdrawallogic.go create mode 100644 app/main/api/internal/logic/agent/getbankcardinfologic.go create mode 100644 deploy/sql/bank_card_withdrawal.sql create mode 100644 银行卡提现功能实施计划.md diff --git a/app/main/api/desc/admin/admin_agent.api b/app/main/api/desc/admin/admin_agent.api index ac505a8..b948f8a 100644 --- a/app/main/api/desc/admin/admin_agent.api +++ b/app/main/api/desc/admin/admin_agent.api @@ -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"` // 是否成功 + } ) \ No newline at end of file diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index 254ea2f..e4bc269 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -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 ( diff --git a/app/main/api/internal/handler/admin_agent/adminreviewbankcardwithdrawalhandler.go b/app/main/api/internal/handler/admin_agent/adminreviewbankcardwithdrawalhandler.go new file mode 100644 index 0000000..c2263c5 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/adminreviewbankcardwithdrawalhandler.go @@ -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) + } +} diff --git a/app/main/api/internal/handler/agent/bankcardwithdrawalhandler.go b/app/main/api/internal/handler/agent/bankcardwithdrawalhandler.go new file mode 100644 index 0000000..19eb01e --- /dev/null +++ b/app/main/api/internal/handler/agent/bankcardwithdrawalhandler.go @@ -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) + } +} diff --git a/app/main/api/internal/handler/agent/getbankcardinfohandler.go b/app/main/api/internal/handler/agent/getbankcardinfohandler.go new file mode 100644 index 0000000..abaf9ec --- /dev/null +++ b/app/main/api/internal/handler/agent/getbankcardinfohandler.go @@ -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) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index 9f1baba..78a4708 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -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", diff --git a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go index 8bdac3e..c979f7d 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go @@ -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{ diff --git a/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go b/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go new file mode 100644 index 0000000..9ddf578 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go @@ -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 +} diff --git a/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go b/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go new file mode 100644 index 0000000..c5e37a5 --- /dev/null +++ b/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go @@ -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 +} diff --git a/app/main/api/internal/logic/agent/getbankcardinfologic.go b/app/main/api/internal/logic/agent/getbankcardinfologic.go new file mode 100644 index 0000000..7fc94b4 --- /dev/null +++ b/app/main/api/internal/logic/agent/getbankcardinfologic.go @@ -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 +} diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 2d6aea0..56bd954 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -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"` // 每页数据量 diff --git a/app/main/model/agentWithdrawalModel_gen.go b/app/main/model/agentWithdrawalModel_gen.go index 8e799b1..ffffa01 100644 --- a/app/main/model/agentWithdrawalModel_gen.go +++ b/app/main/model/agentWithdrawalModel_gen.go @@ -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) diff --git a/deploy/script/gen_models.ps1 b/deploy/script/gen_models.ps1 index 3e6c3b6..bc892f5 100644 --- a/deploy/script/gen_models.ps1 +++ b/deploy/script/gen_models.ps1 @@ -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", diff --git a/deploy/sql/bank_card_withdrawal.sql b/deploy/sql/bank_card_withdrawal.sql new file mode 100644 index 0000000..bd17a01 --- /dev/null +++ b/deploy/sql/bank_card_withdrawal.sql @@ -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 必须与实名认证的姓名一致 + diff --git a/银行卡提现功能实施计划.md b/银行卡提现功能实施计划.md new file mode 100644 index 0000000..9f2411a --- /dev/null +++ b/银行卡提现功能实施计划.md @@ -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个工作日** + From 7c32f4131a61abce22f79de2e6f748e7eb69fcab Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 24 Dec 2025 19:16:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=EF=BC=9A=E9=93=B6?= =?UTF-8?q?=E8=A1=8C=E5=8D=A1=E6=8F=90=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main/api/desc/admin/admin_agent.api | 2 + .../admingetagentwithdrawallistlogic.go | 15 ++++-- .../adminreviewbankcardwithdrawallogic.go | 9 ++-- .../logic/agent/agentwithdrawallogic.go | 1 + .../logic/agent/bankcardwithdrawallogic.go | 13 ++--- .../logic/agent/getbankcardinfologic.go | 48 ++++++++----------- .../internal/service/authorizationService.go | 6 +-- app/main/api/internal/types/types.go | 9 ++-- 8 files changed, 55 insertions(+), 48 deletions(-) diff --git a/app/main/api/desc/admin/admin_agent.api b/app/main/api/desc/admin/admin_agent.api index b948f8a..5e426a6 100644 --- a/app/main/api/desc/admin/admin_agent.api +++ b/app/main/api/desc/admin/admin_agent.api @@ -193,6 +193,8 @@ type ( AgentId int64 `json:"agent_id"` // 代理ID WithdrawNo string `json:"withdraw_no"` // 提现单号 Amount float64 `json:"amount"` // 金额 + ActualAmount float64 `json:"actual_amount"` // 实际到账金额(扣税后) + TaxAmount float64 `json:"tax_amount"` // 扣税金额 Status int64 `json:"status"` // 状态 PayeeAccount string `json:"payee_account"` // 收款账户 Remark string `json:"remark"` // 备注 diff --git a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go index c979f7d..423277f 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go @@ -50,10 +50,17 @@ func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *type } item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") - // 设置银行卡信息(如果模型有这些字段,copier会自动复制;如果没有,这里手动设置默认值) - // 注意:如果数据库还没有迁移,这些字段可能不存在,需要先注释 - // 如果模型有这些字段,copier会自动复制,这里不需要手动设置 - // 如果模型没有这些字段,需要等数据库迁移后重新生成模型 + // 手动设置银行卡信息(copier不会自动处理sql.NullString) + item.WithdrawType = v.WithdrawType + if v.BankCardNo.Valid { + item.BankCardNo = v.BankCardNo.String + } + if v.BankName.Valid { + item.BankName = v.BankName.String + } + if v.PayeeName.Valid { + item.PayeeName = v.PayeeName.String + } items = append(items, item) } diff --git a/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go b/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go index 9ddf578..a60004a 100644 --- a/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go +++ b/app/main/api/internal/logic/admin_agent/adminreviewbankcardwithdrawallogic.go @@ -80,11 +80,10 @@ func (l *AdminReviewBankCardWithdrawalLogic) AdminReviewBankCardWithdrawal(req * return errors.Wrapf(xerr.NewErrMsg("该提现记录已处理,无法重复操作"), "状态验证失败") } - // 验证提现类型(如果模型有WithdrawType字段) - // 注意:如果数据库还没有迁移,可以先注释这个验证 - // if record.WithdrawType != WithdrawTypeBankCard { - // return errors.Wrapf(xerr.NewErrMsg("该记录不是银行卡提现,无法审核"), "提现类型验证失败") - // } + // 验证提现类型 + if record.WithdrawType != WithdrawTypeBankCard { + return errors.Wrapf(xerr.NewErrMsg("该记录不是银行卡提现,无法审核"), "提现类型验证失败") + } if req.Action == ReviewActionApprove { // 确认提现 diff --git a/app/main/api/internal/logic/agent/agentwithdrawallogic.go b/app/main/api/internal/logic/agent/agentwithdrawallogic.go index 83020cf..5914108 100644 --- a/app/main/api/internal/logic/agent/agentwithdrawallogic.go +++ b/app/main/api/internal/logic/agent/agentwithdrawallogic.go @@ -220,6 +220,7 @@ func (l *AgentWithdrawalLogic) mapAlipayError(code string) string { func (l *AgentWithdrawalLogic) createWithdrawalRecord(session sqlx.Session, agentID int64, payeeAccount string, amount float64, finalWithdrawAmount float64, taxAmount float64, outBizNo string) (int64, error) { record := &model.AgentWithdrawal{ AgentId: agentID, + WithdrawType: 1, // 支付宝提现 WithdrawNo: outBizNo, PayeeAccount: payeeAccount, Amount: amount, diff --git a/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go b/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go index c5e37a5..b17eed7 100644 --- a/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go +++ b/app/main/api/internal/logic/agent/bankcardwithdrawallogic.go @@ -42,6 +42,7 @@ func (l *BankCardWithdrawalLogic) BankCardWithdrawal(req *types.BankCardWithdraw var ( outBizNo string withdrawRes = &types.WithdrawalResp{} + agentID int64 ) var finalWithdrawAmount float64 // 实际到账金额 @@ -68,6 +69,7 @@ func (l *BankCardWithdrawalLogic) BankCardWithdrawal(req *types.BankCardWithdraw if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err) } + agentID = agentModel.Id // 保存agentId用于日志 // 查询实名认证信息 agentRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agentModel.Id) @@ -158,7 +160,7 @@ func (l *BankCardWithdrawalLogic) BankCardWithdrawal(req *types.BankCardWithdraw withdrawRes.Status = WithdrawStatusProcessing withdrawRes.FailMsg = "" - l.Logger.Infof("银行卡提现申请成功 outBizNo:%s agentId:%d amount:%f", outBizNo, 0, req.Amount) + l.Logger.Infof("银行卡提现申请成功 outBizNo:%s agentId:%d amount:%f", outBizNo, agentID, req.Amount) return withdrawRes, nil } @@ -166,17 +168,16 @@ func (l *BankCardWithdrawalLogic) BankCardWithdrawal(req *types.BankCardWithdraw 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, + WithdrawType: WithdrawTypeBankCard, // 银行卡提现 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}, + 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) diff --git a/app/main/api/internal/logic/agent/getbankcardinfologic.go b/app/main/api/internal/logic/agent/getbankcardinfologic.go index 7fc94b4..58f965a 100644 --- a/app/main/api/internal/logic/agent/getbankcardinfologic.go +++ b/app/main/api/internal/logic/agent/getbankcardinfologic.go @@ -6,6 +6,7 @@ import ( "tydata-server/common/ctxdata" "tydata-server/common/xerr" + "github.com/Masterminds/squirrel" "github.com/pkg/errors" "tydata-server/app/main/api/internal/svc" @@ -49,39 +50,32 @@ func (l *GetBankCardInfoLogic) GetBankCardInfo(req *types.GetBankCardInfoReq) (r return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理实名信息失败: %v", err) } - // 查询最近一次银行卡提现记录,用于自动填充 - // 注意:这里需要查询 withdraw_type = 2 的记录 - // 如果数据库还没有迁移,可以先返回空值 + // 初始化响应,包含实名认证信息 resp = &types.GetBankCardInfoResp{ - PayeeName: agentRealName.Name, - IdCard: agentRealName.IdCard, + 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 - // } - // } + // 查询最近一次成功的银行卡提现记录,用于自动填充 + builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder() + builder = builder.Where(squirrel.Eq{"agent_id": agentModel.Id}) + builder = builder.Where(squirrel.Eq{"withdraw_type": 2}) // 银行卡提现 + builder = builder.Where(squirrel.Eq{"status": 2}) // 成功状态 + builder = builder.OrderBy("create_time DESC") + builder = builder.Limit(1) - // 临时方案:从PayeeAccount字段读取(如果之前有银行卡提现记录) - // 注意:这个方案不够准确,因为PayeeAccount也可能存储支付宝账号 - // 建议数据库迁移后使用上面的方案 + 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 + } + } return resp, nil } diff --git a/app/main/api/internal/service/authorizationService.go b/app/main/api/internal/service/authorizationService.go index 536e1f2..2c52518 100644 --- a/app/main/api/internal/service/authorizationService.go +++ b/app/main/api/internal/service/authorizationService.go @@ -269,7 +269,7 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ } // 构建授权书内容(去掉标题部分) - content := fmt.Sprintf(`海南天远大数据科技有限公司: + content := fmt.Sprintf(`海南海宇大数据有限公司: 本人%s拟向贵司申请大数据分析报告查询业务,贵司需要了解本人相关状况,用于查询大数据分析报告,因此本人同意向贵司提供本人的姓名和手机号等个人信息,并同意贵司向第三方传送上述信息。第三方将使用上述信息核实信息真实情况,查询信用记录,并生成报告。 授权内容如下: @@ -292,8 +292,8 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ 附加说明: 本人在授权的相关数据将依据法律法规及贵司内部数据管理规范妥善存储,存储期限为法律要求的最短必要时间。超过存储期限或在数据使用目的达成后,贵司将对相关数据进行销毁或匿名化处理。 本人有权随时撤回本授权书中的授权,但撤回前的授权行为及其法律后果仍具有法律效力。若需撤回授权,本人可通过贵司官方渠道提交书面申请,贵司将在收到申请后依法停止对本人数据的使用。 -你通过"天远数据",自愿支付相应费用,用于购买海南天远大数据科技有限公司的大数据报告产品。如若对产品内容存在异议,可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈,贵司将在收到异议之日起20日内进行核查和处理,并将结果答复。 -你向海南天远大数据科技有限公司的支付方式为:海南天远大数据科技有限公司及其经官方授权的相关企业的支付宝账户。 +你通过"天远数据",自愿支付相应费用,用于购买海南海宇大数据有限公司的大数据报告产品。如若对产品内容存在异议,可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈,贵司将在收到异议之日起20日内进行核查和处理,并将结果答复。 +你向海南海宇大数据有限公司的支付方式为:海南海宇大数据有限公司及其经官方授权的相关企业的支付宝账户。 争议解决机制: 若因本授权书引发争议,双方应友好协商解决;协商不成的,双方同意将争议提交至授权书签署地(海南省)有管辖权的人民法院解决。 diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 56bd954..b449a5a 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -1135,6 +1135,8 @@ type AgentWithdrawalListItem struct { AgentId int64 `json:"agent_id"` // 代理ID WithdrawNo string `json:"withdraw_no"` // 提现单号 Amount float64 `json:"amount"` // 金额 + ActualAmount float64 `json:"actual_amount"` // 实际到账金额(扣税后) + TaxAmount float64 `json:"tax_amount"` // 扣税金额 Status int64 `json:"status"` // 状态 PayeeAccount string `json:"payee_account"` // 收款账户 Remark string `json:"remark"` // 备注 @@ -1176,9 +1178,10 @@ type BindMobileResp struct { } type Commission struct { - ProductName string `json:"product_name"` - Amount float64 `json:"amount"` - CreateTime string `json:"create_time"` + ProductName string `json:"product_name"` + Amount float64 `json:"amount"` + CreateTime string `json:"create_time"` + QueryParams map[string]interface{} `json:"query_params,omitempty"` } type CreateMenuReq struct { From 77c8435178b2a5ddde04e0e79762222c655596ca Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Thu, 25 Dec 2025 12:57:16 +0800 Subject: [PATCH 3/3] add --- app/main/api/desc/front/agent.api | 7 ++++--- .../api/internal/logic/agent/getagentcommissionlogic.go | 8 ++++++++ app/main/api/internal/types/types.go | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index 4560a48..4944b6d 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -326,9 +326,10 @@ type ( SubWithdrawReward float64 `json:"sub_withdraw_reward"` } Commission { - ProductName string `json:"product_name"` - Amount float64 `json:"amount"` - CreateTime string `json:"create_time"` + OrderId string `json:"order_id"` // 订单号 + ProductName string `json:"product_name"` + Amount float64 `json:"amount"` + CreateTime string `json:"create_time"` QueryParams map[string]interface{} `json:"query_params,omitempty"` } GetCommissionReq { diff --git a/app/main/api/internal/logic/agent/getagentcommissionlogic.go b/app/main/api/internal/logic/agent/getagentcommissionlogic.go index aa51456..48bc8f6 100644 --- a/app/main/api/internal/logic/agent/getagentcommissionlogic.go +++ b/app/main/api/internal/logic/agent/getagentcommissionlogic.go @@ -64,6 +64,14 @@ func (l *GetAgentCommissionLogic) GetAgentCommission(req *types.GetCommissionReq } commission.CreateTime = agentCommissionModel.CreateTime.Format("2006-01-02 15:04:05") commission.ProductName = product.ProductName + + // 从 order 表获取 platform_order_id + orderModel, findOrderErr := l.svcCtx.OrderModel.FindOne(l.ctx, agentCommissionModel.OrderId) + if findOrderErr == nil && orderModel != nil && orderModel.PlatformOrderId.Valid { + commission.OrderId = orderModel.PlatformOrderId.String + } else { + commission.OrderId = "" + } queryModel, queryErr := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, agentCommissionModel.OrderId) if queryErr == nil && queryModel != nil { diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index b449a5a..e6416b9 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -1178,6 +1178,7 @@ type BindMobileResp struct { } type Commission struct { + OrderId string `json:"order_id"` // 订单号 ProductName string `json:"product_name"` Amount float64 `json:"amount"` CreateTime string `json:"create_time"`