Compare commits

...

25 Commits

Author SHA1 Message Date
Mrx
a50b2823b3 fadd 2026-06-07 15:11:33 +08:00
Mrx
079b5b39ef f 2026-06-07 14:47:50 +08:00
Mrx
0dada87a54 f 2026-06-07 14:39:21 +08:00
Mrx
f9ec2e51fd f 2026-06-07 13:57:18 +08:00
Mrx
35e9191981 f 2026-06-06 17:03:08 +08:00
Mrx
a85436950e f 2026-06-06 11:52:06 +08:00
Mrx
a79c464329 f 2026-06-04 18:13:55 +08:00
Mrx
932f2773d4 f 2026-06-04 18:04:04 +08:00
Mrx
7506ee718b f 2026-05-28 14:54:29 +08:00
Mrx
e2aab6af71 f 2026-05-23 16:13:30 +08:00
Mrx
effe82ce65 f 2026-05-22 11:28:44 +08:00
Mrx
d818a0a1c7 f 2026-05-21 20:56:04 +08:00
Mrx
144d5507dd feat: add toolbox query, upload module, update config and gitignore
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 17:29:35 +08:00
0a0ca7bf9b Merge branch 'main' of http://1.117.67.95:3000/team/qnc-server-v3 2026-05-13 14:43:52 +08:00
3399de0dc5 f 2026-05-13 14:43:10 +08:00
Mrx
7f0383b7d6 f 2026-05-06 16:42:27 +08:00
2312f54e1e Merge branch 'main' of http://1.117.67.95:3000/team/qnc-server-v3 2026-05-01 14:09:46 +08:00
28c084f608 f 2026-05-01 14:09:42 +08:00
Mrx
c9b4c50540 f 2026-03-11 15:28:54 +08:00
Mrx
fdfb2d76f8 Merge branch 'main' of http://1.117.67.95:3000/team/qnc-server-v3 2026-03-11 15:05:55 +08:00
Mrx
0dc053420a f 2026-03-11 15:05:51 +08:00
357ac705c7 f 2026-03-02 13:05:11 +08:00
a8b4887571 f 2026-03-02 12:57:38 +08:00
e8da86f1a6 Merge branch 'main' of http://1.117.67.95:3000/team/qnc-server-v3 2026-03-02 12:57:17 +08:00
6db59f1dea f 2026-03-02 12:46:26 +08:00
134 changed files with 20403 additions and 601 deletions

3
.gitignore vendored
View File

@@ -22,6 +22,9 @@ data/*
/app/api /app/api
/app/main/api/main /app/main/api/main
/app/main/api/_* /app/main/api/_*
__debug_bin.exe
**/.../
*.exe
# 文档目录 # 文档目录

BIN
app/main/api/api.exe Normal file

Binary file not shown.

View File

