This commit is contained in:
2026-03-02 12:57:17 +08:00
45 changed files with 1788 additions and 246 deletions

View File

@@ -83,9 +83,9 @@ service main {
@handler AdminGetInviteCodeList @handler AdminGetInviteCodeList
get /invite_code/list (AdminGetInviteCodeListReq) returns (AdminGetInviteCodeListResp) get /invite_code/list (AdminGetInviteCodeListReq) returns (AdminGetInviteCodeListResp)
// 平台升级代理等级(免费升级,遵守代理系统逻辑规则) // 平台升级代理等级
@handler AdminUpgradeAgent @handler AdminUpgradeAgent
post /upgrade (AdminUpgradeAgentReq) returns (AdminUpgradeAgentResp) post /upgrade/agent (AdminUpgradeAgentReq) returns (AdminUpgradeAgentResp)
} }
type ( type (
@@ -128,14 +128,6 @@ type (
AdminAuditAgentResp { AdminAuditAgentResp {
Success bool `json:"success"` Success bool `json:"success"`
} }
// 平台升级代理等级
AdminUpgradeAgentReq {
AgentId string `json:"agent_id"` // 代理ID
ToLevel int64 `json:"to_level"` // 目标等级2=黄金3=钻石
}
AdminUpgradeAgentResp {
Success bool `json:"success"`
}
// 推广链接分页查询 // 推广链接分页查询
AdminGetAgentLinkListReq { AdminGetAgentLinkListReq {
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
@@ -165,6 +157,7 @@ type (
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
OrderId *string `form:"order_id,optional"` // 订单ID可选 OrderId *string `form:"order_id,optional"` // 订单ID可选
ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选) ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选)
OrderStatus *string `form:"order_status,optional"` // 订单状态可选pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败
} }
AgentOrderListItem { AgentOrderListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
@@ -177,7 +170,8 @@ type (
ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 ActualBasePrice float64 `json:"actual_base_price"` // 实际底价
PriceCost float64 `json:"price_cost"` // 提价成本 PriceCost float64 `json:"price_cost"` // 提价成本
AgentProfit float64 `json:"agent_profit"` // 代理收益 AgentProfit float64 `json:"agent_profit"` // 代理收益
ProcessStatus int64 `json:"process_status"` // 处理状态 ProcessStatus int64 `json:"process_status"` // 处理状态(保留用于筛选,前端不显示)
OrderStatus string `json:"order_status"` // 订单状态pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
AdminGetAgentOrderListResp { AdminGetAgentOrderListResp {
@@ -212,6 +206,7 @@ type (
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
SourceAgentId *string `form:"source_agent_id,optional"` // 来源代理ID可选 SourceAgentId *string `form:"source_agent_id,optional"` // 来源代理ID可选
RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选) RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选)
Status *int64 `form:"status,optional"` // 状态可选1=已发放2=已冻结3=已取消(已退款)
} }
AgentRebateListItem { AgentRebateListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
@@ -220,6 +215,7 @@ type (
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
RebateType int64 `json:"rebate_type"` // 返佣类型 RebateType int64 `json:"rebate_type"` // 返佣类型
Amount float64 `json:"amount"` // 金额 Amount float64 `json:"amount"` // 金额
Status int64 `json:"status"` // 状态1=已发放2=已冻结3=已取消(已退款)
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
AdminGetAgentRebateListResp { AdminGetAgentRebateListResp {
@@ -258,17 +254,20 @@ type (
WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选)
} }
AgentWithdrawalListItem { AgentWithdrawalListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
WithdrawNo string `json:"withdraw_no"` // 提现单号 WithdrawNo string `json:"withdraw_no"` // 提现单号
Amount float64 `json:"amount"` // 金额 Amount float64 `json:"amount"` // 金额
TaxAmount float64 `json:"tax_amount"` // 税费金额 TaxAmount float64 `json:"tax_amount"` // 税费金额
ActualAmount float64 `json:"actual_amount"` // 实际到账金额 ActualAmount float64 `json:"actual_amount"` // 实际到账金额
Status int64 `json:"status"` // 状态 Status int64 `json:"status"` // 状态
PayeeAccount string `json:"payee_account"` // 收款账户 WithdrawalType int64 `json:"withdrawal_type"` // 提现方式1=支付宝2=银行卡
PayeeName string `json:"payee_name"` // 收款人姓名 PayeeAccount string `json:"payee_account"` // 收款账户(支付宝账号或银行卡号)
Remark string `json:"remark"` // 备注 PayeeName string `json:"payee_name"` // 收款人姓名
CreateTime string `json:"create_time"` // 创建时间 BankCardNo string `json:"bank_card_no"` // 银行卡号(银行卡提现时使用)
BankName string `json:"bank_name"` // 开户行名称(银行卡提现时使用)
Remark string `json:"remark"` // 备注
CreateTime string `json:"create_time"` // 创建时间
} }
AdminGetAgentWithdrawalListResp { AdminGetAgentWithdrawalListResp {
Total int64 `json:"total"` // 总数 Total int64 `json:"total"` // 总数
@@ -434,5 +433,13 @@ type (
Total int64 `json:"total"` // 总数 Total int64 `json:"total"` // 总数
Items []InviteCodeListItem `json:"items"` // 列表数据 Items []InviteCodeListItem `json:"items"` // 列表数据
} }
// 平台升级代理等级
AdminUpgradeAgentReq {
AgentId string `json:"agent_id"` // 代理ID
ToLevel int64 `json:"to_level"` // 目标等级2=黄金3=钻石
}
AdminUpgradeAgentResp {
Success bool `json:"success"` // 是否成功
}
) )

View File

@@ -79,6 +79,7 @@ type (
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
PayTime string `json:"pay_time"` // 支付时间 PayTime string `json:"pay_time"` // 支付时间
RefundTime string `json:"refund_time"` // 退款时间 RefundTime string `json:"refund_time"` // 退款时间
UpdateTime string `json:"update_time"` // 更新时间
IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单
AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态not_agent-非代理订单success-处理成功failed-处理失败pending-待处理 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态not_agent-非代理订单success-处理成功failed-处理失败pending-待处理
} }

View File

@@ -1,5 +1,7 @@
syntax = "v1" syntax = "v1"
import "product.api"
info ( info (
title: "代理服务" title: "代理服务"
desc: "新代理系统接口" desc: "新代理系统接口"
@@ -121,6 +123,14 @@ type (
GetInviteLinkResp { GetInviteLinkResp {
InviteLink string `json:"invite_link"` // 邀请链接 InviteLink string `json:"invite_link"` // 邀请链接
} }
// 生成邀请海报请求
GenerateInvitePosterReq {
InviteLink string `form:"invite_link"` // 邀请链接(用于生成二维码)
}
// 生成邀请海报响应
GenerateInvitePosterResp {
PosterUrl string `json:"poster_url"` // 海报图片URLbase64编码的data URL
}
// 获取代理等级特权信息 // 获取代理等级特权信息
GetLevelPrivilegeResp { GetLevelPrivilegeResp {
Levels []LevelPrivilegeItem `json:"levels"` Levels []LevelPrivilegeItem `json:"levels"`
@@ -224,6 +234,10 @@ service main {
@handler ApplyWithdrawal @handler ApplyWithdrawal
post /withdrawal/apply (ApplyWithdrawalReq) returns (ApplyWithdrawalResp) post /withdrawal/apply (ApplyWithdrawalReq) returns (ApplyWithdrawalResp)
// 获取上次提现信息(用于前端预填)
@handler GetLastWithdrawalInfo
get /withdrawal/last_info (GetLastWithdrawalInfoReq) returns (GetLastWithdrawalInfoResp)
// 实名认证 // 实名认证
@handler RealNameAuth @handler RealNameAuth
post /real_name (RealNameAuthReq) returns (RealNameAuthResp) post /real_name (RealNameAuthReq) returns (RealNameAuthResp)
@@ -244,6 +258,10 @@ service main {
@handler GetInviteLink @handler GetInviteLink
get /invite_link (GetInviteLinkReq) returns (GetInviteLinkResp) get /invite_link (GetInviteLinkReq) returns (GetInviteLinkResp)
// 生成邀请海报(带二维码的图片)
@handler GenerateInvitePoster
get /invite/poster (GenerateInvitePosterReq) returns (GenerateInvitePosterResp)
// 获取代理等级特权信息 // 获取代理等级特权信息
@handler GetLevelPrivilege @handler GetLevelPrivilege
get /level/privilege returns (GetLevelPrivilegeResp) get /level/privilege returns (GetLevelPrivilegeResp)
@@ -277,6 +295,32 @@ service main {
// 检查订单是否属于当前代理推广 // 检查订单是否属于当前代理推广
@handler CheckOrderAgent @handler CheckOrderAgent
get /order/agent (CheckOrderAgentReq) returns (CheckOrderAgentResp) get /order/agent (CheckOrderAgentReq) returns (CheckOrderAgentResp)
// ============================================
// 用户模块白名单相关接口
// ============================================
@handler GetWhitelistFeatures
get /whitelist/features (GetWhitelistFeaturesReq) returns (GetWhitelistFeaturesResp)
// 创建白名单订单
@handler CreateWhitelistOrder
post /whitelist/order/create (CreateWhitelistOrderReq) returns (CreateWhitelistOrderResp)
// 获取用户白名单列表
@handler GetWhitelistList
get /whitelist/list (GetWhitelistListReq) returns (GetWhitelistListResp)
// 检查模块是否已下架(用于显示下架按钮状态)
@handler CheckFeatureWhitelistStatus
get /whitelist/check (CheckFeatureWhitelistStatusReq) returns (CheckFeatureWhitelistStatusResp)
// 下架单个模块(创建订单并支付)
@handler OfflineFeature
post /whitelist/offline (OfflineFeatureReq) returns (OfflineFeatureResp)
// 检查订单是否属于当前代理推广
@handler CheckOrderAgent
get /order/agent (CheckOrderAgentReq) returns (CheckOrderAgentResp)
} }
type ( type (
@@ -439,28 +483,30 @@ type (
LevelName string `json:"level_name"` // 等级名称 LevelName string `json:"level_name"` // 等级名称
Mobile string `json:"mobile"` // 手机号 Mobile string `json:"mobile"` // 手机号
CreateTime string `json:"create_time"` // 加入团队时间 CreateTime string `json:"create_time"` // 加入团队时间
TotalRebateAmount float64 `json:"total_rebate_amount"` // 返佣给我的总金额
TodayRebateAmount float64 `json:"today_rebate_amount"` // 返佣给我的今日金额
TodayInvites int64 `json:"today_invites"` // 邀请加入团队的今日人数
MonthInvites int64 `json:"month_invites"` // 邀请加入团队的本月人数
TotalInvites int64 `json:"total_invites"` // 邀请加入团队的总人数
TodayQueries int64 `json:"today_queries"` // 当日查询量 TodayQueries int64 `json:"today_queries"` // 当日查询量
MonthQueries int64 `json:"month_queries"` // 本月查询量 MonthQueries int64 `json:"month_queries"` // 本月查询量
TotalQueries int64 `json:"total_queries"` // 总查询量 TotalQueries int64 `json:"total_queries"` // 总查询量
TotalRebateAmount float64 `json:"total_rebate_amount"` // 返佣给我的总金额
TodayRebateAmount float64 `json:"today_rebate_amount"` // 返佣给我的今日金额
TotalInvites int64 `json:"total_invites"` // 邀请加入团队的总人数
TodayInvites int64 `json:"today_invites"` // 邀请加入团队的今日人数
MonthInvites int64 `json:"month_invites"` // 邀请加入团队的本月人数
IsDirect bool `json:"is_direct"` // 是否直接下级 IsDirect bool `json:"is_direct"` // 是否直接下级
} }
// 收益信息 // 收益信息
GetRevenueInfoResp { GetRevenueInfoResp {
Balance float64 `json:"balance"` // 可用余额 Balance float64 `json:"balance"` // 可用余额
FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 FrozenBalance float64 `json:"frozen_balance"` // 冻结余额
TotalEarnings float64 `json:"total_earnings"` // 累计收益(钱包总收益) TotalEarnings float64 `json:"total_earnings"` // 累计收益(钱包总收益)
WithdrawnAmount float64 `json:"withdrawn_amount"` // 累计提现 WithdrawnAmount float64 `json:"withdrawn_amount"` // 累计提现
CommissionTotal float64 `json:"commission_total"` // 佣金累计总收益(推广订单获得的佣金) CommissionTotal float64 `json:"commission_total"` // 佣金累计总收益(推广订单获得的佣金)
CommissionToday float64 `json:"commission_today"` // 佣金今日收益 CommissionToday float64 `json:"commission_today"` // 佣金今日收益
CommissionMonth float64 `json:"commission_month"` // 佣金本月收益 CommissionMonth float64 `json:"commission_month"` // 佣金本月收益
RebateTotal float64 `json:"rebate_total"` // 返佣累计总收益(包括推广返佣和升级返佣) RebateTotal float64 `json:"rebate_total"` // 返佣累计总收益(包括推广返佣和升级返佣)
RebateToday float64 `json:"rebate_today"` // 返佣今日收益 RebateToday float64 `json:"rebate_today"` // 返佣今日收益
RebateMonth float64 `json:"rebate_month"` // 返佣本月收益 RebateMonth float64 `json:"rebate_month"` // 返佣本月收益
AlipayMonthQuota float64 `json:"alipay_month_quota"` // 支付宝每月提现总额度
AlipayMonthUsed float64 `json:"alipay_month_used"` // 本月已使用的支付宝提现额度
} }
// 佣金记录 // 佣金记录
GetCommissionListReq { GetCommissionListReq {
@@ -499,6 +545,7 @@ type (
OrderNo string `json:"order_no"` // 订单号 OrderNo string `json:"order_no"` // 订单号
RebateType int64 `json:"rebate_type"` // 返佣类型1=直接上级2=钻石上级3=黄金上级 RebateType int64 `json:"rebate_type"` // 返佣类型1=直接上级2=钻石上级3=黄金上级
Amount float64 `json:"amount"` // 返佣金额 Amount float64 `json:"amount"` // 返佣金额
Status int64 `json:"status"` // 状态1=已发放2=已冻结3=已取消(已退款)
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
// 升级返佣记录 // 升级返佣记录
@@ -565,28 +612,46 @@ type (
Total int64 `json:"total"` // 总数 Total int64 `json:"total"` // 总数
List []WithdrawalItem `json:"list"` // 列表 List []WithdrawalItem `json:"list"` // 列表
} }
// 提现记录
WithdrawalItem { WithdrawalItem {
Id string `json:"id"` // 记录ID Id string `json:"id"` // 记录ID
WithdrawalNo string `json:"withdrawal_no"` // 提现单号 WithdrawalNo string `json:"withdrawal_no"` // 提现单号
Amount float64 `json:"amount"` // 提现金额 Amount float64 `json:"amount"` // 提现金额
TaxAmount float64 `json:"tax_amount"` // 税费金额 TaxAmount float64 `json:"tax_amount"` // 税费金额
ActualAmount float64 `json:"actual_amount"` // 实际到账金额 ActualAmount float64 `json:"actual_amount"` // 实际到账金额
Status int64 `json:"status"` // 状态1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败 Status int64 `json:"status"` // 状态1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败
PayeeAccount string `json:"payee_account"` // 收款账户 WithdrawalType int64 `json:"withdrawal_type"` // 提现方式1=支付宝2=银行卡
PayeeName string `json:"payee_name"` // 收款人姓名 PayeeAccount string `json:"payee_account"` // 收款账户(支付宝账号或银行卡号)
Remark string `json:"remark"` // 备注 PayeeName string `json:"payee_name"` // 收款人姓名
CreateTime string `json:"create_time"` // 创建时间 BankCardNo string `json:"bank_card_no"` // 银行卡号(银行卡提现时填写)
BankName string `json:"bank_name"` // 开户行名称(银行卡提现时填写)
Remark string `json:"remark"` // 备注
CreateTime string `json:"create_time"` // 创建时间
} }
// 申请提现 // 申请提现
ApplyWithdrawalReq { ApplyWithdrawalReq {
Amount float64 `json:"amount"` // 提现金额 Amount float64 `json:"amount"` // 提现金额
PayeeAccount string `json:"payee_account"` // 收款账户 WithdrawalType int64 `json:"withdrawal_type"` // 提现方式1=支付宝2=银行卡
PayeeName string `json:"payee_name"` // 收款人姓名 PayeeAccount string `json:"payee_account"` // 收款账户(支付宝账号或银行卡号)
PayeeName string `json:"payee_name"` // 收款人姓名
BankCardNo string `json:"bank_card_no,optional"` // 银行卡号(银行卡提现必填)
BankName string `json:"bank_name,optional"` // 开户行名称(银行卡提现必填)
} }
ApplyWithdrawalResp { ApplyWithdrawalResp {
WithdrawalId string `json:"withdrawal_id"` // 提现记录ID WithdrawalId string `json:"withdrawal_id"` // 提现记录ID
WithdrawalNo string `json:"withdrawal_no"` // 提现单号 WithdrawalNo string `json:"withdrawal_no"` // 提现单号
} }
// 获取上次提现信息(用于前端预填)
GetLastWithdrawalInfoReq {
WithdrawalType int64 `form:"withdrawal_type"` // 提现方式1=支付宝2=银行卡
}
GetLastWithdrawalInfoResp {
WithdrawalType int64 `json:"withdrawal_type"` // 提现方式
PayeeAccount string `json:"payee_account"` // 收款账户(支付宝账号或银行卡号)
PayeeName string `json:"payee_name"` // 收款人姓名
BankCardNo string `json:"bank_card_no"` // 银行卡号
BankName string `json:"bank_name"` // 开户行名称
}
// 实名认证 // 实名认证
RealNameAuthReq { RealNameAuthReq {
Name string `json:"name"` // 姓名 Name string `json:"name"` // 姓名
@@ -607,13 +672,13 @@ type (
List []PromotionQueryItem `json:"list"` // 列表 List []PromotionQueryItem `json:"list"` // 列表
} }
PromotionQueryItem { PromotionQueryItem {
Id string `json:"id"` // 查询ID Id string `json:"id"` // 查询ID
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
ProductName string `json:"product_name"` // 产品名称 ProductName string `json:"product_name"` // 产品名称
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
QueryState string `json:"query_state"` // 查询状态 QueryState string `json:"query_state"` // 查询状态
Params map[string]interface{} `json:"params,optional"` // 查询参数(已脱敏) Params map[string]interface{} `json:"params,optional"` // 查询参数(已脱敏)
Price float64 `json:"price"` // 查询价格 Price float64 `json:"price"` // 查询价格
} }
// ============================================ // ============================================
// 用户模块白名单相关类型 // 用户模块白名单相关类型

View File

@@ -47,6 +47,7 @@ type (
Id string `json:"id"` Id string `json:"id"`
PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式) PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式)
PayType string `json:"pay_type" validate:"required,oneof=query agent_vip agent_upgrade"` PayType string `json:"pay_type" validate:"required,oneof=query agent_vip agent_upgrade"`
Code string `json:"code,optional"` // 微信小程序/H5授权码用于自动绑定微信账号当用户未绑定微信时
} }
PaymentResp { PaymentResp {
PrepayData interface{} `json:"prepay_data"` PrepayData interface{} `json:"prepay_data"`

View File

@@ -156,8 +156,26 @@ service main {
type ( type (
sendSmsReq { sendSmsReq {
Mobile string `json:"mobile" validate:"required,mobile"` Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"` ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
CaptchaVerifyParam string `json:"captchaVerifyParam,optional"`
}
)
//============================> captcha v1 <============================
@server (
prefix: api/v1
group: captcha
)
service main {
@doc "get encrypted scene id for aliyun captcha"
@handler getEncryptedSceneId
post /captcha/encryptedSceneId returns (GetEncryptedSceneIdResp)
}
type (
GetEncryptedSceneIdResp {
EncryptedSceneId string `json:"encryptedSceneId"`
} }
) )

View File

@@ -18,6 +18,12 @@ VerifyCode:
SignName: "天远查" SignName: "天远查"
TemplateCode: "SMS_302641455" TemplateCode: "SMS_302641455"
ValidTime: 300 ValidTime: 300
Captcha:
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
SceneID: "wynt39to"
EKey: ""
Encrypt: Encrypt:
SecretKey: "ff83609b2b24fc73196aac3d3dfb874f" SecretKey: "ff83609b2b24fc73196aac3d3dfb874f"
WestConfig: WestConfig:
@@ -67,10 +73,8 @@ WechatH5:
AppID: "wx442ee1ac1ee75917" AppID: "wx442ee1ac1ee75917"
AppSecret: "c80474909db42f63913b7a307b3bee17" AppSecret: "c80474909db42f63913b7a307b3bee17"
WechatMini: WechatMini:
AppID: "wx781abb66b3368963" # 小程序的AppID AppID: "wx5bacc94add2da981" # 小程序的AppID
AppSecret: "c7d02cdb0fc23c35c93187af9243b00d" # 小程序的AppSecret AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
TycAppID: "wxe74617f3dd56c196"
TycAppSecret: "c8207e54aef5689b2a7c1f91ed7ae8a0"
Query: Query:
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒 ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
AdminConfig: AdminConfig:

View File

@@ -19,6 +19,12 @@ VerifyCode:
SignName: "海南海宇大数据" SignName: "海南海宇大数据"
TemplateCode: "SMS_302641455" TemplateCode: "SMS_302641455"
ValidTime: 300 ValidTime: 300
Captcha:
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
SceneID: "wynt39to"
EKey: ""
Encrypt: Encrypt:
SecretKey: "ff83609b2b24fc73196aac3d3dfb874f" SecretKey: "ff83609b2b24fc73196aac3d3dfb874f"
Alipay: Alipay:

View File

@@ -11,6 +11,7 @@ type Config struct {
CacheRedis cache.CacheConf CacheRedis cache.CacheConf
JwtAuth JwtAuth // JWT 鉴权相关配置 JwtAuth JwtAuth // JWT 鉴权相关配置
VerifyCode VerifyCode VerifyCode VerifyCode
Captcha CaptchaConfig // 阿里云验证码配置
Encrypt Encrypt Encrypt Encrypt
Alipay AlipayConfig Alipay AlipayConfig
Wxpay WxpayConfig Wxpay WxpayConfig
@@ -40,6 +41,15 @@ type VerifyCode struct {
TemplateCode string TemplateCode string
ValidTime int ValidTime int
} }
type CaptchaConfig struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
EKey string // 加密模式用的 ekeyBase64
}
type Encrypt struct { type Encrypt struct {
SecretKey string SecretKey string
} }

View File

@@ -0,0 +1,33 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package agent
import (
"net/http"
"qnc-server/app/main/api/internal/logic/agent"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GenerateInvitePosterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GenerateInvitePosterReq
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.NewGenerateInvitePosterLogic(r.Context(), svcCtx)
resp, err := l.GenerateInvitePoster(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,30 @@
package agent
import (
"net/http"
"qnc-server/app/main/api/internal/logic/agent"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetLastWithdrawalInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetLastWithdrawalInfoReq
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.NewGetLastWithdrawalInfoLogic(r.Context(), svcCtx)
resp, err := l.GetLastWithdrawalInfo(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -23,8 +23,27 @@ func SendSmsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
result.ParamValidateErrorResult(r, w, err) result.ParamValidateErrorResult(r, w, err)
return return
} }
// 获取客户端真实 IP
clientIP := getClientIP(r)
// 获取 User-Agent 用于判断微信环境
userAgent := r.Header.Get("User-Agent")
l := auth.NewSendSmsLogic(r.Context(), svcCtx) l := auth.NewSendSmsLogic(r.Context(), svcCtx)
err := l.SendSms(&req) err := l.SendSms(&req, clientIP, userAgent)
result.HttpResult(r, w, nil, err) result.HttpResult(r, w, nil, err)
} }
} }
// getClientIP 获取客户端真实 IP
func getClientIP(r *http.Request) string {
// 优先从 X-Forwarded-For 获取
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// X-Forwarded-For 可能包含多个 IP取第一个
return xff
}
// 其次从 X-Real-IP 获取
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
// 最后使用 RemoteAddr
return r.RemoteAddr
}

View File

@@ -0,0 +1,21 @@
package captcha
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/main/api/internal/logic/captcha"
"qnc-server/app/main/api/internal/svc"
)
func GetEncryptedSceneIdHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := captcha.NewGetEncryptedSceneIdLogic(r.Context(), svcCtx)
resp, err := l.GetEncryptedSceneId()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@@ -22,6 +22,7 @@ import (
app "qnc-server/app/main/api/internal/handler/app" app "qnc-server/app/main/api/internal/handler/app"
auth "qnc-server/app/main/api/internal/handler/auth" auth "qnc-server/app/main/api/internal/handler/auth"
authorization "qnc-server/app/main/api/internal/handler/authorization" authorization "qnc-server/app/main/api/internal/handler/authorization"
captcha "qnc-server/app/main/api/internal/handler/captcha"
notification "qnc-server/app/main/api/internal/handler/notification" notification "qnc-server/app/main/api/internal/handler/notification"
pay "qnc-server/app/main/api/internal/handler/pay" pay "qnc-server/app/main/api/internal/handler/pay"
product "qnc-server/app/main/api/internal/handler/product" product "qnc-server/app/main/api/internal/handler/product"
@@ -104,7 +105,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
}, },
{ {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/upgrade", Path: "/upgrade/agent",
Handler: admin_agent.AdminUpgradeAgentHandler(serverCtx), Handler: admin_agent.AdminUpgradeAgentHandler(serverCtx),
}, },
{ {
@@ -661,6 +662,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/info", Path: "/info",
Handler: agent.GetAgentInfoHandler(serverCtx), Handler: agent.GetAgentInfoHandler(serverCtx),
}, },
{
Method: http.MethodGet,
Path: "/invite/poster",
Handler: agent.GenerateInvitePosterHandler(serverCtx),
},
{ {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/invite_code/delete", Path: "/invite_code/delete",
@@ -786,6 +792,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/withdrawal/apply", Path: "/withdrawal/apply",
Handler: agent.ApplyWithdrawalHandler(serverCtx), Handler: agent.ApplyWithdrawalHandler(serverCtx),
}, },
{
Method: http.MethodGet,
Path: "/withdrawal/last_info",
Handler: agent.GetLastWithdrawalInfoHandler(serverCtx),
},
{ {
Method: http.MethodGet, Method: http.MethodGet,
Path: "/withdrawal/list", Path: "/withdrawal/list",
@@ -862,6 +873,18 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"), rest.WithPrefix("/api/v1"),
) )
server.AddRoutes(
[]rest.Route{
{
// get encrypted scene id for aliyun captcha
Method: http.MethodPost,
Path: "/captcha/encryptedSceneId",
Handler: captcha.GetEncryptedSceneIdHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes( server.AddRoutes(
[]rest.Route{ []rest.Route{
{ {

View File

@@ -4,10 +4,10 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"time" "os"
"qnc-server/common/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stores/sqlx"
@@ -32,6 +32,13 @@ func NewAdminAuditWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContex
} }
} }
// parseFloat 解析配置中的浮点数
func (l *AdminAuditWithdrawalLogic) parseFloat(s string) (float64, error) {
var result float64
_, err := fmt.Sscanf(s, "%f", &result)
return result, err
}
func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWithdrawalReq) (resp *types.AdminAuditWithdrawalResp, err error) { func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWithdrawalReq) (resp *types.AdminAuditWithdrawalResp, err error) {
// 1. 查询提现记录 // 1. 查询提现记录
withdrawal, err := l.svcCtx.AgentWithdrawalModel.FindOne(l.ctx, req.WithdrawalId) withdrawal, err := l.svcCtx.AgentWithdrawalModel.FindOne(l.ctx, req.WithdrawalId)
@@ -46,39 +53,126 @@ func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWi
// 4. 使用事务处理审核 // 4. 使用事务处理审核
err = l.svcCtx.AgentWithdrawalModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { err = l.svcCtx.AgentWithdrawalModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
if req.Status == 2 { // 审核通过 switch req.Status {
// 4.1 更新提现记录状态为提现中 case 2: // 审核通过
withdrawal.Status = 4 // 提现中 // 4.1 根据提现方式处理
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true} switch withdrawal.WithdrawalType {
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil { case 1:
return errors.Wrapf(err, "更新提现记录失败") // 支付宝提现:审核通过前再次校验月度额度,避免一次性通过多笔超限
} now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
nextMonthStart := monthStart.AddDate(0, 1, 0)
// 4.2 调用支付宝转账接口 // 获取支付宝月度额度配置(默认 800 元)
outBizNo := withdrawal.WithdrawNo alipayQuota := 800.0
transferResp, err := l.svcCtx.AlipayService.AliTransfer(transCtx, withdrawal.PayeeAccount, withdrawal.PayeeName, withdrawal.ActualAmount, "代理提现", outBizNo) if cfg, cfgErr := l.svcCtx.AgentConfigModel.FindOneByConfigKey(transCtx, "alipay_month_quota"); cfgErr == nil {
if err != nil { if parsed, parseErr := l.parseFloat(cfg.ConfigValue); parseErr == nil && parsed > 0 {
// 转账失败,更新状态为失败 alipayQuota = parsed
withdrawal.Status = 6 // 提现失败 }
withdrawal.Remark = sql.NullString{String: fmt.Sprintf("转账失败: %v", err), Valid: true}
l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal)
// 解冻余额
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
if err == nil {
wallet.FrozenBalance -= withdrawal.Amount
wallet.Balance += withdrawal.Amount
l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet)
} }
return errors.Wrapf(err, "支付宝转账失败") // 统计本月已成功的支付宝提现金额status=5
} withdrawBuilder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND withdrawal_type = ? AND status = ? AND create_time >= ? AND create_time < ?",
withdrawal.AgentId, 1, 5, monthStart, nextMonthStart)
usedAmount, sumErr := l.svcCtx.AgentWithdrawalModel.FindSum(transCtx, withdrawBuilder, "amount")
if sumErr != nil {
return errors.Wrapf(sumErr, "查询本月支付宝提现额度使用情况失败")
}
// 4.3 根据转账结果更新状态 if usedAmount+withdrawal.Amount > alipayQuota {
switch transferResp.Status { // 超出额度,不允许通过,保持待审核状态并提示原因
case "SUCCESS": withdrawal.Status = 1
// 转账成功 withdrawal.Remark = sql.NullString{
String: fmt.Sprintf("超过本月支付宝提现额度(限额:%.2f 元,已用:%.2f 元),请使用银行卡提现或调整金额", alipayQuota, usedAmount),
Valid: true,
}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
return errors.Wrapf(xerr.NewErrMsg("超过本月支付宝提现额度,无法通过该笔提现"), "")
}
// 支付宝提现:开发环境下做模拟,不调用真实支付宝转账
mockTransferStatus := "SUCCESS"
var transferResp struct {
Status string
SubCode string
}
if os.Getenv("ENV") == "development" {
transferResp.Status = mockTransferStatus
logx.Infof("【DEV】模拟支付宝转账成功withdrawNo=%s, amount=%.2f, payee=%s",
withdrawal.WithdrawNo, withdrawal.ActualAmount, withdrawal.PayeeAccount)
} else {
// 生产环境:同步调用支付宝转账接口
outBizNo := withdrawal.WithdrawNo
resp, err := l.svcCtx.AlipayService.AliTransfer(transCtx, withdrawal.PayeeAccount, withdrawal.PayeeName, withdrawal.ActualAmount, "代理提现", outBizNo)
if err != nil {
// 调用失败保持状态为待审核1只记录备注方便管理员重试
withdrawal.Status = 1 // 待审核
withdrawal.Remark = sql.NullString{String: fmt.Sprintf("支付宝转账调用失败: %v", err), Valid: true}
_ = l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal)
return errors.Wrapf(err, "支付宝转账失败")
}
transferResp.Status = resp.Status
transferResp.SubCode = resp.SubCode
}
// 4.2 根据转账结果更新状态
switch transferResp.Status {
case "SUCCESS":
// 转账成功
withdrawal.Status = 5 // 提现成功
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
// 更新钱包(解冻并扣除)
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
if err != nil {
return errors.Wrapf(err, "查询钱包失败")
}
wallet.FrozenBalance -= withdrawal.Amount
wallet.WithdrawnAmount += withdrawal.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "更新钱包失败")
}
// 更新扣税记录状态
taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("withdrawal_id = ?", withdrawal.Id)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "")
if err == nil && len(taxRecords) > 0 {
taxRecord := taxRecords[0]
taxRecord.TaxStatus = 2 // 已扣税
taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now())
l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord)
}
case "FAIL":
// 转账失败:保持待审核状态,方便人工处理或重试
withdrawal.Status = 1 // 待审核
errorMsg := l.mapAlipayError(transferResp.SubCode)
withdrawal.Remark = sql.NullString{String: errorMsg, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
case "DEALING":
// 处理中同样保持待审核状态1由管理员后续确认
withdrawal.Status = 1
withdrawal.Remark = sql.NullString{String: "支付宝处理中,请稍后重试或联系平台", Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
}
case 2:
// 银行卡提现:审核通过即视为提现成功(线下已/将立即打款)
withdrawal.Status = 5 // 提现成功 withdrawal.Status = 5 // 提现成功
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil { if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败") return errors.Wrapf(err, "更新提现记录失败")
} }
@@ -96,7 +190,7 @@ func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWi
// 更新扣税记录状态 // 更新扣税记录状态
taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder(). taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("withdrawal_id = ? AND del_state = ?", withdrawal.Id, globalkey.DelStateNo) Where("withdrawal_id = ?", withdrawal.Id)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "") taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "")
if err == nil && len(taxRecords) > 0 { if err == nil && len(taxRecords) > 0 {
taxRecord := taxRecords[0] taxRecord := taxRecords[0]
@@ -104,30 +198,9 @@ func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWi
taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now()) taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now())
l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord) l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord)
} }
case "FAIL":
// 转账失败
withdrawal.Status = 6 // 提现失败
errorMsg := l.mapAlipayError(transferResp.SubCode)
withdrawal.Remark = sql.NullString{String: errorMsg, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
// 解冻余额
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
if err == nil {
wallet.FrozenBalance -= withdrawal.Amount
wallet.Balance += withdrawal.Amount
l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet)
}
case "DEALING":
// 处理中,保持提现中状态,后续通过轮询更新
// 状态已经是4提现中无需更新
} }
} else if req.Status == 3 { // 审核拒绝 case 3: // 审核拒绝
// 4.1 更新提现记录状态为拒绝 // 4.1 更新提现记录状态为拒绝
withdrawal.Status = 3 // 审核拒绝 withdrawal.Status = 3 // 审核拒绝
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true} withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}

View File

@@ -49,6 +49,17 @@ func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *type
item.Remark = v.Remark.String item.Remark = v.Remark.String
} }
item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05")
// 如果是银行卡提现,填充银行卡信息
if v.WithdrawalType == 2 {
if v.BankCardNo.Valid {
item.BankCardNo = v.BankCardNo.String
}
if v.BankName.Valid {
item.BankName = v.BankName.String
}
}
items = append(items, item) items = append(items, item)
} }
resp = &types.AdminGetAgentWithdrawalListResp{ resp = &types.AdminGetAgentWithdrawalListResp{

View File

@@ -86,7 +86,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
return nil return nil
}, func() error { }, func() error {
var err error var err error
orders, err = l.svcCtx.OrderModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "id DESC") orders, err = l.svcCtx.OrderModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "update_time DESC")
if err != nil { if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询订单列表失败 err: %v", err) return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询订单列表失败 err: %v", err)
} }
@@ -264,6 +264,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
Status: order.Status, Status: order.Status,
CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"),
QueryState: queryStateMap[order.Id], QueryState: queryStateMap[order.Id],
UpdateTime: order.UpdateTime.Format("2006-01-02 15:04:05"),
} }
if order.PayTime.Valid { if order.PayTime.Valid {
item.PayTime = order.PayTime.Time.Format("2006-01-02 15:04:05") item.PayTime = order.PayTime.Time.Format("2006-01-02 15:04:05")

View File

@@ -3,12 +3,11 @@ package agent
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -62,53 +61,138 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "") return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "")
} }
// 3. 验证提现金额 // 3. 验证提现方式
if req.WithdrawalType != 1 && req.WithdrawalType != 2 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现方式无效"), "")
}
// 4. 验证提现信息
if req.WithdrawalType == 1 {
// 支付宝提现:验证支付宝账号和姓名
if req.PayeeAccount == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入支付宝账号"), "")
}
if req.PayeeName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入收款人姓名"), "")
}
} else if req.WithdrawalType == 2 {
// 银行卡提现:验证银行卡号、开户行和姓名
if req.BankCardNo == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入银行卡号"), "")
}
if req.BankName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入开户行名称"), "")
}
if req.PayeeName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入收款人姓名"), "")
}
}
// 5. 验证提现金额
if req.Amount <= 0 { if req.Amount <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "") return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "")
} }
// 4. 获取钱包信息 // 6. 获取钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id) wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败, %v", err)
} }
// 5. 验证余额 // 7. 验证余额(包括检查是否为负数)
if wallet.Balance < 0 {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("账户存在欠款,请先补足欠款后再申请提现,当前余额:%.2f", wallet.Balance)), "")
}
if wallet.Balance < req.Amount { if wallet.Balance < req.Amount {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "") return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "")
} }
// 6. 计算税费 // 8. 支付宝月度提现额度校验(仅针对支付宝提现)
if req.WithdrawalType == 1 {
now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
nextMonthStart := monthStart.AddDate(0, 1, 0)
// 8.1 获取支付宝月度额度配置(默认 800 元)
alipayQuota := 800.0
if cfg, cfgErr := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, "alipay_month_quota"); cfgErr == nil {
if parsed, parseErr := l.parseFloat(cfg.ConfigValue); parseErr == nil && parsed > 0 {
alipayQuota = parsed
}
}
// 8.2 统计本月已申请/成功的支付宝提现金额status IN (1,5)),避免多次申请占用超额
withdrawBuilder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND withdrawal_type = ? AND status IN (1,5) AND create_time >= ? AND create_time < ?",
agent.Id, 1, monthStart, nextMonthStart)
usedAmount, sumErr := l.svcCtx.AgentWithdrawalModel.FindSum(l.ctx, withdrawBuilder, "amount")
if sumErr != nil {
return nil, errors.Wrapf(sumErr, "查询本月支付宝提现额度使用情况失败")
}
remainQuota := alipayQuota - usedAmount
if remainQuota <= 0 {
return nil, errors.Wrapf(
xerr.NewErrMsg(fmt.Sprintf("本月支付宝提现额度已用完(额度:%.2f 元),请使用银行卡提现", alipayQuota)),
"",
)
}
if req.Amount > remainQuota {
return nil, errors.Wrapf(
xerr.NewErrMsg(fmt.Sprintf("本月支付宝最高可提现 %.2f 元,请调整提现金额或使用银行卡提现", remainQuota)),
"",
)
}
}
// 9. 计算税费
yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month())) yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month()))
taxInfo, err := l.calculateTax(l.ctx, agent.Id, req.Amount, yearMonth) taxInfo, err := l.calculateTax(l.ctx, agent.Id, req.Amount, yearMonth)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "计算税费失败") return nil, errors.Wrapf(err, "计算税费失败")
} }
// 7. 生成提现单号 // 10. 生成提现单号WD开头 + GenerateOutTradeNo生成的订单号确保总长度不超过32个字符
withdrawNo := fmt.Sprintf("WD%d%d", time.Now().Unix(), agent.Id) orderNo := l.svcCtx.AlipayService.GenerateOutTradeNo()
withdrawNo := "WD" + orderNo
// 确保总长度不超过32个字符
if len(withdrawNo) > 32 {
withdrawNo = withdrawNo[:32]
}
// 8. 使用事务处理提现申请 // 11. 使用事务处理提现申请
var withdrawalId string var withdrawalId string
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 8.1 冻结余额 // 11.1 冻结余额
wallet.FrozenBalance += req.Amount wallet.FrozenBalance += req.Amount
wallet.Balance -= req.Amount wallet.Balance -= req.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil { if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "冻结余额失败") return errors.Wrapf(err, "冻结余额失败")
} }
// 8.2 创建提现记录 // 11.2 创建提现记录
withdrawal := &model.AgentWithdrawal{ withdrawal := &model.AgentWithdrawal{
Id: uuid.New().String(), Id: uuid.New().String(),
AgentId: agent.Id, AgentId: agent.Id,
WithdrawNo: withdrawNo, WithdrawNo: withdrawNo,
PayeeAccount: req.PayeeAccount, WithdrawalType: req.WithdrawalType,
PayeeName: req.PayeeName, PayeeAccount: req.PayeeAccount,
Amount: req.Amount, PayeeName: req.PayeeName,
ActualAmount: taxInfo.ActualAmount, Amount: req.Amount,
TaxAmount: taxInfo.TaxAmount, ActualAmount: taxInfo.ActualAmount,
Status: 1, // 处理中(待审核) TaxAmount: taxInfo.TaxAmount,
Status: 1, // 待审核
}
// 如果是银行卡提现,设置银行卡相关字段
if req.WithdrawalType == 2 {
withdrawal.BankCardNo = lzUtils.StringToNullString(req.BankCardNo)
withdrawal.BankName = lzUtils.StringToNullString(req.BankName)
// 银行卡提现时payee_account 可以存储银行卡号(便于查询),也可以留空
if req.PayeeAccount == "" {
withdrawal.PayeeAccount = req.BankCardNo
}
} }
_, err := l.svcCtx.AgentWithdrawalModel.Insert(transCtx, session, withdrawal) _, err := l.svcCtx.AgentWithdrawalModel.Insert(transCtx, session, withdrawal)
@@ -117,7 +201,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
} }
withdrawalId = withdrawal.Id withdrawalId = withdrawal.Id
// 8.3 创建扣税记录 // 11.3 创建扣税记录
taxRecord := &model.AgentWithdrawalTax{ taxRecord := &model.AgentWithdrawalTax{
AgentId: agent.Id, AgentId: agent.Id,
WithdrawalId: withdrawalId, WithdrawalId: withdrawalId,
@@ -167,8 +251,10 @@ func (l *ApplyWithdrawalLogic) calculateTax(ctx context.Context, agentId string,
} }
// 查询本月已提现金额 // 查询本月已提现金额
// 注意FindAll 方法会自动添加 del_state = ? 条件,所以这里不需要手动添加
// 这里对 year_month 使用反引号包裹,避免与某些数据库版本/SQL 模式下的关键字冲突
builder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder(). builder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("agent_id = ? AND year_month = ? AND del_state = ?", agentId, yearMonth, globalkey.DelStateNo) Where("agent_id = ? AND `year_month` = ?", agentId, yearMonth)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(ctx, builder, "") taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(ctx, builder, "")
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "查询月度提现记录失败") return nil, errors.Wrapf(err, "查询月度提现记录失败")