@@ -20,6 +20,10 @@ service main {
@handler AdminGetAgentList @handler AdminGetAgentList
get /list (AdminGetAgentListReq) returns (AdminGetAgentListResp) get /list (AdminGetAgentListReq) returns (AdminGetAgentListResp)
// 代理团队树(同钻石团队 + 邀请关系)
@handler AdminGetAgentTeamTree
get /team/tree (AdminGetAgentTeamTreeReq) returns (AdminGetAgentTeamTreeResp)
// 代理审核 // 代理审核
@handler AdminAuditAgent @handler AdminAuditAgent
post /audit (AdminAuditAgentReq) returns (AdminAuditAgentResp) post /audit (AdminAuditAgentReq) returns (AdminAuditAgentResp)
@@ -32,6 +36,10 @@ service main {
@handler AdminGetAgentOrderList @handler AdminGetAgentOrderList
get /order/list (AdminGetAgentOrderListReq) returns (AdminGetAgentOrderListResp) get /order/list (AdminGetAgentOrderListReq) returns (AdminGetAgentOrderListResp)
// 单笔订单分账视图:同一响应返回佣金 + 返佣(避免前端双请求重复走鉴权)
@handler AdminGetAgentOrderSettlement
get /order/settlement (AdminGetAgentOrderSettlementReq) returns (AdminGetAgentOrderSettlementResp)
// 代理佣金分页查询 // 代理佣金分页查询
@handler AdminGetAgentCommissionList @handler AdminGetAgentCommissionList
get /commission/list (AdminGetAgentCommissionListReq) returns (AdminGetAgentCommissionListResp) get /commission/list (AdminGetAgentCommissionListReq) returns (AdminGetAgentCommissionListResp)
@@ -94,9 +102,12 @@ type (
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
Mobile *string `form:"mobile,optional"` // 手机号(可选) Mobile *string `form:"mobile,optional"` // 手机号(可选)
AgentCode *string `form:"agent_code,optional"` // 代理编码(可选)
Region *string `form:"region,optional"` // 区域(可选) Region *string `form:"region,optional"` // 区域(可选)
Level *int64 `form:"level,optional"` // 等级(可选) Level *int64 `form:"level,optional"` // 等级(可选)
TeamLeaderId *string `form:"team_leader_id,optional"` // 团队首领ID可选 TeamLeaderId *string `form:"team_leader_id,optional"` // 团队首领ID可选
AgentId *string `form:"agent_id,optional"` // 代理主键UUID精确查询传入时不再按 team_leader 与其它条件组合筛选
RealName *string `form:"real_name,optional"` // 实名姓名(可选,模糊匹配已三要素核验记录)
} }
AgentListItem { AgentListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
@@ -105,8 +116,11 @@ type (
LevelName string `json:"level_name"` // 等级名称 LevelName string `json:"level_name"` // 等级名称
Region string `json:"region"` // 区域 Region string `json:"region"` // 区域
Mobile string `json:"mobile"` // 手机号 Mobile string `json:"mobile"` // 手机号
RealName string `json:"real_name"` // 姓名(来自实名表)
IdCard string `json:"id_card"` // 身份证(解密后明文返回)
WechatId string `json:"wechat_id"` // 微信号 WechatId string `json:"wechat_id"` // 微信号
TeamLeaderId string `json:"team_leader_id"` // 团队首领ID TeamLeaderId string `json:"team_leader_id"` // 团队首领ID
SubordinateCount int64 `json:"subordinate_count"` // 直属下级代理数team_leader_id 为本代理 id
AgentCode int64 `json:"agent_code"` AgentCode int64 `json:"agent_code"`
Balance float64 `json:"balance"` // 钱包余额 Balance float64 `json:"balance"` // 钱包余额
TotalEarnings float64 `json:"total_earnings"` // 累计收益 TotalEarnings float64 `json:"total_earnings"` // 累计收益
@@ -119,6 +133,21 @@ type (
Total int64 `json:"total"` // 总数 Total int64 `json:"total"` // 总数
Items []AgentListItem `json:"items"` // 列表数据 Items []AgentListItem `json:"items"` // 列表数据
} }
AdminGetAgentTeamTreeReq {
AgentId string `form:"agent_id"` // 锚点代理IDUUID
}
AgentTeamTreeNode {
Id string `json:"id"`
AgentCode int64 `json:"agent_code"`
Level int64 `json:"level"`
LevelName string `json:"level_name"`
Mobile string `json:"mobile"`
IsAnchor bool `json:"is_anchor"`
Children []AgentTeamTreeNode `json:"children"`
}
AdminGetAgentTeamTreeResp {
Root AgentTeamTreeNode `json:"root"`
}
// 代理审核 // 代理审核
AdminAuditAgentReq { AdminAuditAgentReq {
AuditId int64 `json:"audit_id"` // 审核记录ID AuditId int64 `json:"audit_id"` // 审核记录ID
@@ -133,12 +162,14 @@ type (
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
AgentCode *string `form:"agent_code,optional"` // 代理编号(可选)
ProductId *string `form:"product_id,optional"` // 产品ID可选 ProductId *string `form:"product_id,optional"` // 产品ID可选
LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选)
} }
AgentLinkListItem { AgentLinkListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
AgentCode int64 `json:"agent_code"` // 代理编号
ProductId string `json:"product_id"` // 产品ID ProductId string `json:"product_id"` // 产品ID
ProductName string `json:"product_name"` // 产品名称 ProductName string `json:"product_name"` // 产品名称
SetPrice float64 `json:"set_price"` // 设定价格 SetPrice float64 `json:"set_price"` // 设定价格
@@ -155,14 +186,18 @@ type (
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
AgentCode *string `form:"agent_code,optional"` // 代理编号(可选)
OrderId *string `form:"order_id,optional"` // 订单ID可选 OrderId *string `form:"order_id,optional"` // 订单ID可选
OrderNo *string `form:"order_no,optional"` // 商户订单号(可选)
ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选) ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选)
OrderStatus *string `form:"order_status,optional"` // 订单状态可选pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败 OrderStatus *string `form:"order_status,optional"` // 订单状态可选pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败
} }
AgentOrderListItem { AgentOrderListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
AgentCode int64 `json:"agent_code"` // 代理编号
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
OrderNo string `json:"order_no"` // 商户订单号
ProductId string `json:"product_id"` // 产品ID ProductId string `json:"product_id"` // 产品ID
ProductName string `json:"product_name"` // 产品名称 ProductName string `json:"product_name"` // 产品名称
OrderAmount float64 `json:"order_amount"` // 订单金额 OrderAmount float64 `json:"order_amount"` // 订单金额
@@ -178,18 +213,28 @@ type (
Total int64 `json:"total"` // 总数 Total int64 `json:"total"` // 总数
Items []AgentOrderListItem `json:"items"` // 列表数据 Items []AgentOrderListItem `json:"items"` // 列表数据
} }
AdminGetAgentOrderSettlementReq {
OrderNo string `form:"order_no"` // 商户订单号
}
AdminGetAgentOrderSettlementResp {
Commissions []AgentCommissionListItem `json:"commissions"` // 本单销售佣金
Rebates []AgentRebateListItem `json:"rebates"` // 本单返佣
}
// 代理佣金分页查询 // 代理佣金分页查询
AdminGetAgentCommissionListReq { AdminGetAgentCommissionListReq {
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
OrderId *string `form:"order_id,optional"` // 订单ID(可选) AgentCode *string `form:"agent_code,optional"` // 代理编号(可选)
OrderNo *string `form:"order_no,optional"` // 商户订单号(可选)
Status *int64 `form:"status,optional"` // 状态(可选) Status *int64 `form:"status,optional"` // 状态(可选)
} }
AgentCommissionListItem { AgentCommissionListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
AgentCode int64 `json:"agent_code"` // 代理编号
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
OrderNo string `json:"order_no"` // 商户订单号
ProductName string `json:"product_name"` // 产品名称 ProductName string `json:"product_name"` // 产品名称
Amount float64 `json:"amount"` // 金额 Amount float64 `json:"amount"` // 金额
Status int64 `json:"status"` // 状态 Status int64 `json:"status"` // 状态
@@ -204,15 +249,20 @@ type (
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
SourceAgentId *string `form:"source_agent_id,optional"` // 来源代理ID(可选) AgentCode *string `form:"agent_code,optional"` // 代理编号(可选)
SourceAgentCode *string `form:"source_agent_code,optional"` // 来源代理编号(可选)
OrderNo *string `form:"order_no,optional"` // 商户订单号(可选)
RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选) RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选)
Status *int64 `form:"status,optional"` // 状态可选1=已发放2=已冻结3=已取消(已退款) Status *int64 `form:"status,optional"` // 状态可选1=已发放2=已冻结3=已取消(已退款)
} }
AgentRebateListItem { AgentRebateListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 获得返佣的代理ID AgentId string `json:"agent_id"` // 获得返佣的代理ID
AgentCode int64 `json:"agent_code"` // 获得返佣代理编号
SourceAgentId string `json:"source_agent_id"` // 来源代理ID SourceAgentId string `json:"source_agent_id"` // 来源代理ID
SourceAgentCode int64 `json:"source_agent_code"` // 来源代理编号
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID
OrderNo string `json:"order_no"` // 商户订单号
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=已取消(已退款) Status int64 `json:"status"` // 状态1=已发放2=已冻结3=已取消(已退款)
@@ -227,12 +277,14 @@ type (
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理ID可选
AgentCode *string `form:"agent_code,optional"` // 代理编号(可选)
UpgradeType *int64 `form:"upgrade_type,optional"` // 升级类型(可选) UpgradeType *int64 `form:"upgrade_type,optional"` // 升级类型(可选)
Status *int64 `form:"status,optional"` // 状态(可选) Status *int64 `form:"status,optional"` // 状态(可选)
} }
AgentUpgradeListItem { AgentUpgradeListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
AgentCode int64 `json:"agent_code"` // 代理编号
FromLevel int64 `json:"from_level"` // 原等级 FromLevel int64 `json:"from_level"` // 原等级
ToLevel int64 `json:"to_level"` // 目标等级 ToLevel int64 `json:"to_level"` // 目标等级
UpgradeType int64 `json:"upgrade_type"` // 升级类型 UpgradeType int64 `json:"upgrade_type"` // 升级类型
@@ -249,13 +301,18 @@ type (
AdminGetAgentWithdrawalListReq { AdminGetAgentWithdrawalListReq {
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
AgentId *string `form:"agent_id,optional"` // 代理ID可选 AgentId *string `form:"agent_id,optional"` // 代理主键 UUID弹窗内按代理筛选时传入
Status *int64 `form:"status,optional"` // 状态(可选) AgentCode *string `form:"agent_code,optional"` // 代理编号(可选)
WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选,模糊匹配
Status string `form:"status,optional"` // 状态可选1=待审核2=已通过3=已拒绝5=已打款
WithdrawalType string `form:"withdrawal_type,optional"` // 提现方式可选1=支付宝2=银行卡
PayeeAccount *string `form:"payee_account,optional"` // 收款账户(可选,模糊)
PayeeName *string `form:"payee_name,optional"` // 收款人(可选,模糊)
} }
AgentWithdrawalListItem { AgentWithdrawalListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
AgentCode int64 `json:"agent_code"` // 代理编号
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"` // 税费金额
@@ -292,6 +349,7 @@ type (
AgentRealNameListItem { AgentRealNameListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
AgentId string `json:"agent_id"` // 代理ID AgentId string `json:"agent_id"` // 代理ID
AgentCode int64 `json:"agent_code"` // 代理编号
Name string `json:"name"` // 姓名 Name string `json:"name"` // 姓名
IdCard string `json:"id_card"` // 身份证号 IdCard string `json:"id_card"` // 身份证号
Mobile string `json:"mobile"` // 手机号 Mobile string `json:"mobile"` // 手机号
@@ -410,20 +468,21 @@ type (
AdminGetInviteCodeListReq { AdminGetInviteCodeListReq {
Page int64 `form:"page"` // 页码 Page int64 `form:"page"` // 页码
PageSize int64 `form:"pageSize"` // 每页数量 PageSize int64 `form:"pageSize"` // 每页数量
Code *string `form:"code,optional"` // 邀请码(可选) Code *string `form:"code,optional"` // 邀请码(可选,模糊匹配
AgentId *string `form:"agent_id,optional"` // 发放代理ID可选NULL表示平台发放
TargetLevel *int64 `form:"target_level,optional"` // 目标等级(可选) TargetLevel *int64 `form:"target_level,optional"` // 目标等级(可选)
Status *int64 `form:"status,optional"` // 状态(可选) Status string `form:"status,optional"` // 状态(可选)0=未使用1=已使用2=已失效
} }
InviteCodeListItem { InviteCodeListItem {
Id string `json:"id"` // 主键 Id string `json:"id"` // 主键
Code string `json:"code"` // 邀请码 Code string `json:"code"` // 邀请码
AgentId string `json:"agent_id"` // 发放代理ID0表示平台发放 AgentId string `json:"agent_id"` // 发放代理ID0表示平台发放
AgentCode int64 `json:"agent_code"` // 发放代理编号平台为0
AgentMobile string `json:"agent_mobile"` // 发放代理手机号 AgentMobile string `json:"agent_mobile"` // 发放代理手机号
TargetLevel int64 `json:"target_level"` // 目标等级 TargetLevel int64 `json:"target_level"` // 目标等级
Status int64 `json:"status"` // 状态0=未使用1=已使用2=已失效 Status int64 `json:"status"` // 状态0=未使用1=已使用2=已失效
UsedUserId string `json:"used_user_id"` // 使用用户ID UsedUserId string `json:"used_user_id"` // 使用用户ID
UsedAgentId string `json:"used_agent_id"` // 使用代理ID UsedAgentId string `json:"used_agent_id"` // 使用代理ID
UsedAgentCode int64 `json:"used_agent_code"` // 使用代理编号
UsedTime string `json:"used_time"` // 使用时间 UsedTime string `json:"used_time"` // 使用时间
ExpireTime string `json:"expire_time"` // 过期时间 ExpireTime string `json:"expire_time"` // 过期时间
Remark string `json:"remark"` // 备注 Remark string `json:"remark"` // 备注

View File

@@ -47,6 +47,8 @@ type (
AdminCreateFeatureReq { AdminCreateFeatureReq {
ApiId string `json:"api_id"` // API标识 ApiId string `json:"api_id"` // API标识
Name string `json:"name"` // 描述 Name string `json:"name"` // 描述
WhitelistPrice *float64 `json:"whitelist_price,optional"` // 白名单屏蔽价格(单位:元)
CostPrice *float64 `json:"cost_price,optional"` // 天远API调用成本价单位
} }
// 创建功能响应 // 创建功能响应
AdminCreateFeatureResp { AdminCreateFeatureResp {
@@ -57,6 +59,8 @@ type (
Id string `path:"id"` // 功能ID Id string `path:"id"` // 功能ID
ApiId *string `json:"api_id,optional"` // API标识 ApiId *string `json:"api_id,optional"` // API标识
Name *string `json:"name,optional"` // 描述 Name *string `json:"name,optional"` // 描述
WhitelistPrice *float64 `json:"whitelist_price,optional"` // 白名单屏蔽价格(单位:元)
CostPrice *float64 `json:"cost_price,optional"` // 天远API调用成本价单位
} }
// 更新功能响应 // 更新功能响应
AdminUpdateFeatureResp { AdminUpdateFeatureResp {
@@ -82,6 +86,8 @@ type (
Id string `json:"id"` // 功能ID Id string `json:"id"` // 功能ID
ApiId string `json:"api_id"` // API标识 ApiId string `json:"api_id"` // API标识
Name string `json:"name"` // 描述 Name string `json:"name"` // 描述
WhitelistPrice float64 `json:"whitelist_price"` // 白名单屏蔽价格(单位:元)
CostPrice float64 `json:"cost_price"` // 天远API调用成本价单位
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
UpdateTime string `json:"update_time"` // 更新时间 UpdateTime string `json:"update_time"` // 更新时间
} }
@@ -99,6 +105,8 @@ type (
Id string `json:"id"` // 功能ID Id string `json:"id"` // 功能ID
ApiId string `json:"api_id"` // API标识 ApiId string `json:"api_id"` // API标识
Name string `json:"name"` // 描述 Name string `json:"name"` // 描述
WhitelistPrice float64 `json:"whitelist_price"` // 白名单屏蔽价格(单位:元)
CostPrice float64 `json:"cost_price"` // 天远API调用成本价单位
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
UpdateTime string `json:"update_time"` // 更新时间 UpdateTime string `json:"update_time"` // 更新时间
} }

View File

@@ -49,7 +49,6 @@ type (
ProductEn string `json:"product_en"` // 英文名 ProductEn string `json:"product_en"` // 英文名
Description string `json:"description"` // 描述 Description string `json:"description"` // 描述
Notes string `json:"notes,optional"` // 备注 Notes string `json:"notes,optional"` // 备注
CostPrice float64 `json:"cost_price"` // 成本
SellPrice float64 `json:"sell_price"` // 售价 SellPrice float64 `json:"sell_price"` // 售价
} }
@@ -65,7 +64,6 @@ type (
ProductEn *string `json:"product_en,optional"` // 英文名 ProductEn *string `json:"product_en,optional"` // 英文名
Description *string `json:"description,optional"` // 描述 Description *string `json:"description,optional"` // 描述
Notes *string `json:"notes,optional"` // 备注 Notes *string `json:"notes,optional"` // 备注
CostPrice *float64 `json:"cost_price,optional"` // 成本
SellPrice *float64 `json:"sell_price,optional"` // 售价 SellPrice *float64 `json:"sell_price,optional"` // 售价
} }
@@ -99,7 +97,7 @@ type (
ProductEn string `json:"product_en"` // 英文名 ProductEn string `json:"product_en"` // 英文名
Description string `json:"description"` // 描述 Description string `json:"description"` // 描述
Notes string `json:"notes"` // 备注 Notes string `json:"notes"` // 备注
CostPrice float64 `json:"cost_price"` // 成本 CostPrice float64 `json:"cost_price"` // 成本(由关联模块成本汇总,只读)
SellPrice float64 `json:"sell_price"` // 售价 SellPrice float64 `json:"sell_price"` // 售价
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
UpdateTime string `json:"update_time"` // 更新时间 UpdateTime string `json:"update_time"` // 更新时间
@@ -123,7 +121,7 @@ type (
ProductEn string `json:"product_en"` // 英文名 ProductEn string `json:"product_en"` // 英文名
Description string `json:"description"` // 描述 Description string `json:"description"` // 描述
Notes string `json:"notes"` // 备注 Notes string `json:"notes"` // 备注
CostPrice float64 `json:"cost_price"` // 成本 CostPrice float64 `json:"cost_price"` // 成本(由关联模块成本汇总,只读)
SellPrice float64 `json:"sell_price"` // 售价 SellPrice float64 `json:"sell_price"` // 售价
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
UpdateTime string `json:"update_time"` // 更新时间 UpdateTime string `json:"update_time"` // 更新时间
@@ -141,6 +139,7 @@ type (
FeatureId string `json:"feature_id"` // 功能ID FeatureId string `json:"feature_id"` // 功能ID
ApiId string `json:"api_id"` // API标识 ApiId string `json:"api_id"` // API标识
Name string `json:"name"` // 功能描述 Name string `json:"name"` // 功能描述
CostPrice float64 `json:"cost_price"` // 模块成本价(元,来自 feature
Sort int64 `json:"sort"` // 排序 Sort int64 `json:"sort"` // 排序
Enable int64 `json:"enable"` // 是否启用 Enable int64 `json:"enable"` // 是否启用
IsImportant int64 `json:"is_important"` // 是否重要 IsImportant int64 `json:"is_important"` // 是否重要

View File

@@ -16,6 +16,10 @@ service main {
@handler AdminGetQueryDetailByOrderId @handler AdminGetQueryDetailByOrderId
get /detail/:order_id (AdminGetQueryDetailByOrderIdReq) returns (AdminGetQueryDetailByOrderIdResp) get /detail/:order_id (AdminGetQueryDetailByOrderIdReq) returns (AdminGetQueryDetailByOrderIdResp)
@doc "生成报告分享链接(供后台预览 H5"
@handler AdminGenerateQueryShareLink
post /generate_share_link (AdminGenerateQueryShareLinkReq) returns (QueryGenerateShareLinkResp)
@doc "获取清理日志列表" @doc "获取清理日志列表"
@handler AdminGetQueryCleanupLogList @handler AdminGetQueryCleanupLogList
get /cleanup/logs (AdminGetQueryCleanupLogListReq) returns (AdminGetQueryCleanupLogListResp) get /cleanup/logs (AdminGetQueryCleanupLogListReq) returns (AdminGetQueryCleanupLogListResp)
@@ -37,6 +41,10 @@ service main {
OrderId string `path:"order_id"` OrderId string `path:"order_id"`
} }
type AdminGenerateQueryShareLinkReq {
OrderId string `json:"order_id"`
}
type AdminGetQueryDetailByOrderIdResp { type AdminGetQueryDetailByOrderIdResp {
Id string `json:"id"` // 主键ID Id string `json:"id"` // 主键ID
OrderId string `json:"order_id"` // 订单ID OrderId string `json:"order_id"` // 订单ID

View File

@@ -0,0 +1,93 @@
syntax = "v1"
info (
title: "后台统计面板服务"
desc: "后台统计面板相关接口"
author: "team"
version: "v1"
)
// ============================================
// 统计面板接口
// ============================================
@server (
prefix: /api/v1/admin/dashboard
group: admin_dashboard
middleware: AdminAuthInterceptor
)
service main {
// 获取统计面板数据
@handler AdminGetDashboardStatistics
get /statistics returns (AdminGetDashboardStatisticsResp)
}
type (
// 统计面板响应
AdminGetDashboardStatisticsResp {
// 订单统计
OrderStats AdminOrderStatistics `json:"order_stats"`
// 营收统计
RevenueStats AdminRevenueStatistics `json:"revenue_stats"`
// 代理统计
AgentStats AdminAgentStatistics `json:"agent_stats"`
// 利润统计
ProfitStats AdminProfitStatistics `json:"profit_stats"`
// 订单趋势最近7天
OrderTrend []AdminTrendData `json:"order_trend"`
// 营收趋势最近7天
RevenueTrend []AdminTrendData `json:"revenue_trend"`
}
// 订单统计
AdminOrderStatistics {
TodayCount int64 `json:"today_count"` // 今日订单数
MonthCount int64 `json:"month_count"` // 当月订单数
TotalCount int64 `json:"total_count"` // 总订单数
YesterdayCount int64 `json:"yesterday_count"` // 昨日订单数
ChangeRate float64 `json:"change_rate"` // 变化率(百分比)
}
// 营收统计
AdminRevenueStatistics {
TodayAmount float64 `json:"today_amount"` // 今日营收
MonthAmount float64 `json:"month_amount"` // 当月营收
TotalAmount float64 `json:"total_amount"` // 总营收
YesterdayAmount float64 `json:"yesterday_amount"` // 昨日营收
ChangeRate float64 `json:"change_rate"` // 变化率(百分比)
}
// 代理统计
AdminAgentStatistics {
TotalCount int64 `json:"total_count"` // 代理总数
TodayNew int64 `json:"today_new"` // 今日新增
MonthNew int64 `json:"month_new"` // 当月新增
}
// 利润统计
AdminProfitStatistics {
TodayProfit float64 `json:"today_profit"` // 今日利润
MonthProfit float64 `json:"month_profit"` // 当月利润
TotalProfit float64 `json:"total_profit"` // 总利润
TodayProfitRate float64 `json:"today_profit_rate"` // 今日利润率
MonthProfitRate float64 `json:"month_profit_rate"` // 当月利润率
TotalProfitRate float64 `json:"total_profit_rate"` // 总利润率
// 今日明细
TodayDetail AdminProfitDetail `json:"today_detail"`
// 当月明细
MonthDetail AdminProfitDetail `json:"month_detail"`
// 总计明细
TotalDetail AdminProfitDetail `json:"total_detail"`
}
// 利润明细
AdminProfitDetail {
Revenue float64 `json:"revenue"` // 营收
Commission float64 `json:"commission"` // 佣金
Rebate float64 `json:"rebate"` // 返利
CompanyTax float64 `json:"company_tax"` // 税务成本
ApiCost float64 `json:"api_cost"` // API调用成本
TaxIncome float64 `json:"tax_income"` // 提现收税
Profit float64 `json:"profit"` // 利润
ProfitRate float64 `json:"profit_rate"` // 利润率
}
// 趋势数据
AdminTrendData {
Date string `json:"date"` // 日期格式MM-DD
Value float64 `json:"value"` // 数值
}
)

View File

@@ -39,6 +39,10 @@ service main {
@doc "重新执行代理处理" @doc "重新执行代理处理"
@handler AdminRetryAgentProcess @handler AdminRetryAgentProcess
post /retry-agent-process/:id (AdminRetryAgentProcessReq) returns (AdminRetryAgentProcessResp) post /retry-agent-process/:id (AdminRetryAgentProcessReq) returns (AdminRetryAgentProcessResp)
@doc "xpay补发货查微信单并到账"
@handler AdminXpayDeliver
post /xpay-deliver/:id (AdminXpayDeliverReq) returns (AdminXpayDeliverResp)
} }
type ( type (
@@ -53,6 +57,8 @@ type (
PaymentScene string `form:"payment_scene,optional"` // 支付平台 PaymentScene string `form:"payment_scene,optional"` // 支付平台
Amount float64 `form:"amount,optional"` // 金额 Amount float64 `form:"amount,optional"` // 金额
Status string `form:"status,optional"` // 支付状态pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败 Status string `form:"status,optional"` // 支付状态pending-待支付paid-已支付refunded-已退款closed-已关闭failed-支付失败
IsAgentOrder *bool `form:"is_agent_order,optional"` // 是否代理订单
AgentCode string `form:"agent_code,optional"` // 代理编号
CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始
CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束
PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始
@@ -81,6 +87,7 @@ type (
RefundTime string `json:"refund_time"` // 退款时间 RefundTime string `json:"refund_time"` // 退款时间
UpdateTime string `json:"update_time"` // 更新时间 UpdateTime string `json:"update_time"` // 更新时间
IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单
AgentCode string `json:"agent_code"` // 代理编号
AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态not_agent-非代理订单success-处理成功failed-处理失败pending-待处理 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态not_agent-非代理订单success-处理成功failed-处理失败pending-待处理
} }
// 详情请求 // 详情请求
@@ -166,4 +173,15 @@ type (
Message string `json:"message"` // 执行结果消息 Message string `json:"message"` // 执行结果消息
ProcessedAt string `json:"processed_at"` // 处理时间 ProcessedAt string `json:"processed_at"` // 处理时间
} }
// xpay 补发货
AdminXpayDeliverReq {
Id string `path:"id"` // 订单ID
}
AdminXpayDeliverResp {
Credited bool `json:"credited"`
Notified bool `json:"notified"`
WechatDetail string `json:"wechat_detail"`
Errors []string `json:"errors"`
Message string `json:"message"`
}
) )

View File

@@ -39,6 +39,7 @@ type (
AdminGetPlatformUserListReq { AdminGetPlatformUserListReq {
Page int64 `form:"page,default=1"` // 页码 Page int64 `form:"page,default=1"` // 页码
PageSize int64 `form:"pageSize,default=20"` // 每页数量 PageSize int64 `form:"pageSize,default=20"` // 每页数量
UserId string `form:"user_id,optional"` // 平台用户主键 ID精确筛选
Mobile string `form:"mobile,optional"` // 手机号 Mobile string `form:"mobile,optional"` // 手机号
Nickname string `form:"nickname,optional"` // 昵称 Nickname string `form:"nickname,optional"` // 昵称
Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否

View File

@@ -313,7 +313,7 @@ type (
// 生成推广链接 // 生成推广链接
AgentGeneratingLinkReq { AgentGeneratingLinkReq {
ProductId string `json:"product_id"` // 产品ID ProductId string `json:"product_id"` // 产品ID
SetPrice float64 `json:"set_price"` // 设定价格 SetPrice string `json:"set_price"` // 设定价格
TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面)
} }
AgentGeneratingLinkResp { AgentGeneratingLinkResp {
@@ -651,7 +651,7 @@ type (
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"` // 查询参数(已脱敏) Params map[string]interface{} `json:"params,optional"` // 查询参数(已脱敏)
Price float64 `json:"price"` // 查询价格 Price float64 `json:"price"` // 查询价格
} }
// ============================================ // ============================================
@@ -697,35 +697,29 @@ type (
StatusText string `json:"status_text"` // 状态文本 StatusText string `json:"status_text"` // 状态文本
CreateTime string `json:"create_time"` // 创建时间 CreateTime string `json:"create_time"` // 创建时间
} }
// 检查模块白名单状态请求
CheckFeatureWhitelistStatusReq { CheckFeatureWhitelistStatusReq {
IdCard string `form:"id_card"` // 身份证号 IdCard string `form:"id_card"` // 身份证号
FeatureApiId string `form:"feature_api_id"` // Feature的API标识 FeatureApiId string `form:"feature_api_id"` // Feature的API标识
QueryId string `form:"query_id,optional"` // 查询记录ID可选,用于检查报告数据是否已删除 QueryId string `form:"query_id,optional"` // 查询记录ID可选
} }
// 检查模块白名单状态响应
CheckFeatureWhitelistStatusResp { CheckFeatureWhitelistStatusResp {
IsWhitelisted bool `json:"is_whitelisted"` // 是否在白名单中 IsWhitelisted bool `json:"is_whitelisted"` // 是否在白名单中
WhitelistPrice float64 `json:"whitelist_price"` // 屏蔽价格(单位:元),如果为0表示不支持下架 WhitelistPrice float64 `json:"whitelist_price"` // 屏蔽价格单位0表示不支持下架
FeatureId string `json:"feature_id"` // Feature的UUID FeatureId string `json:"feature_id"` // Feature的UUID
DataDeleted bool `json:"data_deleted"` // 报告数据是否已删除仅当提供了query_id时有效 DataDeleted bool `json:"data_deleted"` // 报告数据是否已删除
} }
// 下架单个模块请求
OfflineFeatureReq { OfflineFeatureReq {
FeatureApiId string `json:"feature_api_id"` // Feature的API标识 FeatureApiId string `json:"feature_api_id"` // Feature的API标识
QueryId string `json:"query_id"` // 查询记录IDQuery表的ID必选 QueryId string `json:"query_id"` // 查询记录IDQuery表的ID必选
} }
// 下架单个模块响应
OfflineFeatureResp { OfflineFeatureResp {
Success bool `json:"success"` // 是否已完成下架 Success bool `json:"success"` // 是否已完成下架
NeedPay bool `json:"need_pay"` // 是否需要发起支付 NeedPay bool `json:"need_pay"` // 是否需要发起支付
Amount float64 `json:"amount"` // 需要支付的金额(单位:0表示无需支付 Amount float64 `json:"amount"` // 需要支付的金额0表示无需支付
} }
// 检查订单是否属于当前代理请求
CheckOrderAgentReq { CheckOrderAgentReq {
OrderId string `form:"order_id"` // 订单ID OrderId string `form:"order_id"` // 订单ID
} }
// 检查订单是否属于当前代理响应
CheckOrderAgentResp { CheckOrderAgentResp {
IsAgentOrder bool `json:"is_agent_order"` // 是否是当前代理推广的订单 IsAgentOrder bool `json:"is_agent_order"` // 是否是当前代理推广的订单
} }

View File

@@ -22,8 +22,31 @@ service main {
@handler getAppConfig @handler getAppConfig
get /app/config returns (getAppConfigResp) get /app/config returns (getAppConfigResp)
@handler getHomeDynamicData
get /app/home/dynamic (getHomeDynamicDataReq) returns (getHomeDynamicDataResp)
} }
type (
getHomeDynamicDataReq {
LastId int64 `form:"lastId,optional"`
}
InquiryRecordItem {
Id int64 `json:"id"`
Tag string `json:"tag"`
Vin string `json:"vin"`
Model string `json:"model"`
}
ReviewItem {
Name string `json:"name"`
Content string `json:"content"`
}
getHomeDynamicDataResp {
Cases []InquiryRecordItem `json:"cases"`
Reviews []ReviewItem `json:"reviews"`
}
)
type ( type (
// 心跳检测响应 // 心跳检测响应
HealthCheckResp { HealthCheckResp {

View File

@@ -22,6 +22,11 @@ service main {
// 微信退款回调 // 微信退款回调
@handler WechatPayRefundCallback @handler WechatPayRefundCallback
post /pay/wechat/refund_callback post /pay/wechat/refund_callback
// 微信小程序虚拟支付发货推送GET 验签 + POST 事件)
@handler XpayPush
get /pay/xpay/push
post /pay/xpay/push
} }
@server ( @server (
@@ -40,6 +45,10 @@ service main {
@handler PaymentCheck @handler PaymentCheck
post /pay/check (PaymentCheckReq) returns (PaymentCheckResp) post /pay/check (PaymentCheckReq) returns (PaymentCheckResp)
// 小程序虚拟支付:上报微信/Apple 客户端提示与步骤(写入服务端日志并可选查微信单)
@handler XpayClientEvent
post /pay/xpay/client-event (XpayClientEventReq) returns (XpayClientEventResp)
} }
type ( type (
@@ -60,6 +69,26 @@ type (
PaymentCheckResp { PaymentCheckResp {
Type string `json:"type"` Type string `json:"type"`
Status string `json:"status"` Status string `json:"status"`
WxSyncError string `json:"wx_sync_error,optional"` // query_order 失败时微信返回
WxOrderStatus int `json:"wx_order_status,optional"` // 微信侧订单 status
WxOrderDetail string `json:"wx_order_detail,optional"` // query_order 原始 order 摘要(排查用)
}
XpayClientEventReq {
OrderNo string `json:"order_no,optional"`
Stage string `json:"stage" validate:"required"` // prepay_ok, invoke, success, fail, check
EventType string `json:"event_type" validate:"required,oneof=info tip fail success"`
Message string `json:"message,optional"` // 微信/Apple 展示给用户的话术(非 HTTP 报错)
ErrMsg string `json:"err_msg,optional"`
ErrCode int `json:"err_code,optional"`
Errno int `json:"errno,optional"`
SignData string `json:"sign_data,optional"`
Device string `json:"device,optional"`
Extra string `json:"extra,optional"`
}
XpayClientEventResp {
WxOrderStatus int `json:"wx_order_status,optional"`
WxOrderDetail string `json:"wx_order_detail,optional"`
WxSyncError string `json:"wx_sync_error,optional"`
} }
) )

View File

@@ -0,0 +1,29 @@
syntax = "v1"
info (
title: "工具箱服务"
desc: "免费工具箱(天行聚合等)"
version: "v1"
)
//============================> toolbox v1 <============================
@server (
prefix: api/v1
group: toolbox
)
service main {
@doc "工具箱统一查询"
@handler toolboxQuery
post /toolbox/query (ToolboxQueryReq) returns (ToolboxQueryResp)
}
type (
ToolboxQueryReq {
ToolKey string `json:"tool_key"`
Params map[string]interface{} `json:"params,optional"`
}
ToolboxQueryResp {
ToolKey string `json:"tool_key"`
Result map[string]interface{} `json:"result"`
}
)

View File

@@ -0,0 +1,44 @@
syntax = "v1"
info (
title: "上传服务"
desc: "图片 Base64 上传与文件访问"
version: "v1"
)
//============================> upload v1 <============================
// 访问已上传图片(直接输出二进制,无 JSON 包装)
@server (
prefix: api/v1
group: upload
)
service main {
@doc "访问已上传图片"
@handler serveUpload
get /upload/file/:name (ServeUploadFileReq)
}
type ServeUploadFileReq {
Name string `path:"name"`
}
@server (
prefix: api/v1
group: upload
jwt: JwtAuth
middleware: AuthInterceptor
)
service main {
@doc "图片 Base64 上传"
@handler uploadImage
post /upload/image (UploadImageReq) returns (UploadImageResp)
}
type (
UploadImageReq {
ImageBase64 string `json:"image_base64"`
}
UploadImageResp {
Url string `json:"url"`
}
)

View File

@@ -14,6 +14,8 @@ import "./front/product.api"
import "./front/agent.api" import "./front/agent.api"
import "./front/app.api" import "./front/app.api"
import "./front/authorization.api" import "./front/authorization.api"
import "./front/toolbox.api"
import "./front/upload.api"
// 后台 // 后台
import "./admin/auth.api" import "./admin/auth.api"
import "./admin/menu.api" import "./admin/menu.api"
@@ -24,7 +26,11 @@ import "./admin/platform_user.api"
import "./admin/notification.api" import "./admin/notification.api"
import "./admin/admin_product.api" import "./admin/admin_product.api"
import "./admin/admin_feature.api" import "./admin/admin_feature.api"
import "./admin/dashboard.api"
import "./admin/admin_query.api" import "./admin/admin_query.api"
import "./admin/admin_agent.api" import "./admin/admin_agent.api"
import "./admin/admin_api.api" import "./admin/admin_api.api"
import "./admin/admin_role_api.api" import "./admin/admin_role_api.api"

View File

@@ -75,6 +75,20 @@ WechatH5:
WechatMini: WechatMini:
AppID: "wx5bacc94add2da981" # 小程序的AppID AppID: "wx5bacc94add2da981" # 小程序的AppID
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
WechatXpay:
Enabled: true
Env: 1 # 本地联调用沙箱;现网单请改 0 + 现网 AppKey
OfferId: "1450552691"
AppKey: "n1SSzeMiitPjiQwLlBn1s4Hn7SpkG3qD" # 沙箱 AppKey仅 env=1
MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
SessionKeyTTL: 2592000 # 30 天,与 JwtAuth.AccessExpire 对齐
# Enabled: true
# Env: 0 # 0 现网 / 1 沙箱(须与 AppKey 配套)
# OfferId: "1450552691"
# AppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb" # 现网 AppKeyenv=0 必填现网密钥)
# MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
# SessionKeyTTL: 2592000 # 30 天,与 JwtAuth.AccessExpire 对齐
Query: Query:
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒 ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
AdminConfig: AdminConfig:
@@ -89,6 +103,10 @@ Tianyuanapi:
Key: "04c6b4c559be6d5ba5351c04c8713a64" Key: "04c6b4c559be6d5ba5351c04c8713a64"
BaseURL: "https://api.tianyuanapi.com" BaseURL: "https://api.tianyuanapi.com"
Timeout: 60 Timeout: 60
tianxingjuhe:
url: "https://apis.tianapi.com"
key: "4ceffb1ffb95b83230b9a9c9df2467e1"
timeout: 30
Authorization: Authorization:
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
Promotion: Promotion:

View File

@@ -64,6 +64,13 @@ WechatH5:
WechatMini: WechatMini:
AppID: "wx5bacc94add2da981" # 小程序的AppID AppID: "wx5bacc94add2da981" # 小程序的AppID
AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret
WechatXpay:
Enabled: true
Env: 0 # 0 现网 / 1 沙箱(须与 AppKey 配套)
OfferId: "1450552691"
AppKey: "oYv2wooYzRmPDLdXkpBpqaml8cFaY0Bb" # 现网 AppKeyenv=0 必填现网密钥)
MessagePushToken: "qncXpayPush2026" # 与 mp 后台消息推送 Token 保持一致
SessionKeyTTL: 2592000 # 30 天,与 JwtAuth.AccessExpire 对齐
Query: Query:
ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒 ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒
AdminConfig: AdminConfig:
@@ -78,6 +85,10 @@ Tianyuanapi:
Key: "04c6b4c559be6d5ba5351c04c8713a64" Key: "04c6b4c559be6d5ba5351c04c8713a64"
BaseURL: "https://api.tianyuanapi.com" BaseURL: "https://api.tianyuanapi.com"
Timeout: 60 Timeout: 60
tianxingjuhe:
url: "https://apis.tianapi.com"
key: "4ceffb1ffb95b83230b9a9c9df2467e1"
timeout: 30
Authorization: Authorization:
FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL
Promotion: Promotion:

View File

@@ -21,10 +21,12 @@ type Config struct {
WechatH5 WechatH5Config WechatH5 WechatH5Config
Authorization AuthorizationConfig // 授权书配置 Authorization AuthorizationConfig // 授权书配置
WechatMini WechatMiniConfig WechatMini WechatMiniConfig
WechatXpay WechatXpayConfig
Query QueryConfig Query QueryConfig
AdminConfig AdminConfig AdminConfig AdminConfig
TaxConfig TaxConfig TaxConfig TaxConfig
Promotion PromotionConfig // 推广链接配置 Promotion PromotionConfig // 推广链接配置
Tianxingjuhe TianxingjuheConfig // 天行聚合API配置
} }
// JwtAuth 用于 JWT 鉴权配置 // JwtAuth 用于 JWT 鉴权配置
@@ -97,6 +99,16 @@ type WechatMiniConfig struct {
AppID string AppID string
AppSecret string AppSecret string
} }
// WechatXpayConfig 微信小程序虚拟支付xpay
type WechatXpayConfig struct {
Enabled bool
Env int // 0 现网 / 1 沙箱
OfferId string
AppKey string // 与 Env 配套的 AppKey沙箱/现网各自在 yaml 中配置)
MessagePushToken string
SessionKeyTTL int64
}
type QueryConfig struct { type QueryConfig struct {
ShareLinkExpire int64 ShareLinkExpire int64
} }
@@ -126,3 +138,10 @@ type PromotionConfig struct {
PromotionDomain string // 推广域名(用于生成短链) PromotionDomain string // 推广域名(用于生成短链)
OfficialDomain string // 正式站点域名(短链重定向的目标域名) OfficialDomain string // 正式站点域名(短链重定向的目标域名)
} }
// TianxingjuheConfig 天行聚合API配置
type TianxingjuheConfig struct {
URL string // API基础URL
Key string // API密钥
Timeout int // 超时时间默认30秒
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
package admin_dashboard
import (
"net/http"
"qnc-server/app/main/api/internal/logic/admin_dashboard"
"qnc-server/app/main/api/internal/svc"
"qnc-server/common/result"
)
func AdminGetDashboardStatisticsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := admin_dashboard.NewAdminGetDashboardStatisticsLogic(r.Context(), svcCtx)
resp, err := l.AdminGetDashboardStatistics()
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,30 @@
package admin_order
import (
"net/http"
"qnc-server/app/main/api/internal/logic/admin_order"
"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 AdminXpayDeliverHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminXpayDeliverReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_order.NewAdminXpayDeliverLogic(r.Context(), svcCtx)
resp, err := l.AdminXpayDeliver(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,30 @@
package admin_query
import (
"net/http"
"qnc-server/app/main/api/internal/logic/admin_query"
"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 AdminGenerateQueryShareLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminGenerateQueryShareLinkReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_query.NewAdminGenerateQueryShareLinkLogic(r.Context(), svcCtx)
resp, err := l.AdminGenerateQueryShareLink(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,29 @@
package app
import (
"net/http"
"qnc-server/app/main/api/internal/logic/app"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetHomeDynamicDataHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetHomeDynamicDataReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := app.NewGetHomeDynamicDataLogic(r.Context(), svcCtx)
resp, err := l.GetHomeDynamicData(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,30 @@
package pay
import (
"net/http"
"qnc-server/app/main/api/internal/logic/pay"
"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 XpayClientEventHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.XpayClientEventReq
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 := pay.NewXpayClientEventLogic(r.Context(), svcCtx)
resp, err := l.XpayClientEvent(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,22 @@
package pay
import (
"net/http"
"qnc-server/app/main/api/internal/logic/pay"
"qnc-server/app/main/api/internal/svc"
)
func XpayPushHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
l := pay.NewXpayPushLogic(svcCtx)
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
l.HandleGET(w, r)
case http.MethodPost:
l.HandlePOST(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
}

View File

@@ -7,6 +7,7 @@ import (
admin_agent "qnc-server/app/main/api/internal/handler/admin_agent" admin_agent "qnc-server/app/main/api/internal/handler/admin_agent"
admin_api "qnc-server/app/main/api/internal/handler/admin_api" admin_api "qnc-server/app/main/api/internal/handler/admin_api"
admin_auth "qnc-server/app/main/api/internal/handler/admin_auth" admin_auth "qnc-server/app/main/api/internal/handler/admin_auth"
admin_dashboard "qnc-server/app/main/api/internal/handler/admin_dashboard"
admin_feature "qnc-server/app/main/api/internal/handler/admin_feature" admin_feature "qnc-server/app/main/api/internal/handler/admin_feature"
admin_menu "qnc-server/app/main/api/internal/handler/admin_menu" admin_menu "qnc-server/app/main/api/internal/handler/admin_menu"
admin_notification "qnc-server/app/main/api/internal/handler/admin_notification" admin_notification "qnc-server/app/main/api/internal/handler/admin_notification"
@@ -26,6 +27,8 @@ import (
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"
query "qnc-server/app/main/api/internal/handler/query" query "qnc-server/app/main/api/internal/handler/query"
toolbox "qnc-server/app/main/api/internal/handler/toolbox"
upload "qnc-server/app/main/api/internal/handler/upload"
user "qnc-server/app/main/api/internal/handler/user" user "qnc-server/app/main/api/internal/handler/user"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
@@ -82,6 +85,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/order/list", Path: "/order/list",
Handler: admin_agent.AdminGetAgentOrderListHandler(serverCtx), Handler: admin_agent.AdminGetAgentOrderListHandler(serverCtx),
}, },
{
Method: http.MethodGet,
Path: "/order/settlement",
Handler: admin_agent.AdminGetAgentOrderSettlementHandler(serverCtx),
},
{ {
Method: http.MethodGet, Method: http.MethodGet,
Path: "/product_config/list", Path: "/product_config/list",
@@ -102,6 +110,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/rebate/list", Path: "/rebate/list",
Handler: admin_agent.AdminGetAgentRebateListHandler(serverCtx), Handler: admin_agent.AdminGetAgentRebateListHandler(serverCtx),
}, },
{
Method: http.MethodGet,
Path: "/team/tree",
Handler: admin_agent.AdminGetAgentTeamTreeHandler(serverCtx),
},
{ {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/upgrade/agent", Path: "/upgrade/agent",
@@ -178,6 +191,20 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1/admin/auth"), rest.WithPrefix("/api/v1/admin/auth"),
) )
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AdminAuthInterceptor},
[]rest.Route{
{
Method: http.MethodGet,
Path: "/statistics",
Handler: admin_dashboard.AdminGetDashboardStatisticsHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/admin/dashboard"),
)
server.AddRoutes( server.AddRoutes(
rest.WithMiddlewares( rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AdminAuthInterceptor}, []rest.Middleware{serverCtx.AdminAuthInterceptor},
@@ -341,6 +368,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/retry-agent-process/:id", Path: "/retry-agent-process/:id",
Handler: admin_order.AdminRetryAgentProcessHandler(serverCtx), Handler: admin_order.AdminRetryAgentProcessHandler(serverCtx),
}, },
{
// xpay补发货查微信单并到账
Method: http.MethodPost,
Path: "/xpay-deliver/:id",
Handler: admin_order.AdminXpayDeliverHandler(serverCtx),
},
{ {
// 更新订单 // 更新订单
Method: http.MethodPut, Method: http.MethodPut,
@@ -464,6 +497,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/detail/:order_id", Path: "/detail/:order_id",
Handler: admin_query.AdminGetQueryDetailByOrderIdHandler(serverCtx), Handler: admin_query.AdminGetQueryDetailByOrderIdHandler(serverCtx),
}, },
{
// 生成报告分享链接(供后台预览 H5
Method: http.MethodPost,
Path: "/generate_share_link",
Handler: admin_query.AdminGenerateQueryShareLinkHandler(serverCtx),
},
}..., }...,
), ),
rest.WithPrefix("/api/v1/admin/query"), rest.WithPrefix("/api/v1/admin/query"),
@@ -815,6 +854,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/app/version", Path: "/app/version",
Handler: app.GetAppVersionHandler(serverCtx), Handler: app.GetAppVersionHandler(serverCtx),
}, },
{
Method: http.MethodGet,
Path: "/app/home/dynamic",
Handler: app.GetHomeDynamicDataHandler(serverCtx),
},
{ {
// 心跳检测接口 // 心跳检测接口
Method: http.MethodGet, Method: http.MethodGet,
@@ -899,6 +943,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/pay/wechat/refund_callback", Path: "/pay/wechat/refund_callback",
Handler: pay.WechatPayRefundCallbackHandler(serverCtx), Handler: pay.WechatPayRefundCallbackHandler(serverCtx),
}, },
{
Method: http.MethodGet,
Path: "/pay/xpay/push",
Handler: pay.XpayPushHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/pay/xpay/push",
Handler: pay.XpayPushHandler(serverCtx),
},
}, },
rest.WithPrefix("/api/v1"), rest.WithPrefix("/api/v1"),
) )
@@ -922,6 +976,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/pay/payment", Path: "/pay/payment",
Handler: pay.PaymentHandler(serverCtx), Handler: pay.PaymentHandler(serverCtx),
}, },
{
Method: http.MethodPost,
Path: "/pay/xpay/client-event",
Handler: pay.XpayClientEventHandler(serverCtx),
},
}..., }...,
), ),
rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret),
@@ -1066,6 +1125,46 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"), rest.WithPrefix("/api/v1"),
) )
server.AddRoutes(
[]rest.Route{
{
// 工具箱统一查询
Method: http.MethodPost,
Path: "/toolbox/query",
Handler: toolbox.ToolboxQueryHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// 访问已上传图片
Method: http.MethodGet,
Path: "/upload/file/:name",
Handler: upload.ServeUploadHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthInterceptor},
[]rest.Route{
{
// 图片 Base64 上传
Method: http.MethodPost,
Path: "/upload/image",
Handler: upload.UploadImageHandler(serverCtx),
},
}...,
),
rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret),
rest.WithPrefix("/api/v1"),
)
server.AddRoutes( server.AddRoutes(
[]rest.Route{ []rest.Route{
{ {

View File

@@ -0,0 +1,25 @@
package toolbox
import (
"net/http"
"qnc-server/app/main/api/internal/logic/toolbox"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ToolboxQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ToolboxQueryReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
l := toolbox.NewToolboxQueryLogic(r.Context(), svcCtx)
resp, err := l.ToolboxQuery(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,26 @@
package upload
import (
"net/http"
uploadlogic "qnc-server/app/main/api/internal/logic/upload"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ServeUploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ServeUploadFileReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
l := uploadlogic.NewServeUploadLogic(r.Context(), svcCtx)
if err := l.ServeUpload(req.Name, w); err != nil {
result.HttpResult(r, w, nil, err)
}
}
}

View File

@@ -0,0 +1,25 @@
package upload
import (
"net/http"
uploadlogic "qnc-server/app/main/api/internal/logic/upload"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UploadImageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UploadImageReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
l := uploadlogic.NewUploadImageLogic(r.Context(), svcCtx)
resp, err := l.UploadImage(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -2,6 +2,7 @@ package admin_agent
import ( import (
"context" "context"
"strconv"
"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"
@@ -27,20 +28,46 @@ func NewAdminGetAgentCommissionListLogic(ctx context.Context, svcCtx *svc.Servic
func (l *AdminGetAgentCommissionListLogic) AdminGetAgentCommissionList(req *types.AdminGetAgentCommissionListReq) (resp *types.AdminGetAgentCommissionListResp, err error) { func (l *AdminGetAgentCommissionListLogic) AdminGetAgentCommissionList(req *types.AdminGetAgentCommissionListReq) (resp *types.AdminGetAgentCommissionListResp, err error) {
builder := l.svcCtx.AgentCommissionModel.SelectBuilder() builder := l.svcCtx.AgentCommissionModel.SelectBuilder()
// 按 agent_code 筛选:先查出 agent_id
if req.AgentCode != nil && *req.AgentCode != "" {
agentCodeInt, parseErr := strconv.ParseInt(*req.AgentCode, 10, 64)
if parseErr == nil {
agent, findErr := l.svcCtx.AgentModel.FindOneByAgentCode(l.ctx, agentCodeInt)
if findErr == nil && agent != nil {
builder = builder.Where(squirrel.Eq{"agent_id": agent.Id})
} else {
// 找不到对应代理,返回空列表
return &types.AdminGetAgentCommissionListResp{Total: 0, Items: []types.AgentCommissionListItem{}}, nil
}
} else {
return &types.AdminGetAgentCommissionListResp{Total: 0, Items: []types.AgentCommissionListItem{}}, nil
}
}
// 按 order_no 筛选:先查出 order_id
if req.OrderNo != nil && *req.OrderNo != "" {
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *req.OrderNo)
if findErr == nil && order != nil {
builder = builder.Where(squirrel.Eq{"order_id": order.Id})
} else {
return &types.AdminGetAgentCommissionListResp{Total: 0, Items: []types.AgentCommissionListItem{}}, nil
}
}
if req.AgentId != nil { if req.AgentId != nil {
builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId})
} }
if req.Status != nil { if req.Status != nil {
builder = builder.Where(squirrel.Eq{"status": *req.Status}) builder = builder.Where(squirrel.Eq{"status": *req.Status})
} }
// 产品名称筛选功能已移除如需可按product_id筛选
list, total, err := l.svcCtx.AgentCommissionModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") list, total, err := l.svcCtx.AgentCommissionModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 批量查product_name // 批量查 product_name
productIds := make(map[string]struct{}) productIds := make(map[string]struct{})
for _, v := range list { for _, v := range list {
productIds[v.ProductId] = struct{}{} productIds[v.ProductId] = struct{}{}
@@ -58,11 +85,38 @@ func (l *AdminGetAgentCommissionListLogic) AdminGetAgentCommissionList(req *type
} }
} }
// 批量查 agent_code
agentIds := make([]string, 0, len(list))
for _, v := range list {
if v.AgentId != "" {
agentIds = append(agentIds, v.AgentId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
// 批量查 order_no
orderIds := make([]string, 0, len(list))
for _, v := range list {
if v.OrderId != "" {
orderIds = append(orderIds, v.OrderId)
}
}
orderNoMap := make(map[string]string)
if len(orderIds) > 0 {
orderBuilder := l.svcCtx.OrderModel.SelectBuilder().Where(squirrel.Eq{"id": orderIds})
orders, _ := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "")
for _, o := range orders {
orderNoMap[o.Id] = o.OrderNo
}
}
items := make([]types.AgentCommissionListItem, 0, len(list)) items := make([]types.AgentCommissionListItem, 0, len(list))
for _, v := range list { for _, v := range list {
item := types.AgentCommissionListItem{} item := types.AgentCommissionListItem{}
_ = copier.Copy(&item, v) _ = copier.Copy(&item, v)
item.AgentCode = agentCodeMap[v.AgentId]
item.ProductName = productNameMap[v.ProductId] item.ProductName = productNameMap[v.ProductId]
item.OrderNo = orderNoMap[v.OrderId]
item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05")
items = append(items, item) items = append(items, item)
} }

View File

@@ -2,9 +2,12 @@ package admin_agent
import ( import (
"context" "context"
"fmt"
"strings"
"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/globalkey"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
@@ -29,6 +32,16 @@ func (l *AdminGetAgentLinkListLogic) AdminGetAgentLinkList(req *types.AdminGetAg
if req.AgentId != nil { if req.AgentId != nil {
builder = builder.Where("agent_id = ?", *req.AgentId) builder = builder.Where("agent_id = ?", *req.AgentId)
} }
if req.AgentCode != nil && strings.TrimSpace(*req.AgentCode) != "" {
var agentCode int64
if _, err := fmt.Sscanf(strings.TrimSpace(*req.AgentCode), "%d", &agentCode); err == nil {
builder = builder.Where(
"agent_id IN (SELECT id FROM agent WHERE agent_code = ? AND del_state = ?)",
agentCode,
globalkey.DelStateNo,
)
}
}
if req.LinkIdentifier != nil && *req.LinkIdentifier != "" { if req.LinkIdentifier != nil && *req.LinkIdentifier != "" {
builder = builder.Where("link_identifier = ?", *req.LinkIdentifier) builder = builder.Where("link_identifier = ?", *req.LinkIdentifier)
} }
@@ -60,11 +73,20 @@ func (l *AdminGetAgentLinkListLogic) AdminGetAgentLinkList(req *types.AdminGetAg
} }
} }
agentIds := make([]string, 0, len(links))
for _, link := range links {
if link.AgentId != "" {
agentIds = append(agentIds, link.AgentId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
items := make([]types.AgentLinkListItem, 0, len(links)) items := make([]types.AgentLinkListItem, 0, len(links))
for _, link := range links { for _, link := range links {
items = append(items, types.AgentLinkListItem{ items = append(items, types.AgentLinkListItem{
Id: link.Id, Id: link.Id,
AgentId: link.AgentId, AgentId: link.AgentId,
AgentCode: agentCodeMap[link.AgentId],
ProductId: link.ProductId, ProductId: link.ProductId,
ProductName: productNameMap[link.ProductId], ProductName: productNameMap[link.ProductId],
SetPrice: link.SetPrice, SetPrice: link.SetPrice,

View File

@@ -2,9 +2,13 @@ package admin_agent
import ( import (
"context" "context"
"encoding/hex"
"fmt"
"strings"
"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/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
@@ -30,9 +34,15 @@ func NewAdminGetAgentListLogic(ctx context.Context, svcCtx *svc.ServiceContext)
func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListReq) (resp *types.AdminGetAgentListResp, err error) { func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListReq) (resp *types.AdminGetAgentListResp, err error) {
builder := l.svcCtx.AgentModel.SelectBuilder() builder := l.svcCtx.AgentModel.SelectBuilder()
// 如果传入TeamLeaderId则查找该团队首领下的所有代理 exactByAgentId := req.AgentId != nil && *req.AgentId != ""
if req.TeamLeaderId != nil { if exactByAgentId {
builder = builder.Where(squirrel.Eq{"team_leader_id": *req.TeamLeaderId}) builder = builder.Where(squirrel.Eq{"id": *req.AgentId})
} else {
// 如果传入TeamLeaderId则查找该团队首领下的直属代理不含首领本人钻石等业务会把 team_leader_id 写成自己)
if req.TeamLeaderId != nil && *req.TeamLeaderId != "" {
tl := *req.TeamLeaderId
builder = builder.Where(squirrel.Eq{"team_leader_id": tl})
builder = builder.Where(squirrel.NotEq{"id": tl})
} }
if req.Level != nil { if req.Level != nil {
builder = builder.Where(squirrel.Eq{"level": *req.Level}) builder = builder.Where(squirrel.Eq{"level": *req.Level})
@@ -51,12 +61,43 @@ func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListR
// 这里简化处理直接使用等值查询如果数据库中有NULL值需要特殊处理 // 这里简化处理直接使用等值查询如果数据库中有NULL值需要特殊处理
builder = builder.Where(squirrel.Eq{"region": *req.Region}) builder = builder.Where(squirrel.Eq{"region": *req.Region})
} }
if req.AgentCode != nil && *req.AgentCode != "" {
// 将字符串转换为int64
var agentCode int64
if _, err := fmt.Sscanf(*req.AgentCode, "%d", &agentCode); err == nil {
builder = builder.Where(squirrel.Eq{"agent_code": agentCode})
}
}
}
if req.RealName != nil && strings.TrimSpace(*req.RealName) != "" {
name := strings.TrimSpace(*req.RealName)
likePattern := "%" + name + "%"
builder = builder.Where(`EXISTS (
SELECT 1 FROM agent_real_name arn
WHERE arn.agent_id = agent.id
AND arn.del_state = ?
AND arn.verify_time IS NOT NULL
AND arn.name LIKE ?
)`, globalkey.DelStateNo, likePattern)
}
agents, total, err := l.svcCtx.AgentModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "id DESC") agents, total, err := l.svcCtx.AgentModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "id DESC")
if err != nil { if err != nil {
return nil, err return nil, err
} }
leaderIds := make([]string, 0, len(agents))
for _, a := range agents {
if a.Id != "" {
leaderIds = append(leaderIds, a.Id)
}
}
subCountByLeader, err := l.svcCtx.AgentModel.CountSubordinatesByTeamLeaderIds(l.ctx, leaderIds)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "统计直属下级数量失败: %v", err)
}
items := make([]types.AgentListItem, 0, len(agents)) items := make([]types.AgentListItem, 0, len(agents))
for _, agent := range agents { for _, agent := range agents {
@@ -79,12 +120,26 @@ func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListR
// 查询钱包信息 // 查询钱包信息
wallet, _ := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id) wallet, _ := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
// 查询实名认证信息 // 查询实名认证信息(数据库姓名明文、身份证密文,解密后明文返回不脱敏)
realNameInfo, _ := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id) realNameInfo, _ := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id)
isRealName := false isRealName := false
if realNameInfo != nil && realNameInfo.VerifyTime.Valid { realName := ""
idCardPlain := "" // 解密后明文返回
if realNameInfo != nil {
if realNameInfo.VerifyTime.Valid {
isRealName = true // verify_time不为空表示已通过三要素核验 isRealName = true // verify_time不为空表示已通过三要素核验
} }
realName = realNameInfo.Name
if realNameInfo.IdCard != "" {
key, keyErr := hex.DecodeString(l.svcCtx.Config.Encrypt.SecretKey)
if keyErr == nil {
decrypted, err := crypto.DecryptIDCard(realNameInfo.IdCard, key)
if err == nil {
idCardPlain = decrypted
}
}
}
}
wechatId := "" wechatId := ""
if agent.WechatId.Valid { if agent.WechatId.Valid {
@@ -108,8 +163,11 @@ func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListR
LevelName: levelName, LevelName: levelName,
Region: region, Region: region,
Mobile: agent.Mobile, Mobile: agent.Mobile,
RealName: realName,
IdCard: idCardPlain,
WechatId: wechatId, WechatId: wechatId,
TeamLeaderId: teamLeaderId, TeamLeaderId: teamLeaderId,
SubordinateCount: subCountByLeader[agent.Id],
AgentCode: agent.AgentCode, AgentCode: agent.AgentCode,
Balance: 0, Balance: 0,
TotalEarnings: 0, TotalEarnings: 0,

View File

@@ -2,6 +2,8 @@ package admin_agent
import ( import (
"context" "context"
"strconv"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
@@ -28,21 +30,57 @@ func NewAdminGetAgentOrderListLogic(ctx context.Context, svcCtx *svc.ServiceCont
} }
} }
func emptyAgentOrderListResp() *types.AdminGetAgentOrderListResp {
return &types.AdminGetAgentOrderListResp{
Total: 0,
Items: []types.AgentOrderListItem{},
}
}
func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGetAgentOrderListReq) (resp *types.AdminGetAgentOrderListResp, err error) { func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGetAgentOrderListReq) (resp *types.AdminGetAgentOrderListResp, err error) {
builder := l.svcCtx.AgentOrderModel.SelectBuilder(). builder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo) Where("del_state = ?", globalkey.DelStateNo)
if req.AgentId != nil { if req.AgentId != nil && *req.AgentId != "" {
builder = builder.Where("agent_id = ?", *req.AgentId) builder = builder.Where("agent_id = ?", *req.AgentId)
} }
if req.OrderId != nil {
if req.AgentCode != nil && *req.AgentCode != "" {
code, parseErr := strconv.ParseInt(*req.AgentCode, 10, 64)
if parseErr != nil {
return emptyAgentOrderListResp(), nil
}
agent, findErr := l.svcCtx.AgentModel.FindOneByAgentCode(l.ctx, code)
if findErr != nil || agent == nil {
return emptyAgentOrderListResp(), nil
}
builder = builder.Where("agent_id = ?", agent.Id)
}
if req.OrderId != nil && *req.OrderId != "" {
builder = builder.Where("order_id = ?", *req.OrderId) builder = builder.Where("order_id = ?", *req.OrderId)
} }
if req.OrderNo != nil && *req.OrderNo != "" {
o, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *req.OrderNo)
if findErr != nil || o == nil {
return emptyAgentOrderListResp(), nil
}
builder = builder.Where("order_id = ?", o.Id)
}
if req.ProcessStatus != nil { if req.ProcessStatus != nil {
builder = builder.Where("process_status = ?", *req.ProcessStatus) builder = builder.Where("process_status = ?", *req.ProcessStatus)
} }
// 分页查询 if req.OrderStatus != nil && *req.OrderStatus != "" {
builder = builder.Where(squirrel.Expr(
"order_id IN (SELECT id FROM `order` WHERE status = ? AND del_state = ?)",
*req.OrderStatus,
globalkey.DelStateNo,
))
}
page := req.Page page := req.Page
if page <= 0 { if page <= 0 {
page = 1 page = 1
@@ -57,7 +95,6 @@ func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGet
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理订单列表失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理订单列表失败, %v", err)
} }
// 批量查询产品名称
productIdSet := make(map[string]struct{}) productIdSet := make(map[string]struct{})
for _, order := range orders { for _, order := range orders {
productIdSet[order.ProductId] = struct{}{} productIdSet[order.ProductId] = struct{}{}
@@ -74,13 +111,50 @@ func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGet
} }
} }
// 组装响应 agentIds := make([]string, 0, len(orders))
orderIds := make([]string, 0, len(orders))
for _, order := range orders {
if order.AgentId != "" {
agentIds = append(agentIds, order.AgentId)
}
if order.OrderId != "" {
orderIds = append(orderIds, order.OrderId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
orderNoMap := make(map[string]string)
orderStatusMap := make(map[string]string)
if len(orderIds) > 0 {
uniq := make([]string, 0, len(orderIds))
seen := make(map[string]struct{}, len(orderIds))
for _, id := range orderIds {
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
uniq = append(uniq, id)
}
if len(uniq) > 0 {
os, _ := l.svcCtx.OrderModel.FindAll(l.ctx, l.svcCtx.OrderModel.SelectBuilder().Where(squirrel.Eq{"id": uniq}), "")
for _, o := range os {
orderNoMap[o.Id] = o.OrderNo
orderStatusMap[o.Id] = o.Status
}
}
}
items := make([]types.AgentOrderListItem, 0, len(orders)) items := make([]types.AgentOrderListItem, 0, len(orders))
for _, order := range orders { for _, order := range orders {
items = append(items, types.AgentOrderListItem{ items = append(items, types.AgentOrderListItem{
Id: order.Id, Id: order.Id,
AgentId: order.AgentId, AgentId: order.AgentId,
AgentCode: agentCodeMap[order.AgentId],
OrderId: order.OrderId, OrderId: order.OrderId,
OrderNo: orderNoMap[order.OrderId],
ProductId: order.ProductId, ProductId: order.ProductId,
ProductName: productNameMap[order.ProductId], ProductName: productNameMap[order.ProductId],
OrderAmount: order.OrderAmount, OrderAmount: order.OrderAmount,
@@ -89,6 +163,7 @@ func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGet
PriceCost: order.PriceCost, PriceCost: order.PriceCost,
AgentProfit: order.AgentProfit, AgentProfit: order.AgentProfit,
ProcessStatus: order.ProcessStatus, ProcessStatus: order.ProcessStatus,
OrderStatus: orderStatusMap[order.OrderId],
CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"),
}) })
} }

View File

@@ -0,0 +1,98 @@
package admin_agent
import (
"context"
"sync"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type AdminGetAgentOrderSettlementLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminGetAgentOrderSettlementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentOrderSettlementLogic {
return &AdminGetAgentOrderSettlementLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AdminGetAgentOrderSettlementLogic) AdminGetAgentOrderSettlement(req *types.AdminGetAgentOrderSettlementReq) (resp *types.AdminGetAgentOrderSettlementResp, err error) {
empty := &types.AdminGetAgentOrderSettlementResp{
Commissions: []types.AgentCommissionListItem{},
Rebates: []types.AgentRebateListItem{},
}
if req.OrderNo == "" {
return empty, nil
}
on := req.OrderNo
page := int64(1)
pageSize := int64(200)
var wg sync.WaitGroup
var mu sync.Mutex
var cResp *types.AdminGetAgentCommissionListResp
var cErr error
var rResp *types.AdminGetAgentRebateListResp
var rErr error
wg.Add(2)
go func() {
defer wg.Done()
resp, e := NewAdminGetAgentCommissionListLogic(l.ctx, l.svcCtx).AdminGetAgentCommissionList(
&types.AdminGetAgentCommissionListReq{
Page: page,
PageSize: pageSize,
OrderNo: &on,
},
)
mu.Lock()
cResp, cErr = resp, e
mu.Unlock()
}()
go func() {
defer wg.Done()
resp, e := NewAdminGetAgentRebateListLogic(l.ctx, l.svcCtx).AdminGetAgentRebateList(
&types.AdminGetAgentRebateListReq{
Page: page,
PageSize: pageSize,
OrderNo: &on,
},
)
mu.Lock()
rResp, rErr = resp, e
mu.Unlock()
}()
wg.Wait()
if cErr != nil {
return nil, cErr
}
if rErr != nil {
return nil, rErr
}
if cResp == nil {
cResp = &types.AdminGetAgentCommissionListResp{Items: []types.AgentCommissionListItem{}}
}
if rResp == nil {
rResp = &types.AdminGetAgentRebateListResp{Items: []types.AgentRebateListItem{}}
}
commissions := cResp.Items
if commissions == nil {
commissions = []types.AgentCommissionListItem{}
}
rebates := rResp.Items
if rebates == nil {
rebates = []types.AgentRebateListItem{}
}
return &types.AdminGetAgentOrderSettlementResp{
Commissions: commissions,
Rebates: rebates,
}, nil
}

View File

@@ -2,15 +2,15 @@ package admin_agent
import ( import (
"context" "context"
"encoding/hex"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"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"
"github.com/pkg/errors" "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" "github.com/zeromicro/go-zero/core/logx"
) )
@@ -60,6 +60,14 @@ func (l *AdminGetAgentRealNameListLogic) AdminGetAgentRealNameList(req *types.Ad
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询实名认证列表失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询实名认证列表失败, %v", err)
} }
agentIds := make([]string, 0, len(realNames))
for _, realName := range realNames {
if realName.AgentId != "" {
agentIds = append(agentIds, realName.AgentId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
// 组装响应 // 组装响应
items := make([]types.AgentRealNameListItem, 0, len(realNames)) items := make([]types.AgentRealNameListItem, 0, len(realNames))
for _, realName := range realNames { for _, realName := range realNames {
@@ -72,10 +80,12 @@ func (l *AdminGetAgentRealNameListLogic) AdminGetAgentRealNameList(req *types.Ad
} }
} }
// 解密身份证号(仅显示部分) // 解密身份证号(仅显示部分密钥与实名认证写入时一致hex 解码后使用
idCard := "" idCard := ""
if realName.IdCard != "" { if realName.IdCard != "" {
decrypted, err := crypto.DecryptIDCard(realName.IdCard, []byte(l.svcCtx.Config.Encrypt.SecretKey)) key, keyErr := hex.DecodeString(l.svcCtx.Config.Encrypt.SecretKey)
if keyErr == nil {
decrypted, err := crypto.DecryptIDCard(realName.IdCard, key)
if err == nil { if err == nil {
// 脱敏显示 // 脱敏显示
if len(decrypted) > 10 { if len(decrypted) > 10 {
@@ -85,6 +95,7 @@ func (l *AdminGetAgentRealNameListLogic) AdminGetAgentRealNameList(req *types.Ad
} }
} }
} }
}
// 根据verify_time判断状态NULL=未验证(1)不为NULL=已通过(2) // 根据verify_time判断状态NULL=未验证(1)不为NULL=已通过(2)
statusInt := int64(1) // 默认未验证 statusInt := int64(1) // 默认未验证
@@ -97,6 +108,7 @@ func (l *AdminGetAgentRealNameListLogic) AdminGetAgentRealNameList(req *types.Ad
item := types.AgentRealNameListItem{ item := types.AgentRealNameListItem{
Id: realName.Id, Id: realName.Id,
AgentId: realName.AgentId, AgentId: realName.AgentId,
AgentCode: agentCodeMap[realName.AgentId],
Name: realName.Name, Name: realName.Name,
IdCard: idCard, IdCard: idCard,
Mobile: mobile, Mobile: mobile,

View File

@@ -2,6 +2,8 @@ package admin_agent
import ( import (
"context" "context"
"strconv"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
@@ -35,9 +37,47 @@ func (l *AdminGetAgentRebateListLogic) AdminGetAgentRebateList(req *types.AdminG
if req.AgentId != nil { if req.AgentId != nil {
builder = builder.Where("agent_id = ?", *req.AgentId) builder = builder.Where("agent_id = ?", *req.AgentId)
} }
if req.SourceAgentId != nil {
builder = builder.Where("source_agent_id = ?", *req.SourceAgentId) // 按 agent_code 筛选:先查出 agent_id
if req.AgentCode != nil && *req.AgentCode != "" {
agentCodeInt, parseErr := strconv.ParseInt(*req.AgentCode, 10, 64)
if parseErr == nil {
agent, findErr := l.svcCtx.AgentModel.FindOneByAgentCode(l.ctx, agentCodeInt)
if findErr == nil && agent != nil {
builder = builder.Where("agent_id = ?", agent.Id)
} else {
return &types.AdminGetAgentRebateListResp{Total: 0, Items: []types.AgentRebateListItem{}}, nil
} }
} else {
return &types.AdminGetAgentRebateListResp{Total: 0, Items: []types.AgentRebateListItem{}}, nil
}
}
// 按 source_agent_code 筛选:先查出 source_agent_id
if req.SourceAgentCode != nil && *req.SourceAgentCode != "" {
sourceAgentCodeInt, parseErr := strconv.ParseInt(*req.SourceAgentCode, 10, 64)
if parseErr == nil {
agent, findErr := l.svcCtx.AgentModel.FindOneByAgentCode(l.ctx, sourceAgentCodeInt)
if findErr == nil && agent != nil {
builder = builder.Where("source_agent_id = ?", agent.Id)
} else {
return &types.AdminGetAgentRebateListResp{Total: 0, Items: []types.AgentRebateListItem{}}, nil
}
} else {
return &types.AdminGetAgentRebateListResp{Total: 0, Items: []types.AgentRebateListItem{}}, nil
}
}
// 按 order_no 筛选:先查出 order_id
if req.OrderNo != nil && *req.OrderNo != "" {
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *req.OrderNo)
if findErr == nil && order != nil {
builder = builder.Where("order_id = ?", order.Id)
} else {
return &types.AdminGetAgentRebateListResp{Total: 0, Items: []types.AgentRebateListItem{}}, nil
}
}
if req.RebateType != nil { if req.RebateType != nil {
builder = builder.Where("rebate_type = ?", *req.RebateType) builder = builder.Where("rebate_type = ?", *req.RebateType)
} }
@@ -74,16 +114,47 @@ func (l *AdminGetAgentRebateListLogic) AdminGetAgentRebateList(req *types.AdminG
} }
} }
agentIds := make([]string, 0, len(rebates)*2)
for _, rebate := range rebates {
if rebate.AgentId != "" {
agentIds = append(agentIds, rebate.AgentId)
}
if rebate.SourceAgentId != "" {
agentIds = append(agentIds, rebate.SourceAgentId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
// 批量查 order_no
orderIds := make([]string, 0, len(rebates))
for _, rebate := range rebates {
if rebate.OrderId != "" {
orderIds = append(orderIds, rebate.OrderId)
}
}
orderNoMap := make(map[string]string)
if len(orderIds) > 0 {
orderBuilder := l.svcCtx.OrderModel.SelectBuilder().Where(squirrel.Eq{"id": orderIds})
orders, _ := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "")
for _, o := range orders {
orderNoMap[o.Id] = o.OrderNo
}
}
// 组装响应 // 组装响应
items := make([]types.AgentRebateListItem, 0, len(rebates)) items := make([]types.AgentRebateListItem, 0, len(rebates))
for _, rebate := range rebates { for _, rebate := range rebates {
items = append(items, types.AgentRebateListItem{ items = append(items, types.AgentRebateListItem{
Id: rebate.Id, Id: rebate.Id,
AgentId: rebate.AgentId, AgentId: rebate.AgentId,
AgentCode: agentCodeMap[rebate.AgentId],
SourceAgentId: rebate.SourceAgentId, SourceAgentId: rebate.SourceAgentId,
SourceAgentCode: agentCodeMap[rebate.SourceAgentId],
OrderId: rebate.OrderId, OrderId: rebate.OrderId,
OrderNo: orderNoMap[rebate.OrderId],
RebateType: rebate.RebateType, RebateType: rebate.RebateType,
Amount: rebate.RebateAmount, Amount: rebate.RebateAmount,
Status: rebate.Status,
CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"),
}) })
} }

View File

@@ -0,0 +1,207 @@
package admin_agent
import (
"context"
"sort"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type AdminGetAgentTeamTreeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminGetAgentTeamTreeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentTeamTreeLogic {
return &AdminGetAgentTeamTreeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func resolveDiamondTeamRootId(agent *model.Agent) string {
if agent.TeamLeaderId.Valid && agent.TeamLeaderId.String != "" && agent.TeamLeaderId.String != agent.Id {
return agent.TeamLeaderId.String
}
return agent.Id
}
func levelName(lv int64) string {
switch lv {
case 1:
return "普通"
case 2:
return "黄金"
case 3:
return "钻石"
default:
return ""
}
}
func (l *AdminGetAgentTeamTreeLogic) AdminGetAgentTeamTree(req *types.AdminGetAgentTeamTreeReq) (resp *types.AdminGetAgentTeamTreeResp, err error) {
if req.AgentId == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("agent_id 不能为空"), "")
}
anchor, err := l.svcCtx.AgentModel.FindOne(l.ctx, req.AgentId)
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)
}
teamRootId := resolveDiamondTeamRootId(anchor)
builder := l.svcCtx.AgentModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo).
Where(squirrel.Or{
squirrel.Eq{"id": teamRootId},
squirrel.And{
squirrel.Eq{"team_leader_id": teamRootId},
squirrel.NotEq{"id": teamRootId},
},
})
members, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败: %v", err)
}
agentById := make(map[string]*model.Agent, len(members)+1)
memberIDs := make([]string, 0, len(members)+1)
anchorInTeam := false
for _, m := range members {
agentById[m.Id] = m
memberIDs = append(memberIDs, m.Id)
if m.Id == anchor.Id {
anchorInTeam = true
}
}
if !anchorInTeam {
agentById[anchor.Id] = anchor
memberIDs = append(memberIDs, anchor.Id)
members = append(members, anchor)
}
relBuilder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("relation_type = ? AND del_state = ?", 1, globalkey.DelStateNo).
Where(squirrel.Eq{"parent_id": memberIDs}).
Where(squirrel.Eq{"child_id": memberIDs})
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, relBuilder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请关系失败: %v", err)
}
childSet := make(map[string]map[string]struct{})
addEdge := func(parentId, childId string) {
if parentId == "" || childId == "" || parentId == childId {
return
}
if _, ok := agentById[parentId]; !ok {
return
}
if _, ok := agentById[childId]; !ok {
return
}
if childSet[parentId] == nil {
childSet[parentId] = make(map[string]struct{})
}
childSet[parentId][childId] = struct{}{}
}
for _, r := range relations {
addEdge(r.ParentId, r.ChildId)
}
reachable := map[string]bool{teamRootId: true}
var walkReach func(string)
walkReach = func(id string) {
for cid := range childSet[id] {
if reachable[cid] {
continue
}
reachable[cid] = true
walkReach(cid)
}
}
walkReach(teamRootId)
for _, m := range members {
if m.Id == teamRootId {
continue
}
if !reachable[m.Id] {
addEdge(teamRootId, m.Id)
if !reachable[m.Id] {
reachable[m.Id] = true
walkReach(m.Id)
}
}
}
childrenMap := make(map[string][]string)
for pid, set := range childSet {
slice := make([]string, 0, len(set))
for cid := range set {
slice = append(slice, cid)
}
sort.Slice(slice, func(i, j int) bool {
return agentById[slice[i]].AgentCode < agentById[slice[j]].AgentCode
})
childrenMap[pid] = slice
}
var build func(string) (types.AgentTeamTreeNode, error)
build = func(agentId string) (types.AgentTeamTreeNode, error) {
ag := agentById[agentId]
if ag == nil {
return types.AgentTeamTreeNode{}, errors.New("missing agent in tree")
}
mobile := ag.Mobile
mobile, err = crypto.DecryptMobile(mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return types.AgentTeamTreeNode{}, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密手机号失败: %v", err)
}
node := types.AgentTeamTreeNode{
Id: ag.Id,
AgentCode: ag.AgentCode,
Level: ag.Level,
LevelName: levelName(ag.Level),
Mobile: mobile,
IsAnchor: ag.Id == anchor.Id,
Children: []types.AgentTeamTreeNode{},
}
kids := childrenMap[agentId]
for _, cid := range kids {
childNode, err := build(cid)
if err != nil {
return types.AgentTeamTreeNode{}, err
}
node.Children = append(node.Children, childNode)
}
return node, nil
}
root, err := build(teamRootId)
if err != nil {
return nil, err
}
return &types.AdminGetAgentTeamTreeResp{Root: root}, nil
}

View File

@@ -2,6 +2,8 @@ package admin_agent
import ( import (
"context" "context"
"strconv"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/xerr" "qnc-server/common/xerr"
@@ -31,9 +33,26 @@ func (l *AdminGetAgentUpgradeListLogic) AdminGetAgentUpgradeList(req *types.Admi
builder := l.svcCtx.AgentUpgradeModel.SelectBuilder(). builder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo) Where("del_state = ?", globalkey.DelStateNo)
if req.AgentId != nil { if req.AgentCode != nil && *req.AgentCode != "" {
agentCodeInt, parseErr := strconv.ParseInt(*req.AgentCode, 10, 64)
if parseErr != nil {
return &types.AdminGetAgentUpgradeListResp{
Total: 0,
Items: []types.AgentUpgradeListItem{},
}, nil
}
agent, findErr := l.svcCtx.AgentModel.FindOneByAgentCode(l.ctx, agentCodeInt)
if findErr != nil || agent == nil {
return &types.AdminGetAgentUpgradeListResp{
Total: 0,
Items: []types.AgentUpgradeListItem{},
}, nil
}
builder = builder.Where("agent_id = ?", agent.Id)
} else if req.AgentId != nil && *req.AgentId != "" {
builder = builder.Where("agent_id = ?", *req.AgentId) builder = builder.Where("agent_id = ?", *req.AgentId)
} }
if req.UpgradeType != nil { if req.UpgradeType != nil {
builder = builder.Where("upgrade_type = ?", *req.UpgradeType) builder = builder.Where("upgrade_type = ?", *req.UpgradeType)
} }
@@ -56,12 +75,21 @@ func (l *AdminGetAgentUpgradeListLogic) AdminGetAgentUpgradeList(req *types.Admi
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录列表失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录列表失败, %v", err)
} }
agentIds := make([]string, 0, len(upgrades))
for _, upgrade := range upgrades {
if upgrade.AgentId != "" {
agentIds = append(agentIds, upgrade.AgentId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
// 组装响应 // 组装响应
items := make([]types.AgentUpgradeListItem, 0, len(upgrades)) items := make([]types.AgentUpgradeListItem, 0, len(upgrades))
for _, upgrade := range upgrades { for _, upgrade := range upgrades {
items = append(items, types.AgentUpgradeListItem{ items = append(items, types.AgentUpgradeListItem{
Id: upgrade.Id, Id: upgrade.Id,
AgentId: upgrade.AgentId, AgentId: upgrade.AgentId,
AgentCode: agentCodeMap[upgrade.AgentId],
FromLevel: upgrade.FromLevel, FromLevel: upgrade.FromLevel,
ToLevel: upgrade.ToLevel, ToLevel: upgrade.ToLevel,
UpgradeType: upgrade.UpgradeType, UpgradeType: upgrade.UpgradeType,

View File

@@ -2,9 +2,13 @@ package admin_agent
import ( import (
"context" "context"
"fmt"
"strconv"
"strings"
"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/globalkey"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
@@ -27,23 +31,60 @@ func NewAdminGetAgentWithdrawalListLogic(ctx context.Context, svcCtx *svc.Servic
func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *types.AdminGetAgentWithdrawalListReq) (resp *types.AdminGetAgentWithdrawalListResp, err error) { func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *types.AdminGetAgentWithdrawalListReq) (resp *types.AdminGetAgentWithdrawalListResp, err error) {
builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder() builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder()
if req.AgentId != nil { if req.AgentId != nil && strings.TrimSpace(*req.AgentId) != "" {
builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) builder = builder.Where(squirrel.Eq{"agent_id": strings.TrimSpace(*req.AgentId)})
} }
if req.Status != nil { if req.AgentCode != nil && strings.TrimSpace(*req.AgentCode) != "" {
builder = builder.Where(squirrel.Eq{"status": *req.Status}) var code int64
if _, scanErr := fmt.Sscanf(strings.TrimSpace(*req.AgentCode), "%d", &code); scanErr == nil {
builder = builder.Where(
"agent_id IN (SELECT id FROM agent WHERE agent_code = ? AND del_state = ?)",
code,
globalkey.DelStateNo,
)
} }
if req.WithdrawNo != nil && *req.WithdrawNo != "" { }
builder = builder.Where(squirrel.Eq{"withdraw_no": *req.WithdrawNo}) if strings.TrimSpace(req.Status) != "" {
st, perr := strconv.ParseInt(strings.TrimSpace(req.Status), 10, 64)
if perr == nil {
builder = builder.Where(squirrel.Eq{"status": st})
}
}
if strings.TrimSpace(req.WithdrawalType) != "" {
wt, perr := strconv.ParseInt(strings.TrimSpace(req.WithdrawalType), 10, 64)
if perr == nil {
builder = builder.Where(squirrel.Eq{"withdrawal_type": wt})
}
}
if req.WithdrawNo != nil && strings.TrimSpace(*req.WithdrawNo) != "" {
pat := "%" + strings.TrimSpace(*req.WithdrawNo) + "%"
builder = builder.Where("withdraw_no LIKE ?", pat)
}
if req.PayeeAccount != nil && strings.TrimSpace(*req.PayeeAccount) != "" {
pat := "%" + strings.TrimSpace(*req.PayeeAccount) + "%"
builder = builder.Where("payee_account LIKE ?", pat)
}
if req.PayeeName != nil && strings.TrimSpace(*req.PayeeName) != "" {
pat := "%" + strings.TrimSpace(*req.PayeeName) + "%"
builder = builder.Where("payee_name LIKE ?", pat)
} }
list, total, err := l.svcCtx.AgentWithdrawalModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") list, total, err := l.svcCtx.AgentWithdrawalModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil { if err != nil {
return nil, err return nil, err
} }
agentIds := make([]string, 0, len(list))
for _, v := range list {
if v.AgentId != "" {
agentIds = append(agentIds, v.AgentId)
}
}
agentCodeMap := batchAgentCodesByIds(l.ctx, l.svcCtx, agentIds)
items := make([]types.AgentWithdrawalListItem, 0, len(list)) items := make([]types.AgentWithdrawalListItem, 0, len(list))
for _, v := range list { for _, v := range list {
item := types.AgentWithdrawalListItem{} item := types.AgentWithdrawalListItem{}
_ = copier.Copy(&item, v) _ = copier.Copy(&item, v)
item.AgentCode = agentCodeMap[v.AgentId]
item.Remark = "" item.Remark = ""
if v.Remark.Valid { if v.Remark.Valid {
item.Remark = v.Remark.String item.Remark = v.Remark.String

View File

@@ -2,6 +2,9 @@ package admin_agent
import ( import (
"context" "context"
"strconv"
"strings"
"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"
@@ -34,22 +37,18 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet
builder := l.svcCtx.AgentInviteCodeModel.SelectBuilder(). builder := l.svcCtx.AgentInviteCodeModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo) Where("del_state = ?", globalkey.DelStateNo)
if req.Code != nil && *req.Code != "" { if req.Code != nil && strings.TrimSpace(*req.Code) != "" {
builder = builder.Where("code = ?", *req.Code) pat := "%" + strings.TrimSpace(*req.Code) + "%"
} builder = builder.Where("code LIKE ?", pat)
if req.AgentId != nil && *req.AgentId != "" {
if *req.AgentId == "0" {
// agent_id = 0 表示查询平台发放的邀请码agent_id为NULL
builder = builder.Where("agent_id IS NULL")
} else {
builder = builder.Where("agent_id = ?", *req.AgentId)
}
} }
if req.TargetLevel != nil { if req.TargetLevel != nil {
builder = builder.Where("target_level = ?", *req.TargetLevel) builder = builder.Where("target_level = ?", *req.TargetLevel)
} }
if req.Status != nil { if strings.TrimSpace(req.Status) != "" {
builder = builder.Where("status = ?", *req.Status) st, perr := strconv.ParseInt(strings.TrimSpace(req.Status), 10, 64)
if perr == nil {
builder = builder.Where("status = ?", st)
}
} }
// 2. 分页查询 // 2. 分页查询
@@ -58,15 +57,19 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码列表失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码列表失败, %v", err)
} }
// 3. 批量查询代理信息(用于显示代理手机号) // 3. 批量查询代理信息(用于显示代理手机号、代理编号
agentIds := make(map[string]struct{}) agentIds := make(map[string]struct{})
for _, v := range list { for _, v := range list {
if v.AgentId.Valid && v.AgentId.String != "" { if v.AgentId.Valid && v.AgentId.String != "" {
agentIds[v.AgentId.String] = struct{}{} agentIds[v.AgentId.String] = struct{}{}
} }
if v.UsedAgentId.Valid && v.UsedAgentId.String != "" {
agentIds[v.UsedAgentId.String] = struct{}{}
}
} }
agentMobileMap := make(map[string]string) agentMobileMap := make(map[string]string)
agentCodeMap := make(map[string]int64)
if len(agentIds) > 0 { if len(agentIds) > 0 {
agentIdList := make([]string, 0, len(agentIds)) agentIdList := make([]string, 0, len(agentIds))
for id := range agentIds { for id := range agentIds {
@@ -75,6 +78,7 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet
agents, _ := l.svcCtx.AgentModel.FindAll(l.ctx, l.svcCtx.AgentModel.SelectBuilder().Where(squirrel.Eq{"id": agentIdList}), "") agents, _ := l.svcCtx.AgentModel.FindAll(l.ctx, l.svcCtx.AgentModel.SelectBuilder().Where(squirrel.Eq{"id": agentIdList}), "")
secretKey := l.svcCtx.Config.Encrypt.SecretKey secretKey := l.svcCtx.Config.Encrypt.SecretKey
for _, agent := range agents { for _, agent := range agents {
agentCodeMap[agent.Id] = agent.AgentCode
mobile, decErr := crypto.DecryptMobile(agent.Mobile, secretKey) mobile, decErr := crypto.DecryptMobile(agent.Mobile, secretKey)
if decErr == nil { if decErr == nil {
agentMobileMap[agent.Id] = mobile agentMobileMap[agent.Id] = mobile
@@ -100,6 +104,7 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet
if v.AgentId.Valid { if v.AgentId.Valid {
item.AgentId = v.AgentId.String item.AgentId = v.AgentId.String
item.AgentMobile = agentMobileMap[v.AgentId.String] item.AgentMobile = agentMobileMap[v.AgentId.String]
item.AgentCode = agentCodeMap[v.AgentId.String]
} }
if v.UsedUserId.Valid { if v.UsedUserId.Valid {
@@ -107,6 +112,7 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet
} }
if v.UsedAgentId.Valid { if v.UsedAgentId.Valid {
item.UsedAgentId = v.UsedAgentId.String item.UsedAgentId = v.UsedAgentId.String
item.UsedAgentCode = agentCodeMap[v.UsedAgentId.String]
} }
if v.UsedTime.Valid { if v.UsedTime.Valid {
item.UsedTime = v.UsedTime.Time.Format("2006-01-02 15:04:05") item.UsedTime = v.UsedTime.Time.Format("2006-01-02 15:04:05")

View File

@@ -163,14 +163,11 @@ func (l *AdminUpdateAgentConfigLogic) AdminUpdateAgentConfig(req *types.AdminUpd
if err := updateConfig("commission_freeze_threshold", &req.CommissionFreeze.Threshold); err != nil { if err := updateConfig("commission_freeze_threshold", &req.CommissionFreeze.Threshold); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结阈值失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结阈值失败, %v", err)
} }
// 更新解冻天数(整数类型)
if req.CommissionFreeze.Days > 0 {
daysFloat := float64(req.CommissionFreeze.Days) daysFloat := float64(req.CommissionFreeze.Days)
if err := updateConfig("commission_freeze_days", &daysFloat); err != nil { if err := updateConfig("commission_freeze_days", &daysFloat); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结解冻天数失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结解冻天数失败, %v", err)
} }
} }
}
// 更新税费配置 // 更新税费配置
if err := updateConfig("tax_rate", req.TaxRate); err != nil { if err := updateConfig("tax_rate", req.TaxRate); err != nil {

View File

@@ -0,0 +1,37 @@
package admin_agent
import (
"context"
"qnc-server/app/main/api/internal/svc"
"github.com/Masterminds/squirrel"
)
// batchAgentCodesByIds 按代理主键批量查询 agent_code用于列表软关联展示。
func batchAgentCodesByIds(ctx context.Context, svcCtx *svc.ServiceContext, ids []string) map[string]int64 {
out := make(map[string]int64)
uniq := make([]string, 0, len(ids))
seen := make(map[string]struct{}, len(ids))
for _, id := range ids {
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
uniq = append(uniq, id)
}
if len(uniq) == 0 {
return out
}
agents, err := svcCtx.AgentModel.FindAll(ctx, svcCtx.AgentModel.SelectBuilder().Where(squirrel.Eq{"id": uniq}), "")
if err != nil {
return out
}
for _, a := range agents {
out[a.Id] = a.AgentCode
}
return out
}

View File

@@ -0,0 +1,256 @@
package admin_dashboard
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
)
type AdminGetDashboardStatisticsLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminGetDashboardStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetDashboardStatisticsLogic {
return &AdminGetDashboardStatisticsLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AdminGetDashboardStatisticsLogic) AdminGetDashboardStatistics() (resp *types.AdminGetDashboardStatisticsResp, err error) {
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
todayEnd := todayStart.AddDate(0, 0, 1)
yesterdayStart := todayStart.AddDate(0, 0, -1)
yesterdayEnd := todayStart
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
monthEnd := monthStart.AddDate(0, 1, 0)
orderStats, err := l.calculateOrderStatistics(todayStart, todayEnd, yesterdayStart, yesterdayEnd, monthStart, monthEnd)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "计算订单统计失败, %v", err)
}
revenueStats, err := l.calculateRevenueStatistics(todayStart, todayEnd, yesterdayStart, yesterdayEnd, monthStart, monthEnd)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "计算营收统计失败, %v", err)
}
agentStats, err := l.calculateAgentStatistics(todayStart, monthStart)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "计算代理统计失败, %v", err)
}
profitStats, err := l.calculateProfitStatistics(todayStart, todayEnd, monthStart, monthEnd, revenueStats)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "计算利润统计失败, %v", err)
}
orderTrend, err := l.calculateOrderTrend(now, loc)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "计算订单趋势失败, %v", err)
}
revenueTrend, err := l.calculateRevenueTrend(now, loc)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "计算营收趋势失败, %v", err)
}
return &types.AdminGetDashboardStatisticsResp{
OrderStats: orderStats,
RevenueStats: revenueStats,
AgentStats: agentStats,
ProfitStats: profitStats,
OrderTrend: orderTrend,
RevenueTrend: revenueTrend,
}, nil
}
func (l *AdminGetDashboardStatisticsLogic) calculateOrderStatistics(todayStart, todayEnd, yesterdayStart, yesterdayEnd, monthStart, monthEnd time.Time) (types.AdminOrderStatistics, error) {
var stats types.AdminOrderStatistics
todayBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", todayStart, todayEnd)
todayCount, err := l.svcCtx.OrderModel.FindCount(l.ctx, todayBuilder, "id")
if err != nil {
return stats, err
}
stats.TodayCount = todayCount
yesterdayBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", yesterdayStart, yesterdayEnd)
yesterdayCount, _ := l.svcCtx.OrderModel.FindCount(l.ctx, yesterdayBuilder, "id")
stats.YesterdayCount = yesterdayCount
monthBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", monthStart, monthEnd)
monthCount, _ := l.svcCtx.OrderModel.FindCount(l.ctx, monthBuilder, "id")
stats.MonthCount = monthCount
totalBuilder := l.svcCtx.OrderModel.SelectBuilder().Where("status = ?", "paid")
totalCount, _ := l.svcCtx.OrderModel.FindCount(l.ctx, totalBuilder, "id")
stats.TotalCount = totalCount
if stats.YesterdayCount > 0 {
stats.ChangeRate = float64(stats.TodayCount-stats.YesterdayCount) / float64(stats.YesterdayCount) * 100
} else if stats.TodayCount > 0 {
stats.ChangeRate = 100
}
return stats, nil
}
func (l *AdminGetDashboardStatisticsLogic) calculateRevenueStatistics(todayStart, todayEnd, yesterdayStart, yesterdayEnd, monthStart, monthEnd time.Time) (types.AdminRevenueStatistics, error) {
var stats types.AdminRevenueStatistics
todayBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", todayStart, todayEnd)
todayAmount, _ := l.svcCtx.OrderModel.FindSum(l.ctx, todayBuilder, "amount")
stats.TodayAmount = todayAmount
yesterdayBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", yesterdayStart, yesterdayEnd)
yesterdayAmount, _ := l.svcCtx.OrderModel.FindSum(l.ctx, yesterdayBuilder, "amount")
stats.YesterdayAmount = yesterdayAmount
monthBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", monthStart, monthEnd)
monthAmount, _ := l.svcCtx.OrderModel.FindSum(l.ctx, monthBuilder, "amount")
stats.MonthAmount = monthAmount
totalBuilder := l.svcCtx.OrderModel.SelectBuilder().Where("status = ?", "paid")
totalAmount, _ := l.svcCtx.OrderModel.FindSum(l.ctx, totalBuilder, "amount")
stats.TotalAmount = totalAmount
if stats.YesterdayAmount > 0 {
stats.ChangeRate = (stats.TodayAmount - stats.YesterdayAmount) / stats.YesterdayAmount * 100
} else if stats.TodayAmount > 0 {
stats.ChangeRate = 100
}
return stats, nil
}
func (l *AdminGetDashboardStatisticsLogic) calculateAgentStatistics(todayStart, monthStart time.Time) (types.AdminAgentStatistics, error) {
var stats types.AdminAgentStatistics
totalCount, _ := l.svcCtx.AgentModel.FindCount(l.ctx, l.svcCtx.AgentModel.SelectBuilder(), "id")
stats.TotalCount = totalCount
todayBuilder := l.svcCtx.AgentModel.SelectBuilder().Where("create_time >= ?", todayStart)
todayNew, _ := l.svcCtx.AgentModel.FindCount(l.ctx, todayBuilder, "id")
stats.TodayNew = todayNew
monthBuilder := l.svcCtx.AgentModel.SelectBuilder().Where("create_time >= ?", monthStart)
monthNew, _ := l.svcCtx.AgentModel.FindCount(l.ctx, monthBuilder, "id")
stats.MonthNew = monthNew
return stats, nil
}
func (l *AdminGetDashboardStatisticsLogic) calculateProfitStatistics(todayStart, todayEnd, monthStart, monthEnd time.Time, revenueStats types.AdminRevenueStatistics) (types.AdminProfitStatistics, error) {
var stats types.AdminProfitStatistics
const companyTaxRate = 0.06
todayRevenue := revenueStats.TodayAmount
todayCommissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("del_state = ? AND status != ? AND create_time >= ? AND create_time < ?", globalkey.DelStateNo, 3, todayStart, todayEnd)
todayCommission, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, todayCommissionBuilder, "amount")
todayRebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("del_state = ? AND status != ? AND create_time >= ? AND create_time < ?", globalkey.DelStateNo, 3, todayStart, todayEnd)
todayRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, todayRebateBuilder, "rebate_amount")
todayCompanyTax := todayRevenue * companyTaxRate
todayTaxIncomeBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("del_state = ? AND tax_status = ? AND create_time >= ? AND create_time < ?", globalkey.DelStateNo, 2, todayStart, todayEnd)
todayTaxIncome, _ := l.svcCtx.AgentWithdrawalTaxModel.FindSum(l.ctx, todayTaxIncomeBuilder, "tax_amount")
todayApiCost := 0.0
if l.svcCtx.TianyuanapiCallLogService != nil {
todayApiStats, e := l.svcCtx.TianyuanapiCallLogService.GetStatistics(l.ctx, service.StatisticsFilter{StartDate: todayStart, EndDate: todayEnd})
if e == nil {
todayApiCost = todayApiStats.TotalCost
}
}
stats.TodayProfit = todayRevenue - todayCommission - todayRebate - todayCompanyTax - todayApiCost + todayTaxIncome
if todayRevenue > 0 {
stats.TodayProfitRate = stats.TodayProfit / todayRevenue * 100
}
stats.TodayDetail = types.AdminProfitDetail{
Revenue: todayRevenue, Commission: todayCommission, Rebate: todayRebate,
CompanyTax: todayCompanyTax, ApiCost: todayApiCost, TaxIncome: todayTaxIncome,
Profit: stats.TodayProfit, ProfitRate: stats.TodayProfitRate,
}
monthRevenue := revenueStats.MonthAmount
monthCommissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("del_state = ? AND status != ? AND create_time >= ? AND create_time < ?", globalkey.DelStateNo, 3, monthStart, monthEnd)
monthCommission, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, monthCommissionBuilder, "amount")
monthRebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("del_state = ? AND status != ? AND create_time >= ? AND create_time < ?", globalkey.DelStateNo, 3, monthStart, monthEnd)
monthRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, monthRebateBuilder, "rebate_amount")
monthCompanyTax := monthRevenue * companyTaxRate
monthTaxIncomeBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("del_state = ? AND tax_status = ? AND create_time >= ? AND create_time < ?", globalkey.DelStateNo, 2, monthStart, monthEnd)
monthTaxIncome, _ := l.svcCtx.AgentWithdrawalTaxModel.FindSum(l.ctx, monthTaxIncomeBuilder, "tax_amount")
monthApiCost := 0.0
if l.svcCtx.TianyuanapiCallLogService != nil {
monthApiStats, e := l.svcCtx.TianyuanapiCallLogService.GetStatistics(l.ctx, service.StatisticsFilter{StartDate: monthStart, EndDate: monthEnd})
if e == nil {
monthApiCost = monthApiStats.TotalCost
}
}
stats.MonthProfit = monthRevenue - monthCommission - monthRebate - monthCompanyTax - monthApiCost + monthTaxIncome
if monthRevenue > 0 {
stats.MonthProfitRate = stats.MonthProfit / monthRevenue * 100
}
stats.MonthDetail = types.AdminProfitDetail{
Revenue: monthRevenue, Commission: monthCommission, Rebate: monthRebate,
CompanyTax: monthCompanyTax, ApiCost: monthApiCost, TaxIncome: monthTaxIncome,
Profit: stats.MonthProfit, ProfitRate: stats.MonthProfitRate,
}
totalRevenue := revenueStats.TotalAmount
totalCommissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().Where("del_state = ? AND status != ?", globalkey.DelStateNo, 3)
totalCommission, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, totalCommissionBuilder, "amount")
totalRebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().Where("status != ?", 3)
totalRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, totalRebateBuilder, "rebate_amount")
totalCompanyTax := totalRevenue * companyTaxRate
totalTaxIncomeBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().Where("tax_status = ?", 2)
totalTaxIncome, _ := l.svcCtx.AgentWithdrawalTaxModel.FindSum(l.ctx, totalTaxIncomeBuilder, "tax_amount")
totalApiCost := 0.0
if l.svcCtx.TianyuanapiCallLogService != nil {
totalApiStats, e := l.svcCtx.TianyuanapiCallLogService.GetStatistics(l.ctx, service.StatisticsFilter{})
if e == nil {
totalApiCost = totalApiStats.TotalCost
}
}
stats.TotalProfit = totalRevenue - totalCommission - totalRebate - totalCompanyTax - totalApiCost + totalTaxIncome
if totalRevenue > 0 {
stats.TotalProfitRate = stats.TotalProfit / totalRevenue * 100
}
stats.TotalDetail = types.AdminProfitDetail{
Revenue: totalRevenue, Commission: totalCommission, Rebate: totalRebate,
CompanyTax: totalCompanyTax, ApiCost: totalApiCost, TaxIncome: totalTaxIncome,
Profit: stats.TotalProfit, ProfitRate: stats.TotalProfitRate,
}
return stats, nil
}
func (l *AdminGetDashboardStatisticsLogic) calculateOrderTrend(now time.Time, loc *time.Location) ([]types.AdminTrendData, error) {
var trend []types.AdminTrendData
for i := 6; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc)
dateEnd := dateStart.AddDate(0, 0, 1)
builder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", dateStart, dateEnd)
count, err := l.svcCtx.OrderModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, err
}
trend = append(trend, types.AdminTrendData{Date: date.Format("01-02"), Value: float64(count)})
}
return trend, nil
}
func (l *AdminGetDashboardStatisticsLogic) calculateRevenueTrend(now time.Time, loc *time.Location) ([]types.AdminTrendData, error) {
var trend []types.AdminTrendData
for i := 6; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc)
dateEnd := dateStart.AddDate(0, 0, 1)
builder := l.svcCtx.OrderModel.SelectBuilder().
Where("status = ? AND create_time >= ? AND create_time < ?", "paid", dateStart, dateEnd)
amount, err := l.svcCtx.OrderModel.FindSum(l.ctx, builder, "amount")
if err != nil {
return nil, err
}
trend = append(trend, types.AdminTrendData{Date: date.Format("01-02"), Value: amount})
}
return trend, nil
}

View File

@@ -34,6 +34,12 @@ func (l *AdminCreateFeatureLogic) AdminCreateFeature(req *types.AdminCreateFeatu
ApiId: req.ApiId, ApiId: req.ApiId,
Name: req.Name, Name: req.Name,
} }
if req.WhitelistPrice != nil {
data.WhitelistPrice = *req.WhitelistPrice
}
if req.CostPrice != nil {
data.CostPrice = *req.CostPrice
}
// 2. 数据库操作 // 2. 数据库操作
result, err := l.svcCtx.FeatureModel.Insert(l.ctx, nil, data) result, err := l.svcCtx.FeatureModel.Insert(l.ctx, nil, data)

View File

@@ -38,6 +38,8 @@ func (l *AdminGetFeatureDetailLogic) AdminGetFeatureDetail(req *types.AdminGetFe
Id: record.Id, Id: record.Id,
ApiId: record.ApiId, ApiId: record.ApiId,
Name: record.Name, Name: record.Name,
WhitelistPrice: record.WhitelistPrice,
CostPrice: record.CostPrice,
CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"), UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"),
} }

View File

@@ -52,6 +52,8 @@ func (l *AdminGetFeatureListLogic) AdminGetFeatureList(req *types.AdminGetFeatur
Id: item.Id, Id: item.Id,
ApiId: item.ApiId, ApiId: item.ApiId,
Name: item.Name, Name: item.Name,
WhitelistPrice: item.WhitelistPrice,
CostPrice: item.CostPrice,
CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"),
} }

View File

@@ -3,6 +3,7 @@ package admin_feature
import ( import (
"context" "context"
"qnc-server/app/main/api/internal/logic/productcost"
"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"
@@ -46,6 +47,12 @@ func (l *AdminUpdateFeatureLogic) AdminUpdateFeature(req *types.AdminUpdateFeatu
if req.Name != nil && *req.Name != "" { if req.Name != nil && *req.Name != "" {
record.Name = *req.Name record.Name = *req.Name
} }
if req.WhitelistPrice != nil {
record.WhitelistPrice = *req.WhitelistPrice
}
if req.CostPrice != nil {
record.CostPrice = *req.CostPrice
}
// 4. 执行更新操作 // 4. 执行更新操作
err = l.svcCtx.FeatureModel.UpdateWithVersion(l.ctx, nil, record) err = l.svcCtx.FeatureModel.UpdateWithVersion(l.ctx, nil, record)
@@ -54,6 +61,10 @@ func (l *AdminUpdateFeatureLogic) AdminUpdateFeature(req *types.AdminUpdateFeatu
"更新功能失败, err: %v, id: %d", err, req.Id) "更新功能失败, err: %v, id: %d", err, req.Id)
} }
if err := productcost.SyncAllProductsUsingFeature(l.ctx, l.svcCtx, req.Id); err != nil {
return nil, err
}
// 5. 返回成功结果 // 5. 返回成功结果
return &types.AdminUpdateFeatureResp{Success: true}, nil return &types.AdminUpdateFeatureResp{Success: true}, nil
} }

View File

@@ -2,6 +2,7 @@ package admin_notification
import ( import (
"context" "context"
"strings"
"time" "time"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
@@ -32,7 +33,10 @@ func (l *AdminGetNotificationListLogic) AdminGetNotificationList(req *types.Admi
builder = builder.Where("title LIKE ?", "%"+*req.Title+"%") builder = builder.Where("title LIKE ?", "%"+*req.Title+"%")
} }
if req.NotificationPage != nil { if req.NotificationPage != nil {
builder = builder.Where("notification_page = ?", *req.NotificationPage) s := strings.TrimSpace(*req.NotificationPage)
if s != "" {
builder = builder.Where("notification_page LIKE ?", "%"+s+"%")
}
} }
if req.Status != nil { if req.Status != nil {
builder = builder.Where("status = ?", *req.Status) builder = builder.Where("status = ?", *req.Status)

View File

@@ -3,6 +3,7 @@ package admin_notification
import ( import (
"context" "context"
"database/sql" "database/sql"
"strings"
"time" "time"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
@@ -33,13 +34,29 @@ func (l *AdminUpdateNotificationLogic) AdminUpdateNotification(req *types.AdminU
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找通知失败, err: %v, id: %d", err, req.Id) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找通知失败, err: %v, id: %d", err, req.Id)
} }
if req.StartDate != nil { if req.StartDate != nil {
startDate, _ := time.Parse("2006-01-02", *req.StartDate) s := strings.TrimSpace(*req.StartDate)
if s == "" {
notification.StartDate = sql.NullTime{Valid: false}
} else {
startDate, err := time.Parse("2006-01-02", s)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "start_date 格式错误: %v", err)
}
notification.StartDate = sql.NullTime{Time: startDate, Valid: true} notification.StartDate = sql.NullTime{Time: startDate, Valid: true}
} }
}
if req.EndDate != nil { if req.EndDate != nil {
endDate, _ := time.Parse("2006-01-02", *req.EndDate) s := strings.TrimSpace(*req.EndDate)
if s == "" {
notification.EndDate = sql.NullTime{Valid: false}
} else {
endDate, err := time.Parse("2006-01-02", s)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "end_date 格式错误: %v", err)
}
notification.EndDate = sql.NullTime{Time: endDate, Valid: true} notification.EndDate = sql.NullTime{Time: endDate, Valid: true}
} }
}
if req.Title != nil { if req.Title != nil {
notification.Title = *req.Title notification.Title = *req.Title
} }

View File

@@ -2,6 +2,7 @@ package admin_order
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
@@ -54,6 +55,24 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
if req.Status != "" { if req.Status != "" {
builder = builder.Where("status = ?", req.Status) builder = builder.Where("status = ?", req.Status)
} }
// 是否代理订单筛选
if req.IsAgentOrder != nil {
if *req.IsAgentOrder {
// 筛选代理订单订单ID在agent_order表中
builder = builder.Where("id IN (SELECT order_id FROM agent_order WHERE del_state = 0)")
} else {
// 筛选非代理订单订单ID不在agent_order表中
builder = builder.Where("id NOT IN (SELECT order_id FROM agent_order WHERE del_state = 0)")
}
}
// 代理编号筛选
if req.AgentCode != "" {
// 将字符串转换为int64进行查询
var agentCode int64
if _, err := fmt.Sscanf(req.AgentCode, "%d", &agentCode); err == nil {
builder = builder.Where("id IN (SELECT order_id FROM agent_order ao JOIN agent a ON ao.agent_id = a.id WHERE a.agent_code = ? AND ao.del_state = 0)", agentCode)
}
}
// 时间范围查询 // 时间范围查询
if req.CreateTimeStart != "" { if req.CreateTimeStart != "" {
builder = builder.Where("create_time >= ?", req.CreateTimeStart) builder = builder.Where("create_time >= ?", req.CreateTimeStart)
@@ -101,6 +120,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
queryStateMap := make(map[string]string) queryStateMap := make(map[string]string)
agentOrderMap := make(map[string]bool) // 代理订单映射 agentOrderMap := make(map[string]bool) // 代理订单映射
agentProcessStatusMap := make(map[string]string) // 代理处理状态映射 agentProcessStatusMap := make(map[string]string) // 代理处理状态映射
agentCodeMap := make(map[string]string) // 代理编号映射
var mu sync.Mutex var mu sync.Mutex
@@ -169,8 +189,32 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
} }
// 记录代理订单 // 记录代理订单
agentIds := make([]string, 0, len(agentOrders))
for _, agentOrder := range agentOrders { for _, agentOrder := range agentOrders {
agentOrderMap[agentOrder.OrderId] = true agentOrderMap[agentOrder.OrderId] = true
agentIds = append(agentIds, agentOrder.AgentId)
}
// 批量获取代理编号
if len(agentIds) > 0 {
agents, err := l.svcCtx.AgentModel.FindAll(l.ctx,
l.svcCtx.AgentModel.SelectBuilder().Where(squirrel.Eq{"id": agentIds}), "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 批量查询代理信息失败 err: %v", err)
}
// 构建代理ID到代理编号的映射
agentIdToCodeMap := make(map[string]string)
for _, agent := range agents {
agentIdToCodeMap[agent.Id] = fmt.Sprintf("%d", agent.AgentCode)
}
// 为每个代理订单设置代理编号
for _, agentOrder := range agentOrders {
if code, exists := agentIdToCodeMap[agentOrder.AgentId]; exists {
agentCodeMap[agentOrder.OrderId] = code
}
}
} }
// 对于代理订单,查询代理处理状态 // 对于代理订单,查询代理处理状态
@@ -277,6 +321,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
if agentOrderMap[order.Id] { if agentOrderMap[order.Id] {
item.IsAgentOrder = true item.IsAgentOrder = true
item.AgentProcessStatus = agentProcessStatusMap[order.Id] item.AgentProcessStatus = agentProcessStatusMap[order.Id]
item.AgentCode = agentCodeMap[order.Id]
} else { } else {
item.IsAgentOrder = false item.IsAgentOrder = false
item.AgentProcessStatus = "not_agent" item.AgentProcessStatus = "not_agent"

View File

@@ -6,15 +6,16 @@ import (
"fmt" "fmt"
"time" "time"
paylogic "qnc-server/app/main/api/internal/logic/pay"
"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/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/google/uuid"
) )
const ( const (
@@ -44,6 +45,10 @@ func (l *AdminRefundOrderLogic) AdminRefundOrder(req *types.AdminRefundOrderReq)
return nil, err return nil, err
} }
if model.IsXpayOrder(order) {
return l.handleXpayRefund(order, req)
}
// 根据支付平台处理退款 // 根据支付平台处理退款
switch order.PaymentPlatform { switch order.PaymentPlatform {
case PaymentPlatformAlipay: case PaymentPlatformAlipay:
@@ -107,6 +112,24 @@ func (l *AdminRefundOrderLogic) handleAlipayRefund(order *model.Order, req *type
} }
} }
// handleXpayRefund 处理小程序虚拟支付退款
func (l *AdminRefundOrderLogic) handleXpayRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) {
if req.RefundAmount != order.Amount {
return nil, errors.Wrapf(xerr.NewErrMsg("虚拟支付订单仅支持全额退款"), "")
}
if err := paylogic.RefundXpayQueryOrder(l.ctx, l.svcCtx, order); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "虚拟支付退款失败: %v", err)
}
refundNo := l.generateRefundNo(order.OrderNo)
_ = l.createRefundRecordOnly(order, req, refundNo, "", model.OrderRefundStatusSuccess)
l.Infof("[xpay] admin refund OK order_no=%s amount=%.2f", order.OrderNo, req.RefundAmount)
return &types.AdminRefundOrderResp{
Status: model.OrderStatusRefunded,
RefundNo: refundNo,
Amount: req.RefundAmount,
}, nil
}
// handleWechatRefund 处理微信退款 // handleWechatRefund 处理微信退款
func (l *AdminRefundOrderLogic) handleWechatRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) { func (l *AdminRefundOrderLogic) handleWechatRefund(order *model.Order, req *types.AdminRefundOrderReq) (*types.AdminRefundOrderResp, error) {
// 调用微信退款接口 // 调用微信退款接口
@@ -156,6 +179,13 @@ func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Or
return fmt.Errorf("更新订单状态失败: %v", err) return fmt.Errorf("更新订单状态失败: %v", err)
} }
// 退款成功落库后冲正代理佣金/返佣(部分退款与全额退款均全额冲回本单分账;仅已退款时执行,不含「退款中」)
if orderStatus == model.OrderStatusRefunded {
if revErr := l.svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(ctx, session, order.Id); revErr != nil {
return revErr
}
}
return nil return nil
}) })
} }

View File

@@ -0,0 +1,56 @@
package admin_order
import (
"context"
"qnc-server/app/main/api/internal/logic/pay"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type AdminXpayDeliverLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminXpayDeliverLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminXpayDeliverLogic {
return &AdminXpayDeliverLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AdminXpayDeliverLogic) AdminXpayDeliver(req *types.AdminXpayDeliverReq) (resp *types.AdminXpayDeliverResp, err error) {
order, findErr := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id)
if findErr != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "%v", findErr)
}
if !model.IsXpayOrder(order) {
return nil, errors.Wrapf(xerr.NewErrMsg("仅支持小程序虚拟支付订单"), "")
}
if l.svcCtx.XpayService == nil || !l.svcCtx.XpayService.Enabled() {
return nil, errors.Wrapf(xerr.NewErrMsg("虚拟支付未启用"), "")
}
result, deliverErr := pay.DeliverXpayQueryOrder(l.ctx, l.svcCtx, order.OrderNo)
if deliverErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "补发货失败: %v", deliverErr)
}
return &types.AdminXpayDeliverResp{
Credited: result.Credited,
Notified: result.Notified,
WechatDetail: result.WechatDetail,
Errors: result.Errors,
Message: result.Message,
}, nil
}

View File

@@ -30,8 +30,17 @@ func NewAdminGetPlatformUserListLogic(ctx context.Context, svcCtx *svc.ServiceCo
func (l *AdminGetPlatformUserListLogic) AdminGetPlatformUserList(req *types.AdminGetPlatformUserListReq) (resp *types.AdminGetPlatformUserListResp, err error) { func (l *AdminGetPlatformUserListLogic) AdminGetPlatformUserList(req *types.AdminGetPlatformUserListReq) (resp *types.AdminGetPlatformUserListResp, err error) {
builder := l.svcCtx.UserModel.SelectBuilder() builder := l.svcCtx.UserModel.SelectBuilder()
secretKey := l.svcCtx.Config.Encrypt.SecretKey
if req.UserId != "" {
builder = builder.Where("id = ?", req.UserId)
}
if req.Mobile != "" { if req.Mobile != "" {
builder = builder.Where("mobile = ?", req.Mobile) // 数据库存密文,搜索时把明文手机号加密后再查询
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机号加密失败: %v", err)
}
builder = builder.Where("mobile = ?", encryptedMobile)
} }
if req.Nickname != "" { if req.Nickname != "" {
builder = builder.Where("nickname = ?", req.Nickname) builder = builder.Where("nickname = ?", req.Nickname)
@@ -55,16 +64,15 @@ func (l *AdminGetPlatformUserListLogic) AdminGetPlatformUserList(req *types.Admi
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户分页失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户分页失败: %v", err)
} }
var items []types.PlatformUserListItem var items []types.PlatformUserListItem
secretKey := l.svcCtx.Config.Encrypt.SecretKey
for _, user := range users { for _, user := range users {
mobile := user.Mobile mobile := user.Mobile
if mobile.Valid { if mobile.Valid {
encryptedMobile, err := crypto.DecryptMobile(mobile.String, secretKey) DecryptMobile, err := crypto.DecryptMobile(mobile.String, secretKey)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 解密手机号失败: %+v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 解密手机号失败: %+v", err)
} }
mobile = sql.NullString{String: encryptedMobile, Valid: true} mobile = sql.NullString{String: DecryptMobile, Valid: true}
} }
itemData := types.PlatformUserListItem{ itemData := types.PlatformUserListItem{
Id: user.Id, Id: user.Id,

View File

@@ -28,6 +28,7 @@ func NewAdminCreateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext)
} }
} }
// AdminCreateProduct 创建产品后,在同一事务内写入一条 agent_product_config与代理产品配置列表一致
func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProductReq) (resp *types.AdminCreateProductResp, err error) { func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProductReq) (resp *types.AdminCreateProductResp, err error) {
// 1. 数据转换 // 1. 数据转换
data := &model.Product{ data := &model.Product{
@@ -36,7 +37,7 @@ func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProdu
ProductEn: req.ProductEn, ProductEn: req.ProductEn,
Description: req.Description, Description: req.Description,
Notes: sql.NullString{String: req.Notes, Valid: req.Notes != ""}, Notes: sql.NullString{String: req.Notes, Valid: req.Notes != ""},
CostPrice: req.CostPrice, CostPrice: 0, // 成本由关联模块汇总,创建后为 0保存模块关联后写入
SellPrice: req.SellPrice, SellPrice: req.SellPrice,
} }
@@ -51,7 +52,14 @@ func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProdu
} }
productId = data.Id productId = data.Id
// 2.2 同步创建代理产品配置(使用默认值) // 2.2 同步创建代理产品配置(使用默认值);已存在则跳过(幂等)
if _, findErr := l.svcCtx.AgentProductConfigModel.FindOneByProductId(ctx, productId); findErr == nil {
return nil
} else if findErr != model.ErrNotFound {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"检查代理产品配置失败, err: %v, productId: %s", findErr, productId)
}
agentProductConfig := &model.AgentProductConfig{ agentProductConfig := &model.AgentProductConfig{
Id: uuid.NewString(), Id: uuid.NewString(),
ProductId: productId, ProductId: productId,

View File

@@ -2,8 +2,10 @@ package admin_product
import ( import (
"context" "context"
"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/common/xerr" "qnc-server/common/xerr"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -25,12 +27,13 @@ func NewAdminDeleteProductLogic(ctx context.Context, svcCtx *svc.ServiceContext)
} }
} }
// AdminDeleteProduct 软删除产品后,在同一事务内软删除对应 agent_product_config与产品列表一致
func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProductReq) (resp *types.AdminDeleteProductResp, err error) { func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProductReq) (resp *types.AdminDeleteProductResp, err error) {
// 1. 查询记录是否存在 // 1. 查询记录是否存在
record, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id) record, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"查找产品失败, err: %v, id: %d", err, req.Id) "查找产品失败, err: %v, id: %s", err, req.Id)
} }
// 2. 执行软删除(使用事务确保产品表和代理产品配置表同步) // 2. 执行软删除(使用事务确保产品表和代理产品配置表同步)
@@ -39,20 +42,22 @@ func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProdu
err = l.svcCtx.ProductModel.DeleteSoft(ctx, session, record) err = l.svcCtx.ProductModel.DeleteSoft(ctx, session, record)
if err != nil { if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"删除产品失败, err: %v, id: %d", err, req.Id) "删除产品失败, err: %v, id: %s", err, record.Id)
} }
// 2.2 同步软删除代理产品配置 // 2.2 同步软删除代理产品配置(按产品主键 product_id
agentProductConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(ctx, req.Id) agentProductConfig, findErr := l.svcCtx.AgentProductConfigModel.FindOneByProductId(ctx, record.Id)
if err != nil { switch {
// 如果代理产品配置不存在,记录日志但不影响主流程 case findErr == nil:
l.Infof("同步删除代理产品配置失败:代理产品配置不存在, productId: %d, err: %v", req.Id, err) if err = l.svcCtx.AgentProductConfigModel.DeleteSoft(ctx, session, agentProductConfig); err != nil {
} else {
err = l.svcCtx.AgentProductConfigModel.DeleteSoft(ctx, session, agentProductConfig)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"同步删除代理产品配置失败, err: %v, productId: %d", err, req.Id) "同步删除代理产品配置失败, err: %v, productId: %s", err, record.Id)
} }
case findErr == model.ErrNotFound:
l.Infof("产品无对应代理产品配置,跳过同步删除, productId: %s", record.Id)
default:
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"查询代理产品配置失败, err: %v, productId: %s", findErr, record.Id)
} }
return nil return nil