View File

@@ -4,12 +4,13 @@ import (
"context" "context"
"strings" "strings"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
) )
type CheckFeatureWhitelistStatusLogic struct { type CheckFeatureWhitelistStatusLogic struct {

View File

@@ -3,13 +3,14 @@ package agent
import ( import (
"context" "context"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
) )
type CheckOrderAgentLogic struct { type CheckOrderAgentLogic struct {

View File

@@ -4,15 +4,16 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
) )
type CreateWhitelistOrderLogic struct { type CreateWhitelistOrderLogic struct {

View File

@@ -0,0 +1,52 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package agent
import (
"context"
"encoding/base64"
"fmt"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type GenerateInvitePosterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGenerateInvitePosterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateInvitePosterLogic {
return &GenerateInvitePosterLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GenerateInvitePosterLogic) GenerateInvitePoster(req *types.GenerateInvitePosterReq) (resp *types.GenerateInvitePosterResp, err error) {
if req.InviteLink == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请链接不能为空"), "")
}
// 调用ImageService生成海报
imageData, mimeType, err := l.svcCtx.ImageService.ProcessImageWithQRCode("invitation", req.InviteLink)
if err != nil {
l.Errorf("生成邀请海报失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成海报失败: %v", err)
}
// 将图片数据转换为base64编码的data URL
base64Data := base64.StdEncoding.EncodeToString(imageData)
posterUrl := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
return &types.GenerateInvitePosterResp{
PosterUrl: posterUrl,
}, nil
}

View File

@@ -4,15 +4,15 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"strconv"
"strings"
"time"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/tool" "qnc-server/common/tool"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
"strconv"
"strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -124,7 +124,18 @@ func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCo
// 先查询是否已存在短链 // 先查询是否已存在短链
existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}, 2, globalkey.DelStateNo) existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}, 2, globalkey.DelStateNo)
if err == nil && existingShortLink != nil { if err == nil && existingShortLink != nil {
// 已存在短链,直接返回 // 已存在短链,检查 target_path 是否需要更新
if existingShortLink.TargetPath != targetPath {
// target_path 已变化,更新短链记录
oldTargetPath := existingShortLink.TargetPath
existingShortLink.TargetPath = targetPath
if updateErr := l.svcCtx.AgentShortLinkModel.UpdateWithVersion(l.ctx, nil, existingShortLink); updateErr != nil {
l.Errorf("更新短链 target_path 失败: %v", updateErr)
// 即使更新失败,也返回旧短链,避免影响用户体验
} else {
l.Infof("短链 target_path 已更新: shortCode=%s, old=%s, new=%s", existingShortLink.ShortCode, oldTargetPath, targetPath)
}
}
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil
} }
@@ -136,6 +147,18 @@ func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCo
if inviteCodeId == "" && inviteCode != "" { if inviteCodeId == "" && inviteCode != "" {
existingByCode, err2 := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeTypeDelState(l.ctx, sql.NullString{String: inviteCode, Valid: true}, 2, globalkey.DelStateNo) existingByCode, err2 := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeTypeDelState(l.ctx, sql.NullString{String: inviteCode, Valid: true}, 2, globalkey.DelStateNo)
if err2 == nil && existingByCode != nil { if err2 == nil && existingByCode != nil {
// 已存在短链,检查 target_path 是否需要更新
if existingByCode.TargetPath != targetPath {
// target_path 已变化,更新短链记录
oldTargetPath := existingByCode.TargetPath
existingByCode.TargetPath = targetPath
if updateErr := l.svcCtx.AgentShortLinkModel.UpdateWithVersion(l.ctx, nil, existingByCode); updateErr != nil {
l.Errorf("更新短链 target_path 失败: %v", updateErr)
// 即使更新失败,也返回旧短链,避免影响用户体验
} else {
l.Infof("短链 target_path 已更新: shortCode=%s, old=%s, new=%s", existingByCode.ShortCode, oldTargetPath, targetPath)
}
}
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingByCode.ShortCode), nil return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingByCode.ShortCode), nil
} }
if err2 != nil && !errors.Is(err2, model.ErrNotFound) { if err2 != nil && !errors.Is(err2, model.ErrNotFound) {

View File

@@ -0,0 +1,95 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLastWithdrawalInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLastWithdrawalInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLastWithdrawalInfoLogic {
return &GetLastWithdrawalInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLastWithdrawalInfoLogic) GetLastWithdrawalInfo(req *types.GetLastWithdrawalInfoReq) (resp *types.GetLastWithdrawalInfoResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证提现方式
if req.WithdrawalType != 1 && req.WithdrawalType != 2 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现方式无效"), "")
}
// 3. 查询该代理最近一次该类型的提现记录
// 注意FindAll 方法会自动添加 del_state = ? 条件,所以这里不需要手动添加
builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND withdrawal_type = ?", agent.Id, req.WithdrawalType).
OrderBy("create_time DESC").
Limit(1)
withdrawals, err := l.svcCtx.AgentWithdrawalModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上次提现记录失败, %v", err)
}
// 4. 如果没有找到记录,返回空信息
if len(withdrawals) == 0 {
return &types.GetLastWithdrawalInfoResp{
WithdrawalType: req.WithdrawalType,
PayeeAccount: "",
PayeeName: "",
BankCardNo: "",
BankName: "",
}, nil
}
// 5. 组装响应
lastWithdrawal := withdrawals[0]
resp = &types.GetLastWithdrawalInfoResp{
WithdrawalType: lastWithdrawal.WithdrawalType,
PayeeAccount: lastWithdrawal.PayeeAccount,
PayeeName: lastWithdrawal.PayeeName,
BankCardNo: "",
BankName: "",
}
// 如果是银行卡提现,填充银行卡信息
if lastWithdrawal.WithdrawalType == 2 {
if lastWithdrawal.BankCardNo.Valid {
resp.BankCardNo = lastWithdrawal.BankCardNo.String
}
if lastWithdrawal.BankName.Valid {
resp.BankName = lastWithdrawal.BankName.String
}
}
return resp, nil
}