View File

@@ -32,14 +32,21 @@ func (l *AdminGetProductDetailLogic) AdminGetProductDetail(req *types.AdminGetPr
"查找产品失败, err: %v, id: %d", err, req.Id) "查找产品失败, err: %v, id: %d", err, req.Id)
} }
// 2. 构建响应 // 2. 成本价为关联模块 cost_price 之和
costSum, err := l.svcCtx.ProductFeatureModel.SumFeatureCostPriceByProductId(l.ctx, req.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"汇总产品模块成本失败, err: %v, id: %s", err, req.Id)
}
// 3. 构建响应
resp = &types.AdminGetProductDetailResp{ resp = &types.AdminGetProductDetailResp{
Id: record.Id, Id: record.Id,
ProductName: record.ProductName, ProductName: record.ProductName,
ProductEn: record.ProductEn, ProductEn: record.ProductEn,
Description: record.Description, Description: record.Description,
Notes: record.Notes.String, Notes: record.Notes.String,
CostPrice: record.CostPrice, CostPrice: costSum,
SellPrice: record.SellPrice, SellPrice: record.SellPrice,
CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"), UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"),

View File

@@ -105,6 +105,7 @@ func (l *AdminGetProductFeatureListLogic) AdminGetProductFeatureList(req *types.
FeatureId: item.FeatureId, FeatureId: item.FeatureId,
ApiId: feature.ApiId, ApiId: feature.ApiId,
Name: feature.Name, Name: feature.Name,
CostPrice: feature.CostPrice,
Sort: item.Sort, Sort: item.Sort,
Enable: item.Enable, Enable: item.Enable,
IsImportant: item.IsImportant, IsImportant: item.IsImportant,

View File

@@ -44,16 +44,26 @@ func (l *AdminGetProductListLogic) AdminGetProductList(req *types.AdminGetProduc
"查询产品列表失败, err: %v, req: %+v", err, req) "查询产品列表失败, err: %v, req: %+v", err, req)
} }
// 4. 构建响应列表 // 4. 构建响应列表(成本价为关联模块 cost_price 之和,非库表字段直读)
items := make([]types.ProductListItem, 0, len(list)) items := make([]types.ProductListItem, 0, len(list))
productIds := make([]string, 0, len(list))
for _, item := range list { for _, item := range list {
productIds = append(productIds, item.Id)
}
costByProduct, err := l.svcCtx.ProductFeatureModel.SumFeatureCostPriceByProductIds(l.ctx, productIds)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"汇总产品模块成本失败, err: %v, req: %+v", err, req)
}
for _, item := range list {
cost := costByProduct[item.Id]
listItem := types.ProductListItem{ listItem := types.ProductListItem{
Id: item.Id, Id: item.Id,
ProductName: item.ProductName, ProductName: item.ProductName,
ProductEn: item.ProductEn, ProductEn: item.ProductEn,
Description: item.Description, Description: item.Description,
Notes: item.Notes.String, Notes: item.Notes.String,
CostPrice: item.CostPrice, CostPrice: cost,
SellPrice: item.SellPrice, SellPrice: item.SellPrice,
CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"),

View File

@@ -3,16 +3,18 @@ package admin_product
import ( import (
"context" "context"
"sync" "sync"
"qnc-server/app/main/api/internal/logic/productcost"
"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/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mr" "github.com/zeromicro/go-zero/core/mr"
"github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/google/uuid"
) )
type AdminUpdateProductFeaturesLogic struct { type AdminUpdateProductFeaturesLogic struct {
@@ -156,6 +158,10 @@ func (l *AdminUpdateProductFeaturesLogic) AdminUpdateProductFeatures(req *types.
"更新产品功能关联失败, err: %v, req: %+v", err, req) "更新产品功能关联失败, err: %v, req: %+v", err, req)
} }
if err := productcost.SyncProductCostFromModules(l.ctx, l.svcCtx, req.ProductId); err != nil {
return nil, err
}
// 5. 返回结果 // 5. 返回结果
return &types.AdminUpdateProductFeaturesResp{Success: true}, nil return &types.AdminUpdateProductFeaturesResp{Success: true}, nil
} }

View File

@@ -47,9 +47,6 @@ func (l *AdminUpdateProductLogic) AdminUpdateProduct(req *types.AdminUpdateProdu
if req.Notes != nil { if req.Notes != nil {
record.Notes = sql.NullString{String: *req.Notes, Valid: *req.Notes != ""} record.Notes = sql.NullString{String: *req.Notes, Valid: *req.Notes != ""}
} }
if req.CostPrice != nil {
record.CostPrice = *req.CostPrice
}
if req.SellPrice != nil { if req.SellPrice != nil {
record.SellPrice = *req.SellPrice record.SellPrice = *req.SellPrice
} }

View File

@@ -0,0 +1,80 @@
package admin_query
import (
"context"
"encoding/hex"
"encoding/json"
"time"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type AdminGenerateQueryShareLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminGenerateQueryShareLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGenerateQueryShareLinkLogic {
return &AdminGenerateQueryShareLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AdminGenerateQueryShareLinkLogic) AdminGenerateQueryShareLink(req *types.AdminGenerateQueryShareLinkReq) (resp *types.QueryGenerateShareLinkResp, err error) {
if req.OrderId == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("订单ID不能为空"), "")
}
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.OrderId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取订单失败: %v", err)
}
if order.Status != model.OrderStatusPaid {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 订单未支付")
}
query, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取查询失败: %v", err)
}
if query.QueryState != model.QueryStateSuccess {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 查询未成功")
}
expireAt := time.Now().Add(time.Duration(l.svcCtx.Config.Query.ShareLinkExpire) * time.Second)
payload := types.QueryShareLinkPayload{
OrderId: order.Id,
ExpireAt: expireAt.Unix(),
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, err := hex.DecodeString(secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 解密失败: %v", err)
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 序列化失败: %v", err)
}
encryptedPayload, err := crypto.AesEncryptURL(payloadBytes, key)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 加密失败: %v", err)
}
return &types.QueryGenerateShareLinkResp{
ShareLink: encryptedPayload,
}, nil
}

View File

@@ -10,6 +10,7 @@ import (
"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/reviewphone"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
@@ -53,8 +54,12 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
if req.Referrer == "" { if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "") return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
} }
// 2. 校验验证码开发环境跳过验证码校验) // 2. 校验验证码开发环境跳过;审核预留号段 + 固定码;其余 Redis
if os.Getenv("ENV") != "development" { if os.Getenv("ENV") == "development" {
// skip
} else if reviewphone.IsAppReviewDemoMobile(req.Mobile) && req.Code == reviewphone.DemoVerifyCode {
l.Infof("[ApplyForAgent] 审核体验号段固定验证码通过, mobile: %s", req.Mobile)
} else {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey) cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil { if err != nil {

View File

@@ -88,26 +88,29 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
} }
} }
// 5. 验证提现金额 // 5. 获取钱包信息
if req.Amount <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "")
}
// 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)
} }
// 7. 验证余额(包括检查是否为负数) // 6. 提现金额与可用余额:仅可提「可用余额」内金额;可用余额须大于 0
if wallet.Balance < 0 { withdrawAmount := lzUtils.RoundMoney(req.Amount)
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("账户存在欠款,请先补足欠款后再申请提现,当前余额:%.2f", wallet.Balance)), "") if withdrawAmount <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "")
} }
if wallet.Balance < req.Amount { bal := lzUtils.RoundMoney(wallet.Balance)
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "") if bal < 0 {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("账户存在欠款,请先补足欠款后再申请提现,当前可用余额:%.2f", bal)), "")
}
if bal <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("可用余额须大于0才能申请提现"), "")
}
if withdrawAmount > bal {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("提现金额不能超过可用余额(不含冻结),当前可用:%.2f", bal)), "")
} }
// 8. 支付宝月度提现额度校验(仅针对支付宝提现) // 7. 支付宝月度提现额度校验(仅针对支付宝提现)
if req.WithdrawalType == 1 { if req.WithdrawalType == 1 {
now := time.Now() now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
@@ -138,7 +141,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
) )
} }
if req.Amount > remainQuota { if withdrawAmount > remainQuota {
return nil, errors.Wrapf( return nil, errors.Wrapf(
xerr.NewErrMsg(fmt.Sprintf("本月支付宝最高可提现 %.2f 元,请调整提现金额或使用银行卡提现", remainQuota)), xerr.NewErrMsg(fmt.Sprintf("本月支付宝最高可提现 %.2f 元,请调整提现金额或使用银行卡提现", remainQuota)),
"", "",
@@ -148,7 +151,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
// 9. 计算税费 // 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, withdrawAmount, yearMonth)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "计算税费失败") return nil, errors.Wrapf(err, "计算税费失败")
} }
@@ -164,9 +167,26 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
// 11. 使用事务处理提现申请 // 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 {
// 提交前再次校验可用余额(防止并发入账/冲正与申请提现竞态)
fresh, wErr := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, agent.Id)
if wErr != nil {
return errors.Wrapf(wErr, "事务内查询钱包失败")
}
freshBal := lzUtils.RoundMoney(fresh.Balance)
if freshBal < 0 {
return errors.New("账户存在欠款,无法申请提现")
}
if freshBal <= 0 {
return errors.New("可用余额须大于0才能申请提现")
}
if withdrawAmount > freshBal {
return fmt.Errorf("提现金额不能超过当前可用余额(%.2f", freshBal)
}
wallet = fresh
// 11.1 冻结余额 // 11.1 冻结余额
wallet.FrozenBalance += req.Amount wallet.FrozenBalance += withdrawAmount
wallet.Balance -= req.Amount wallet.Balance -= withdrawAmount
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, "冻结余额失败")
} }
@@ -179,7 +199,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
WithdrawalType: req.WithdrawalType, WithdrawalType: req.WithdrawalType,
PayeeAccount: req.PayeeAccount, PayeeAccount: req.PayeeAccount,
PayeeName: req.PayeeName, PayeeName: req.PayeeName,
Amount: req.Amount, Amount: withdrawAmount,
ActualAmount: taxInfo.ActualAmount, ActualAmount: taxInfo.ActualAmount,
TaxAmount: taxInfo.TaxAmount, TaxAmount: taxInfo.TaxAmount,
Status: 1, // 待审核 Status: 1, // 待审核
@@ -206,7 +226,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
AgentId: agent.Id, AgentId: agent.Id,
WithdrawalId: withdrawalId, WithdrawalId: withdrawalId,
YearMonth: yearMonth, YearMonth: yearMonth,
WithdrawalAmount: req.Amount, WithdrawalAmount: withdrawAmount,
TaxableAmount: taxInfo.TaxableAmount, TaxableAmount: taxInfo.TaxableAmount,
TaxRate: taxInfo.TaxRate, TaxRate: taxInfo.TaxRate,
TaxAmount: taxInfo.TaxAmount, TaxAmount: taxInfo.TaxAmount,
@@ -293,8 +313,8 @@ func (l *ApplyWithdrawalLogic) calculateTax(ctx context.Context, agentId string,
} }
// 计算税费 // 计算税费
taxAmount := taxableAmount * taxRate taxAmount := lzUtils.RoundMoney(taxableAmount * taxRate)
actualAmount := amount - taxAmount actualAmount := lzUtils.RoundMoney(amount - taxAmount)
return &TaxInfo{ return &TaxInfo{
TaxableAmount: taxableAmount, TaxableAmount: taxableAmount,

View File

@@ -2,10 +2,14 @@ package agent
import ( import (
"context" "context"
"strings"
"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/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
@@ -24,7 +28,46 @@ func NewCheckFeatureWhitelistStatusLogic(ctx context.Context, svcCtx *svc.Servic
} }
func (l *CheckFeatureWhitelistStatusLogic) CheckFeatureWhitelistStatus(req *types.CheckFeatureWhitelistStatusReq) (resp *types.CheckFeatureWhitelistStatusResp, err error) { func (l *CheckFeatureWhitelistStatusLogic) CheckFeatureWhitelistStatus(req *types.CheckFeatureWhitelistStatusReq) (resp *types.CheckFeatureWhitelistStatusResp, err error) {
// todo: add your logic here and delete this line if req.IdCard == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("身份证号不能为空"), "")
return }
if req.FeatureApiId == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("模块API标识不能为空"), "")
}
mainApiId := req.FeatureApiId
if idx := strings.Index(req.FeatureApiId, "_"); idx > 0 {
mainApiId = req.FeatureApiId[:idx]
}
feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, mainApiId)
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)
}
whitelistBuilder := l.svcCtx.UserFeatureWhitelistModel.SelectBuilder().
Where("id_card = ? AND feature_api_id = ? AND status = ?", req.IdCard, mainApiId, 1)
whitelists, err := l.svcCtx.UserFeatureWhitelistModel.FindAll(l.ctx, whitelistBuilder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询白名单记录失败, %v", err)
}
isWhitelisted := len(whitelists) > 0
dataDeleted := false
if req.QueryId != "" {
containsFeature, err := l.svcCtx.WhitelistService.CheckQueryDataContainsFeature(l.ctx, req.QueryId, req.FeatureApiId)
if err != nil {
logx.Errorf("检查报告数据是否包含模块失败:%v", err)
dataDeleted = true
} else {
dataDeleted = !containsFeature
}
} else {
dataDeleted = true
}
return &types.CheckFeatureWhitelistStatusResp{
IsWhitelisted: isWhitelisted,
WhitelistPrice: feature.WhitelistPrice,
FeatureId: feature.Id,
DataDeleted: dataDeleted,
}, nil
} }

View File

@@ -5,7 +5,11 @@ import (
"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/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
@@ -23,8 +27,28 @@ func NewCheckOrderAgentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *C
} }
} }
// CheckOrderAgent 判断订单是否为当前代理推广的订单(通过 agent_order 关联)
func (l *CheckOrderAgentLogic) CheckOrderAgent(req *types.CheckOrderAgentReq) (resp *types.CheckOrderAgentResp, err error) { func (l *CheckOrderAgentLogic) CheckOrderAgent(req *types.CheckOrderAgentReq) (resp *types.CheckOrderAgentResp, err error) {
// todo: add your logic here and delete this line if req.OrderId == "" {
return &types.CheckOrderAgentResp{IsAgentOrder: false}, nil
return }
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return &types.CheckOrderAgentResp{IsAgentOrder: false}, nil
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
agentOrder, err := l.svcCtx.AgentOrderModel.FindOneByOrderId(l.ctx, req.OrderId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return &types.CheckOrderAgentResp{IsAgentOrder: false}, nil
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单关联失败, %v", err)
}
return &types.CheckOrderAgentResp{IsAgentOrder: agentOrder.AgentId == agent.Id}, nil
} }

View File

@@ -2,11 +2,18 @@ package agent
import ( import (
"context" "context"
"fmt"
"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/common/ctxdata"
"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/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
) )
type CreateWhitelistOrderLogic struct { type CreateWhitelistOrderLogic struct {
@@ -24,7 +31,98 @@ func NewCreateWhitelistOrderLogic(ctx context.Context, svcCtx *svc.ServiceContex
} }
func (l *CreateWhitelistOrderLogic) CreateWhitelistOrder(req *types.CreateWhitelistOrderReq) (resp *types.CreateWhitelistOrderResp, err error) { func (l *CreateWhitelistOrderLogic) CreateWhitelistOrder(req *types.CreateWhitelistOrderReq) (resp *types.CreateWhitelistOrderResp, err error) {
// todo: add your logic here and delete this line userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
_, 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)
}
if req.IdCard == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("身份证号不能为空"), "")
}
if len(req.FeatureIds) == 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("请至少选择一个模块"), "")
}
return var totalAmount float64
var orderItems []struct {
FeatureId string
FeatureApiId string
FeatureName string
Price float64
}
for _, featureId := range req.FeatureIds {
feature, err := l.svcCtx.FeatureModel.FindOne(l.ctx, featureId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("模块不存在: %s", featureId)), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询模块信息失败, %v", err)
}
whitelistPrice := feature.WhitelistPrice
if whitelistPrice <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("模块 %s 不支持白名单屏蔽", feature.Name)), "")
}
whitelistBuilder := l.svcCtx.UserFeatureWhitelistModel.SelectBuilder().
Where("id_card = ? AND feature_id = ?", req.IdCard, featureId)
existing, err := l.svcCtx.UserFeatureWhitelistModel.FindAll(l.ctx, whitelistBuilder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询白名单记录失败, %v", err)
}
for _, item := range existing {
if item.Status == 1 {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("身份证号 %s 的模块 %s 已经加入白名单", req.IdCard, feature.Name)), "")
}
}
totalAmount += whitelistPrice
orderItems = append(orderItems, struct {
FeatureId string
FeatureApiId string
FeatureName string
Price float64
}{
FeatureId: feature.Id, FeatureApiId: feature.ApiId, FeatureName: feature.Name, Price: whitelistPrice,
})
}
base := l.svcCtx.AlipayService.GenerateOutTradeNo()
orderNo := "W_" + base
if len(orderNo) > 32 {
orderNo = orderNo[:32]
}
var orderId string
err = l.svcCtx.WhitelistOrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
order := &model.WhitelistOrder{
Id: uuid.NewString(), OrderNo: orderNo, UserId: userID, IdCard: req.IdCard,
TotalAmount: totalAmount, Status: 1,
}
_, err := l.svcCtx.WhitelistOrderModel.Insert(ctx, session, order)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建订单失败, %v", err)
}
orderId = order.Id
for _, item := range orderItems {
orderItem := &model.WhitelistOrderItem{
Id: uuid.NewString(), OrderId: orderId, FeatureId: item.FeatureId,
FeatureApiId: item.FeatureApiId, FeatureName: item.FeatureName, Price: item.Price,
}
if _, err := l.svcCtx.WhitelistOrderItemModel.Insert(ctx, session, orderItem); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建订单明细失败, %v", err)
}
}
return nil
})
if err != nil {
return nil, err
}
return &types.CreateWhitelistOrderResp{
OrderId: orderId, OrderNo: orderNo, TotalAmount: totalAmount,
}, nil
} }

View File

@@ -15,6 +15,7 @@ import (
"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"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -41,6 +42,12 @@ func NewGeneratingLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Ge
} }
func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) (resp *types.AgentGeneratingLinkResp, err error) { func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) (resp *types.AgentGeneratingLinkResp, err error) {
// 将 set_price 从 string 转为 float64
setPrice, err := strconv.ParseFloat(req.SetPrice, 64)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("设定价格格式不正确"), "")
}
userID, err := ctxdata.GetUidFromCtx(l.ctx) userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成推广链接失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成推广链接失败, %v", err)
@@ -71,14 +78,14 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
} }
basePrice := productConfig.BasePrice basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus) actualBasePrice := lzUtils.RoundMoney(basePrice + float64(levelBonus))
systemMaxPrice := productConfig.SystemMaxPrice systemMaxPrice := productConfig.SystemMaxPrice
upliftAmount, err := l.getLevelMaxUpliftAmount(agentModel.Level) upliftAmount, err := l.getLevelMaxUpliftAmount(agentModel.Level)
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)
} }
levelMaxPrice := systemMaxPrice + upliftAmount levelMaxPrice := lzUtils.RoundMoney(systemMaxPrice + upliftAmount)
if req.SetPrice < actualBasePrice || req.SetPrice > levelMaxPrice { if setPrice < actualBasePrice || setPrice > levelMaxPrice {
return nil, errors.Wrapf(xerr.NewErrMsg("设定价格必须在 %.2f 到 %.2f 之间"), "设定价格必须在 %.2f 到 %.2f 之间", actualBasePrice, levelMaxPrice) return nil, errors.Wrapf(xerr.NewErrMsg("设定价格必须在 %.2f 到 %.2f 之间"), "设定价格必须在 %.2f 到 %.2f 之间", actualBasePrice, levelMaxPrice)
} }
@@ -86,7 +93,7 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
builder := l.svcCtx.AgentLinkModel.SelectBuilder().Where(squirrel.And{ builder := l.svcCtx.AgentLinkModel.SelectBuilder().Where(squirrel.And{
squirrel.Eq{"agent_id": agentModel.Id}, squirrel.Eq{"agent_id": agentModel.Id},
squirrel.Eq{"product_id": req.ProductId}, squirrel.Eq{"product_id": req.ProductId},
squirrel.Eq{"set_price": req.SetPrice}, squirrel.Eq{"set_price": setPrice},
squirrel.Eq{"del_state": globalkey.DelStateNo}, squirrel.Eq{"del_state": globalkey.DelStateNo},
}) })
@@ -115,7 +122,7 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
var agentIdentifier types.AgentIdentifier var agentIdentifier types.AgentIdentifier
agentIdentifier.AgentID = agentModel.Id agentIdentifier.AgentID = agentModel.Id
agentIdentifier.ProductID = req.ProductId agentIdentifier.ProductID = req.ProductId
agentIdentifier.SetPrice = req.SetPrice agentIdentifier.SetPrice = setPrice
agentIdentifierByte, err := json.Marshal(agentIdentifier) agentIdentifierByte, err := json.Marshal(agentIdentifier)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "序列化标识失败, %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "序列化标识失败, %v", err)
@@ -140,7 +147,7 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
UserId: userID, UserId: userID,
ProductId: req.ProductId, ProductId: req.ProductId,
LinkIdentifier: encrypted, LinkIdentifier: encrypted,
SetPrice: req.SetPrice, SetPrice: setPrice,
ActualBasePrice: actualBasePrice, ActualBasePrice: actualBasePrice,
} }

View File

@@ -6,6 +6,7 @@ import (
"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"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -76,11 +77,11 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP
productBasePrice := productConfig.BasePrice productBasePrice := productConfig.BasePrice
// 计算该产品的实际底价 // 计算该产品的实际底价
productActualBasePrice := productBasePrice + float64(levelBonus) productActualBasePrice := lzUtils.RoundMoney(productBasePrice + float64(levelBonus))
priceRangeMin := productActualBasePrice priceRangeMin := productActualBasePrice
upliftAmount, _ := l.getLevelMaxUpliftAmount(agentModel.Level) upliftAmount, _ := l.getLevelMaxUpliftAmount(agentModel.Level)
priceRangeMax := productConfig.SystemMaxPrice + upliftAmount priceRangeMax := lzUtils.RoundMoney(productConfig.SystemMaxPrice + upliftAmount)
// 使用产品配置的提价阈值和手续费比例如果为NULL则使用0 // 使用产品配置的提价阈值和手续费比例如果为NULL则使用0
productPriceThreshold := 0.0 productPriceThreshold := 0.0

View File

@@ -3,10 +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"
"github.com/zeromicro/go-zero/core/logx" "github.com/pkg/errors"
) )
type GetWhitelistFeaturesLogic struct { type GetWhitelistFeaturesLogic struct {
@@ -23,8 +25,21 @@ func NewGetWhitelistFeaturesLogic(ctx context.Context, svcCtx *svc.ServiceContex
} }
} }
// GetWhitelistFeatures 返回支持白名单屏蔽的 feature 列表whitelist_price > 0
func (l *GetWhitelistFeaturesLogic) GetWhitelistFeatures(req *types.GetWhitelistFeaturesReq) (resp *types.GetWhitelistFeaturesResp, err error) { func (l *GetWhitelistFeaturesLogic) GetWhitelistFeatures(req *types.GetWhitelistFeaturesReq) (resp *types.GetWhitelistFeaturesResp, err error) {
// todo: add your logic here and delete this line builder := l.svcCtx.FeatureModel.SelectBuilder().Where("whitelist_price > ?", 0)
list, err := l.svcCtx.FeatureModel.FindAll(l.ctx, builder, "id ASC")
return if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询功能列表失败, %v", err)
}
items := make([]types.WhitelistFeatureItem, 0, len(list))
for _, f := range list {
items = append(items, types.WhitelistFeatureItem{
FeatureId: f.Id,
FeatureApiId: f.ApiId,
FeatureName: f.Name,
WhitelistPrice: f.WhitelistPrice,
})
}
return &types.GetWhitelistFeaturesResp{List: items}, nil
} }

View File

@@ -5,7 +5,10 @@ import (
"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/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
@@ -23,8 +26,53 @@ func NewGetWhitelistListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *
} }
} }
func (l *GetWhitelistListLogic) GetWhitelistList(req *types.GetWhitelistListReq) (resp *types.GetWhitelistListResp, err error) { func statusToText(status int64) string {
// todo: add your logic here and delete this line switch status {
case 1:
return return "生效"
case 2:
return "已失效"
default:
return ""
}
}
func (l *GetWhitelistListLogic) GetWhitelistList(req *types.GetWhitelistListReq) (resp *types.GetWhitelistListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 10
}
builder := l.svcCtx.UserFeatureWhitelistModel.SelectBuilder().Where("user_id = ?", userID)
if req.IdCard != "" {
builder = builder.Where("id_card = ?", req.IdCard)
}
list, total, err := l.svcCtx.UserFeatureWhitelistModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询白名单列表失败, %v", err)
}
items := make([]types.WhitelistItem, 0, len(list))
for _, r := range list {
featureName := r.FeatureApiId
if f, e := l.svcCtx.FeatureModel.FindOne(l.ctx, r.FeatureId); e == nil {
featureName = f.Name
}
items = append(items, types.WhitelistItem{
Id: r.Id,
IdCard: r.IdCard,
FeatureId: r.FeatureId,
FeatureApiId: r.FeatureApiId,
FeatureName: featureName,
Amount: r.Amount,
Status: r.Status,
StatusText: statusToText(r.Status),
CreateTime: r.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetWhitelistListResp{Total: total, List: items}, nil
} }

View File

@@ -2,10 +2,14 @@ package agent
import ( import (
"context" "context"
"encoding/json"
"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/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
@@ -24,7 +28,44 @@ func NewOfflineFeatureLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Of
} }
func (l *OfflineFeatureLogic) OfflineFeature(req *types.OfflineFeatureReq) (resp *types.OfflineFeatureResp, err error) { func (l *OfflineFeatureLogic) OfflineFeature(req *types.OfflineFeatureReq) (resp *types.OfflineFeatureResp, err error) {
// todo: add your logic here and delete this line userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
if req.FeatureApiId == "" || req.QueryId == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("模块标识和查询记录ID不能为空"), "")
}
// 从 Query 获取 id_card
query, err := l.svcCtx.QueryModel.FindOne(l.ctx, req.QueryId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询记录不存在或查询失败, %v", err)
}
idCard := ""
if query.QueryParams != "" {
var params map[string]interface{}
if e := json.Unmarshal([]byte(query.QueryParams), &params); e == nil {
if v, ok := params["id_card"]; ok {
if s, ok := v.(string); ok {
idCard = s
}
}
}
}
if idCard == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("无法从查询记录中获取身份证号"), "")
}
needPay, amount, whitelistCreated, err := l.svcCtx.WhitelistService.ProcessOfflineFeature(l.ctx, nil, idCard, req.FeatureApiId, userID, req.QueryId)
if err != nil {
return nil, err
}
if whitelistCreated && !needPay {
if err := l.svcCtx.WhitelistService.DeleteFeatureFromQueryData(l.ctx, nil, req.QueryId, req.FeatureApiId); err != nil {
logx.Errorf("从报告数据中删除模块失败: %v", err)
}
return &types.OfflineFeatureResp{Success: true, NeedPay: false, Amount: 0}, nil
}
if needPay {
return &types.OfflineFeatureResp{Success: false, NeedPay: true, Amount: amount}, nil
}
return &types.OfflineFeatureResp{Success: true, NeedPay: false, Amount: 0}, nil
} }

View File

@@ -8,11 +8,13 @@ import (
"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/reviewphone"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
"strconv" "strconv"
"time" "time"
"github.com/go-sql-driver/mysql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/redis"
@@ -24,6 +26,36 @@ import (
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
func isMySQLDuplicateEntry(err error) bool {
var me *mysql.MySQLError
return errors.As(err, &me) && me.Number == 1062
}
// insertMobileUserAuthOrSkip 写入 mobile 认证;若并发重复插入触发唯一键冲突,且该手机号已绑定同一用户则视为成功(幂等)。
func (l *RegisterByInviteCodeLogic) insertMobileUserAuthOrSkip(ctx context.Context, session sqlx.Session, userID string, encryptedMobile string) error {
_, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: userID,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
})
if err == nil {
return nil
}
if !isMySQLDuplicateEntry(err) {
return err
}
exist, findErr := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(ctx, model.UserAuthTypeMobile, encryptedMobile)
if findErr != nil {
return findErr
}
if exist != nil && exist.UserId == userID {
l.Infof("[insertMobileUserAuthOrSkip] mobile 认证已由并发请求创建,跳过: userId=%s", userID)
return nil
}
return errors.Wrapf(xerr.NewErrMsg("该手机号已被占用"), "")
}
type RegisterByInviteCodeLogic struct { type RegisterByInviteCodeLogic struct {
logx.Logger logx.Logger
ctx context.Context ctx context.Context
@@ -48,8 +80,12 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
} }
l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile) l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile)
// 校验验证码开发环境跳过验证码校验) // 校验验证码开发环境跳过;审核预留号段 + 固定码走专用通道;其余走 Redis
if os.Getenv("ENV") != "development" && req.Code != "143838" { if os.Getenv("ENV") == "development" {
l.Infof("[RegisterByInviteCode] 开发环境跳过验证码校验")
} else if reviewphone.IsAppReviewDemoMobile(req.Mobile) && req.Code == reviewphone.DemoVerifyCode {
l.Infof("[RegisterByInviteCode] 审核体验号段固定验证码通过, mobile: %s", req.Mobile)
} else {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey) cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil { if err != nil {
@@ -64,8 +100,6 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "") return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
} }
l.Infof("[RegisterByInviteCode] 验证码校验通过, mobile: %s", req.Mobile) l.Infof("[RegisterByInviteCode] 验证码校验通过, mobile: %s", req.Mobile)
} else {
l.Infof("[RegisterByInviteCode] 开发环境跳过验证码校验")
} }
// 获取当前登录态(可能为空) // 获取当前登录态(可能为空)
@@ -471,14 +505,8 @@ func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, s
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id) l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id)
// 创建 mobile 认证 if err := l.insertMobileUserAuthOrSkip(ctx, session, newUser.Id, encryptedMobile); err != nil {
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{ return "", errors.Wrap(err, "创建手机号认证失败")
Id: uuid.NewString(),
UserId: newUser.Id,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id) l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id)
return newUser.Id, nil return newUser.Id, nil
@@ -496,14 +524,8 @@ func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, s
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err) return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID) l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID)
// 创建 mobile 认证 if err := l.insertMobileUserAuthOrSkip(ctx, session, currentUserID, encryptedMobile); err != nil {
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{ return "", errors.Wrap(err, "创建手机号认证失败")
Id: uuid.NewString(),
UserId: currentUserID,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID) l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID)
return currentUserID, nil return currentUserID, nil

View File

@@ -0,0 +1,91 @@
package app
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"github.com/zeromicro/go-zero/core/logx"
)
type GetHomeDynamicDataLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetHomeDynamicDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetHomeDynamicDataLogic {
return &GetHomeDynamicDataLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetHomeDynamicDataLogic) GetHomeDynamicData(req *types.GetHomeDynamicDataReq) (resp *types.GetHomeDynamicDataResp, err error) {
// 1. 获取真实查询案例 (支持增量查询)
// 增加日志输出,方便调试
l.Infof("获取首页动态数据, lastId: %d", req.LastId)
builder := l.svcCtx.InquiryRecordModel.SelectBuilder().Where("status = ?", 1)
var records []*model.InquiryRecord
if req.LastId > 0 {
// 增量获取大于 lastId 的记录
records, err = l.svcCtx.InquiryRecordModel.FindPageListByIdASC(l.ctx, builder, req.LastId, 50)
} else {
// 首次加载获取最新的 10 条
records, err = l.svcCtx.InquiryRecordModel.FindPageListByPage(l.ctx, builder, 1, 10, "id DESC")
}
if err != nil {
l.Errorf("查询查询记录失败: %v", err)
}
cases := make([]types.InquiryRecordItem, 0)
if err == nil && len(records) > 0 {
for _, r := range records {
cases = append(cases, types.InquiryRecordItem{
Id: r.Id,
Tag: r.InquiryTag,
Vin: r.VinMasked,
Model: r.CarModel,
})
}
} else {
// 如果数据库没有数据或查询失败,返回一些兜底的 Mock 数据,确保前端有展示
if req.LastId == 0 {
l.Info("使用兜底 Mock 案例数据")
mockCases := []struct {
Id int64
Tag string
Vin string
Model string
}{
{1, "查询成功", "湘A·12345", "奔驰GLC"},
{2, "查询成功", "粤B·67890", "丰田凯美瑞"},
{3, "查询成功", "京A·00011", "特斯拉ModelY"},
{4, "查询成功", "苏E·88888", "大众迈腾"},
{5, "查询成功", "沪A·87654", "宝马325"},
}
for _, m := range mockCases {
cases = append(cases, types.InquiryRecordItem{
Id: m.Id,
Tag: m.Tag,
Vin: m.Vin,
Model: m.Model,
})
}
}
}
// 2. 评价数据不再从后端返回,改为前端硬编码
reviews := make([]types.ReviewItem, 0)
return &types.GetHomeDynamicDataResp{
Cases: cases,
Reviews: reviews,
}, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/rand" "math/rand"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/captcha" "qnc-server/pkg/captcha"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
@@ -42,6 +43,20 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string, userAgent
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err) return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
} }
// 审核体验:预留号段申请代理时不发短信、不校验滑块,仅写入与注册校验一致的固定验证码
if req.ActionType == "agentApply" && reviewphone.IsAppReviewDemoMobile(req.Mobile) {
codeKey := fmt.Sprintf("%s:%s", req.ActionType, encryptedMobile)
limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile)
if err := l.svcCtx.Redis.Setex(codeKey, reviewphone.DemoVerifyCode, l.svcCtx.Config.VerifyCode.ValidTime); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 审核号写入验证码失败: %v", err)
}
if err := l.svcCtx.Redis.Setex(limitCodeKey, "1", 60); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 审核号限流标记失败: %v", err)
}
l.Infof("短信发送, 审核体验号段跳过真实短信, mobile=%s", req.Mobile)
return nil
}
// 1. 滑块验证码校验(可选,支持微信环境跳过验证) // 1. 滑块验证码校验(可选,支持微信环境跳过验证)
cfg := l.svcCtx.Config.Captcha cfg := l.svcCtx.Config.Captcha
captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{ captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{

View File

@@ -3,8 +3,12 @@ package pay
import ( import (
"context" "context"
"strings" "strings"
"qnc-server/app/main/api/internal/service"
"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/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -26,26 +30,98 @@ func NewPaymentCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Paym
} }
func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) { func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) {
// 根据订单号前缀判断订单类型
if strings.HasPrefix(req.OrderNo, "U_") { if strings.HasPrefix(req.OrderNo, "U_") {
// 升级订单 order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) if findErr != nil {
if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", findErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", err)
} }
return &types.PaymentCheckResp{ return &types.PaymentCheckResp{Type: "agent_upgrade", Status: order.Status}, nil
Type: "agent_upgrade",
Status: order.Status,
}, nil
} }
// 查询订单(包括代理订单) order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) if findErr != nil {
if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", findErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err)
} }
return &types.PaymentCheckResp{
Type: "query", resp = &types.PaymentCheckResp{Type: "query", Status: order.Status}
Status: order.Status,
}, nil // xpay 轮询pending 时主动查微信单并到账(不通知微信发货)
if order.Status == model.OrderStatusPending && model.IsXpayOrder(order) &&
l.svcCtx.XpayService != nil && l.svcCtx.XpayService.Enabled() {
wxInfo, syncErr := l.syncXpayOrderStatus(order)
resp.WxOrderStatus = wxInfo.Status
resp.WxOrderDetail = wxInfo.RawOrder
if syncErr != nil {
resp.WxSyncError = syncErr.Error()
l.Errorf("[xpay] 轮询查单失败 order_no=%s local_status=%s wx_status=%d wx_detail=%s err=%v",
req.OrderNo, order.Status, wxInfo.Status, wxInfo.RawOrder, syncErr)
} else if wxInfo.Status > 0 && !service.IsXpayPaidStatus(wxInfo.Status) {
l.Infof("[xpay] 轮询查单未到账 order_no=%s wx_status=%d wx_detail=%s",
req.OrderNo, wxInfo.Status, wxInfo.RawOrder)
}
order, _ = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
resp.Status = order.Status
}
return resp, nil
}
type xpaySyncInfo struct {
Status int
RawOrder string
}
func (l *PaymentCheckLogic) syncXpayOrderStatus(order *model.Order) (xpaySyncInfo, error) {
info := xpaySyncInfo{}
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return info, err
}
if order.UserId != userID {
return info, errors.New("无权查询此订单")
}
openid, err := l.svcCtx.XpayService.GetWxMiniOpenID(l.ctx, l.svcCtx.UserAuthModel, userID)
if err != nil {
return info, err
}
l.Infof("[xpay] check sync start order_no=%s openid=%s env=%d", order.OrderNo, openid, l.svcCtx.XpayService.Env())
status, err := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, order.OrderNo, "")
if err != nil {
return info, err
}
info.Status = status.Status
info.RawOrder = status.RawOrder
if service.IsXpayPaidStatus(status.Status) {
_, fulfillErr := fulfillQueryOrderPaid(l.ctx, l.svcCtx, order, status.WxOrderID, status.PaidFee)
if fulfillErr != nil {
return info, fulfillErr
}
l.Infof("[xpay] check sync paid order_no=%s wx_order_id=%s paid_fee=%d",
order.OrderNo, status.WxOrderID, status.PaidFee)
return info, nil
}
if service.IsXpayRefundedStatus(status.Status) || service.IsXpayAlreadyRefunded(status) {
order.Status = model.OrderStatusRefunded
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil {
return info, updateErr
}
l.Infof("[xpay] check sync refunded order_no=%s wx_status=%d left_fee=%d", order.OrderNo, status.Status, status.LeftFee)
return info, nil
}
if service.IsXpayClosedStatus(status.Status) {
order.Status = model.OrderStatusClosed
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil {
return info, updateErr
}
l.Infof("[xpay] check sync closed order_no=%s wx_status=%d", order.OrderNo, status.Status)
return info, nil
}
return info, nil
} }

View File