View File

@@ -3,11 +3,12 @@ package agent
import ( import (
"context" "context"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/zeromicro/go-zero/core/logx"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View File

@@ -3,12 +3,13 @@ package agent
import ( import (
"context" "context"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
) )
type GetWhitelistListLogic struct { type GetWhitelistListLogic struct {

View File

@@ -81,18 +81,31 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
remark = withdrawal.Remark.String remark = withdrawal.Remark.String
} }
list = append(list, types.WithdrawalItem{ item := types.WithdrawalItem{
Id: withdrawal.Id, Id: withdrawal.Id,
WithdrawalNo: withdrawal.WithdrawNo, WithdrawalNo: withdrawal.WithdrawNo,
Amount: withdrawal.Amount, WithdrawalType: withdrawal.WithdrawalType,
TaxAmount: withdrawal.TaxAmount, Amount: withdrawal.Amount,
ActualAmount: withdrawal.ActualAmount, TaxAmount: withdrawal.TaxAmount,
Status: withdrawal.Status, ActualAmount: withdrawal.ActualAmount,
PayeeAccount: withdrawal.PayeeAccount, Status: withdrawal.Status,
PayeeName: withdrawal.PayeeName, PayeeAccount: withdrawal.PayeeAccount,
Remark: remark, PayeeName: withdrawal.PayeeName,
CreateTime: withdrawal.CreateTime.Format("2006-01-02 15:04:05"), Remark: remark,
}) CreateTime: withdrawal.CreateTime.Format("2006-01-02 15:04:05"),
}
// 如果是银行卡提现,填充银行卡信息
if withdrawal.WithdrawalType == 2 {
if withdrawal.BankCardNo.Valid {
item.BankCardNo = withdrawal.BankCardNo.String
}
if withdrawal.BankName.Valid {
item.BankName = withdrawal.BankName.String
}
}
list = append(list, item)
} }
return &types.GetWithdrawalListResp{ return &types.GetWithdrawalListResp{

View File

@@ -4,12 +4,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
) )
type OfflineFeatureLogic struct { type OfflineFeatureLogic struct {

View File

@@ -5,13 +5,13 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"os" "os"
"strconv"
"time"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
"strconv"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"

View File

@@ -2,10 +2,11 @@ package auth
import ( import (
"context" "context"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"fmt" "fmt"
"math/rand" "math/rand"
"qnc-server/common/xerr"
"qnc-server/pkg/captcha"
"qnc-server/pkg/lzkit/crypto"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -34,13 +35,58 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo
} }
} }
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error { func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string, userAgent string) error {
secretKey := l.svcCtx.Config.Encrypt.SecretKey secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil { if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err) return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
}
// 1. 滑块验证码校验(可选,支持微信环境跳过验证)
cfg := l.svcCtx.Config.Captcha
captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{
AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret,
EndpointURL: cfg.EndpointURL,
SceneID: cfg.SceneID,
}, req.CaptchaVerifyParam, userAgent)
if captchaResult.VerifyErr != nil {
return captchaResult.VerifyErr
}
// 2. 防刷策略
if captchaResult.Skipped {
// 没有滑块验证码,使用更严格的限流策略
// 2.1 IP 限流:同一 IP 每小时最多发送 10 次
ipLimitKey := fmt.Sprintf("ip_limit:%s", clientIP)
ipCount, err := l.svcCtx.Redis.Incr(ipLimitKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取IP限流缓存失败: %v", err)
}
if ipCount == 1 {
// 第一次访问,设置 1 小时过期
l.svcCtx.Redis.Expire(ipLimitKey, 3600)
}
if ipCount > 10 {
return errors.Wrapf(xerr.NewErrMsg("请求过于频繁,请稍后再试"), "短信发送, IP限流: %s, count: %d", clientIP, ipCount)
}
// 2.2 手机号限流:同一手机号每小时最多发送 5 次(无滑块时更严格)
hourLimitKey := fmt.Sprintf("hour_limit:%s:%s", req.ActionType, encryptedMobile)
hourCount, err := l.svcCtx.Redis.Incr(hourLimitKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取小时限流缓存失败: %v", err)
}
if hourCount == 1 {
l.svcCtx.Redis.Expire(hourLimitKey, 3600)
}
if hourCount > 5 {
return errors.Wrapf(xerr.NewErrMsg("该手机号请求过于频繁,请稍后再试"), "短信发送, 手机号小时限流: %s, count: %d", encryptedMobile, hourCount)
}
} }
// 检查手机号是否在一分钟内已发送过验证码
// 3. 检查手机号是否在一分钟内已发送过验证码(通用)
limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile) limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile)
exists, err := l.svcCtx.Redis.Exists(limitCodeKey) exists, err := l.svcCtx.Redis.Exists(limitCodeKey)
if err != nil { if err != nil {
@@ -48,7 +94,6 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
} }
if exists { if exists {
// 如果 Redis 中已经存在标记,说明在 1 分钟内请求过,返回错误
return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "短信发送, 手机号1分钟内重复请求发送验证码: %s", encryptedMobile) return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "短信发送, 手机号1分钟内重复请求发送验证码: %s", encryptedMobile)
} }