@@ -33,6 +33,7 @@ type PaymentTypeResp struct {
outTradeNo string outTradeNo string
description string description string
orderID string // 订单ID用于开发环境测试支付模式 orderID string // 订单ID用于开发环境测试支付模式
productEn string // 产品英文名xpay 道具 ID
} }
// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能 // enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能
@@ -50,6 +51,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
var paymentTypeResp *PaymentTypeResp var paymentTypeResp *PaymentTypeResp
var prepayData interface{} var prepayData interface{}
var orderID string var orderID string
useXpay := l.shouldUseXpay(req)
// 检查是否为开发环境的测试支付模式 // 检查是否为开发环境的测试支付模式
env := os.Getenv("ENV") env := os.Getenv("ENV")
@@ -65,7 +67,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
} }
case "query": case "query":
paymentTypeResp, err = l.QueryOrderPayment(req, session) paymentTypeResp, err = l.QueryOrderPayment(req, session, useXpay)
if err != nil { if err != nil {
return err return err
} }
@@ -95,9 +97,11 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
return nil return nil
} }
// 正常支付流程 // 正常支付流程(微信小程序 xpay 在事务外构造支付参数)
var createOrderErr error var createOrderErr error
if req.PayMethod == "wechat" { if useXpay {
// 订单已在 QueryOrderPayment 中创建,此处跳过 JSAPI 预下单
} else if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, req.Code) 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)
@@ -113,6 +117,26 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
return nil, err return nil, err
} }
// 微信小程序虚拟支付:构造 signData / paySig / signature
if useXpay && paymentTypeResp != nil {
userID, uidErr := ctxdata.GetUidFromCtx(l.ctx)
if uidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败")
}
xpayParams, buildErr := l.svcCtx.XpayService.BuildPayParams(l.ctx, userID, paymentTypeResp.outTradeNo, paymentTypeResp.productEn, paymentTypeResp.amount)
if buildErr != nil {
logx.WithContext(l.ctx).Errorf("[xpay] BuildPayParams FAIL user=%s order_no=%s amount=%.2f product_en=%s env=%d err=%v",
userID, paymentTypeResp.outTradeNo, paymentTypeResp.amount, paymentTypeResp.productEn, l.svcCtx.XpayService.Env(), buildErr)
if strings.Contains(buildErr.Error(), "过期") || strings.Contains(buildErr.Error(), "会话") {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, buildErr.Error()), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "构造虚拟支付参数失败: %v", buildErr)
}
logx.WithContext(l.ctx).Infof("[xpay] BuildPayParams OK user=%s order_no=%s env=%d signData=%s",
userID, paymentTypeResp.outTradeNo, l.svcCtx.XpayService.Env(), xpayParams.SignData)
prepayData = xpayParams
}
// 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程 // 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程
if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != "" { if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != "" {
// 使用 goroutine 异步处理,确保事务已完全提交 // 使用 goroutine 异步处理,确保事务已完全提交
@@ -205,7 +229,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
} }
} }
func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) { func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Session, useXpay bool) (resp *PaymentTypeResp, err error) {
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx) userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil { if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr)
@@ -251,13 +275,44 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
if user.Inside == 1 { if user.Inside == 1 {
amount = 0.01 amount = 0.01
} }
// 同一 queryId 重复发起支付:复用已有订单,避免 order_no 唯一键冲突导致二次支付失败
if existingOrder, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, outTradeNo); findOrderErr == nil {
switch existingOrder.Status {
case model.OrderStatusPaid:
return nil, errors.Wrapf(xerr.NewErrMsg("订单已支付,请前往报告查看"), "订单号: %s", outTradeNo)
case model.OrderStatusPending:
if existingOrder.Status != model.OrderStatusPending {
existingOrder.Status = model.OrderStatusPending
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, session, existingOrder); updateErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "重置订单状态失败: %+v", updateErr)
}
}
return &PaymentTypeResp{
amount: existingOrder.Amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: existingOrder.Id,
productEn: product.ProductEn,
}, nil
default:
return nil, errors.Wrapf(xerr.NewErrMsg("订单状态异常,请重新发起查询"), "status=%s", existingOrder.Status)
}
} else if !errors.Is(findOrderErr, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %+v", findOrderErr)
}
paymentPlatform := req.PayMethod
if useXpay {
paymentPlatform = model.PaymentPlatformWechatXpay
}
order := model.Order{ order := model.Order{
Id: uuid.NewString(), Id: uuid.NewString(),
OrderNo: outTradeNo, OrderNo: outTradeNo,
UserId: userID, UserId: userID,
ProductId: product.Id, ProductId: product.Id,
PaymentPlatform: req.PayMethod, PaymentPlatform: paymentPlatform,
PaymentScene: "app", PaymentScene: resolvePaymentScene(l.ctx),
Amount: amount, Amount: amount,
Status: "pending", Status: "pending",
} }
@@ -292,7 +347,7 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
// 使用产品配置的底价计算实际底价 // 使用产品配置的底价计算实际底价
basePrice := productConfig.BasePrice basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus) actualBasePrice := lzUtils.RoundMoney(basePrice + float64(levelBonus))
// 计算提价成本(使用产品配置) // 计算提价成本(使用产品配置)
priceThreshold := 0.0 priceThreshold := 0.0
@@ -306,11 +361,11 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
priceCost := 0.0 priceCost := 0.0
if agentLinkModel.SetPrice > priceThreshold { if agentLinkModel.SetPrice > priceThreshold {
priceCost = (agentLinkModel.SetPrice - priceThreshold) * priceFeeRate priceCost = lzUtils.RoundMoney((agentLinkModel.SetPrice - priceThreshold) * priceFeeRate)
} }
// 计算代理收益 // 计算代理收益
agentProfit := agentLinkModel.SetPrice - actualBasePrice - priceCost agentProfit := lzUtils.RoundMoney(agentLinkModel.SetPrice - actualBasePrice - priceCost)
// 创建代理订单记录 // 创建代理订单记录
agentOrder := model.AgentOrder{ agentOrder := model.AgentOrder{
@@ -330,7 +385,35 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert)
} }
} }
return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName, orderID: orderID}, nil return &PaymentTypeResp{
amount: amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: orderID,
productEn: product.ProductEn,
}, nil
}
func (l *PaymentLogic) shouldUseXpay(req *types.PaymentReq) bool {
if req.PayMethod != "wechat" || req.PayType != "query" {
return false
}
if l.svcCtx.XpayService == nil || !l.svcCtx.XpayService.Enabled() {
return false
}
platform, err := ctxdata.GetPlatformFromCtx(l.ctx)
if err != nil || platform != model.PlatformWxMini {
return false
}
return true
}
func resolvePaymentScene(ctx context.Context) string {
platform, err := ctxdata.GetPlatformFromCtx(ctx)
if err != nil {
return model.PaymentSceneApp
}
return model.PaymentSceneFromPlatform(platform)
} }
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代) // AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)

View File

@@ -107,6 +107,12 @@ func (l *WechatPayRefundCallbackLogic) handleQueryOrderRefund(orderNo string, st
return errors.Wrapf(err, "更新退款记录状态失败: orderNo=%s", orderNo) return errors.Wrapf(err, "更新退款记录状态失败: orderNo=%s", orderNo)
} }
if status == refunddomestic.STATUS_SUCCESS {
if err := l.svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(ctx, session, order.Id); err != nil {
return err
}
}
return nil return nil
}) })

View File

@@ -0,0 +1,87 @@
package pay
import (
"context"
"strings"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
)
// XpayDeliverResult 同步 xpay 支付状态(查微信单 → 本地到账 → 异步查报告)
type XpayDeliverResult struct {
Credited bool `json:"credited"`
Notified bool `json:"notified"`
WechatDetail string `json:"wechat_detail"`
Errors []string `json:"errors"`
Message string `json:"message"`
}
// DeliverXpayQueryOrder 按商户订单号同步微信虚拟支付到账(不通知微信发货,发货在报告成功后)
func DeliverXpayQueryOrder(ctx context.Context, svcCtx *svc.ServiceContext, orderNo string) (*XpayDeliverResult, error) {
order, err := svcCtx.OrderModel.FindOneByOrderNo(ctx, orderNo)
if err != nil {
return nil, err
}
resp := &XpayDeliverResult{WechatDetail: "ok"}
if order.Status == model.OrderStatusPaid {
resp.Message = "订单已支付,无需同步"
return resp, nil
}
if !model.IsXpayOrder(order) {
resp.Errors = append(resp.Errors, "非小程序虚拟支付订单")
resp.Message = "非小程序虚拟支付订单"
return resp, nil
}
if svcCtx.XpayService == nil || !svcCtx.XpayService.Enabled() {
resp.Errors = append(resp.Errors, "虚拟支付未启用")
resp.Message = "虚拟支付未启用"
return resp, nil
}
openid, err := svcCtx.XpayService.GetWxMiniOpenID(ctx, svcCtx.UserAuthModel, order.UserId)
if err != nil {
resp.Errors = append(resp.Errors, err.Error())
resp.Message = "获取用户 openid 失败"
return resp, nil
}
status, qErr := svcCtx.XpayService.QueryOrder(ctx, openid, order.OrderNo, "")
if qErr != nil {
resp.Errors = append(resp.Errors, qErr.Error())
resp.WechatDetail = qErr.Error()
resp.Message = "查询微信订单失败"
return resp, nil
}
if !service.IsXpayPaidStatus(status.Status) {
resp.Errors = append(resp.Errors, "微信侧订单未支付")
resp.Message = "微信侧订单未支付"
return resp, nil
}
platformOrderID := status.WxOrderID
credited, fulfillErr := fulfillQueryOrderPaid(ctx, svcCtx, order, platformOrderID, status.PaidFee)
resp.Credited = credited
if fulfillErr != nil {
resp.Errors = append(resp.Errors, fulfillErr.Error())
}
resp.Message = buildXpayDeliverMessage(resp)
return resp, nil
}
func buildXpayDeliverMessage(r *XpayDeliverResult) string {
if len(r.Errors) > 0 {
return strings.Join(r.Errors, "")
}
if r.Credited {
return "同步成功,订单已到账并开始查询报告"
}
return "未执行到账"
}

View File

@@ -0,0 +1,48 @@
package pay
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
"github.com/zeromicro/go-zero/core/logx"
)
// NotifyXpayGoodsAfterReport 报告生成成功后通知微信虚拟支付已发货(合规/结算)
func NotifyXpayGoodsAfterReport(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order) {
if order == nil || !model.IsXpayOrder(order) {
return
}
if svcCtx.XpayService == nil || !svcCtx.XpayService.Enabled() {
return
}
already, err := svcCtx.XpayService.AlreadyNotified(ctx, order.OrderNo)
if err != nil {
logx.WithContext(ctx).Errorf("[xpay] 检查发货通知状态失败 order_no=%s err=%v", order.OrderNo, err)
return
}
if already {
return
}
openid, err := svcCtx.XpayService.GetWxMiniOpenID(ctx, svcCtx.UserAuthModel, order.UserId)
if err != nil {
logx.WithContext(ctx).Errorf("[xpay] 报告成功后通知发货:获取 openid 失败 order_no=%s err=%v", order.OrderNo, err)
return
}
sessionKey, _ := svcCtx.XpayService.GetSessionKey(ctx, order.UserId)
wxOrderID := ""
if order.PlatformOrderId.Valid {
wxOrderID = order.PlatformOrderId.String
}
if notifyErr := svcCtx.XpayService.NotifyProvideGoods(ctx, openid, order.OrderNo, wxOrderID, sessionKey); notifyErr != nil {
logx.WithContext(ctx).Errorf("[xpay] 报告成功后 notify_provide_goods 失败 order_no=%s err=%v", order.OrderNo, notifyErr)
return
}
_ = svcCtx.XpayService.MarkNotified(ctx, order.OrderNo)
logx.WithContext(ctx).Infof("[xpay] 报告成功后已通知微信发货 order_no=%s", order.OrderNo)
}

View File

@@ -0,0 +1,84 @@
package pay
import (
"context"
"fmt"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// RefundXpayQueryOrder 对 xpay 查询订单发起全额退款并更新本地订单状态
func RefundXpayQueryOrder(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order) error {
if svcCtx.XpayService == nil || !svcCtx.XpayService.Enabled() {
return fmt.Errorf("虚拟支付未启用")
}
openid, err := svcCtx.XpayService.GetWxMiniOpenID(ctx, svcCtx.UserAuthModel, order.UserId)
if err != nil {
return fmt.Errorf("获取 openid 失败: %w", err)
}
refundFeeFen := lzUtils.ToWechatAmount(order.Amount)
refundOrderID := buildXpayRefundOrderID(order.OrderNo)
if refundErr := svcCtx.XpayService.RefundOrder(ctx, openid, order.OrderNo, refundOrderID, refundFeeFen); refundErr != nil {
return refundErr
}
logx.WithContext(ctx).Infof("[xpay] refund local sync order_no=%s", order.OrderNo)
return svcCtx.OrderModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
order.Status = model.OrderStatusRefunded
if updateErr := svcCtx.OrderModel.UpdateWithVersion(transCtx, session, order); updateErr != nil {
return updateErr
}
return svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(transCtx, session, order.Id)
})
}
func buildXpayRefundOrderID(orderNo string) string {
const prefix = "RF_"
id := prefix + orderNo
if len(id) > 32 {
id = id[:32]
}
if len(id) < 8 {
id = id + "00000000"
id = id[:8]
}
return id
}
// TryRefundOnQueryFailure 查询失败时按支付渠道退款
func TryRefundOnQueryFailure(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order) {
var refundErr error
switch {
case model.IsXpayOrder(order):
refundErr = RefundXpayQueryOrder(ctx, svcCtx, order)
case order.PaymentPlatform == model.PaymentPlatformWechat:
refundErr = svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount)
default:
refund, err := svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
if err != nil {
refundErr = err
} else if !refund.IsSuccess() {
refundErr = fmt.Errorf("支付宝退款失败: %s", refund.Msg)
} else {
transErr := svcCtx.OrderModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
order.Status = model.OrderStatusRefunded
if err := svcCtx.OrderModel.UpdateWithVersion(transCtx, session, order); err != nil {
return err
}
return svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(transCtx, session, order.Id)
})
if transErr != nil {
refundErr = transErr
}
}
}
if refundErr != nil {
logx.WithContext(ctx).Errorf("[refund] 查询失败自动退款未成功 order_no=%s platform=%s err=%v", order.OrderNo, order.PaymentPlatform, refundErr)
}
}

View File

@@ -0,0 +1,65 @@
package pay
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/ctxdata"
"github.com/zeromicro/go-zero/core/logx"
)
type XpayClientEventLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewXpayClientEventLogic(ctx context.Context, svcCtx *svc.ServiceContext) *XpayClientEventLogic {
return &XpayClientEventLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// XpayClientEvent 记录小程序虚拟支付各步骤及微信/Apple 客户端提示(非 HTTP 异常)
func (l *XpayClientEventLogic) XpayClientEvent(req *types.XpayClientEventReq) (*types.XpayClientEventResp, error) {
userID, _ := ctxdata.GetUidFromCtx(l.ctx)
l.Infof("[xpay:client-event] user=%s order_no=%s stage=%s event=%s message=%q err_msg=%q err_code=%d errno=%d sign_data=%s device=%s extra=%s",
userID, req.OrderNo, req.Stage, req.EventType, req.Message, req.ErrMsg, req.ErrCode, req.Errno,
req.SignData, req.Device, req.Extra)
resp := &types.XpayClientEventResp{}
if req.OrderNo == "" || l.svcCtx.XpayService == nil || !l.svcCtx.XpayService.Enabled() {
return resp, nil
}
// 客户端 fail/tip 时主动 query_order把微信服务端侧状态一并记入日志
if req.EventType == "fail" || req.EventType == "tip" {
openid, err := l.svcCtx.XpayService.GetWxMiniOpenID(l.ctx, l.svcCtx.UserAuthModel, userID)
if err != nil {
resp.WxSyncError = "获取 openid 失败: " + err.Error()
l.Errorf("[xpay:client-event] query_order skip order_no=%s reason=%v", req.OrderNo, err)
return resp, nil
}
status, qErr := l.svcCtx.XpayService.QueryOrder(l.ctx, openid, req.OrderNo, "")
if qErr != nil {
resp.WxSyncError = qErr.Error()
l.Errorf("[xpay:client-event] query_order FAIL order_no=%s client_msg=%q wx_err=%v",
req.OrderNo, req.Message, qErr)
return resp, nil
}
resp.WxOrderStatus = status.Status
resp.WxOrderDetail = status.RawOrder
l.Infof("[xpay:client-event] query_order SNAPSHOT order_no=%s client_msg=%q wx_status=%d wx_order_id=%s paid_fee=%d err_msg=%s raw=%s",
req.OrderNo, req.Message, status.Status, status.WxOrderID, status.PaidFee, status.ErrMsg, status.RawOrder)
}
return resp, nil
}

View File

@@ -0,0 +1,54 @@
package pay
import (
"context"
"database/sql"
"time"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/model"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/zeromicro/go-zero/core/logx"
)
// fulfillQueryOrderPaid 幂等将查询订单标记为已支付并触发报告生成
// wechatPaidFen微信侧实付金额>0 时须与订单 amount 一致
func fulfillQueryOrderPaid(ctx context.Context, svcCtx *svc.ServiceContext, order *model.Order, platformOrderID string, wechatPaidFen int64) (credited bool, err error) {
if order.Status == "paid" {
return false, nil
}
orderFen := lzUtils.ToWechatAmount(order.Amount)
if wechatPaidFen > 0 && wechatPaidFen != orderFen {
if model.IsXpayOrder(order) {
order.Amount = lzUtils.RoundMoney(float64(wechatPaidFen) / 100)
orderFen = wechatPaidFen
logx.WithContext(ctx).Infof("[xpay] 同步微信实付金额 order_no=%s amount=%.2f fen=%d", order.OrderNo, order.Amount, wechatPaidFen)
} else {
logx.WithContext(ctx).Errorf("[xpay] 金额不一致 order_no=%s order_fen=%d wechat_fen=%d", order.OrderNo, orderFen, wechatPaidFen)
return false, nil
}
}
order.Status = "paid"
order.PayTime = sql.NullTime{Time: time.Now(), Valid: true}
if platformOrderID != "" {
order.PlatformOrderId = sql.NullString{String: platformOrderID, Valid: true}
}
if order.PaymentScene == "" || order.PaymentScene == model.PaymentSceneApp {
order.PaymentScene = model.PaymentSceneMiniProgram
}
if updateErr := svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order); updateErr != nil {
return false, updateErr
}
if asyncErr := svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil {
logx.WithContext(ctx).Errorf("[xpay] SendQueryTask 失败 order_id=%s err=%v", order.Id, asyncErr)
return true, asyncErr
}
logx.WithContext(ctx).Infof("[xpay] 订单到账 order_no=%s order_id=%s", order.OrderNo, order.Id)
return true, nil
}

View File

@@ -0,0 +1,101 @@
package pay
import (
"encoding/json"
"io"
"net/http"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type XpayPushLogic struct {
logx.Logger
svcCtx *svc.ServiceContext
}
func NewXpayPushLogic(svcCtx *svc.ServiceContext) *XpayPushLogic {
return &XpayPushLogic{svcCtx: svcCtx}
}
// HandleGET mp 后台配置消息推送时的验签
func (l *XpayPushLogic) HandleGET(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
signature := q.Get("signature")
timestamp := q.Get("timestamp")
nonce := q.Get("nonce")
echostr := q.Get("echostr")
if l.svcCtx.XpayService.VerifyPushSignature(signature, timestamp, nonce) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(echostr))
return
}
w.WriteHeader(http.StatusForbidden)
}
// HandlePOST 接收 xpay_goods_deliver_notify
func (l *XpayPushLogic) HandlePOST(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
l.writePushResp(w, 1, "read body failed")
return
}
var notify service.XpayDeliverNotify
if err := json.Unmarshal(body, &notify); err != nil {
l.writePushResp(w, 1, "parse failed")
return
}
if notify.Event != "" && notify.Event != "xpay_goods_deliver_notify" {
l.writePushResp(w, 0, "ignored")
return
}
orderNo := notify.OutTradeNo
if orderNo == "" {
l.writePushResp(w, 1, "missing OutTradeNo")
return
}
ctx := r.Context()
logx.WithContext(ctx).Infof("[xpay:push] recv event=%s order_no=%s openid=%s env=%d body=%s",
notify.Event, orderNo, notify.OpenId, notify.Env, string(body))
already, _ := l.svcCtx.XpayService.AlreadyNotified(ctx, orderNo)
if already {
l.writePushResp(w, 0, "already notified")
return
}
order, findErr := l.svcCtx.OrderModel.FindOneByOrderNo(ctx, orderNo)
if findErr != nil {
logx.WithContext(ctx).Errorf("[xpay push] 订单不存在 order_no=%s err=%v", orderNo, findErr)
l.writePushResp(w, 1, "order not found")
return
}
wxOrderID := notify.WeChatPayInfo.TransactionId
credited, fulfillErr := fulfillQueryOrderPaid(ctx, l.svcCtx, order, wxOrderID, notify.GoodsInfo.ActualPrice)
if fulfillErr != nil {
logx.WithContext(ctx).Errorf("[xpay push] 到账失败 order_no=%s err=%v", orderNo, fulfillErr)
l.writePushResp(w, 1, fulfillErr.Error())
return
}
_ = l.svcCtx.XpayService.MarkNotified(ctx, orderNo)
logx.WithContext(ctx).Infof("[xpay push] event=xpay_goods_deliver_notify order_no=%s credited=%v", orderNo, credited)
l.writePushResp(w, 0, "success")
}
func (l *XpayPushLogic) writePushResp(w http.ResponseWriter, errCode int, errMsg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"ErrCode": errCode,
"ErrMsg": errMsg,
})
}

View File

@@ -0,0 +1,51 @@
package productcost
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/common/xerr"
"github.com/pkg/errors"
)
// SyncProductCostFromModules 将产品 cost_price 更新为当前关联模块的 feature.cost_price 之和
func SyncProductCostFromModules(ctx context.Context, c *svc.ServiceContext, productId string) error {
sum, err := c.ProductFeatureModel.SumFeatureCostPriceByProductId(ctx, productId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"汇总产品模块成本失败, err: %v, productId: %s", err, productId)
}
record, err := c.ProductModel.FindOne(ctx, productId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"查找产品失败, err: %v, productId: %s", err, productId)
}
record.CostPrice = sum
if _, err = c.ProductModel.Update(ctx, nil, record); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"同步产品成本失败, err: %v, productId: %s", err, productId)
}
return nil
}
// SyncAllProductsUsingFeature 在模块成本等变更后,刷新所有关联了该模块的产品的 cost_price
func SyncAllProductsUsingFeature(ctx context.Context, c *svc.ServiceContext, featureId string) error {
builder := c.ProductFeatureModel.SelectBuilder().Where("feature_id = ?", featureId)
list, err := c.ProductFeatureModel.FindAll(ctx, builder, "")
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"查询产品模块关联失败, err: %v, featureId: %s", err, featureId)
}
seen := make(map[string]struct{})
for _, pf := range list {
if _, ok := seen[pf.ProductId]; ok {
continue
}
seen[pf.ProductId] = struct{}{}
if err := SyncProductCostFromModules(ctx, c, pf.ProductId); err != nil {
return err
}
}
return nil
}

View File

@@ -115,6 +115,8 @@ func BuildEncryptedQuery(ctx context.Context, svcCtx *svc.ServiceContext, queryM
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)
} }
// 显式写入查询记录 ID供前端报告页「模块下架」等白名单能力使用
query.Id = queryModel.Id
product, err := svcCtx.ProductModel.FindOne(ctx, queryModel.ProductId) product, err := svcCtx.ProductModel.FindOne(ctx, queryModel.ProductId)
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)

View File

@@ -6,12 +6,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"time"
"qnc-server/app/main/api/internal/service" "qnc-server/app/main/api/internal/service"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
"qnc-server/pkg/lzkit/validator" "qnc-server/pkg/lzkit/validator"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/redis"
@@ -55,6 +55,21 @@ var productProcessors = map[string]func(*QueryServiceLogic, *types.QueryServiceR
"backgroundcheck": (*QueryServiceLogic).ProcessBackgroundCheckLogic, "backgroundcheck": (*QueryServiceLogic).ProcessBackgroundCheckLogic,
"personalData": (*QueryServiceLogic).ProcessPersonalDataLogic, "personalData": (*QueryServiceLogic).ProcessPersonalDataLogic,
"consumerFinanceReport": (*QueryServiceLogic).ProcessConsumerFinanceReportLogic, "consumerFinanceReport": (*QueryServiceLogic).ProcessConsumerFinanceReportLogic,
// 车辆类产品
"toc_VehiclesUnderNameCount": (*QueryServiceLogic).ProcessVehiclesUnderNameCountLogic,
"toc_VehicleStaticInfo": (*QueryServiceLogic).ProcessVehicleVinCodeLogic,
"toc_VehicleMileageMixed": (*QueryServiceLogic).ProcessVehicleMileageMixedLogic,
"toc_VehicleVinValuation": (*QueryServiceLogic).ProcessVehicleVinValuationLogic,
"toc_VehicleTransferSimple": (*QueryServiceLogic).ProcessVehicleTransferSimpleLogic,
"toc_VehicleTransferDetail": (*QueryServiceLogic).ProcessVehicleVinCodeLogic,
"toc_VehicleMaintenanceSimple": (*QueryServiceLogic).ProcessVehicleMaintenanceSimpleLogic,
"toc_VehicleMaintenanceDetail": (*QueryServiceLogic).ProcessVehicleMaintenanceDetailLogic,
"toc_VehicleClaimDetail": (*QueryServiceLogic).ProcessVehicleClaimDetailLogic,
"toc_VehicleClaimVerify": (*QueryServiceLogic).ProcessVehicleClaimVerifyLogic,
"toc_VehiclesUnderName": (*QueryServiceLogic).ProcessMarriageLogic,
"toc_VehiclesUnderNamePlate": (*QueryServiceLogic).ProcessMarriageLogic,
"toc_PersonVehicleVerification": (*QueryServiceLogic).ProcessPersonVehicleVerificationLogic,
"toc_PersonVehicleVerificationDetail": (*QueryServiceLogic).ProcessPersonVehicleVerificationLogic,
} }
func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) { func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) {
@@ -709,6 +724,435 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str
return outTradeNo, nil return outTradeNo, nil
} }
// ---- 车辆类产品处理方法 ----
// ProcessVehiclesUnderNameCountLogic 名下车辆(数量):仅 user_type + id_card
func (l *QueryServiceLogic) ProcessVehiclesUnderNameCountLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehiclesUnderNameCountReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
userType := data.UserType
if userType == "" {
userType = "1"
}
params := map[string]interface{}{
"user_type": userType,
"id_card": data.IDCard,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehiclesUnderNameCount", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleVinCodeLogic 仅 vin_code车辆静态信息、过户详版等
func (l *QueryServiceLogic) ProcessVehicleVinCodeLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleVinCodeReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
}
product := req.Product
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, product, userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleMileageMixedLogic 车辆里程记录(混合) QCXG1U4U
func (l *QueryServiceLogic) ProcessVehicleMileageMixedLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleMileageMixedReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
"image_url": data.ImageURL,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleMileageMixed", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleVinValuationLogic 二手车VIN估值 QCXGY7F2
func (l *QueryServiceLogic) ProcessVehicleVinValuationLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleVinValuationReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
"vehicle_location": data.VehicleLocation,
"first_registrationdate": data.FirstRegistrationDate,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleVinValuation", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleTransferSimpleLogic 车辆过户简版 QCXG1H7Y
func (l *QueryServiceLogic) ProcessVehicleTransferSimpleLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleTransferSimpleReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleTransferSimple", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleMaintenanceSimpleLogic 车辆维保简版 QCXG3Y6B
func (l *QueryServiceLogic) ProcessVehicleMaintenanceSimpleLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleMaintenanceSimpleReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleMaintenanceSimple", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleMaintenanceDetailLogic 车辆维保详细版 QCXG3Z3L
func (l *QueryServiceLogic) ProcessVehicleMaintenanceDetailLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleMaintenanceDetailReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleMaintenanceDetail", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleClaimDetailLogic 车辆出险详版 QCXGP00W
func (l *QueryServiceLogic) ProcessVehicleClaimDetailLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleClaimDetailReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
params := map[string]interface{}{
"vin_code": data.VinCode,
"vlphoto_data": data.VlphotoData,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleClaimDetail", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessVehicleClaimVerifyLogic 车辆出险记录核验 QCXG6B4E
func (l *QueryServiceLogic) ProcessVehicleClaimVerifyLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocVehicleClaimVerifyReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
auth := data.Authorized
if auth == "" {
auth = "1"
}
params := map[string]interface{}{
"vin_code": data.VINCode,
"authorized": auth,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "toc_VehicleClaimVerify", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// ProcessPersonVehicleVerificationLogic 人车核验简版:姓名+号牌类型+车牌号
func (l *QueryServiceLogic) ProcessPersonVehicleVerificationLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
var data types.TocPersonVehicleVerification
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
params := map[string]interface{}{
"name": data.Name,
"plate_no": data.CarLicense,
"carplate_type": data.CarType,
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, req.Product, userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// GetOrCreateUser 获取或创建用户 // GetOrCreateUser 获取或创建用户
// 1. 如果已登录,使用当前登录用户 // 1. 如果已登录,使用当前登录用户
// 2. 如果未登录创建临时用户UUID用户 // 2. 如果未登录创建临时用户UUID用户

View File

@@ -0,0 +1,921 @@
package query
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"time"
"tyc-server/app/main/api/internal/service"
"tyc-server/app/main/model"
"tyc-server/common/ctxdata"
"tyc-server/common/globalkey"
"tyc-server/common/xerr"
"tyc-server/pkg/captcha"
"tyc-server/pkg/lzkit/crypto"
"tyc-server/pkg/lzkit/validator"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryServiceLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryServiceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServiceLogic {
return &QueryServiceLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryServiceLogic) QueryService(req *types.QueryServiceReq) (resp *types.QueryServiceResp, err error) {
if req.AgentIdentifier != "" {
l.ctx = context.WithValue(l.ctx, "agentIdentifier", req.AgentIdentifier)
} else if req.App {
l.ctx = context.WithValue(l.ctx, "app", req.App)
}
return l.PreprocessLogic(req, req.Product)
}
// queryHandlerFunc 通用查询 handler解密后的数据 + 产品名 -> 校验并返回待缓存的 params
// 新增产品时只需在 productHandlers 里加一行并选用已有 handler 类型即可,无需再写 ProcessXxx
type queryHandlerFunc func(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error)
var productHandlers = map[string]queryHandlerFunc{
"marriage": runMarriageReq,
"homeservice": runMarriageReq,
"riskassessment": runMarriageReq,
"companyinfo": runMarriageReq,
"rentalinfo": runMarriageReq,
"preloanbackgroundcheck": runMarriageReq,
"backgroundcheck": runBackgroundCheckReq,
"personalData": runMarriageReq,
"toc_PersonalBadRecord": runPersonalBadRecordReq,
"toc_PersonalLawsuit": runMarriageReq,
"toc_EnterpriseLawsuit": runEntLawsuitReq,
// 人企关系加强版:仅身份证号
"toc_PersonEnterprisePro": runPersonEnterpriseProReq,
// 新司法涉诉类产品
"toc_EnterpriseLawsuitQYGL66SL": runEnterpriseLawsuitSimpleReq,
"toc_LimitHighExecuted": runLimitHighExecutedReq,
"toc_DishonestExecutedPerson": runDishonestExecutedReq,
"toc_Marriage": runMarriageReq,
"toc_PersonalMarriageStatus": runMarriageReq,
"toc_MarriageStatusRegisterTime": runMarriageReq,
"toc_MarriageStatusSupplement": runMarriageReq,
"toc_MarriageStatusVerify": runMarriageReq,
"toc_DualMarriageStatusRegisterTime": runDualMarriageReq,
"toc_VehiclesUnderName": runMarriageReq,
"toc_VehiclesUnderNamePlate": runMarriageReq,
"toc_PersonVehicleVerification": runPersonVehicleVerificationReq,
"toc_PersonVehicleVerificationDetail": runPersonVehicleVerificationReq,
// 车辆类产品(按 md 传参)
"toc_VehiclesUnderNameCount": runVehiclesUnderNameCountReq,
"toc_VehicleStaticInfo": runVehicleVinCodeReq,
"toc_VehicleMileageMixed": runVehicleMileageMixedReq,
"toc_VehicleVinValuation": runVehicleVinValuationReq,
"toc_VehicleTransferSimple": runVehicleTransferSimpleReq,
"toc_VehicleTransferDetail": runVehicleVinCodeReq,
"toc_VehicleMaintenanceSimple": runVehicleMaintenanceSimpleReq,
"toc_VehicleMaintenanceDetail": runVehicleMaintenanceDetailReq,
"toc_VehicleClaimDetail": runVehicleClaimDetailReq,
"toc_VehicleClaimVerify": runVehicleClaimVerifyReq,
// 核验工具verify feature.md
"toc_PoliceTwoFactors": runVerifyAuthTwoReq,
"toc_PoliceThreeFactors": runVerifyAuthThreeReq,
"toc_ProfessionalCertificate": runVerifyCertReq,
"toc_PersonalConsumptionCapacityLevel": runVerifyConsumptionReq, // 个人消费能力(沿用现有 product_en
"toc_OperatorTwoFactors": runVerifyYysTwoReq,
"toc_MobileThreeFactors": runVerifyYysThreeReq,
"toc_NumberRecycle": runVerifyMobileOnlyReq,
"toc_MobileEmptyCheck": runVerifyMobileOnlyReq,
"toc_MobilePortability": runVerifyMobileOnlyReq,
"toc_MobileOnlineStatus": runVerifyMobileOnlyReq,
"toc_MobileOnlineDuration": runVerifyMobileOnlyReq,
"toc_MobileAttribution": runVerifyMobileOnlyReq,
"toc_MobileConsumptionRange": runVerifyYysConsumptionReq,
"toc_EnterpriseRelation": runVerifyEntRelationReq,
"toc_BankcardFourFactors": runVerifyBankFourReq,
"toc_BankcardBlacklist": runVerifyBankBlackReq,
}
// productHasSmsCode 表示该 product 解密后的请求结构体中是否包含必填短信验证码 Code。
// 有 Code 的产品在「获取验证码」时已经做了滑块,这里不再强制要求 CaptchaVerifyParam。
// 其他产品(无 Code在查询时必须传并校验 CaptchaVerifyParam防止跳过图形验证
// 微信小程序X-Platform: wxmini见 ctxdata.GetPlatformFromCtx不嵌入 H5 滑块,由 PreprocessLogic 跳过图形校验。
func productHasSmsCode(product string) bool {
switch product {
case "marriage",
"homeservice",
"riskassessment",
"companyinfo",
"rentalinfo",
"preloanbackgroundcheck",
"personalData",
"toc_PersonalLawsuit",
"toc_EnterpriseLawsuit",
"toc_Marriage",
"toc_PersonalMarriageStatus",
"toc_MarriageStatusRegisterTime",
"toc_MarriageStatusSupplement",
"toc_MarriageStatusVerify",
"toc_DualMarriageStatusRegisterTime",
"toc_VehiclesUnderName",
"toc_VehiclesUnderNamePlate":
return true
default:
return false
}
}
func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) {
// 无短信验证码 Code 的 product查询前必须传并校验滑块微信小程序端跳过依赖登录态与 X-Platform
requireCaptcha := !productHasSmsCode(product)
if requireCaptcha {
if plat, platErr := ctxdata.GetPlatformFromCtx(l.ctx); platErr == nil && plat == model.PlatformWxMini {
requireCaptcha = false
}
}
if requireCaptcha {
if req.CaptchaVerifyParam == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请完成图形验证"), "product %s requires captcha", product)
}
cfg := l.svcCtx.Config.Captcha
if err := captcha.Verify(captcha.Config{
AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret,
EndpointURL: cfg.EndpointURL,
SceneID: cfg.SceneID,
}, req.CaptchaVerifyParam); err != nil {
return nil, err
}
}
decryptData, err := l.DecryptData(req.Data)
if err != nil {
return nil, err
}
handler, exists := productHandlers[product]
if !exists {
return nil, errors.New("未找到相应的处理程序")
}
params, err := handler(l, decryptData, product)
if err != nil {
return nil, err
}
return l.commonQueryTail(params, product)
}
// commonQueryTail 通用收尾:写缓存、生成 token 并返回
func (l *QueryServiceLogic) commonQueryTail(params map[string]interface{}, product string) (*types.QueryServiceResp, error) {
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, product, userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
}
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
}, nil
}
// runMarriageReq 姓名+身份证+手机+验证码 类产品(婚恋、家政、司法涉诉、婚姻状况、名下车辆等)
func runMarriageReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.MarriageReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
if verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile); verifyErr != nil {
return nil, verifyErr
}
return map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}, nil
}
func runBackgroundCheckReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.BackgroundCheckReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile); verifyErr != nil {
return nil, verifyErr
}
return map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}, nil
}
// runEntLawsuitReq 企业司法涉诉:企业名称+统一社会信用代码+手机+验证码
func runEntLawsuitReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.EntLawsuitReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
return map[string]interface{}{
"ent_name": data.EntName,
"ent_code": data.EntCode,
"mobile": data.Mobile,
}, nil
}
// 企业司法涉诉简版 QYGL66SL仅企业名称auth_date 与授权文件编码由后端自动生成
func runEnterpriseLawsuitSimpleReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.EnterpriseLawsuitSimpleReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
now := time.Now()
start := now.AddDate(0, 0, -7)
end := now.AddDate(0, 0, 7)
authDate := fmt.Sprintf("%s-%s", start.Format("20060102"), end.Format("20060102"))
return map[string]interface{}{
"ent_name": data.EntName,
"auth_date": authDate,
"auth_authorize_file_code": "AUTHTYC0001",
}, nil
}
// 限高被执行人 FLXG3A9B
func runLimitHighExecutedReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.LimitHighExecutedReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
auth := data.Authorized
if auth == "" {
auth = "1"
}
return map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile_no": data.Mobile,
"authorized": auth,
}, nil
}
// 本人不良 FLXGDEA9姓名 + 身份证(授权由 ApiRequest 默认传 1
func runPersonalBadRecordReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.PersonalBadRecordReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
return map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
}, nil
}
// 失信被执行人 QYGL2S0W
func runDishonestExecutedReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.DishonestExecutedReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
tp := data.Type
if tp == "" {
tp = "per"
}
return map[string]interface{}{
"type": tp,
"name": data.Name,
"id_card": data.IDCard,
}, nil
}
// runDualMarriageReq 双人婚姻状态:男方/女方姓名+身份证+手机+验证码
func runDualMarriageReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocDualMarriageReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
if verifyErr := l.VerifyTwo(data.NameMan, data.IDCardMan); verifyErr != nil {
return nil, verifyErr
}
if verifyErr := l.VerifyTwo(data.NameWoman, data.IDCardWoman); verifyErr != nil {
return nil, verifyErr
}
return map[string]interface{}{
"name": data.NameMan,
"id_card": data.IDCardMan,
"mobile": data.Mobile,
"name_man": data.NameMan,
"id_card_man": data.IDCardMan,
"name_woman": data.NameWoman,
"id_card_woman": data.IDCardWoman,
}, nil
}
// runPersonVehicleVerificationReq 人车核验简版:姓名+号牌类型+车牌号
func runPersonVehicleVerificationReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocPersonVehicleVerification
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
return map[string]interface{}{
"name": data.Name,
"plate_no": data.CarLicense,
"carplate_type": data.CarType,
}, nil
}
// runVehiclesUnderNameCountReq 名下车辆(数量) QCXG4D2E仅 user_type + id_card
func runVehiclesUnderNameCountReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehiclesUnderNameCountReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
userType := data.UserType
if userType == "" {
userType = "1"
}
return map[string]interface{}{
"user_type": userType,
"id_card": data.IDCard,
}, nil
}
// runVehicleVinCodeReq 仅 vin_code车辆静态信息、过户详版等
func runVehicleVinCodeReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleVinCodeReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
return map[string]interface{}{
"vin_code": data.VinCode,
}, nil
}
// runVehicleMileageMixedReq 车辆里程记录(混合) QCXG1U4U
func runVehicleMileageMixedReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleMileageMixedReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
"image_url": data.ImageURL,
}, nil
}
// runVehicleVinValuationReq 二手车VIN估值 QCXGY7F2仅必填
func runVehicleVinValuationReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleVinValuationReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
return map[string]interface{}{
"vin_code": data.VinCode,
"vehicle_location": data.VehicleLocation,
"first_registrationdate": data.FirstRegistrationDate,
}, nil
}
// runVehicleTransferSimpleReq 车辆过户简版 QCXG1H7Y仅必填 vin_code
func runVehicleTransferSimpleReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleTransferSimpleReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
return map[string]interface{}{"vin_code": data.VinCode}, nil
}
// runVehicleMaintenanceSimpleReq 车辆维保简版 QCXG3Y6B仅必填 vin_code回调地址后端自动生成
func runVehicleMaintenanceSimpleReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleMaintenanceSimpleReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
}, nil
}
// runVehicleMaintenanceDetailReq 车辆维保详细版 QCXG3Z3L仅必填 vin_code回调地址后端自动生成
func runVehicleMaintenanceDetailReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleMaintenanceDetailReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
}, nil
}
// runVehicleClaimDetailReq 车辆出险详版 QCXGP00W仅必填 vin_code, vlphoto_data回调地址后端自动生成vlphoto_data 由 API 层加密为 data
func runVehicleClaimDetailReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleClaimDetailReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
// 回调地址由后端在 ApiRequestService 中统一生成,此处不再下发 return_url
return map[string]interface{}{
"vin_code": data.VinCode,
"vlphoto_data": data.VlphotoData,
}, nil
}
// runVehicleClaimVerifyReq 车辆出险记录核验 QCXG6B4E
func runVehicleClaimVerifyReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVehicleClaimVerifyReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
if data.Mobile != "" && data.Code != "" {
if verifyCodeErr := l.VerifyCode(data.Mobile, data.Code); verifyCodeErr != nil {
return nil, verifyCodeErr
}
}
auth := data.Authorized
if auth == "" {
auth = "1"
}
return map[string]interface{}{
"vin_code": data.VINCode,
"authorized": auth,
}, nil
}
// runPersonEnterpriseProReq 人企关系加强版预查询:仅身份证号
func runPersonEnterpriseProReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocPersonEnterpriseProReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
}
return map[string]interface{}{
"id_card": data.IDCard,
}, nil
}
// --------------- 核验工具 handlers ---------------
func runVerifyAuthTwoReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyAuthTwoReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyAuthThreeReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyAuthThreeReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"photo_data": data.PhotoData, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyCertReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyCertReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyConsumptionReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyConsumptionReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyYysTwoReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyYysTwoReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "name": data.Name}, nil
}
func runVerifyYysThreeReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyYysThreeReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "id_card": data.IDCard, "name": data.Name}, nil
}
func runVerifyMobileOnlyReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyMobileOnlyReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo}, nil
}
func runVerifyYysConsumptionReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyYysConsumptionReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{"mobile_no": data.MobileNo, "authorized": data.Authorized}, nil
}
func runVerifyEntRelationReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyEntRelationReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
out := map[string]interface{}{}
if data.IDCard != "" {
out["id_card"] = data.IDCard
}
return out, nil
}
func runVerifyBankFourReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyBankFourReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{
"mobile_no": data.MobileNo,
"id_card": data.IDCard,
"bank_card": data.BankCard,
"name": data.Name,
}, nil
}
func runVerifyBankBlackReq(l *QueryServiceLogic, decryptData []byte, product string) (map[string]interface{}, error) {
var data types.TocVerifyBankBlackReq
if err := json.Unmarshal(decryptData, &data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", err)
}
if err := validator.Validate(data); err != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, err.Error()), "查询服务, 参数不正确: %+v", err)
}
return map[string]interface{}{
"mobile_no": data.MobileNo,
"id_card": data.IDCard,
"name": data.Name,
"bank_card": data.BankCard,
}, nil
}
func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) {
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "密钥获取失败: %+v", decodeErr)
}
decryptData, aesDecryptErr := crypto.AesDecrypt(data, key)
if aesDecryptErr != nil || len(decryptData) == 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密失败: %+v", aesDecryptErr)
}
return decryptData, nil
}
// 校验验证码(开发环境 ENV=development 可跳过)
func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error {
if os.Getenv("ENV") == "development" {
return nil
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %+v", err)
}
codeRedisKey := fmt.Sprintf("%s:%s", "query", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(codeRedisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "验证码过期: %s", mobile)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码redis缓存失败, mobile: %s, err: %+v", mobile, err)
}
if cacheCode != code {
return errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "验证码不正确: %s", mobile)
}
return nil
}
func (l *QueryServiceLogic) IsAgentQuery() bool {
agentID, _ := l.ctx.Value("agentIdentifier").(string)
return agentID != ""
}
// 二要素验证(仅姓名+身份证号)(开发环境不调用验证 API
func (l *QueryServiceLogic) VerifyTwo(Name string, IDCard string) error {
if os.Getenv("ENV") == "development" {
return nil
}
twoVerification := service.TwoFactorVerificationRequest{
Name: Name,
IDCard: IDCard,
}
verification, err := l.svcCtx.VerificationService.TwoFactorVerification(twoVerification)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "二要素验证失败: %v", err)
}
if !verification.Passed {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "二要素验证不通过: %v", err)
}
return nil
}
// 按代理/非代理切换要素验证:代理走三要素;非代理走二要素(开发环境不调用验证 API
func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error {
if os.Getenv("ENV") == "development" {
return nil
}
if !l.IsAgentQuery() {
twoVerification := service.TwoFactorVerificationRequest{
Name: Name,
IDCard: IDCard,
}
verification, err := l.svcCtx.VerificationService.TwoFactorVerification(twoVerification)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "二要素验证失败: %v", err)
}
if !verification.Passed {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "二要素验证不通过: %v", err)
}
} else {
// 三要素验证
if Mobile == "" {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, "手机号不能为空"), "三要素验证参数不正确: mobile为空")
}
threeVerification := service.ThreeFactorVerificationRequest{
Name: Name,
IDCard: IDCard,
Mobile: Mobile,
}
verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素验证失败: %v", err)
}
if !verification.Passed {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "三要素验证不通过: %v", err)
}
}
return nil
}
// 缓存
func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product string, userID int64) (string, error) {
agentIdentifier, _ := l.ctx.Value("agentIdentifier").(string)
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 获取AES密钥失败: %+v", decodeErr)
}
paramsMarshal, marshalErr := json.Marshal(params)
if marshalErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr)
}
encryptParams, aesEncryptErr := crypto.AesEncrypt(paramsMarshal, key)
if aesEncryptErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 加密参数失败: %+v", aesEncryptErr)
}
queryCache := types.QueryCacheLoad{
Params: encryptParams,
Product: Product,
AgentIdentifier: agentIdentifier,
}
jsonData, marshalErr := json.Marshal(queryCache)
if marshalErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr)
}
outTradeNo := "Q_" + l.svcCtx.AlipayService.GenerateOutTradeNo()
redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo)
cacheErr := l.svcCtx.Redis.SetexCtx(l.ctx, redisKey, string(jsonData), int(2*time.Hour))
if cacheErr != nil {
return "", cacheErr
}
// 写入 query_user_record用于后台按被查询人姓名/身份证/手机号追溯订单(见 query_user_record.sql 说明 1
nameStr, _ := params["name"].(string)
idCardStr, _ := params["id_card"].(string)
mobileStr, _ := params["mobile"].(string)
if nameStr != "" || idCardStr != "" || mobileStr != "" {
var encName, encIdCard, encMobile string
if nameStr != "" {
encName, _ = crypto.AesEcbEncrypt([]byte(nameStr), key)
}
if idCardStr != "" {
encIdCard, _ = crypto.EncryptIDCard(idCardStr, key)
}
if mobileStr != "" {
encMobile, _ = crypto.EncryptMobile(mobileStr, secretKey)
}
agentIdent := sql.NullString{}
if agentIdentifier != "" {
agentIdent = sql.NullString{String: agentIdentifier, Valid: true}
}
rec := &model.QueryUserRecord{
DeleteTime: sql.NullTime{},
DelState: globalkey.DelStateNo,
Version: 0,
UserId: userID,
Name: encName,
IdCard: encIdCard,
Mobile: encMobile,
Product: Product,
QueryNo: outTradeNo,
OrderId: 0,
PlatformOrderId: sql.NullString{},
AgentIdentifier: agentIdent,
}
_, insertErr := l.svcCtx.QueryUserRecordModel.Insert(l.ctx, nil, rec)
if insertErr != nil {
logx.WithContext(l.ctx).Errorf("CacheData 写入 query_user_record 失败: %v", insertErr)
}
}
return outTradeNo, nil
}
// GetOrCreateUser 获取或创建用户
// 1. 如果上下文中已有用户ID直接返回
// 2. 如果是代理查询或APP请求创建新用户
// 3. 其他情况返回未登录错误
func (l *QueryServiceLogic) GetOrCreateUser() (int64, error) {
// 尝试获取用户ID
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err != nil {
return 0, err
}
userID := claims.UserId
return userID, nil
// // 如果不是未登录错误,说明是其他错误,直接返回
// if !ctxdata.IsNoUserIdError(err) {
// return 0, err
// }
// // 检查是否是代理查询或APP请求
// isAgentQuery := false
// if agentID, ok := l.ctx.Value("agentIdentifier").(string); ok && agentID != "" {
// isAgentQuery = true
// }
// if app, ok := l.ctx.Value("app").(bool); ok && app {
// isAgentQuery = true
// }
// // 如果不是代理查询或APP请求返回未登录错误
// if !isAgentQuery {
// return 0, ctxdata.ErrNoUserIdInCtx
// }
// // 创建新用户
// return l.svcCtx.UserService.RegisterUUIDUser(l.ctx)
}

View File

@@ -0,0 +1,48 @@
package toolbox
import (
"context"
"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 ToolboxQueryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewToolboxQueryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToolboxQueryLogic {
return &ToolboxQueryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ToolboxQueryLogic) ToolboxQuery(req *types.ToolboxQueryReq) (*types.ToolboxQueryResp, error) {
if req.ToolKey == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("tool_key 不能为空"), "")
}
if l.svcCtx.ToolboxService == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "工具箱服务未初始化")
}
params := req.Params
if params == nil {
params = map[string]interface{}{}
}
result, err := l.svcCtx.ToolboxService.Query(l.ctx, req.ToolKey, params)
if err != nil {
return nil, err
}
return &types.ToolboxQueryResp{
ToolKey: req.ToolKey,
Result: result,
}, nil
}

View File

@@ -0,0 +1,67 @@
package upload
import (
"context"
"net/http"
"os"
"path/filepath"
"strings"
"qnc-server/app/main/api/internal/svc"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type ServeUploadLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewServeUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServeUploadLogic {
return &ServeUploadLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ServeUploadLogic) ServeUpload(name string, w http.ResponseWriter) error {
name = filepath.Base(strings.TrimSpace(name))
if name == "" || name == "." || name == ".." || !isSafeUploadFileName(name) {
return errors.Wrapf(xerr.NewErrMsg("无效的文件名"), "")
}
fullPath := filepath.Join(uploadImageDir, name)
data, err := os.ReadFile(fullPath)
if err != nil {
if os.IsNotExist(err) {
return errors.Wrapf(xerr.NewErrMsg("文件不存在"), "")
}
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "读取文件失败: %v", err)
}
w.Header().Set("Cache-Control", "public, max-age=86400")
switch filepath.Ext(name) {
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".webp":
w.Header().Set("Content-Type", "image/webp")
default:
w.Header().Set("Content-Type", "image/jpeg")
}
_, err = w.Write(data)
return err
}
func isSafeUploadFileName(name string) bool {
for _, c := range name {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' {
continue
}
return false
}
return true
}

View File

@@ -0,0 +1,93 @@
package upload
import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
const uploadImageDir = "uploads/images"
type UploadImageLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUploadImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadImageLogic {
return &UploadImageLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UploadImageLogic) UploadImage(req *types.UploadImageReq) (*types.UploadImageResp, error) {
raw := strings.TrimSpace(req.ImageBase64)
if raw == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("image_base64 不能为空"), "")
}
if idx := strings.Index(raw, ","); idx >= 0 {
raw = raw[idx+1:]
}
data, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrMsg("图片 Base64 解析失败"), "")
}
if len(data) == 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("图片内容为空"), "")
}
if len(data) > 5*1024*1024 {
return nil, errors.Wrapf(xerr.NewErrMsg("图片不能超过 5MB"), "")
}
ext := detectImageExt(data)
fileName := fmt.Sprintf("%s%s", uuid.NewString(), ext)
dir := uploadImageDir
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建上传目录失败: %v", err)
}
fullPath := filepath.Join(dir, fileName)
if err := os.WriteFile(fullPath, data, 0o644); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存图片失败: %v", err)
}
base := publicAPIBase(l.svcCtx.Config.Promotion.OfficialDomain)
url := fmt.Sprintf("%s/api/v1/upload/file/%s", base, fileName)
return &types.UploadImageResp{Url: url}, nil
}
func detectImageExt(data []byte) string {
if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return ".jpg"
}
if len(data) >= 8 && string(data[0:8]) == "\x89PNG\r\n\x1a\n" {
return ".png"
}
if len(data) >= 6 && string(data[0:6]) == "GIF87a" || string(data[0:6]) == "GIF89a" {
return ".gif"
}
if len(data) >= 12 && string(data[8:12]) == "WEBP" {
return ".webp"
}
return ".jpg"
}
func publicAPIBase(officialDomain string) string {
base := strings.TrimRight(officialDomain, "/")
if base != "" {
return base
}
return "http://127.0.0.1:8888"
}

View File

@@ -68,7 +68,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
} }
} }
// 通过加密后的手机号查找目标用户(手机号用户视为正式用户) // 通过加密后的手机号查找目标用户(手机号用户视为正式用户;可能命中 go-zero 二级缓存
targetUser, err := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) targetUser, err := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
if err != nil && !errors.Is(err, model.ErrNotFound) { if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找用户失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找用户失败: %v", err)
@@ -90,6 +90,13 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil { if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
} }
// 与 userModel_gen 中 FindOneByMobile 的 QueryRowIndexCtx key 规则一致,避免此前「未命中」缓存导致后续按手机号仍查不到
userMobileCacheKey := fmt.Sprintf("cache:qnc:user:mobile:%v", sql.NullString{String: encryptedMobile, Valid: true})
if _, delErr := l.svcCtx.Redis.DelCtx(l.ctx, userMobileCacheKey); delErr != nil {
l.Errorf("[BindMobile] 失效 FindOneByMobile 缓存失败 | key: %s | err: %v", userMobileCacheKey, delErr)
} else {
l.Infof("[BindMobile] 已失效 FindOneByMobile 缓存 | key: %s", userMobileCacheKey)
}
// 发放tokenuserType会根据mobile字段动态计算 // 发放tokenuserType会根据mobile字段动态计算
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil { if err != nil {

View File

@@ -56,7 +56,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r
var userID string var userID string
user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
if findUserErr != nil && findUserErr != model.ErrNotFound { if findUserErr != nil && findUserErr != model.ErrNotFound {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取数据库获取用户失败, mobile: %s, err: %+v", encryptedMobile, err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取数据库获取用户失败, mobile: %s, err: %+v", encryptedMobile, findUserErr)
} }
if user == nil { if user == nil {
// 用户不存在,自动注册新用户 // 用户不存在,自动注册新用户

View File

@@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strings"
"time" "time"
"qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/svc"
@@ -33,8 +35,14 @@ func NewWxMiniAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WxMini
} }
func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMiniAuthResp, err error) { func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMiniAuthResp, err error) {
code := strings.TrimSpace(req.Code)
if code == "" || code == "the code is a mock one" || strings.Contains(code, " ") {
return nil, errors.Wrapf(xerr.NewErrMsg("微信登录凭证无效,请重新打开小程序"),
"无效的微信 code: %q", req.Code)
}
// 1. 获取session_key和openid // 1. 获取session_key和openid
sessionKeyResp, err := l.GetSessionKey(req.Code) sessionKeyResp, err := l.GetSessionKey(code)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err)
} }
@@ -65,13 +73,20 @@ func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMi
userID = user.Id userID = user.Id
} }
// 4. 生成JWT Token动态计算userType // 4. 缓存 session_keyxpay 虚拟支付签名需要
if l.svcCtx.XpayService != nil && l.svcCtx.XpayService.Enabled() {
if saveErr := l.svcCtx.XpayService.SaveSessionKey(l.ctx, userID, sessionKeyResp.SessionKey); saveErr != nil {
l.Errorf("缓存 xpay session_key 失败 userID=%s err=%v", userID, saveErr)
}
}
// 5. 生成JWT Token动态计算userType
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err)
} }
// 5. 返回登录结果 // 6. 返回登录结果
now := time.Now().Unix() now := time.Now().Unix()
return &types.WXMiniAuthResp{ return &types.WXMiniAuthResp{
AccessToken: token, AccessToken: token,
@@ -97,10 +112,10 @@ func (l *WxMiniAuthLogic) GetSessionKey(code string) (*SessionKeyResp, error) {
appID = l.svcCtx.Config.WechatMini.AppID appID = l.svcCtx.Config.WechatMini.AppID
appSecret = l.svcCtx.Config.WechatMini.AppSecret appSecret = l.svcCtx.Config.WechatMini.AppSecret
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", apiURL := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appID, appSecret, code) url.QueryEscape(appID), url.QueryEscape(appSecret), url.QueryEscape(code))
resp, err := http.Get(url) resp, err := http.Get(apiURL)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err)
} }

View File

@@ -6,13 +6,14 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"regexp" "qnc-server/app/main/api/internal/logic/pay"
"strings"
"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/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"regexp"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
@@ -158,7 +159,7 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
encryptData = encryptedEmptyData encryptData = encryptedEmptyData
} else { } else {
// 正常模式调用API请求服务 // 正常模式调用API请求服务
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id, order.OrderNo)
if err != nil { if err != nil {
return l.handleError(ctx, err, order, query) return l.handleError(ctx, err, order, query)
} }
@@ -185,12 +186,18 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
return l.handleError(ctx, updateQueryErr, order, query) return l.handleError(ctx, updateQueryErr, order, query)
} }
// 记录动态查询记录 (真实案例展示)
go l.recordInquiryRecord(context.Background(), decryptData, product, encryptData)
// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程) // 报告生成成功后,发送代理处理异步任务(不阻塞报告流程)
if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil { if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil {
// 代理处理任务发送失败,只记录日志,不影响报告流程 // 代理处理任务发送失败,只记录日志,不影响报告流程
logx.Errorf("发送代理处理任务失败订单ID: %s, 错误: %v", order.Id, asyncErr) logx.Errorf("发送代理处理任务失败订单ID: %s, 错误: %v", order.Id, asyncErr)
} }
// xpay报告成功后再通知微信虚拟支付已发货
pay.NotifyXpayGoodsAfterReport(ctx, l.svcCtx, order)
_, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey) _, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey)
if delErr != nil { if delErr != nil {
logx.Errorf("删除Redis缓存失败但任务已成功处理订单ID: %s, 错误: %v", order.Id, delErr) logx.Errorf("删除Redis缓存失败但任务已成功处理订单ID: %s, 错误: %v", order.Id, delErr)
@@ -218,36 +225,7 @@ func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error
return asynq.SkipRetry return asynq.SkipRetry
} }
// 退款 pay.TryRefundOnQueryFailure(ctx, l.svcCtx, order)
if order.PaymentPlatform == "wechat" {
refundErr := l.svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount)
if refundErr != nil {
logx.Error(refundErr)
return asynq.SkipRetry
}
} else {
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
if refundErr != nil {
logx.Error(refundErr)
return asynq.SkipRetry
}
if refund.IsSuccess() {
logx.Errorf("支付宝退款成功, orderID: %s", order.Id)
// 更新订单状态为退款
order.Status = "refunded"
updateOrderErr := l.svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order)
if updateOrderErr != nil {
logx.Errorf("更新订单状态失败订单ID: %s, 错误: %v", order.Id, updateOrderErr)
return fmt.Errorf("更新订单状态失败: %v", updateOrderErr)
}
return asynq.SkipRetry
} else {
logx.Errorf("支付宝退款失败:%v", refundErr)
return asynq.SkipRetry
}
// 直接成功
}
} }
return asynq.SkipRetry return asynq.SkipRetry
@@ -372,6 +350,84 @@ func maskPhone(phone string) string {
return phone[:3] + strings.Repeat("*", length-7) + phone[length-4:] return phone[:3] + strings.Repeat("*", length-7) + phone[length-4:]
} }
// recordInquiryRecord 记录成功的查询到动态展示表
func (l *PaySuccessNotifyUserHandler) recordInquiryRecord(ctx context.Context, decryptParams []byte, product *model.Product, encryptedResp string) {
defer func() {
if r := recover(); r != nil {
logx.Errorf("记录查询记录异常: %v", r)
}
}()
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, _ := hex.DecodeString(secretKey)
// 1. 解析参数获取手机号和VIN
var params map[string]interface{}
_ = json.Unmarshal(decryptParams, &params)
mobile, _ := params["mobile"].(string)
vin, _ := params["vin_code"].(string)
if vin == "" {
vin, _ = params["vin"].(string)
}
if mobile == "" || vin == "" {
return
}
// 2. 解密响应数据获取车型
decryptResp, err := crypto.AesDecrypt(encryptedResp, key)
if err != nil {
return
}
// 尝试从响应中寻找车型信息 (这里根据实际响应结构寻找)
// 通常在车辆信息接口的 data 字段中
carModel := "未知车型"
respStr := string(decryptResp)
// 简单的规则寻找车型字段
if strings.Contains(respStr, "model_name") {
re := regexp.MustCompile(`"model_name"\s*:\s*"([^"]+)"`)
match := re.FindStringSubmatch(respStr)
if len(match) > 1 {
carModel = match[1]
}
} else if strings.Contains(respStr, "brand_name") {
re := regexp.MustCompile(`"brand_name"\s*:\s*"([^"]+)"`)
match := re.FindStringSubmatch(respStr)
if len(match) > 1 {
carModel = match[1]
}
}
// 3. 脱敏处理
phoneTail := ""
if len(mobile) >= 4 {
phoneTail = mobile[len(mobile)-4:]
}
displayName := "用户*" + phoneTail
maskedVin := ""
if len(vin) >= 8 {
maskedVin = vin[:4] + "********" + vin[len(vin)-4:]
} else {
maskedVin = vin
}
// 4. 保存记录
record := &model.InquiryRecord{
UserPhoneTail: phoneTail,
DisplayName: displayName,
VinMasked: maskedVin,
CarModel: carModel,
InquiryTag: product.ProductName,
Status: 1,
}
_, _ = l.svcCtx.InquiryRecordModel.Insert(ctx, nil, record)
}
// 通用敏感信息脱敏 - 根据字符串长度比例进行脱敏 // 通用敏感信息脱敏 - 根据字符串长度比例进行脱敏
func maskGeneral(value string) string { func maskGeneral(value string) string {
length := len(value) length := len(value)

View File

@@ -11,6 +11,7 @@ import (
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/pkg/lzkit/lzUtils" "qnc-server/pkg/lzkit/lzUtils"
"github.com/Masterminds/squirrel"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
@@ -120,7 +121,7 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
// 5.2 使用产品配置的底价计算实际底价 // 5.2 使用产品配置的底价计算实际底价
basePrice := productConfig.BasePrice basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus) actualBasePrice := lzUtils.RoundMoney(basePrice + float64(levelBonus))
// 5.3 计算提价成本(使用产品配置) // 5.3 计算提价成本(使用产品配置)
priceThreshold := 0.0 priceThreshold := 0.0
@@ -134,7 +135,7 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate) priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate)
// 5.4 计算代理收益 // 5.4 计算代理收益
agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost agentProfit := lzUtils.RoundMoney(agentOrder.SetPrice - actualBasePrice - priceCost)
// 5.5 更新代理订单记录 // 5.5 更新代理订单记录
agentOrder.ProcessStatus = 1 agentOrder.ProcessStatus = 1
@@ -200,7 +201,7 @@ func (s *AgentService) calculatePriceCost(setPrice, priceThreshold, priceFeeRate
if setPrice <= priceThreshold { if setPrice <= priceThreshold {
return 0 return 0
} }
return (setPrice - priceThreshold) * priceFeeRate return lzUtils.RoundMoney((setPrice - priceThreshold) * priceFeeRate)
} }
// giveAgentCommission 发放代理佣金 // giveAgentCommission 发放代理佣金
@@ -244,7 +245,7 @@ func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Ses
} }
// 计算冻结金额订单单价的10% // 计算冻结金额订单单价的10%
freezeAmountByPrice := orderPrice * freezeRatio freezeAmountByPrice := lzUtils.RoundMoney(orderPrice * freezeRatio)
// 冻结金额不能超过佣金金额 // 冻结金额不能超过佣金金额
if freezeAmountByPrice > amount { if freezeAmountByPrice > amount {
@@ -295,7 +296,7 @@ func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Ses
} }
// 实际到账金额 = 佣金金额 - 冻结金额 // 实际到账金额 = 佣金金额 - 冻结金额
actualAmount := amount - freezeAmount actualAmount := lzUtils.RoundMoney(amount - freezeAmount)
wallet.Balance += actualAmount wallet.Balance += actualAmount
wallet.FrozenBalance += freezeAmount wallet.FrozenBalance += freezeAmount
@@ -431,7 +432,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s
// ========== 步骤2计算剩余金额并分配给钻石上级 ========== // ========== 步骤2计算剩余金额并分配给钻石上级 ==========
// 剩余金额 = 总等级加成 - 已给黄金上级的金额 // 剩余金额 = 总等级加成 - 已给黄金上级的金额
// 例如等级加成6元 - 给黄金上级3元 = 剩余3元 // 例如等级加成6元 - 给黄金上级3元 = 剩余3元
remaining := amount - goldAmount remaining := lzUtils.RoundMoney(amount - goldAmount)
if remaining > 0 { if remaining > 0 {
// 从黄金上级开始向上查找钻石上级 // 从黄金上级开始向上查找钻石上级
// 场景示例: // 场景示例:
@@ -481,7 +482,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s
// ========== 步骤2计算剩余金额 ========== // ========== 步骤2计算剩余金额 ==========
// 剩余金额 = 总等级加成 - 已给直接上级的金额 // 剩余金额 = 总等级加成 - 已给直接上级的金额
// 例如等级加成6元 - 给直接上级2元 = 剩余4元 // 例如等级加成6元 - 给直接上级2元 = 剩余4元
remaining := amount - directAmount remaining := lzUtils.RoundMoney(amount - directAmount)
if remaining <= 0 { if remaining <= 0 {
// 如果没有剩余,直接返回(所有金额已分配给直接上级) // 如果没有剩余,直接返回(所有金额已分配给直接上级)
return nil return nil
@@ -525,7 +526,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s
goldBonus = 3 // 默认3元 goldBonus = 3 // 默认3元
} }
// 计算等级加成的差 // 计算等级加成的差
bonusDiff := normalBonus - float64(goldBonus) // 例如6元 - 3元 = 3元 bonusDiff := lzUtils.RoundMoney(normalBonus - float64(goldBonus)) // 例如6元 - 3元 = 3元
// 步骤A2如果有黄金上级给黄金上级一部分返佣 // 步骤A2如果有黄金上级给黄金上级一部分返佣
// 规则说明: // 规则说明:
@@ -536,7 +537,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s
if goldParent != nil && bonusDiff > 0 { if goldParent != nil && bonusDiff > 0 {
// 计算给黄金上级的金额 = 等级加成差 - 已给普通上级的金额 // 计算给黄金上级的金额 = 等级加成差 - 已给普通上级的金额
// 例如等级加成差3元 - 已给普通2元 = 给黄金1元 // 例如等级加成差3元 - 已给普通2元 = 给黄金1元
goldRebateAmount := bonusDiff - directAmount goldRebateAmount := lzUtils.RoundMoney(bonusDiff - directAmount)
// 如果计算出的金额小于等于0说明差已经被普通代理全部占用了不给黄金 // 如果计算出的金额小于等于0说明差已经被普通代理全部占用了不给黄金
// 例如如果差是2元已给普通2元则 goldRebateAmount = 0不给黄金 // 例如如果差是2元已给普通2元则 goldRebateAmount = 0不给黄金
@@ -554,7 +555,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s
// 更新剩余金额(用于后续分配给钻石) // 更新剩余金额(用于后续分配给钻石)
// 例如剩余4元 - 给黄金1元 = 剩余3元给钻石 // 例如剩余4元 - 给黄金1元 = 剩余3元给钻石
remaining = remaining - goldRebateAmount remaining = lzUtils.RoundMoney(remaining - goldRebateAmount)
} }
} }
@@ -1073,3 +1074,150 @@ func (s *AgentService) GetUpgradeRebate(ctx context.Context, fromLevel, toLevel
} }
return 0, nil return 0, nil
} }
// ReverseAgentSettlementOnOrderRefund 订单退款成功后冲正本单代理销售佣金、冻结任务与返佣(幂等)。
// 与退款是否部分/全额无关:凡触发本方法,均按本单佣金与返佣记录全额冲回。
// session 非 nil 时与外层共用事务;为 nil 时在独立事务中执行。
func (s *AgentService) ReverseAgentSettlementOnOrderRefund(ctx context.Context, session sqlx.Session, orderID string) error {
run := func(c context.Context, sess sqlx.Session) error {
return s.reverseAgentSettlementInTx(c, sess, orderID)
}
if session != nil {
return run(ctx, session)
}
return s.AgentWalletModel.Trans(ctx, run)
}
func (s *AgentService) reverseAgentSettlementInTx(ctx context.Context, session sqlx.Session, orderID string) error {
cb := s.AgentCommissionModel.SelectBuilder().Where(squirrel.Eq{
"order_id": orderID,
"del_state": globalkey.DelStateNo,
})
commissions, err := s.AgentCommissionModel.FindAll(ctx, cb, "")
if err != nil {
return errors.Wrapf(err, "查询订单佣金失败 orderId=%s", orderID)
}
for _, c := range commissions {
if err := s.reverseOneCommissionForRefund(ctx, session, c); err != nil {
return err
}
}
rb := s.AgentRebateModel.SelectBuilder().Where(squirrel.Eq{
"order_id": orderID,
"del_state": globalkey.DelStateNo,
})
rebates, err := s.AgentRebateModel.FindAll(ctx, rb, "")
if err != nil {
return errors.Wrapf(err, "查询订单返佣失败 orderId=%s", orderID)
}
for _, r := range rebates {
if err := s.reverseOneRebateForRefund(ctx, session, r); err != nil {
return err
}
}
return nil
}
func (s *AgentService) reverseOneCommissionForRefund(ctx context.Context, session sqlx.Session, commission *model.AgentCommission) error {
if commission.Status == 3 {
return nil
}
A := lzUtils.RoundMoney(commission.Amount)
if A <= 0 {
commission.Status = 3
return s.AgentCommissionModel.UpdateWithVersion(ctx, session, commission)
}
tb := s.AgentFreezeTaskModel.SelectBuilder().Where(squirrel.Eq{
"commission_id": commission.Id,
"del_state": globalkey.DelStateNo,
})
tasks, err := s.AgentFreezeTaskModel.FindAll(ctx, tb, "")
if err != nil {
return errors.Wrapf(err, "查询佣金冻结任务失败 commissionId=%s", commission.Id)
}
var takeFromFrozen float64
for _, ft := range tasks {
if ft.Status == 1 {
takeFromFrozen = lzUtils.RoundMoney(takeFromFrozen + ft.FreezeAmount)
ft.Status = 3
if err := s.AgentFreezeTaskModel.UpdateWithVersion(ctx, session, ft); err != nil {
return errors.Wrapf(err, "取消冻结任务失败 freezeTaskId=%s", ft.Id)
}
}
}
takeFromFrozen = lzUtils.RoundMoney(takeFromFrozen)
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, commission.AgentId)
if err != nil {
return errors.Wrapf(err, "冲正佣金时查询钱包失败 agentId=%s", commission.AgentId)
}
if takeFromFrozen > 0 {
if wallet.FrozenBalance < takeFromFrozen {
takeFromFrozen = lzUtils.RoundMoney(wallet.FrozenBalance)
}
wallet.FrozenBalance = lzUtils.RoundMoney(wallet.FrozenBalance - takeFromFrozen)
}
takeFromBalance := lzUtils.RoundMoney(A - takeFromFrozen)
if takeFromBalance < 0 {
takeFromBalance = 0
}
// 可用余额不足时允许扣成负数(例如代理已提现),退款侧事务仍可完成
wallet.Balance = lzUtils.RoundMoney(wallet.Balance - takeFromBalance)
if wallet.Balance < 0 {
logx.Infof("退款冲正佣金后代理可用余额为负: agentId=%s, orderId=%s, balance=%.2f",
commission.AgentId, commission.OrderId, wallet.Balance)
}
wallet.TotalEarnings = lzUtils.RoundMoney(wallet.TotalEarnings - A)
if wallet.TotalEarnings < 0 {
wallet.TotalEarnings = 0
}
if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(err, "冲正佣金更新钱包失败 agentId=%s", commission.AgentId)
}
commission.Status = 3
if err := s.AgentCommissionModel.UpdateWithVersion(ctx, session, commission); err != nil {
return errors.Wrapf(err, "更新佣金状态为已取消失败 commissionId=%s", commission.Id)
}
return nil
}
func (s *AgentService) reverseOneRebateForRefund(ctx context.Context, session sqlx.Session, rebate *model.AgentRebate) error {
if rebate.Status == 3 {
return nil
}
amt := lzUtils.RoundMoney(rebate.RebateAmount)
if amt <= 0 {
rebate.Status = 3
return s.AgentRebateModel.UpdateWithVersion(ctx, session, rebate)
}
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, rebate.AgentId)
if err != nil {
return errors.Wrapf(err, "冲正返佣时查询钱包失败 agentId=%s", rebate.AgentId)
}
// 可用余额不足时允许扣成负数(例如代理已提现)
wallet.Balance = lzUtils.RoundMoney(wallet.Balance - amt)
if wallet.Balance < 0 {
logx.Infof("退款冲正返佣后代理可用余额为负: agentId=%s, orderId=%s, balance=%.2f",
rebate.AgentId, rebate.OrderId, wallet.Balance)
}
wallet.TotalEarnings = lzUtils.RoundMoney(wallet.TotalEarnings - amt)
if wallet.TotalEarnings < 0 {
wallet.TotalEarnings = 0
}
if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(err, "冲正返佣更新钱包失败 agentId=%s", rebate.AgentId)
}
rebate.Status = 3
if err := s.AgentRebateModel.UpdateWithVersion(ctx, session, rebate); err != nil {
return errors.Wrapf(err, "更新返佣状态为已取消失败 rebateId=%s", rebate.Id)
}
return nil
}