View File

@@ -0,0 +1,37 @@
package captcha
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/pkg/captcha"
"github.com/zeromicro/go-zero/core/logx"
)
type GetEncryptedSceneIdLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetEncryptedSceneIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEncryptedSceneIdLogic {
return &GetEncryptedSceneIdLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetEncryptedSceneIdLogic) GetEncryptedSceneId() (*types.GetEncryptedSceneIdResp, error) {
cfg := l.svcCtx.Config.Captcha
encrypted, err := captcha.GenerateEncryptedSceneID(cfg.SceneID, cfg.EKey, 3600)
if err != nil {
l.Errorf("generate encrypted scene id error: %+v", err)
return nil, err
}
return &types.GetEncryptedSceneIdResp{
EncryptedSceneId: encrypted,
}, nil
}

View File

@@ -6,15 +6,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strconv"
"strings"
"time"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types" "qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"strconv"
"strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -35,6 +35,9 @@ type PaymentTypeResp struct {
orderID string // 订单ID用于开发环境测试支付模式 orderID string // 订单ID用于开发环境测试支付模式
} }
// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能
const enableDevTestPayment = false
func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic { func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic {
return &PaymentLogic{ return &PaymentLogic{
Logger: logx.WithContext(ctx), Logger: logx.WithContext(ctx),
@@ -50,8 +53,8 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
// 检查是否为开发环境的测试支付模式 // 检查是否为开发环境的测试支付模式
env := os.Getenv("ENV") env := os.Getenv("ENV")
isDevTestPayment := env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty") isDevTestPayment := enableDevTestPayment && env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty")
isEmptyReportMode := env == "development" && req.PayMethod == "test_empty" isEmptyReportMode := enableDevTestPayment && env == "development" && req.PayMethod == "test_empty"
l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
switch req.PayType { switch req.PayType {
@@ -95,7 +98,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
// 正常支付流程 // 正常支付流程
var createOrderErr error var createOrderErr error
if req.PayMethod == "wechat" { if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, req.Code)
} else if req.PayMethod == "alipay" { } else if req.PayMethod == "alipay" {
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
} else if req.PayMethod == "appleiap" { } else if req.PayMethod == "appleiap" {

View File

@@ -118,7 +118,37 @@ func (l *WxMiniAuthLogic) GetSessionKey(code string) (*SessionKeyResp, error) {
// 检查微信返回的错误码 // 检查微信返回的错误码
if sessionKeyResp.ErrCode != 0 { if sessionKeyResp.ErrCode != 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), // 针对不同的微信错误码返回更明确的错误信息
var errMsg string
switch sessionKeyResp.ErrCode {
case 40029:
// code 无效(已使用、已过期或格式错误)
errMsg = "微信授权码无效或已过期,请重新打开小程序"
l.Errorf("微信code无效: errcode=%d, errmsg=%s, code前6位=%s",
sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg,
func() string {
if len(code) > 6 {
return code[:6] + "..."
}
return code
}())
case 40013:
// 无效的 AppID
errMsg = "小程序配置错误,请联系管理员"
l.Errorf("微信AppID无效: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
case 40125:
// 无效的 AppSecret
errMsg = "小程序配置错误,请联系管理员"
l.Errorf("微信AppSecret无效: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
case 45011:
// 频率限制
errMsg = "请求过于频繁,请稍后再试"
l.Errorf("微信API频率限制: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
default:
errMsg = fmt.Sprintf("微信授权失败: %s", sessionKeyResp.ErrMsg)
l.Errorf("微信接口返回错误: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
}
return nil, errors.Wrapf(xerr.NewErrMsg(errMsg),
"微信接口返回错误: errcode=%d, errmsg=%s", "微信接口返回错误: errcode=%d, errmsg=%s",
sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
} }

View File

@@ -6,12 +6,12 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"sync/atomic"
"time"
"qnc-server/app/main/api/internal/config" "qnc-server/app/main/api/internal/config"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"strconv"
"sync/atomic"
"time"
"github.com/smartwalle/alipay/v3" "github.com/smartwalle/alipay/v3"
) )

View File

@@ -2,15 +2,19 @@ package service
import ( import (
"context" "context"
"encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv"
"time"
"qnc-server/app/main/api/internal/config" "qnc-server/app/main/api/internal/config"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"strconv"
"time"
"github.com/google/uuid"
"github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader" "github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
@@ -251,9 +255,16 @@ func (w *WechatPayService) CreateWechatH5Order(ctx context.Context, amount float
} }
// CreateWechatOrder 创建微信支付订单(集成 APP、H5、小程序 // CreateWechatOrder 创建微信支付订单(集成 APP、H5、小程序
func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) { func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string, code string) (interface{}, error) {
// 根据 ctx 中的 platform 判断平台 // 根据 ctx 中的 platform 判断平台(请求头 X-Platform: wxmini / wxh5 / app
platform := ctx.Value("platform").(string) platformValue := ctx.Value("platform")
if platformValue == nil {
return "", fmt.Errorf("平台信息不存在,请检查请求头 X-Platform")
}
platform, ok := platformValue.(string)
if !ok {
return "", fmt.Errorf("平台信息格式错误")
}
var prepayData interface{} var prepayData interface{}
var err error var err error
@@ -266,7 +277,30 @@ func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64
} }
userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID) userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID)
if findAuthModelErr != nil { if findAuthModelErr != nil {
return "", findAuthModelErr if errors.Is(findAuthModelErr, model.ErrNotFound) {
// 用户未绑定微信,尝试通过 code 自动绑定
if code == "" {
return "", fmt.Errorf("用户未绑定微信小程序账号,请先完成微信登录或提供授权码")
}
// 通过 code 获取 OpenID
openid, getOpenidErr := w.getWechatMiniOpenID(ctx, code)
if getOpenidErr != nil {
return "", fmt.Errorf("获取微信小程序OpenID失败: %v", getOpenidErr)
}
// 自动绑定微信账号
bindErr := w.bindWechatAuth(ctx, userID, model.UserAuthTypeWxMiniOpenID, openid)
if bindErr != nil {
return "", fmt.Errorf("绑定微信小程序账号失败: %v", bindErr)
}
// 重新查询绑定后的认证信息
userAuthModel, findAuthModelErr = w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID)
if findAuthModelErr != nil {
return "", fmt.Errorf("查询绑定后的微信认证信息失败: %v", findAuthModelErr)
}
logx.Infof("用户 %s 已自动绑定微信小程序账号OpenID: %s", userID, openid)
} else {
return "", fmt.Errorf("查询用户微信认证信息失败: %v", findAuthModelErr)
}
} }
prepayData, err = w.CreateWechatMiniProgramOrder(ctx, amount, description, outTradeNo, userAuthModel.AuthKey) prepayData, err = w.CreateWechatMiniProgramOrder(ctx, amount, description, outTradeNo, userAuthModel.AuthKey)
if err != nil { if err != nil {
@@ -279,7 +313,30 @@ func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64
} }
userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID) userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID)
if findAuthModelErr != nil { if findAuthModelErr != nil {
return "", findAuthModelErr if errors.Is(findAuthModelErr, model.ErrNotFound) {
// 用户未绑定微信,尝试通过 code 自动绑定
if code == "" {
return "", fmt.Errorf("用户未绑定微信H5账号请先完成微信登录或提供授权码")
}
// 通过 code 获取 OpenID
openid, getOpenidErr := w.getWechatH5OpenID(ctx, code)
if getOpenidErr != nil {
return "", fmt.Errorf("获取微信H5 OpenID失败: %v", getOpenidErr)
}
// 自动绑定微信账号
bindErr := w.bindWechatAuth(ctx, userID, model.UserAuthTypeWxh5OpenID, openid)
if bindErr != nil {
return "", fmt.Errorf("绑定微信H5账号失败: %v", bindErr)
}
// 重新查询绑定后的认证信息
userAuthModel, findAuthModelErr = w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID)
if findAuthModelErr != nil {
return "", fmt.Errorf("查询绑定后的微信认证信息失败: %v", findAuthModelErr)
}
logx.Infof("用户 %s 已自动绑定微信H5账号OpenID: %s", userID, openid)
} else {
return "", fmt.Errorf("查询用户微信认证信息失败: %v", findAuthModelErr)
}
} }
prepayData, err = w.CreateWechatH5Order(ctx, amount, description, outTradeNo, userAuthModel.AuthKey) prepayData, err = w.CreateWechatH5Order(ctx, amount, description, outTradeNo, userAuthModel.AuthKey)
if err != nil { if err != nil {
@@ -384,3 +441,112 @@ func (w *WechatPayService) GenerateOutTradeNo() string {
return combined return combined
} }
// getWechatMiniOpenID 通过 code 获取微信小程序 OpenID
func (w *WechatPayService) getWechatMiniOpenID(ctx context.Context, code string) (string, error) {
appID := w.config.WechatMini.AppID
appSecret := w.config.WechatMini.AppSecret
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appID, appSecret, code)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("请求微信API失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
var data struct {
Openid string `json:"openid"`
ErrCode int `json:"errcode,omitempty"`
ErrMsg string `json:"errmsg,omitempty"`
}
if err := json.Unmarshal(body, &data); err != nil {
return "", fmt.Errorf("解析响应失败: %v", err)
}
if data.ErrCode != 0 {
return "", fmt.Errorf("微信API返回错误: errcode=%d, errmsg=%s", data.ErrCode, data.ErrMsg)
}
if data.Openid == "" {
return "", fmt.Errorf("openid为空")
}
return data.Openid, nil
}
// getWechatH5OpenID 通过 code 获取微信H5 OpenID
func (w *WechatPayService) getWechatH5OpenID(ctx context.Context, code string) (string, error) {
appID := w.config.WechatH5.AppID
appSecret := w.config.WechatH5.AppSecret
url := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", appID, appSecret, code)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("请求微信API失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
var data struct {
Openid string `json:"openid"`
ErrCode int `json:"errcode,omitempty"`
ErrMsg string `json:"errmsg,omitempty"`
}
if err := json.Unmarshal(body, &data); err != nil {
return "", fmt.Errorf("解析响应失败: %v", err)
}
if data.ErrCode != 0 {
return "", fmt.Errorf("微信API返回错误: errcode=%d, errmsg=%s", data.ErrCode, data.ErrMsg)
}
if data.Openid == "" {
return "", fmt.Errorf("openid为空")
}
return data.Openid, nil
}
// bindWechatAuth 绑定微信认证信息到用户账号
func (w *WechatPayService) bindWechatAuth(ctx context.Context, userID string, authType string, openid string) error {
// 检查该 OpenID 是否已被其他用户绑定
existingAuth, err := w.userAuthModel.FindOneByAuthTypeAuthKey(ctx, authType, openid)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return fmt.Errorf("查询认证信息失败: %v", err)
}
if existingAuth != nil {
// 如果 OpenID 已被其他用户绑定,检查是否是当前用户
if existingAuth.UserId != userID {
return fmt.Errorf("该微信账号已被其他用户绑定")
}
// 如果已经是当前用户绑定的,直接返回成功
return nil
}
// 创建新的认证记录
userAuth := &model.UserAuth{
Id: uuid.NewString(),
UserId: userID,
AuthType: authType,
AuthKey: openid,
}
_, err = w.userAuthModel.Insert(ctx, nil, userAuth)
if err != nil {
return fmt.Errorf("创建认证记录失败: %v", err)
}
return nil
}

View File

@@ -38,6 +38,25 @@ type AdminAuditAgentResp struct {
Success bool `json:"success"` Success bool `json:"success"`
} }
type AdminAssignRoleApiReq struct {
RoleId string `json:"role_id"`
ApiIds []string `json:"api_ids"`
}
type AdminAssignRoleApiResp struct {
Success bool `json:"success"`
}
type AdminAuditAgentReq struct {
AuditId int64 `json:"audit_id"` // 审核记录ID
Status int64 `json:"status"` // 审核状态1=通过2=拒绝
AuditReason string `json:"audit_reason"` // 审核原因(拒绝时必填)
}
type AdminAuditAgentResp struct {
Success bool `json:"success"`
}
type AdminAuditRealNameReq struct { type AdminAuditRealNameReq struct {
RealNameId int64 `json:"real_name_id"` // 实名认证记录ID RealNameId int64 `json:"real_name_id"` // 实名认证记录ID
Status int64 `json:"status"` // 审核状态2=通过3=拒绝 Status int64 `json:"status"` // 审核状态2=通过3=拒绝
@@ -1000,6 +1019,22 @@ type AgentApplyResp struct {
AgentCode int64 `json:"agent_code"` AgentCode int64 `json:"agent_code"`
} }
type AgentApplyReq struct {
Region string `json:"region,optional"`
Mobile string `json:"mobile"`
Code string `json:"code"`
Referrer string `json:"referrer"`
InviteCode string `json:"invite_code,optional"`
AgentCode int64 `json:"agent_code,optional"`
}
type AgentApplyResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
AgentCode int64 `json:"agent_code"`
}
type AgentCommissionListItem struct { type AgentCommissionListItem struct {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
@@ -1033,6 +1068,29 @@ type AgentInfoResp struct {
AgentCode int64 `json:"agent_code"` AgentCode int64 `json:"agent_code"`
} }
type AgentGeneratingLinkReq struct {
ProductId string `json:"product_id"` // 产品ID
SetPrice float64 `json:"set_price"` // 设定价格
TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面)
}
type AgentGeneratingLinkResp struct {
LinkIdentifier string `json:"link_identifier"` // 推广链接标识
FullLink string `json:"full_link"` // 完整短链URL
}
type AgentInfoResp struct {
AgentId string `json:"agent_id"`
Level int64 `json:"level"`
LevelName string `json:"level_name"`
Region string `json:"region"`
Mobile string `json:"mobile"`
WechatId string `json:"wechat_id"`
TeamLeaderId string `json:"team_leader_id"`
IsRealName bool `json:"is_real_name"`
AgentCode int64 `json:"agent_code"`
}
type AgentLinkListItem struct { type AgentLinkListItem struct {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
@@ -1073,7 +1131,8 @@ type AgentOrderListItem struct {
ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 ActualBasePrice float64 `json:"actual_base_price"` // 实际底价
PriceCost float64 `json:"price_cost"` // 提价成本 PriceCost float64 `json:"price_cost"` // 提价成本
AgentProfit float64 `json:"agent_profit"` // 代理收益 AgentProfit float64 `json:"agent_profit"` // 代理收益
ProcessStatus int64 `json:"process_status"` // 处理状态 ProcessStatus int64 `json:"process_status"` // 处理状态(保留用于筛选,前端不显示)
OrderStatus string `json:"order_status"` // 订单状态pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
@@ -1093,6 +1152,10 @@ type AgentProductConfigResp struct {
List []ProductConfigItem `json:"list"` List []ProductConfigItem `json:"list"`
} }
type AgentProductConfigResp struct {
List []ProductConfigItem `json:"list"`
}
type AgentRealNameListItem struct { type AgentRealNameListItem struct {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
@@ -1111,6 +1174,7 @@ type AgentRebateListItem struct {
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
RebateType int64 `json:"rebate_type"` // 返佣类型 RebateType int64 `json:"rebate_type"` // 返佣类型
Amount float64 `json:"amount"` // 金额 Amount float64 `json:"amount"` // 金额
Status int64 `json:"status"` // 状态1=已发放2=已冻结3=已取消(已退款)
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
@@ -1310,6 +1374,75 @@ type DeleteRoleResp struct {
Success bool `json:"success"` // 是否成功 Success bool `json:"success"` // 是否成功
} }
type ConversionRateResp struct {
MyConversionRate ConversionRateData `json:"my_conversion_rate"` // 我的转化率
SubordinateConversionRate ConversionRateData `json:"subordinate_conversion_rate"` // 我的下级转化率
}
type CreateMenuReq struct {
Pid string `json:"pid,optional"` // 父菜单ID
Name string `json:"name"` // 路由名称
Path string `json:"path,optional"` // 路由路径
Component string `json:"component,optional"` // 组件路径
Redirect string `json:"redirect,optional"` // 重定向路径
Meta map[string]interface{} `json:"meta"` // 路由元数据
Status int64 `json:"status,optional,default=1"` // 状态0-禁用1-启用
Type string `json:"type"` // 类型
Sort int64 `json:"sort,optional"` // 排序
}
type CreateMenuResp struct {
Id string `json:"id"` // 菜单ID
}
type CreateRoleReq struct {
RoleName string `json:"role_name"` // 角色名称
RoleCode string `json:"role_code"` // 角色编码
Description string `json:"description"` // 角色描述
Status int64 `json:"status,default=1"` // 状态0-禁用1-启用
Sort int64 `json:"sort,default=0"` // 排序
MenuIds []string `json:"menu_ids"` // 关联的菜单ID列表
}
type CreateRoleResp struct {
Id string `json:"id"` // 角色ID
}
type CreateWhitelistOrderReq struct {
IdCard string `json:"id_card"` // 身份证号(查询对象标识)
FeatureIds []string `json:"feature_ids"` // 要屏蔽的feature ID列表
OrderId string `json:"order_id,optional"` // 关联的查询订单ID可选
}
type CreateWhitelistOrderResp struct {
OrderId string `json:"order_id"` // 订单ID
OrderNo string `json:"order_no"` // 订单号
TotalAmount float64 `json:"total_amount"` // 总金额
}
type DeleteInviteCodeReq struct {
Id string `json:"id"` // 邀请码ID
}
type DeleteInviteCodeResp struct {
}
type DeleteMenuReq struct {
Id string `path:"id"` // 菜单ID
}
type DeleteMenuResp struct {
Success bool `json:"success"` // 是否成功
}
type DeleteRoleReq struct {
Id string `path:"id"` // 角色ID
}
type DeleteRoleResp struct {
Success bool `json:"success"` // 是否成功
}
type DirectParentRebateConfig struct { type DirectParentRebateConfig struct {
Diamond float64 `json:"diamond"` // 直接上级是钻石的返佣金额6元 Diamond float64 `json:"diamond"` // 直接上级是钻石的返佣金额6元
Gold float64 `json:"gold"` // 直接上级是黄金的返佣金额3元 Gold float64 `json:"gold"` // 直接上级是黄金的返佣金额3元
@@ -1325,6 +1458,15 @@ type DownloadAuthorizationDocumentResp struct {
FileUrl string `json:"fileUrl"` // 文件访问URL FileUrl string `json:"fileUrl"` // 文件访问URL
} }
type DownloadAuthorizationDocumentReq struct {
DocumentId string `json:"documentId" validate:"required"` // 授权书ID
}
type DownloadAuthorizationDocumentResp struct {
FileName string `json:"fileName"` // 文件名
FileUrl string `json:"fileUrl"` // 文件访问URL
}
type Feature struct { type Feature struct {
ID string `json:"id"` // 功能ID ID string `json:"id"` // 功能ID
ApiID string `json:"api_id"` // API标识 ApiID string `json:"api_id"` // API标识
@@ -1744,6 +1886,32 @@ type MobileCodeLoginResp struct {
RefreshAfter int64 `json:"refreshAfter"` RefreshAfter int64 `json:"refreshAfter"`
} }
type MenuListItem struct {
Id string `json:"id"` // 菜单ID
Pid string `json:"pid"` // 父菜单ID
Name string `json:"name"` // 路由名称
Path string `json:"path"` // 路由路径
Component string `json:"component"` // 组件路径
Redirect string `json:"redirect"` // 重定向路径
Meta map[string]interface{} `json:"meta"` // 路由元数据
Status int64 `json:"status"` // 状态0-禁用1-启用
Type string `json:"type"` // 类型
Sort int64 `json:"sort"` // 排序
CreateTime string `json:"createTime"` // 创建时间
Children []MenuListItem `json:"children"` // 子菜单
}
type MobileCodeLoginReq struct {
Mobile string `json:"mobile"`
Code string `json:"code" validate:"required"`
}
type MobileCodeLoginResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
type Notification struct { type Notification struct {
Title string `json:"title"` // 通知标题 Title string `json:"title"` // 通知标题
Content string `json:"content"` // 通知内容 (富文本) Content string `json:"content"` // 通知内容 (富文本)
@@ -1801,6 +1969,7 @@ type OrderListItem struct {
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
PayTime string `json:"pay_time"` // 支付时间 PayTime string `json:"pay_time"` // 支付时间
RefundTime string `json:"refund_time"` // 退款时间 RefundTime string `json:"refund_time"` // 退款时间
UpdateTime string `json:"update_time"` // 更新时间
IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单
AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态not_agent-非代理订单success-处理成功failed-处理失败pending-待处理 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态not_agent-非代理订单success-处理成功failed-处理失败pending-待处理
} }
@@ -1894,6 +2063,10 @@ type ProductResponse struct {
Product Product
} }
type ProductResponse struct {
Product
}
type PromotionQueryItem struct { type PromotionQueryItem struct {
Id string `json:"id"` // 查询ID Id string `json:"id"` // 查询ID
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
@@ -1971,6 +2144,27 @@ type QueryGenerateShareLinkResp struct {
ShareLink string `json:"share_link"` ShareLink string `json:"share_link"`
} }
type QueryDetailByOrderIdReq struct {
OrderId string `path:"order_id"`
}
type QueryDetailByOrderNoReq struct {
OrderNo string `path:"order_no"`
}
type QueryExampleReq struct {
Feature string `form:"feature"`
}
type QueryGenerateShareLinkReq struct {
OrderId *string `json:"order_id,optional"`
OrderNo *string `json:"order_no,optional"`
}
type QueryGenerateShareLinkResp struct {
ShareLink string `json:"share_link"`
}
type QueryItem struct { type QueryItem struct {
Feature interface{} `json:"feature"` Feature interface{} `json:"feature"`
Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct
@@ -1997,6 +2191,27 @@ type QueryProvisionalOrderResp struct {
Product Product `json:"product"` Product Product `json:"product"`
} }
type QueryListReq struct {
Page int64 `form:"page"` // 页码
PageSize int64 `form:"page_size"` // 每页数据量
}
type QueryListResp struct {
Total int64 `json:"total"` // 总记录数
List []Query `json:"list"` // 查询列表
}
type QueryProvisionalOrderReq struct {
Id string `path:"id"`
}
type QueryProvisionalOrderResp struct {
Name string `json:"name"`
IdCard string `json:"id_card"`
Mobile string `json:"mobile"`
Product Product `json:"product"`
}
type QueryReq struct { type QueryReq struct {
Data string `json:"data" validate:"required"` Data string `json:"data" validate:"required"`
} }
@@ -2052,6 +2267,53 @@ type RealNameAuthResp struct {
Status string `json:"status"` // 状态pending=待审核approved=已通过rejected=已拒绝 Status string `json:"status"` // 状态pending=待审核approved=已通过rejected=已拒绝
} }
type QueryRetryReq struct {
Id string `path:"id"`
}
type QueryRetryResp struct {
Query
}
type QueryServiceReq struct {
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
}
type QueryServiceResp struct {
Id string `json:"id"`
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
type QueryShareDetailReq struct {
Id string `path:"id"`
}
type QuerySingleTestReq struct {
Params map[string]interface{} `json:"params"`
Api string `json:"api"`
}
type QuerySingleTestResp struct {
Data interface{} `json:"data"`
Api string `json:"api"`
}
type RealNameAuthReq struct {
Name string `json:"name"` // 姓名
IdCard string `json:"id_card"` // 身份证号
Mobile string `json:"mobile"` // 手机号
Code string `json:"code"` // 验证码
}
type RealNameAuthResp struct {
Status string `json:"status"` // 状态pending=待审核approved=已通过rejected=已拒绝
}
type RebateItem struct { type RebateItem struct {
Id string `json:"id"` // 记录ID Id string `json:"id"` // 记录ID
SourceAgentId string `json:"source_agent_id"` // 来源代理ID SourceAgentId string `json:"source_agent_id"` // 来源代理ID
@@ -2061,6 +2323,7 @@ type RebateItem struct {
OrderNo string `json:"order_no"` // 订单号 OrderNo string `json:"order_no"` // 订单号
RebateType int64 `json:"rebate_type"` // 返佣类型1=直接上级2=钻石上级3=黄金上级 RebateType int64 `json:"rebate_type"` // 返佣类型1=直接上级2=钻石上级3=黄金上级
Amount float64 `json:"amount"` // 返佣金额 Amount float64 `json:"amount"` // 返佣金额
Status int64 `json:"status"` // 状态1=已发放2=已冻结3=已取消(已退款)
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
@@ -2090,6 +2353,26 @@ type RegisterByInviteCodeResp struct {
AgentCode int64 `json:"agent_code"` AgentCode int64 `json:"agent_code"`
} }
type RegisterByInviteCodeReq struct {
Referrer string `json:"referrer"`
InviteCode string `json:"invite_code,optional"`
AgentCode int64 `json:"agent_code,optional"`
Mobile string `json:"mobile"`
Code string `json:"code"`
Region string `json:"region,optional"`
WechatId string `json:"wechat_id,optional"`
}
type RegisterByInviteCodeResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
AgentId string `json:"agent_id"` // 代理ID
Level int64 `json:"level"` // 代理等级
LevelName string `json:"level_name"` // 等级名称
AgentCode int64 `json:"agent_code"`
}
type RoleListItem struct { type RoleListItem struct {
Id string `json:"id"` // 角色ID Id string `json:"id"` // 角色ID
RoleName string `json:"role_name"` // 角色名称 RoleName string `json:"role_name"` // 角色名称
@@ -2104,6 +2387,9 @@ type RoleListItem struct {
type ShortLinkRedirectResp struct { type ShortLinkRedirectResp struct {
} }
type ShortLinkRedirectResp struct {
}
type SubordinateItem struct { type SubordinateItem struct {
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
Level int64 `json:"level"` // 等级 Level int64 `json:"level"` // 等级
@@ -2120,14 +2406,14 @@ type TeamMemberItem struct {
LevelName string `json:"level_name"` // 等级名称 LevelName string `json:"level_name"` // 等级名称
Mobile string `json:"mobile"` // 手机号 Mobile string `json:"mobile"` // 手机号
CreateTime string `json:"create_time"` // 加入团队时间 CreateTime string `json:"create_time"` // 加入团队时间
TotalRebateAmount float64 `json:"total_rebate_amount"` // 返佣给我的总金额
TodayRebateAmount float64 `json:"today_rebate_amount"` // 返佣给我的今日金额
TodayInvites int64 `json:"today_invites"` // 邀请加入团队的今日人数
MonthInvites int64 `json:"month_invites"` // 邀请加入团队的本月人数
TotalInvites int64 `json:"total_invites"` // 邀请加入团队的总人数
TodayQueries int64 `json:"today_queries"` // 当日查询量 TodayQueries int64 `json:"today_queries"` // 当日查询量
MonthQueries int64 `json:"month_queries"` // 本月查询量 MonthQueries int64 `json:"month_queries"` // 本月查询量
TotalQueries int64 `json:"total_queries"` // 总查询量 TotalQueries int64 `json:"total_queries"` // 总查询量
TotalRebateAmount float64 `json:"total_rebate_amount"` // 返佣给我的总金额
TodayRebateAmount float64 `json:"today_rebate_amount"` // 返佣给我的今日金额
TotalInvites int64 `json:"total_invites"` // 邀请加入团队的总人数
TodayInvites int64 `json:"today_invites"` // 邀请加入团队的今日人数
MonthInvites int64 `json:"month_invites"` // 邀请加入团队的本月人数
IsDirect bool `json:"is_direct"` // 是否直接下级 IsDirect bool `json:"is_direct"` // 是否直接下级
} }
@@ -2194,6 +2480,57 @@ type UpdateRoleResp struct {
Success bool `json:"success"` // 是否成功 Success bool `json:"success"` // 是否成功
} }
type TeamStatisticsResp struct {
TotalCount int64 `json:"total_count"` // 团队总人数(不包括自己)
DirectCount int64 `json:"direct_count"` // 直接下级数量
IndirectCount int64 `json:"indirect_count"` // 间接下级数量
GoldCount int64 `json:"gold_count"` // 黄金代理数量
NormalCount int64 `json:"normal_count"` // 普通代理数量
TodayNewMembers int64 `json:"today_new_members"` // 今日新增成员
MonthNewMembers int64 `json:"month_new_members"` // 本月新增成员
}
type UpdateMenuReq struct {
Id string `path:"id"` // 菜单ID
Pid *string `json:"pid,optional"` // 父菜单ID
Name string `json:"name"` // 路由名称
Path string `json:"path,optional"` // 路由路径
Component string `json:"component,optional"` // 组件路径
Redirect string `json:"redirect,optional"` // 重定向路径
Meta map[string]interface{} `json:"meta"` // 路由元数据
Status int64 `json:"status,optional"` // 状态0-禁用1-启用
Type string `json:"type"` // 类型
Sort int64 `json:"sort,optional"` // 排序
}
type UpdateMenuResp struct {
Success bool `json:"success"` // 是否成功
}
type UpdateQueryDataReq struct {
Id string `json:"id"` // 查询ID
QueryData string `json:"query_data"` // 查询数据(未加密的JSON)
}
type UpdateQueryDataResp struct {
Id string `json:"id"`
UpdatedAt string `json:"updated_at"` // 更新时间
}
type UpdateRoleReq struct {
Id string `path:"id"` // 角色ID
RoleName *string `json:"role_name,optional"` // 角色名称
RoleCode *string `json:"role_code,optional"` // 角色编码
Description *string `json:"description,optional"` // 角色描述
Status *int64 `json:"status,optional"` // 状态0-禁用1-启用
Sort *int64 `json:"sort,optional"` // 排序
MenuIds []string `json:"menu_ids,optional"` // 关联的菜单ID列表
}
type UpdateRoleResp struct {
Success bool `json:"success"` // 是否成功
}
type UpgradeFeeConfig struct { type UpgradeFeeConfig struct {
NormalToGold float64 `json:"normal_to_gold"` // 普通→黄金199 NormalToGold float64 `json:"normal_to_gold"` // 普通→黄金199
NormalToDiamond float64 `json:"normal_to_diamond"` // 普通→钻石980 NormalToDiamond float64 `json:"normal_to_diamond"` // 普通→钻石980
@@ -2236,6 +2573,15 @@ type UpgradeSubordinateResp struct {
Success bool `json:"success"` Success bool `json:"success"`
} }
type UpgradeSubordinateReq struct {
SubordinateId string `json:"subordinate_id"` // 下级代理ID
ToLevel int64 `json:"to_level"` // 目标等级只能是2=黄金)
}
type UpgradeSubordinateResp struct {
Success bool `json:"success"`
}
type User struct { type User struct {
Id string `json:"id"` Id string `json:"id"`
Mobile string `json:"mobile"` Mobile string `json:"mobile"`
@@ -2286,6 +2632,49 @@ type WhitelistItem struct {
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
type UserInfoResp struct {
UserInfo User `json:"userInfo"`
}
type WXH5AuthReq struct {
Code string `json:"code"`
}
type WXH5AuthResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
type WXMiniAuthReq struct {
Code string `json:"code"`
}
type WXMiniAuthResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
type WhitelistFeatureItem struct {
FeatureId string `json:"feature_id"` // Feature的UUID
FeatureApiId string `json:"feature_api_id"` // Feature的API标识
FeatureName string `json:"feature_name"` // Feature的名称
WhitelistPrice float64 `json:"whitelist_price"` // 屏蔽价格(单位:元)
}
type WhitelistItem struct {
Id string `json:"id"` // 白名单记录ID
IdCard string `json:"id_card"` // 身份证号
FeatureId string `json:"feature_id"` // Feature的UUID
FeatureApiId string `json:"feature_api_id"` // Feature的API标识
FeatureName string `json:"feature_name"` // Feature的名称
Amount float64 `json:"amount"` // 费用
Status int64 `json:"status"` // 状态1=生效2=已失效
StatusText string `json:"status_text"` // 状态文本
CreateTime string `json:"create_time"` // 创建时间
}
type WithdrawalItem struct { type WithdrawalItem struct {
Id string `json:"id"` // 记录ID Id string `json:"id"` // 记录ID
WithdrawalNo string `json:"withdrawal_no"` // 提现单号 WithdrawalNo string `json:"withdrawal_no"` // 提现单号

View File

@@ -11,8 +11,6 @@ import (
"reflect" "reflect"
"time" "time"
"qnc-server/common/globalkey"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -21,6 +19,7 @@ import (
"github.com/zeromicro/go-zero/core/stores/sqlc" "github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx" "github.com/zeromicro/go-zero/core/stringx"
"qnc-server/common/globalkey"
) )
var ( var (

View File

@@ -59,21 +59,24 @@ type (
} }
AgentWithdrawal struct { AgentWithdrawal struct {
Id string `db:"id"` Id string `db:"id"`
AgentId string `db:"agent_id"` AgentId string `db:"agent_id"`
WithdrawNo string `db:"withdraw_no"` // 提现单号 WithdrawalType int64 `db:"withdrawal_type"` // 提现方式1=支付宝2=银行卡
PayeeAccount string `db:"payee_account"` // 收款账户 WithdrawNo string `db:"withdraw_no"` // 提现单号
PayeeName string `db:"payee_name"` // 收款人姓名 PayeeAccount string `db:"payee_account"` // 收款账户
Amount float64 `db:"amount"` // 提现金额 BankCardNo sql.NullString `db:"bank_card_no"` // 银行卡号(银行卡提现时使用)
ActualAmount float64 `db:"actual_amount"` // 实际到账金额(扣除税费后 BankName sql.NullString `db:"bank_name"` // 开户行名称(银行卡提现时使用
TaxAmount float64 `db:"tax_amount"` // 税费金额 PayeeName string `db:"payee_name"` // 收款人姓名
Status int64 `db:"status"` // 状态1=处理中2=成功3=失败 Amount float64 `db:"amount"` // 提现金额
Remark sql.NullString `db:"remark"` // 备注 ActualAmount float64 `db:"actual_amount"` // 实际到账金额(扣除税费后)
CreateTime time.Time `db:"create_time"` // 创建时间 TaxAmount float64 `db:"tax_amount"` // 税费金额
UpdateTime time.Time `db:"update_time"` // 更新时间 Status int64 `db:"status"` // 状态1=处理中2=成功3=失败
DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 Remark sql.NullString `db:"remark"` // 备注
DelState int64 `db:"del_state"` // 删除状态0=未删除1=已删除 CreateTime time.Time `db:"create_time"` // 创建时间
Version int64 `db:"version"` // 版本号(乐观锁) UpdateTime time.Time `db:"update_time"` // 更新时间
DeleteTime sql.NullTime `db:"delete_time"` // 删除时间
DelState int64 `db:"del_state"` // 删除状态0=未删除1=已删除
Version int64 `db:"version"` // 版本号(乐观锁)
} }
) )
@@ -90,11 +93,11 @@ func (m *defaultAgentWithdrawalModel) Insert(ctx context.Context, session sqlx.S
qncAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheQncAgentWithdrawalIdPrefix, data.Id) qncAgentWithdrawalIdKey := fmt.Sprintf("%s%v", cacheQncAgentWithdrawalIdPrefix, data.Id)
qncAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheQncAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo) qncAgentWithdrawalWithdrawNoKey := fmt.Sprintf("%s%v", cacheQncAgentWithdrawalWithdrawNoPrefix, data.WithdrawNo)
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { 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 { if session != nil {
return session.ExecCtx(ctx, query, data.Id, data.AgentId, data.WithdrawNo, data.PayeeAccount, data.PayeeName, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.Remark, data.DeleteTime, data.DelState, data.Version) return session.ExecCtx(ctx, query, data.Id, data.AgentId, data.WithdrawalType, data.WithdrawNo, data.PayeeAccount, data.BankCardNo, data.BankName, data.PayeeName, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.Remark, data.DeleteTime, data.DelState, data.Version)
} }
return conn.ExecCtx(ctx, query, data.Id, data.AgentId, data.WithdrawNo, data.PayeeAccount, data.PayeeName, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.Remark, data.DeleteTime, data.DelState, data.Version) return conn.ExecCtx(ctx, query, data.Id, data.AgentId, data.WithdrawalType, data.WithdrawNo, data.PayeeAccount, data.BankCardNo, data.BankName, data.PayeeName, data.Amount, data.ActualAmount, data.TaxAmount, data.Status, data.Remark, data.DeleteTime, data.DelState, data.Version)
}, qncAgentWithdrawalIdKey, qncAgentWithdrawalWithdrawNoKey) }, qncAgentWithdrawalIdKey, qncAgentWithdrawalWithdrawNoKey)
} }
func (m *defaultAgentWithdrawalModel) insertUUID(data *AgentWithdrawal) { func (m *defaultAgentWithdrawalModel) insertUUID(data *AgentWithdrawal) {
@@ -161,9 +164,9 @@ func (m *defaultAgentWithdrawalModel) Update(ctx context.Context, session sqlx.S
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { 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) query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentWithdrawalRowsWithPlaceHolder)
if session != nil { if session != nil {
return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.PayeeAccount, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawalType, newData.WithdrawNo, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
} }
return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.PayeeAccount, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawalType, newData.WithdrawNo, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}, qncAgentWithdrawalIdKey, qncAgentWithdrawalWithdrawNoKey) }, qncAgentWithdrawalIdKey, qncAgentWithdrawalWithdrawNoKey)
} }
@@ -184,9 +187,9 @@ func (m *defaultAgentWithdrawalModel) UpdateWithVersion(ctx context.Context, ses
sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { 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) query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentWithdrawalRowsWithPlaceHolder)
if session != nil { if session != nil {
return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.PayeeAccount, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) return session.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawalType, newData.WithdrawNo, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
} }
return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawNo, newData.PayeeAccount, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) return conn.ExecCtx(ctx, query, newData.AgentId, newData.WithdrawalType, newData.WithdrawNo, newData.PayeeAccount, newData.BankCardNo, newData.BankName, newData.PayeeName, newData.Amount, newData.ActualAmount, newData.TaxAmount, newData.Status, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion)
}, qncAgentWithdrawalIdKey, qncAgentWithdrawalWithdrawNoKey) }, qncAgentWithdrawalIdKey, qncAgentWithdrawalWithdrawNoKey)
if err != nil { if err != nil {
return err return err

View File

@@ -36,7 +36,7 @@ $tables = @(
# "admin_user_role", # "admin_user_role",
# "agent", # "agent",
# "agent_commission", # "agent_commission",
# "agent_config" # # "agent_config"
# "agent_freeze_task", # "agent_freeze_task",
# "agent_invite_code", # "agent_invite_code",
# "agent_invite_code_usage", # "agent_invite_code_usage",
@@ -49,8 +49,8 @@ $tables = @(
# "agent_short_link", # "agent_short_link",
# "agent_upgrade", # "agent_upgrade",
# "agent_wallet", # "agent_wallet",
# "agent_withdrawal", "agent_withdrawal",
# "agent_withdrawal_tax", "agent_withdrawal_tax"
# "authorization_document", # "authorization_document",
# "example", # "example",
# "feature", # "feature",
@@ -70,8 +70,9 @@ $tables = @(
# 为每个表生成模型 # 为每个表生成模型
foreach ($table in $tables) { foreach ($table in $tables) {
Write-Host "正在生成表: $table" -ForegroundColor Green Write-Host "正在生成表: $table" -ForegroundColor Green
# goctl model mysql datasource -url="qnc:5vg67b3UNHu8@tcp(127.0.0.1:21201)/qnc" -table="$table" -dir="./model" --home="$HOME_DIR" -cache=true --style=goZero
goctl model mysql datasource -url="qnc:5vg67b3UNHu8@tcp(127.0.0.1:21201)/qnc" -table="$table" -dir="./model" --home="$HOME_DIR" -cache=true --style=goZero goctl model mysql datasource -url="qnc:5vg67b3UNHu8@tcp(127.0.0.1:21201)/qnc" -table="$table" -dir="./model" --home="$HOME_DIR" -cache=true --style=goZero
# 移动生成的文件到目标目录 # 移动生成的文件到目标目录
if (Test-Path $OUTPUT_DIR) { if (Test-Path $OUTPUT_DIR) {
$sourceFiles = Get-ChildItem -Path $OUTPUT_DIR -File $sourceFiles = Get-ChildItem -Path $OUTPUT_DIR -File

View File

@@ -0,0 +1,26 @@
-- ============================================
-- 支付宝月度提现额度配置
-- ============================================
-- 说明:为代理系统增加支付宝每月提现额度配置,默认 800 元
-- 执行时间2026-01-06
-- ============================================
INSERT INTO
`agent_config` (
`id`,
`config_key`,
`config_value`,
`config_type`,
`description`
)
VALUES (
'f47ac10b-58cc-4372-a567-0e02b2c3d479',
'alipay_month_quota',
'800.00',
'withdrawal',
'支付宝每月提现额度(单位:元)'
)
ON DUPLICATE KEY UPDATE
`config_value` = VALUES(`config_value`),
`config_type` = VALUES(`config_type`),
`description` = VALUES(`description`);

View File

@@ -0,0 +1,36 @@
-- ============================================
-- 代理提现表 - 添加银行卡提现支持
-- ============================================
-- 说明:为 agent_withdrawal 表添加提现方式字段和银行卡相关字段
-- 执行时间2024-XX-XX
-- ============================================
-- 1. 添加提现方式字段1=支付宝2=银行卡)
ALTER TABLE `agent_withdrawal`
ADD COLUMN `withdrawal_type` tinyint NOT NULL DEFAULT 1 COMMENT '提现方式1=支付宝2=银行卡' AFTER `agent_id`;
-- 2. 添加银行卡号字段(银行卡提现时使用)
ALTER TABLE `agent_withdrawal`
ADD COLUMN `bank_card_no` varchar(100) DEFAULT NULL COMMENT '银行卡号(银行卡提现时使用)' AFTER `payee_account`;
-- 3. 添加开户行名称字段(银行卡提现时使用)
ALTER TABLE `agent_withdrawal`
ADD COLUMN `bank_name` varchar(100) DEFAULT NULL COMMENT '开户行名称(银行卡提现时使用)' AFTER `bank_card_no`;
-- 4. 为提现方式字段添加索引(用于按提现方式查询)
ALTER TABLE `agent_withdrawal`
ADD INDEX `idx_withdrawal_type` (`withdrawal_type`);
-- 5. 为提现方式和状态添加复合索引(用于后台审核查询)
ALTER TABLE `agent_withdrawal`
ADD INDEX `idx_type_status` (`withdrawal_type`, `status`);
-- ============================================
-- 说明:
-- 1. withdrawal_type 默认值为 1支付宝兼容历史数据
-- 2. bank_card_no 和 bank_name 为可选字段,支付宝提现时为空
-- 3. 对于支付宝提现payee_account 存储支付宝账号
-- 4. 对于银行卡提现payee_account 可以存储银行卡号(与 bank_card_no 保持一致),或留空
-- ============================================

10
go.mod
View File

@@ -6,9 +6,10 @@ toolchain go1.23.4
require ( require (
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 github.com/alibabacloud-go/captcha-20230305 v1.1.3
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6
github.com/alibabacloud-go/tea v1.2.2 github.com/alibabacloud-go/tea v1.3.13
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/bytedance/sonic v1.13.0 github.com/bytedance/sonic v1.13.0
github.com/fogleman/gg v1.3.0 github.com/fogleman/gg v1.3.0
@@ -39,13 +40,12 @@ require (
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.3.1 // indirect github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect github.com/aliyun/credentials-go v1.4.5 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic/loader v0.2.2 // indirect github.com/bytedance/sonic/loader v0.2.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect

42
go.sum
View File

@@ -13,6 +13,8 @@ github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do2
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
@@ -20,8 +22,8 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
@@ -44,26 +46,24 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I= github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4= github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0= github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA= github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -81,8 +81,9 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
@@ -326,10 +327,13 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -342,6 +346,9 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -356,10 +363,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -370,6 +380,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -384,20 +397,27 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -406,6 +426,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
@@ -420,6 +442,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

153
pkg/captcha/aliyun.go Normal file
View File

@@ -0,0 +1,153 @@
package captcha
import (
"os"
"strings"
"github.com/pkg/errors"
"qnc-server/common/xerr"
captcha20230305 "github.com/alibabacloud-go/captcha-20230305/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
)
type Config struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
}
// VerifyResult 验证结果
type VerifyResult struct {
Verified bool // 是否通过滑块验证
Skipped bool // 是否跳过验证(无滑块参数)
VerifyErr error // 验证错误(如果有)
}
// isWeChatUserAgent 检测是否为微信浏览器的 User-Agent
func isWeChatUserAgent(userAgent string) bool {
if userAgent == "" {
return false
}
ua := strings.ToLower(userAgent)
return strings.Contains(ua, "micromessenger") || strings.Contains(ua, "wechat")
}
// VerifyOptionalWithUserAgent 可选验证阿里云验证码(支持微信环境跳过验证)
// 当 captchaVerifyParam 为空时返回 Skipped=true由调用方决定后续处理
// 当 userAgent 为微信浏览器时,跳过验证返回 Verified=true
func VerifyOptionalWithUserAgent(cfg Config, captchaVerifyParam string, userAgent string) VerifyResult {
// 开发环境可跳过验证
if os.Getenv("ENV") == "development" {
return VerifyResult{Verified: true, Skipped: false}
}
// 微信环境下跳过图形验证码校验
if isWeChatUserAgent(userAgent) {
return VerifyResult{Verified: true, Skipped: false}
}
// 没有滑块验证码参数,报错提示
if captchaVerifyParam == "" {
return VerifyResult{VerifyErr: errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")}
}
clientCfg := &openapi.Config{
AccessKeyId: tea.String(cfg.AccessKeyID),
AccessKeySecret: tea.String(cfg.AccessKeySecret),
}
clientCfg.Endpoint = tea.String(cfg.EndpointURL)
client, err := captcha20230305.NewClient(clientCfg)
if err != nil {
return VerifyResult{VerifyErr: errors.Wrapf(err, "create aliyun captcha client error")}
}
req := &captcha20230305.VerifyIntelligentCaptchaRequest{
SceneId: tea.String(cfg.SceneID),
CaptchaVerifyParam: tea.String(captchaVerifyParam),
}
resp, err := client.VerifyIntelligentCaptcha(req)
if err != nil {
return VerifyResult{VerifyErr: errors.Wrapf(err, "verify aliyun captcha error")}
}
if tea.BoolValue(resp.Body.Result.VerifyResult) {
return VerifyResult{Verified: true}
}
return VerifyResult{
VerifyErr: errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "aliyun captcha verify failed: code=%s, msg=%s",
tea.StringValue(resp.Body.Code), tea.StringValue(resp.Body.Message)),
}
}
// VerifyWithUserAgent 验证阿里云验证码(必须提供验证码参数,支持微信环境跳过验证)
func VerifyWithUserAgent(cfg Config, captchaVerifyParam string, userAgent string) error {
result := VerifyOptionalWithUserAgent(cfg, captchaVerifyParam, userAgent)
if result.VerifyErr != nil {
return result.VerifyErr
}
if result.Skipped {
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")
}
return nil
}
// VerifyOptional 可选验证阿里云验证码(兼容旧接口,不判断 User-Agent
// 当 captchaVerifyParam 为空时返回 Skipped=true由调用方决定后续处理
func VerifyOptional(cfg Config, captchaVerifyParam string) VerifyResult {
// 开发环境可跳过验证
if os.Getenv("ENV") == "development" {
return VerifyResult{Verified: true, Skipped: false}
}
// 没有滑块验证码参数,返回跳过状态
if captchaVerifyParam == "" {
return VerifyResult{Skipped: true}
}
clientCfg := &openapi.Config{
AccessKeyId: tea.String(cfg.AccessKeyID),
AccessKeySecret: tea.String(cfg.AccessKeySecret),
}
clientCfg.Endpoint = tea.String(cfg.EndpointURL)
client, err := captcha20230305.NewClient(clientCfg)
if err != nil {
return VerifyResult{VerifyErr: errors.Wrapf(err, "create aliyun captcha client error")}
}
req := &captcha20230305.VerifyIntelligentCaptchaRequest{
SceneId: tea.String(cfg.SceneID),
CaptchaVerifyParam: tea.String(captchaVerifyParam),
}
resp, err := client.VerifyIntelligentCaptcha(req)
if err != nil {
return VerifyResult{VerifyErr: errors.Wrapf(err, "verify aliyun captcha error")}
}
if tea.BoolValue(resp.Body.Result.VerifyResult) {
return VerifyResult{Verified: true}
}
return VerifyResult{
VerifyErr: errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "aliyun captcha verify failed: code=%s, msg=%s",
tea.StringValue(resp.Body.Code), tea.StringValue(resp.Body.Message)),
}
}
// Verify 验证阿里云验证码(必须提供验证码参数,兼容旧接口)
func Verify(cfg Config, captchaVerifyParam string) error {
result := VerifyOptional(cfg, captchaVerifyParam)
if result.VerifyErr != nil {
return result.VerifyErr
}
if result.Skipped {
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")
}
return nil
}

View File

@@ -0,0 +1,29 @@
package captcha
import (
"encoding/base64"
"fmt"
"time"
"qnc-server/pkg/lzkit/crypto"
)
// GenerateEncryptedSceneID: sceneId&timestamp&expireTime -> AES-256-CBC + PKCS7 -> Base64(IV + ciphertext)
func GenerateEncryptedSceneID(sceneId, ekey string, expireSeconds int) (string, error) {
if expireSeconds <= 0 || expireSeconds > 86400 {
expireSeconds = 3600
}
ts := time.Now().Unix()
plaintext := fmt.Sprintf("%s&%d&%d", sceneId, ts, expireSeconds)
keyBytes, err := base64.StdEncoding.DecodeString(ekey)
if err != nil {
return "", fmt.Errorf("decode ekey error: %w", err)
}
if len(keyBytes) != 32 {
return "", fmt.Errorf("invalid ekey length, need 32 bytes after base64 decode, got %d", len(keyBytes))
}
return crypto.AesEncrypt([]byte(plaintext), keyBytes)
}

View File

@@ -104,7 +104,6 @@ func validDate(fl validator.FieldLevel) bool {
return matched return matched
} }
// 自定义身份证校验(增强版) // 自定义身份证校验(增强版)
// 校验规则: // 校验规则:
// 1. 格式18位前6位地区码首位不为07-14位出生日期15-17位顺序码18位校验码 // 1. 格式18位前6位地区码首位不为07-14位出生日期15-17位顺序码18位校验码
@@ -216,8 +215,12 @@ func validatePayMethod(fl validator.FieldLevel) bool {
} }
validTypes := map[string]bool{ validTypes := map[string]bool{
"alipay": true, // 中国电信 "alipay": true,
"wechatpay": true, // 中国移动 "wechat": true,
"wechatpay": true,
"appleiap": true,
"test": true,
"test_empty": true,
} }
return validTypes[payMethod] return validTypes[payMethod]