View File

@@ -2,12 +2,14 @@ package service
import ( import (
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"qnc-server/app/main/api/internal/config" "qnc-server/app/main/api/internal/config"
tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk" tianyuanapi "qnc-server/app/main/api/internal/service/tianyuanapi_sdk"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/pkg/lzkit/crypto"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -36,16 +38,33 @@ type ApiRequestService struct {
config config.Config config config.Config
featureModel model.FeatureModel featureModel model.FeatureModel
productFeatureModel model.ProductFeatureModel productFeatureModel model.ProductFeatureModel
userFeatureWhitelistModel model.UserFeatureWhitelistModel
whitelistService *WhitelistService
tianyuanapi *tianyuanapi.Client tianyuanapi *tianyuanapi.Client
tianyuanapiCallLogService *TianyuanapiCallLogService
authService *AuthorizationService
} }
// NewApiRequestService 是一个构造函数,用于初始化 ApiRequestService // NewApiRequestService 构造函数
func NewApiRequestService(c config.Config, featureModel model.FeatureModel, productFeatureModel model.ProductFeatureModel, tianyuanapi *tianyuanapi.Client) *ApiRequestService { func NewApiRequestService(
c config.Config,
featureModel model.FeatureModel,
productFeatureModel model.ProductFeatureModel,
userFeatureWhitelistModel model.UserFeatureWhitelistModel,
tianyuanapi *tianyuanapi.Client,
tianyuanapiCallLogService *TianyuanapiCallLogService,
whitelistService *WhitelistService,
authService *AuthorizationService,
) *ApiRequestService {
return &ApiRequestService{ return &ApiRequestService{
config: c, config: c,
featureModel: featureModel, featureModel: featureModel,
productFeatureModel: productFeatureModel, productFeatureModel: productFeatureModel,
userFeatureWhitelistModel: userFeatureWhitelistModel,
tianyuanapi: tianyuanapi, tianyuanapi: tianyuanapi,
tianyuanapiCallLogService: tianyuanapiCallLogService,
whitelistService: whitelistService,
authService: authService,
} }
} }
@@ -58,9 +77,20 @@ type APIResponseData struct {
} }
// ProcessRequests 处理请求 // ProcessRequests 处理请求
func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]byte, error) { // orderNo: 当前查询订单号,供异步车辆类接口生成 return_url 回调地址
func (a *ApiRequestService) ProcessRequests(params []byte, productID string, orderNo string) ([]byte, error) {
var ctx, cancel = context.WithCancel(context.Background()) var ctx, cancel = context.WithCancel(context.Background())
defer cancel() defer cancel()
// 白名单:已下架模块直接返回占位成功,不调用天远 API
idCard := gjson.GetBytes(params, "id_card").String()
var whitelistedFeatureApiIds map[string]bool
if a.whitelistService != nil {
whitelistedFeatureApiIds, _ = a.whitelistService.GetWhitelistedFeatureApisByIdCard(ctx, idCard)
} else {
whitelistedFeatureApiIds = make(map[string]bool)
}
build := a.productFeatureModel.SelectBuilder().Where(squirrel.Eq{ build := a.productFeatureModel.SelectBuilder().Where(squirrel.Eq{
"product_id": productID, "product_id": productID,
}) })
@@ -85,6 +115,25 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]
if len(featureList) == 0 { if len(featureList) == 0 {
return nil, errors.New("处理请求错误,产品无对应接口功能") return nil, errors.New("处理请求错误,产品无对应接口功能")
} }
// 在原始 params 上附加 order_no供异步车辆类接口自动生成回调地址
var baseParams map[string]interface{}
if err := json.Unmarshal(params, &baseParams); err != nil {
logx.Errorf("解析查询参数失败, Params: %s, Error: %v", string(params), err)
return nil, fmt.Errorf("解析查询参数失败: %w", err)
}
if mobile, exists := baseParams["mobile"]; exists {
baseParams["mobile_no"] = mobile
}
if orderNo != "" {
baseParams["order_no"] = orderNo
}
paramsWithOrder, err := json.Marshal(baseParams)
if err != nil {
logx.Errorf("序列化查询参数失败, Params: %s, Error: %v", string(params), err)
return nil, fmt.Errorf("序列化查询参数失败: %w", err)
}
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
resultsCh = make(chan APIResponseData, len(featureList)) resultsCh = make(chan APIResponseData, len(featureList))
@@ -109,16 +158,23 @@ func (a *ApiRequestService) ProcessRequests(params []byte, productID string) ([]
Success: false, Success: false,
} }
timestamp := time.Now().Format("2006-01-02 15:04:05") timestamp := time.Now().Format("2006-01-02 15:04:05")
// 白名单:已下架模块直接返回占位成功
if whitelistedFeatureApiIds[feature.ApiId] {
result.Success = true
result.Timestamp = timestamp
result.Data = []byte("null")
resultsCh <- result
return
}
var ( var (
resp json.RawMessage resp json.RawMessage
preprocessErr error preprocessErr error
) )
// 若 isImportantMap[feature.ID] == 1则表示需要在出错时重试
isImportant := isImportantMap[feature.Id] == 1 isImportant := isImportantMap[feature.Id] == 1
tryCount := 0 tryCount := 0
for { for {
tryCount++ tryCount++
resp, preprocessErr = a.PreprocessRequestApi(params, feature.ApiId) resp, preprocessErr = a.PreprocessRequestApi(paramsWithOrder, feature.ApiId)
if preprocessErr == nil { if preprocessErr == nil {
break break
} }
@@ -190,7 +246,21 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
"QYGL6F2D": (*ApiRequestService).ProcessQYGL6F2DRequest, "QYGL6F2D": (*ApiRequestService).ProcessQYGL6F2DRequest,
"JRZQ8203": (*ApiRequestService).ProcessJRZQ8203Request, "JRZQ8203": (*ApiRequestService).ProcessJRZQ8203Request,
"JRZQ4AA8": (*ApiRequestService).ProcessJRZQ4AA8Request, "JRZQ4AA8": (*ApiRequestService).ProcessJRZQ4AA8Request,
"QCXGGB2Q": (*ApiRequestService).ProcessQCXGGB2QRequest,
"QCXGYTS2": (*ApiRequestService).ProcessQCXGYTS2Request,
"QCXG5F3A": (*ApiRequestService).ProcessQCXG5F3ARequest,
"QCXG7A2B": (*ApiRequestService).ProcessQCXG7A2BRequest, "QCXG7A2B": (*ApiRequestService).ProcessQCXG7A2BRequest,
"QCXG9P1C": (*ApiRequestService).ProcessQCXG9P1CFRequest,
"QCXG4D2E": (*ApiRequestService).ProcessQCXG4D2ERequest,
"QCXG5U0Z": (*ApiRequestService).ProcessQCXG5U0ZRequest,
"QCXG1U4U": (*ApiRequestService).ProcessQCXG1U4URequest,
"QCXGY7F2": (*ApiRequestService).ProcessQCXGY7F2Request,
"QCXG1H7Y": (*ApiRequestService).ProcessQCXG1H7YRequest,
"QCXG4I1Z": (*ApiRequestService).ProcessQCXG4I1ZRequest,
"QCXG3Y6B": (*ApiRequestService).ProcessQCXG3Y6BRequest,
"QCXG3Z3L": (*ApiRequestService).ProcessQCXG3Z3LRequest,
"QCXGP00W": (*ApiRequestService).ProcessQCXGP00WRequest,
"QCXG6B4E": (*ApiRequestService).ProcessQCXG6B4ERequest,
"DWBG8B4D": (*ApiRequestService).ProcessDWBG8B4DRequest, "DWBG8B4D": (*ApiRequestService).ProcessDWBG8B4DRequest,
"DWBG6A2C": (*ApiRequestService).ProcessDWBG6A2CRequest, "DWBG6A2C": (*ApiRequestService).ProcessDWBG6A2CRequest,
"JRZQ4B6C": (*ApiRequestService).ProcessJRZQ4B6CRequest, "JRZQ4B6C": (*ApiRequestService).ProcessJRZQ4B6CRequest,
@@ -207,6 +277,7 @@ var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, err
"IVYZ8I9J": (*ApiRequestService).ProcessIVYZ8I9JRequest, "IVYZ8I9J": (*ApiRequestService).ProcessIVYZ8I9JRequest,
"JRZQ7F1A": (*ApiRequestService).ProcessJRZQ7F1ARequest, "JRZQ7F1A": (*ApiRequestService).ProcessJRZQ7F1ARequest,
"IVYZ3P9M": (*ApiRequestService).ProcessIVYZ3P9MRequest, "IVYZ3P9M": (*ApiRequestService).ProcessIVYZ3P9MRequest,
"IVYZ4Y27": (*ApiRequestService).ProcessIVYZ4Y27Request,
} }
// PreprocessRequestApi 调用指定的请求处理函数 // PreprocessRequestApi 调用指定的请求处理函数
@@ -1091,24 +1162,290 @@ func (a *ApiRequestService) ProcessQYGL6F2DRequest(params []byte) ([]byte, error
return nil, fmt.Errorf("响应code错误%s", code.String()) return nil, fmt.Errorf("响应code错误%s", code.String())
} }
// ProcessQCXGGB2QRequest 人车核验简版
func (a *ApiRequestService) ProcessQCXGGB2QRequest(params []byte) ([]byte, error) {
plateNo := gjson.GetBytes(params, "plate_no")
carplateType := gjson.GetBytes(params, "carplate_type")
name := gjson.GetBytes(params, "name")
if !plateNo.Exists() || !carplateType.Exists() || !name.Exists() {
return nil, errors.New("api请求, QCXGGB2Q, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXGGB2Q", map[string]interface{}{
"plate_no": plateNo.String(),
"carplate_type": carplateType.String(),
"name": name.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessQCXGYTS2Request 人车核验详版
func (a *ApiRequestService) ProcessQCXGYTS2Request(params []byte) ([]byte, error) {
plateNo := gjson.GetBytes(params, "plate_no")
carplateType := gjson.GetBytes(params, "carplate_type")
name := gjson.GetBytes(params, "name")
if !plateNo.Exists() || !carplateType.Exists() || !name.Exists() {
return nil, errors.New("api请求, QCXGYTS2, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXGYTS2", map[string]interface{}{
"plate_no": plateNo.String(),
"carplate_type": carplateType.String(),
"name": name.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessQCXG5F3ARequest 名下车辆(车牌)
func (a *ApiRequestService) ProcessQCXG5F3ARequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card")
name := gjson.GetBytes(params, "name")
if !idCard.Exists() || !name.Exists() {
return nil, errors.New("api请求, QCXG5F3A, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXG5F3A", map[string]interface{}{
"id_card": idCard.String(),
"name": name.String(),
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessQCXG7A2BRequest 名下车辆 // ProcessQCXG7A2BRequest 名下车辆
func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) { func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card") idCard := gjson.GetBytes(params, "id_card")
if !idCard.Exists() { if !idCard.Exists() {
return nil, errors.New("api请求, QCXG7A2B, 获取相关参数失败") return nil, errors.New("api请求, QCXG7A2B, 获取相关参数失败")
} }
resp, err := a.tianyuanapi.CallInterface("QCXG7A2B", map[string]interface{}{ resp, err := a.tianyuanapi.CallInterface("QCXG7A2B", map[string]interface{}{
"id_card": idCard.String(), "id_card": idCard.String(),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
return convertTianyuanResponse(resp) return convertTianyuanResponse(resp)
} }
// ProcessQCXG9P1CFRequest 名下车辆车牌查询 A
func (a *ApiRequestService) ProcessQCXG9P1CFRequest(params []byte) ([]byte, error) {
idCard := gjson.GetBytes(params, "id_card")
name := gjson.GetBytes(params, "name")
mobile := gjson.GetBytes(params, "mobile")
if !idCard.Exists() || !name.Exists() || !mobile.Exists() {
return nil, errors.New("api请求, QCXG9P1C, 获取相关参数失败")
}
resp, err := a.tianyuanapi.CallInterface("QCXG9P1C", map[string]interface{}{
"id_card": idCard.String(),
"authorized": "1",
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG4D2ERequest(params []byte) ([]byte, error) {
m := map[string]interface{}{}
if err := json.Unmarshal(params, &m); err != nil {
return nil, fmt.Errorf("api请求, QCXG4D2E, 解析参数失败: %w", err)
}
body := map[string]interface{}{}
if v, ok := m["user_type"].(string); ok && v != "" {
body["user_type"] = v
} else {
body["user_type"] = "1"
}
if v, ok := m["id_card"].(string); ok {
body["id_card"] = v
} else {
return nil, errors.New("api请求, QCXG4D2E, 缺少 id_card")
}
resp, err := a.tianyuanapi.CallInterface("QCXG4D2E", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG5U0ZRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
if !vin.Exists() || vin.String() == "" {
return nil, errors.New("api请求, QCXG5U0Z, 缺少 vin_code")
}
resp, err := a.tianyuanapi.CallInterface("QCXG5U0Z", map[string]interface{}{"vin_code": vin.String()})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG1U4URequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "image_url"}, nil)
orderNo := gjson.GetBytes(params, "order_no").String()
if body["vin_code"] == nil || body["image_url"] == nil || orderNo == "" {
return nil, errors.New("api请求, QCXG1U4U, 缺少必填参数 vin_code/image_url/order_no")
}
logx.Infof("vehicle api request QCXG1U4U, order_no=%s, vin_code=%v, image_url=%v", orderNo, body["vin_code"], body["image_url"])
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG1U4U")
resp, err := a.tianyuanapi.CallInterface("QCXG1U4U", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXGY7F2Request(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code", "vehicle_location", "first_registrationdate"}, nil)
if body["vin_code"] == nil || body["vehicle_location"] == nil || body["first_registrationdate"] == nil {
return nil, errors.New("api请求, QCXGY7F2, 缺少必填参数 vin_code/vehicle_location/first_registrationdate")
}
logx.Infof("vehicle api request QCXGY7F2, vin_code=%v, vehicle_location=%v, first_registrationdate=%v", body["vin_code"], body["vehicle_location"], body["first_registrationdate"])
resp, err := a.tianyuanapi.CallInterface("QCXGY7F2", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG1H7YRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
plate := gjson.GetBytes(params, "car_license")
if !vin.Exists() || vin.String() == "" || !plate.Exists() || plate.String() == "" {
return nil, errors.New("api请求, QCXG1H7Y, 缺少 vin_code 或 car_license")
}
body := map[string]interface{}{
"vin_code": vin.String(),
"plate_no": plate.String(),
}
logx.Infof("vehicle api request QCXG1H7Y, vin_code=%s, plate_no=%s", vin.String(), plate.String())
resp, err := a.tianyuanapi.CallInterface("QCXG1H7Y", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG4I1ZRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
if !vin.Exists() || vin.String() == "" {
return nil, errors.New("api请求, QCXG4I1Z, 缺少 vin_code")
}
resp, err := a.tianyuanapi.CallInterface("QCXG4I1Z", map[string]interface{}{"vin_code": vin.String()})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG3Y6BRequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code"}, nil)
orderNo := gjson.GetBytes(params, "order_no").String()
if body["vin_code"] == nil || orderNo == "" {
return nil, errors.New("api请求, QCXG3Y6B, 缺少必填参数 vin_code/order_no")
}
logx.Infof("vehicle api request QCXG3Y6B, order_no=%s, vin_code=%v", orderNo, body["vin_code"])
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG3Y6B")
resp, err := a.tianyuanapi.CallInterface("QCXG3Y6B", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG3Z3LRequest(params []byte) ([]byte, error) {
body := buildVehicleBody(params, []string{"vin_code"}, nil)
orderNo := gjson.GetBytes(params, "order_no").String()
if body["vin_code"] == nil || orderNo == "" {
return nil, errors.New("api请求, QCXG3Z3L, 缺少必填参数 vin_code/order_no")
}
logx.Infof("vehicle api request QCXG3Z3L, order_no=%s, vin_code=%v", orderNo, body["vin_code"])
body["return_url"] = a.buildVehicleCallbackURL(orderNo, "QCXG3Z3L")
resp, err := a.tianyuanapi.CallInterface("QCXG3Z3L", body)
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXGP00WRequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
orderNo := gjson.GetBytes(params, "order_no").String()
vlphoto := gjson.GetBytes(params, "vlphoto_data")
if !vin.Exists() || vin.String() == "" || orderNo == "" || !vlphoto.Exists() || vlphoto.String() == "" {
return nil, errors.New("api请求, QCXGP00W, 缺少必填参数 vin_code/order_no/vlphoto_data")
}
logx.Infof("vehicle api request QCXGP00W, order_no=%s, vin_code=%s, vlphoto_data_len=%d", orderNo, vin.String(), len(vlphoto.String()))
key, err := hex.DecodeString(a.config.Encrypt.SecretKey)
if err != nil {
return nil, fmt.Errorf("api请求, QCXGP00W, 密钥解析失败: %w", err)
}
encData, err := crypto.AesEncrypt([]byte(vlphoto.String()), key)
if err != nil {
return nil, fmt.Errorf("api请求, QCXGP00W, 加密行驶证数据失败: %w", err)
}
resp, err := a.tianyuanapi.CallInterface("QCXGP00W", map[string]interface{}{
"vin_code": vin.String(),
"return_url": a.buildVehicleCallbackURL(orderNo, "QCXGP00W"),
"data": encData,
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
func (a *ApiRequestService) ProcessQCXG6B4ERequest(params []byte) ([]byte, error) {
vin := gjson.GetBytes(params, "vin_code")
if !vin.Exists() || vin.String() == "" {
return nil, errors.New("api请求, QCXG6B4E, 缺少 vin_code")
}
auth := gjson.GetBytes(params, "authorized").String()
if auth == "" {
auth = "1"
}
resp, err := a.tianyuanapi.CallInterface("QCXG6B4E", map[string]interface{}{
"vin_code": vin.String(),
"authorized": auth,
})
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// buildVehicleBody 从 params 中取 required 与 optional 键,仅非空才写入 body
func buildVehicleBody(params []byte, required, optional []string) map[string]interface{} {
body := make(map[string]interface{})
for _, k := range required {
v := gjson.GetBytes(params, k)
if v.Exists() {
body[k] = v.String()
}
}
for _, k := range optional {
v := gjson.GetBytes(params, k)
if v.Exists() && v.String() != "" {
body[k] = v.String()
}
}
return body
}
// buildVehicleCallbackURL 生成车辆类接口的异步回调地址
func (a *ApiRequestService) buildVehicleCallbackURL(orderNo, apiID string) string {
base := strings.TrimRight(a.config.Promotion.OfficialDomain, "/")
if base == "" {
return fmt.Sprintf("/api/v1/tianyuan/vehicle/callback?order_no=%s&api_id=%s", orderNo, apiID)
}
return fmt.Sprintf("%s/api/v1/tianyuan/vehicle/callback?order_no=%s&api_id=%s", base, orderNo, apiID)
}
// ProcessYYSY09CDRequest 三要素 // ProcessYYSY09CDRequest 三要素
func (a *ApiRequestService) ProcessYYSY09CDRequest(params []byte) ([]byte, error) { func (a *ApiRequestService) ProcessYYSY09CDRequest(params []byte) ([]byte, error) {
name := gjson.GetBytes(params, "name") name := gjson.GetBytes(params, "name")
@@ -1571,6 +1908,35 @@ func (a *ApiRequestService) ProcessIVYZ3P9MRequest(params []byte) ([]byte, error
"name": name.String(), "name": name.String(),
}) })
if err != nil {
return nil, err
}
return convertTianyuanResponse(resp)
}
// ProcessIVYZ4Y27Request 学历信息查询需生成专用授权书PDF并Base64编码传入
func (a *ApiRequestService) ProcessIVYZ4Y27Request(params []byte) ([]byte, error) {
name := gjson.GetBytes(params, "name")
idCard := gjson.GetBytes(params, "id_card")
if !name.Exists() || !idCard.Exists() {
return nil, errors.New("api请求, IVYZ4Y27, 获取相关参数失败")
}
// 生成专用授权书PDF并Base64编码
userInfo := map[string]interface{}{
"name": name.String(),
"id_card": idCard.String(),
}
authFileBase64, err := a.authService.GenerateIVYZ4Y27AuthorizationBase64(userInfo)
if err != nil {
return nil, fmt.Errorf("生成IVYZ4Y27授权书失败: %v", err)
}
resp, err := a.tianyuanapi.CallInterface("IVYZ4Y27", map[string]interface{}{
"name": name.String(),
"id_card": idCard.String(),
"auth_authorize_file_base64": authFileBase64,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,13 @@ import (
"bytes" "bytes"
"context" "context"
"database/sql" "database/sql"
"encoding/base64"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"qnc-server/app/main/api/internal/config" "qnc-server/app/main/api/internal/config"
"qnc-server/app/main/model" "qnc-server/app/main/model"
"text/template"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@@ -223,3 +225,149 @@ func getUserInfoString(userInfo map[string]interface{}, key string) string {
} }
return "" return ""
} }
// generatePDFFromTemplate 从模板文件生成PDF内容
func (s *AuthorizationService) generatePDFFromTemplate(templatePath string, data map[string]string) ([]byte, error) {
// 查找模板文件
tmplPaths := []string{
templatePath,
"/app/" + templatePath,
"app/main/api/" + templatePath,
"../../" + templatePath,
"../../../" + templatePath,
"../../../../" + templatePath,
"../../../../../" + templatePath,
"../../static/" + filepath.Base(templatePath),
"../../../static/" + filepath.Base(templatePath),
"../../../../static/" + filepath.Base(templatePath),
}
var tmplFile string
for _, p := range tmplPaths {
if _, err := os.Stat(p); err == nil {
tmplFile = p
break
}
}
if tmplFile == "" {
return nil, fmt.Errorf("未找到模板文件: %s", templatePath)
}
// 解析模板文件
tmpl, err := template.ParseFiles(tmplFile)
if err != nil {
return nil, errors.Wrapf(err, "解析模板文件失败: %s", tmplFile)
}
// 渲染模板
var contentBuf bytes.Buffer
if err := tmpl.Execute(&contentBuf, data); err != nil {
return nil, errors.Wrapf(err, "渲染模板失败")
}
renderedContent := contentBuf.String()
// 创建PDF文档
pdf := gofpdf.New("P", "mm", "A4", "")
// 加载中文字体
fontPaths := []string{
"static/SIMHEI.TTF",
"/app/static/SIMHEI.TTF",
"app/main/api/static/SIMHEI.TTF",
"../../static/SIMHEI.TTF",
"../../../static/SIMHEI.TTF",
"../../../../static/SIMHEI.TTF",
}
fontAdded := false
for _, fontPath := range fontPaths {
if _, err := os.Stat(fontPath); err == nil {
pdf.AddUTF8Font("ChineseFont", "", fontPath)
fontAdded = true
logx.Infof("成功加载字体: %s", fontPath)
break
} else {
logx.Debugf("字体文件不存在: %s, 错误: %v", fontPath, err)
}
}
if !fontAdded {
pdf.SetFont("Arial", "", 11)
logx.Errorf("未找到中文字体文件使用默认Arial字体可能无法正确显示中文")
} else {
pdf.SetFont("ChineseFont", "", 11)
}
// 设置每页Footer回调用于在每页添加水印
companyName := data["CompanyName"]
if companyName == "" {
companyName = "海南海宇大数据科技有限公司"
}
capturedFontAdded := fontAdded
capturedCompanyName := companyName
pdf.SetFooterFunc(func() {
addWatermarkToPage(pdf, capturedFontAdded, capturedCompanyName)
})
pdf.AddPage()
// 写入渲染后的内容
pdf.MultiCell(0, 6, renderedContent, "", "", false)
// 输出PDF字节
var pdfBuf bytes.Buffer
if err := pdf.Output(&pdfBuf); err != nil {
return nil, errors.Wrapf(err, "生成PDF字节数组失败")
}
return pdfBuf.Bytes(), nil
}
// addWatermarkToPage 给当前页面添加水印(红色公司名,居中单个水印)
func addWatermarkToPage(pdf *gofpdf.Fpdf, fontAdded bool, companyName string) {
if fontAdded {
pdf.SetFont("ChineseFont", "", 40)
} else {
pdf.SetFont("Arial", "", 40)
}
pdf.SetTextColor(255, 200, 200) // 浅红色
// A4页面尺寸 210x297mm水印放在页面正中央
pageWidth, pageHeight := pdf.GetPageSize()
x := pageWidth / 2
y := pageHeight / 2
pdf.TransformBegin()
pdf.TransformRotate(45, x, y)
pdf.Text(x, y, companyName)
pdf.TransformEnd()
pdf.SetTextColor(0, 0, 0)
}
// GenerateIVYZ4Y27AuthorizationBase64 生成IVYZ4Y27专用授权书PDF并返回Base64编码字符串
func (s *AuthorizationService) GenerateIVYZ4Y27AuthorizationBase64(userInfo map[string]interface{}) (string, error) {
name := getUserInfoString(userInfo, "name")
idCard := getUserInfoString(userInfo, "id_card")
if name == "" || idCard == "" {
return "", fmt.Errorf("缺少必要的用户信息name或id_card")
}
// 构建模板变量
data := map[string]string{
"CompanyName": "海南海宇大数据科技有限公司",
"Name": name,
"IdCard": idCard,
"Mobile": getUserInfoString(userInfo, "mobile"),
"Date": time.Now().Format("2006年1月2日"),
}
// 从模板生成PDF
pdfBytes, err := s.generatePDFFromTemplate("static/authorization_ivyz4y27.tmpl", data)
if err != nil {
return "", errors.Wrapf(err, "生成IVYZ4Y27授权书PDF失败")
}
// Base64编码
return base64.StdEncoding.EncodeToString(pdfBytes), nil
}

View File

@@ -0,0 +1,150 @@
package tianxingjuhe
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Client 天行聚合API客户端
type Client struct {
baseURL string
key string
client *http.Client
}
// Config 客户端配置
type Config struct {
BaseURL string // API基础URL
Key string // API密钥
Timeout int // 超时时间(秒)
}
// Response 通用API响应结构
type Response struct {
Code int `json:"code"` // 状态码200表示成功
Msg string `json:"msg"` // 返回说明
Result interface{} `json:"result"` // 返回结果集,具体内容根据接口而定
}
// NewClient 创建新的客户端实例
func NewClient(config Config) (*Client, error) {
if config.BaseURL == "" {
return nil, fmt.Errorf("baseURL不能为空")
}
if config.Key == "" {
return nil, fmt.Errorf("key不能为空")
}
return &Client{
baseURL: config.BaseURL,
key: config.Key,
client: &http.Client{
Timeout: time.Duration(config.Timeout) * time.Second,
},
}, nil
}
// Get 发送GET请求
func (c *Client) Get(endpoint string, params map[string]interface{}) (*Response, error) {
// 构建完整URL
fullURL := fmt.Sprintf("%s/%s", c.baseURL, endpoint)
// 添加请求参数
queryParams := url.Values{}
for key, value := range params {
queryParams.Set(key, fmt.Sprintf("%v", value))
}
// 添加key参数
queryParams.Set("key", c.key)
// 拼接查询参数
if len(queryParams) > 0 {
fullURL = fmt.Sprintf("%s?%s", fullURL, queryParams.Encode())
}
// 创建HTTP请求
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Tianxingjuhe-Go-SDK/1.0.0")
// 发送请求
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var apiResp Response
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
return &apiResp, nil
}
// Post 发送POST请求
func (c *Client) Post(endpoint string, params map[string]interface{}) (*Response, error) {
// 构建完整URL
fullURL := fmt.Sprintf("%s/%s", c.baseURL, endpoint)
// 添加key参数
if params == nil {
params = make(map[string]interface{})
}
params["key"] = c.key
// 序列化请求体
requestBody, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %v", err)
}
// 创建HTTP请求
req, err := http.NewRequest("POST", fullURL, bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Tianxingjuhe-Go-SDK/1.0.0")
// 发送请求
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var apiResp Response
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
return &apiResp, nil
}

Some files were not shown because too many files have changed in this diff Show More