From 3399de0dc52293002708d4db46a273773ef15440 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 13 May 2026 14:43:10 +0800 Subject: [PATCH] f --- app/main/api/desc/admin/admin_agent.api | 121 +++++++--- app/main/api/desc/admin/admin_product.api | 7 +- app/main/api/desc/admin/order.api | 3 + app/main/api/desc/admin/platform_user.api | 1 + app/main/api/desc/front/agent.api | 6 +- .../admingetagentordersettlementhandler.go | 29 +++ .../admingetagentteamtreehandler.go | 29 +++ app/main/api/internal/handler/routes.go | 10 + .../admingetagentcommissionlistlogic.go | 58 ++++- .../admin_agent/admingetagentlinklistlogic.go | 22 ++ .../admin_agent/admingetagentlistlogic.go | 99 ++++++--- .../admingetagentorderlistlogic.go | 85 ++++++- .../admingetagentordersettlementlogic.go | 98 +++++++++ .../admingetagentrealnamelistlogic.go | 9 + .../admingetagentrebatelistlogic.go | 89 +++++++- .../admin_agent/admingetagentteamtreelogic.go | 207 ++++++++++++++++++ .../admingetagentupgradelistlogic.go | 30 ++- .../admingetagentwithdrawallistlogic.go | 53 ++++- .../admingetinvitecodelistlogic.go | 32 +-- .../adminupdateagentconfiglogic.go | 9 +- .../logic/admin_agent/agentcodecollection.go | 37 ++++ .../admin_feature/adminupdatefeaturelogic.go | 5 + .../admingetnotificationlistlogic.go | 6 +- .../adminupdatenotificationlogic.go | 25 ++- .../admin_order/admingetorderlistlogic.go | 45 ++++ .../admin_order/adminrefundorderlogic.go | 7 + .../admingetplatformuserlistlogic.go | 16 +- .../admin_product/admincreateproductlogic.go | 12 +- .../admin_product/admindeleteproductlogic.go | 27 ++- .../admingetproductdetaillogic.go | 11 +- .../admingetproductfeaturelistlogic.go | 1 + .../admin_product/admingetproductlistlogic.go | 14 +- .../adminupdateproductfeatureslogic.go | 46 ++-- .../admin_product/adminupdateproductlogic.go | 3 - .../logic/agent/applywithdrawallogic.go | 60 +++-- .../logic/agent/generatinglinklogic.go | 19 +- .../logic/agent/getagentproductconfiglogic.go | 5 +- .../api/internal/logic/pay/paymentlogic.go | 6 +- .../logic/pay/wechatpayrefundcallbacklogic.go | 6 + .../api/internal/logic/productcost/sync.go | 51 +++++ .../internal/logic/user/bindmobilelogic.go | 9 +- .../logic/user/mobilecodeloginlogic.go | 2 +- .../api/internal/queue/paySuccessNotify.go | 17 +- app/main/api/internal/service/agentService.go | 170 +++++++++++++- app/main/api/internal/types/types.go | 203 +++++++++++------ app/main/model/agentModel.go | 38 ++++ app/main/model/productFeatureModel.go | 55 +++++ deploy/sql/agent_system_migration.sql | 3 + pkg/lzkit/lzUtils/utils.go | 28 ++- 49 files changed, 1637 insertions(+), 287 deletions(-) create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentordersettlementhandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentteamtreehandler.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentordersettlementlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentteamtreelogic.go create mode 100644 app/main/api/internal/logic/admin_agent/agentcodecollection.go create mode 100644 app/main/api/internal/logic/productcost/sync.go diff --git a/app/main/api/desc/admin/admin_agent.api b/app/main/api/desc/admin/admin_agent.api index 2094386..6619a5e 100644 --- a/app/main/api/desc/admin/admin_agent.api +++ b/app/main/api/desc/admin/admin_agent.api @@ -20,6 +20,10 @@ service main { @handler AdminGetAgentList get /list (AdminGetAgentListReq) returns (AdminGetAgentListResp) + // 代理团队树(同钻石团队 + 邀请关系) + @handler AdminGetAgentTeamTree + get /team/tree (AdminGetAgentTeamTreeReq) returns (AdminGetAgentTeamTreeResp) + // 代理审核 @handler AdminAuditAgent post /audit (AdminAuditAgentReq) returns (AdminAuditAgentResp) @@ -32,6 +36,10 @@ service main { @handler AdminGetAgentOrderList get /order/list (AdminGetAgentOrderListReq) returns (AdminGetAgentOrderListResp) + // 单笔订单分账视图:同一响应返回佣金 + 返佣(避免前端双请求重复走鉴权) + @handler AdminGetAgentOrderSettlement + get /order/settlement (AdminGetAgentOrderSettlementReq) returns (AdminGetAgentOrderSettlementResp) + // 代理佣金分页查询 @handler AdminGetAgentCommissionList get /commission/list (AdminGetAgentCommissionListReq) returns (AdminGetAgentCommissionListResp) @@ -94,9 +102,12 @@ type ( Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 Mobile *string `form:"mobile,optional"` // 手机号(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编码(可选) Region *string `form:"region,optional"` // 区域(可选) Level *int64 `form:"level,optional"` // 等级(可选) TeamLeaderId *string `form:"team_leader_id,optional"` // 团队首领ID(可选) + AgentId *string `form:"agent_id,optional"` // 代理主键UUID,精确查询;传入时不再按 team_leader 与其它条件组合筛选 + RealName *string `form:"real_name,optional"` // 实名姓名(可选,模糊匹配已三要素核验记录) } AgentListItem { Id string `json:"id"` // 主键 @@ -108,7 +119,8 @@ type ( RealName string `json:"real_name"` // 姓名(来自实名表) IdCard string `json:"id_card"` // 身份证(解密后明文返回) 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"` Balance float64 `json:"balance"` // 钱包余额 TotalEarnings float64 `json:"total_earnings"` // 累计收益 @@ -121,6 +133,21 @@ type ( Total int64 `json:"total"` // 总数 Items []AgentListItem `json:"items"` // 列表数据 } + AdminGetAgentTeamTreeReq { + AgentId string `form:"agent_id"` // 锚点代理ID(UUID) + } + 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 { AuditId int64 `json:"audit_id"` // 审核记录ID @@ -135,12 +162,14 @@ type ( Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) ProductId *string `form:"product_id,optional"` // 产品ID(可选) LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) } AgentLinkListItem { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 ProductId string `json:"product_id"` // 产品ID ProductName string `json:"product_name"` // 产品名称 SetPrice float64 `json:"set_price"` // 设定价格 @@ -157,14 +186,18 @@ type ( Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) OrderId *string `form:"order_id,optional"` // 订单ID(可选) + OrderNo *string `form:"order_no,optional"` // 商户订单号(可选) ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选) OrderStatus *string `form:"order_status,optional"` // 订单状态(可选):pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 } AgentOrderListItem { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 OrderId string `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 ProductId string `json:"product_id"` // 产品ID ProductName string `json:"product_name"` // 产品名称 OrderAmount float64 `json:"order_amount"` // 订单金额 @@ -180,18 +213,28 @@ type ( Total int64 `json:"total"` // 总数 Items []AgentOrderListItem `json:"items"` // 列表数据 } + AdminGetAgentOrderSettlementReq { + OrderNo string `form:"order_no"` // 商户订单号 + } + AdminGetAgentOrderSettlementResp { + Commissions []AgentCommissionListItem `json:"commissions"` // 本单销售佣金 + Rebates []AgentRebateListItem `json:"rebates"` // 本单返佣 + } // 代理佣金分页查询 AdminGetAgentCommissionListReq { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *string `form:"agent_id,optional"` // 代理ID(可选) - OrderId *string `form:"order_id,optional"` // 订单ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) + OrderNo *string `form:"order_no,optional"` // 商户订单号(可选) + Status *int64 `form:"status,optional"` // 状态(可选) } AgentCommissionListItem { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 OrderId string `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 ProductName string `json:"product_name"` // 产品名称 Amount float64 `json:"amount"` // 金额 Status int64 `json:"status"` // 状态 @@ -203,18 +246,23 @@ type ( } // 代理返佣分页查询 AdminGetAgentRebateListReq { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *string `form:"agent_id,optional"` // 代理ID(可选) - SourceAgentId *string `form:"source_agent_id,optional"` // 来源代理ID(可选) - RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选) - Status *int64 `form:"status,optional"` // 状态(可选):1=已发放,2=已冻结,3=已取消(已退款) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *string `form:"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"` // 返佣类型(可选) + Status *int64 `form:"status,optional"` // 状态(可选):1=已发放,2=已冻结,3=已取消(已退款) } AgentRebateListItem { - Id string `json:"id"` // 主键 - AgentId string `json:"agent_id"` // 获得返佣的代理ID - SourceAgentId string `json:"source_agent_id"` // 来源代理ID - OrderId string `json:"order_id"` // 订单ID + Id string `json:"id"` // 主键 + AgentId string `json:"agent_id"` // 获得返佣的代理ID + AgentCode int64 `json:"agent_code"` // 获得返佣代理编号 + SourceAgentId string `json:"source_agent_id"` // 来源代理ID + SourceAgentCode int64 `json:"source_agent_code"` // 来源代理编号 + OrderId string `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 RebateType int64 `json:"rebate_type"` // 返佣类型 Amount float64 `json:"amount"` // 金额 Status int64 `json:"status"` // 状态:1=已发放,2=已冻结,3=已取消(已退款) @@ -229,12 +277,14 @@ type ( Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) UpgradeType *int64 `form:"upgrade_type,optional"` // 升级类型(可选) Status *int64 `form:"status,optional"` // 状态(可选) } AgentUpgradeListItem { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 FromLevel int64 `json:"from_level"` // 原等级 ToLevel int64 `json:"to_level"` // 目标等级 UpgradeType int64 `json:"upgrade_type"` // 升级类型 @@ -249,15 +299,20 @@ type ( } // 代理提现分页查询 AdminGetAgentWithdrawalListReq { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *string `form:"agent_id,optional"` // 代理ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选) - WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *string `form:"agent_id,optional"` // 代理主键 UUID(弹窗内按代理筛选时传入) + AgentCode *string `form:"agent_code,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 { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 WithdrawNo string `json:"withdraw_no"` // 提现单号 Amount float64 `json:"amount"` // 金额 TaxAmount float64 `json:"tax_amount"` // 税费金额 @@ -294,6 +349,7 @@ type ( AgentRealNameListItem { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 Name string `json:"name"` // 姓名 IdCard string `json:"id_card"` // 身份证号 Mobile string `json:"mobile"` // 手机号 @@ -412,20 +468,21 @@ type ( AdminGetInviteCodeListReq { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 - Code *string `form:"code,optional"` // 邀请码(可选) - AgentId *string `form:"agent_id,optional"` // 发放代理ID(可选,NULL表示平台发放) + Code *string `form:"code,optional"` // 邀请码(可选,模糊匹配) TargetLevel *int64 `form:"target_level,optional"` // 目标等级(可选) - Status *int64 `form:"status,optional"` // 状态(可选) + Status string `form:"status,optional"` // 状态(可选):0=未使用,1=已使用,2=已失效 } InviteCodeListItem { - Id string `json:"id"` // 主键 - Code string `json:"code"` // 邀请码 - AgentId string `json:"agent_id"` // 发放代理ID(0表示平台发放) - AgentMobile string `json:"agent_mobile"` // 发放代理手机号 - TargetLevel int64 `json:"target_level"` // 目标等级 - Status int64 `json:"status"` // 状态:0=未使用,1=已使用,2=已失效 - UsedUserId string `json:"used_user_id"` // 使用用户ID - UsedAgentId string `json:"used_agent_id"` // 使用代理ID + Id string `json:"id"` // 主键 + Code string `json:"code"` // 邀请码 + AgentId string `json:"agent_id"` // 发放代理ID(0表示平台发放) + AgentCode int64 `json:"agent_code"` // 发放代理编号(平台为0) + AgentMobile string `json:"agent_mobile"` // 发放代理手机号 + TargetLevel int64 `json:"target_level"` // 目标等级 + Status int64 `json:"status"` // 状态:0=未使用,1=已使用,2=已失效 + UsedUserId string `json:"used_user_id"` // 使用用户ID + UsedAgentId string `json:"used_agent_id"` // 使用代理ID + UsedAgentCode int64 `json:"used_agent_code"` // 使用代理编号 UsedTime string `json:"used_time"` // 使用时间 ExpireTime string `json:"expire_time"` // 过期时间 Remark string `json:"remark"` // 备注 diff --git a/app/main/api/desc/admin/admin_product.api b/app/main/api/desc/admin/admin_product.api index c3ba2c7..8fda14c 100644 --- a/app/main/api/desc/admin/admin_product.api +++ b/app/main/api/desc/admin/admin_product.api @@ -49,7 +49,6 @@ type ( ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes,optional"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 SellPrice float64 `json:"sell_price"` // 售价 } @@ -65,7 +64,6 @@ type ( ProductEn *string `json:"product_en,optional"` // 英文名 Description *string `json:"description,optional"` // 描述 Notes *string `json:"notes,optional"` // 备注 - CostPrice *float64 `json:"cost_price,optional"` // 成本 SellPrice *float64 `json:"sell_price,optional"` // 售价 } @@ -99,7 +97,7 @@ type ( ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 + CostPrice float64 `json:"cost_price"` // 成本(由关联模块成本汇总,只读) SellPrice float64 `json:"sell_price"` // 售价 CreateTime string `json:"create_time"` // 创建时间 UpdateTime string `json:"update_time"` // 更新时间 @@ -123,7 +121,7 @@ type ( ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 + CostPrice float64 `json:"cost_price"` // 成本(由关联模块成本汇总,只读) SellPrice float64 `json:"sell_price"` // 售价 CreateTime string `json:"create_time"` // 创建时间 UpdateTime string `json:"update_time"` // 更新时间 @@ -141,6 +139,7 @@ type ( FeatureId string `json:"feature_id"` // 功能ID ApiId string `json:"api_id"` // API标识 Name string `json:"name"` // 功能描述 + CostPrice float64 `json:"cost_price"` // 模块成本价(元,来自 feature) Sort int64 `json:"sort"` // 排序 Enable int64 `json:"enable"` // 是否启用 IsImportant int64 `json:"is_important"` // 是否重要 diff --git a/app/main/api/desc/admin/order.api b/app/main/api/desc/admin/order.api index 0f9f5b7..2bfdf3f 100644 --- a/app/main/api/desc/admin/order.api +++ b/app/main/api/desc/admin/order.api @@ -53,6 +53,8 @@ type ( PaymentScene string `form:"payment_scene,optional"` // 支付平台 Amount float64 `form:"amount,optional"` // 金额 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"` // 创建时间开始 CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 @@ -81,6 +83,7 @@ type ( RefundTime string `json:"refund_time"` // 退款时间 UpdateTime string `json:"update_time"` // 更新时间 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 + AgentCode string `json:"agent_code"` // 代理编号 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 } // 详情请求 diff --git a/app/main/api/desc/admin/platform_user.api b/app/main/api/desc/admin/platform_user.api index 1b3b56f..f9ccaeb 100644 --- a/app/main/api/desc/admin/platform_user.api +++ b/app/main/api/desc/admin/platform_user.api @@ -39,6 +39,7 @@ type ( AdminGetPlatformUserListReq { Page int64 `form:"page,default=1"` // 页码 PageSize int64 `form:"pageSize,default=20"` // 每页数量 + UserId string `form:"user_id,optional"` // 平台用户主键 ID,精确筛选 Mobile string `form:"mobile,optional"` // 手机号 Nickname string `form:"nickname,optional"` // 昵称 Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index 6adcc3a..a5be4db 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -312,9 +312,9 @@ type ( } // 生成推广链接 AgentGeneratingLinkReq { - ProductId string `json:"product_id"` // 产品ID - SetPrice float64 `json:"set_price"` // 设定价格 - TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) + ProductId string `json:"product_id"` // 产品ID + SetPrice string `json:"set_price"` // 设定价格 + TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) } AgentGeneratingLinkResp { LinkIdentifier string `json:"link_identifier"` // 推广链接标识 diff --git a/app/main/api/internal/handler/admin_agent/admingetagentordersettlementhandler.go b/app/main/api/internal/handler/admin_agent/admingetagentordersettlementhandler.go new file mode 100644 index 0000000..9c757c9 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentordersettlementhandler.go @@ -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) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentteamtreehandler.go b/app/main/api/internal/handler/admin_agent/admingetagentteamtreehandler.go new file mode 100644 index 0000000..9a20c31 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentteamtreehandler.go @@ -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) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index 4dd8855..4f309a7 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -83,6 +83,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/order/list", Handler: admin_agent.AdminGetAgentOrderListHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/order/settlement", + Handler: admin_agent.AdminGetAgentOrderSettlementHandler(serverCtx), + }, { Method: http.MethodGet, Path: "/product_config/list", @@ -103,6 +108,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/rebate/list", Handler: admin_agent.AdminGetAgentRebateListHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/team/tree", + Handler: admin_agent.AdminGetAgentTeamTreeHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/upgrade/agent", diff --git a/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go index c9f31af..9cabb4f 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go @@ -2,6 +2,7 @@ package admin_agent import ( "context" + "strconv" "qnc-server/app/main/api/internal/svc" "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) { 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 { builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) } if req.Status != nil { 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") if err != nil { return nil, err } - // 批量查product_name + // 批量查 product_name productIds := make(map[string]struct{}) for _, v := range list { 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)) for _, v := range list { item := types.AgentCommissionListItem{} _ = copier.Copy(&item, v) + item.AgentCode = agentCodeMap[v.AgentId] item.ProductName = productNameMap[v.ProductId] + item.OrderNo = orderNoMap[v.OrderId] item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") items = append(items, item) } diff --git a/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go index 312cf22..d351f89 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go @@ -2,9 +2,12 @@ package admin_agent import ( "context" + "fmt" + "strings" "qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/types" + "qnc-server/common/globalkey" "github.com/Masterminds/squirrel" "github.com/zeromicro/go-zero/core/logx" @@ -29,6 +32,16 @@ func (l *AdminGetAgentLinkListLogic) AdminGetAgentLinkList(req *types.AdminGetAg if req.AgentId != nil { 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 != "" { 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)) for _, link := range links { items = append(items, types.AgentLinkListItem{ Id: link.Id, AgentId: link.AgentId, + AgentCode: agentCodeMap[link.AgentId], ProductId: link.ProductId, ProductName: productNameMap[link.ProductId], SetPrice: link.SetPrice, diff --git a/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go index 08423c0..fdbb333 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go @@ -3,9 +3,12 @@ package admin_agent import ( "context" "encoding/hex" + "fmt" + "strings" "qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/types" + "qnc-server/common/globalkey" "qnc-server/common/xerr" "qnc-server/pkg/lzkit/crypto" @@ -31,26 +34,52 @@ func NewAdminGetAgentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListReq) (resp *types.AdminGetAgentListResp, err error) { builder := l.svcCtx.AgentModel.SelectBuilder() - // 如果传入TeamLeaderId,则查找该团队首领下的所有代理 - if req.TeamLeaderId != nil { - builder = builder.Where(squirrel.Eq{"team_leader_id": *req.TeamLeaderId}) - } - if req.Level != nil { - builder = builder.Where(squirrel.Eq{"level": *req.Level}) - } - if req.Mobile != nil && *req.Mobile != "" { - // 加密手机号进行查询 - encryptedMobile, err := crypto.EncryptMobile(*req.Mobile, l.svcCtx.Config.Encrypt.SecretKey) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) + exactByAgentId := req.AgentId != nil && *req.AgentId != "" + if exactByAgentId { + 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 { + builder = builder.Where(squirrel.Eq{"level": *req.Level}) + } + if req.Mobile != nil && *req.Mobile != "" { + // 加密手机号进行查询 + encryptedMobile, err := crypto.EncryptMobile(*req.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) + } + builder = builder.Where(squirrel.Eq{"mobile": encryptedMobile}) + } + if req.Region != nil && *req.Region != "" { + // 注意:region字段现在是可选的,查询时需要处理NULL值 + // 如果region字段是NULL,使用IS NULL查询;否则使用等值查询 + // 这里简化处理,直接使用等值查询(如果数据库中有NULL值,需要特殊处理) + 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}) + } } - builder = builder.Where(squirrel.Eq{"mobile": encryptedMobile}) } - if req.Region != nil && *req.Region != "" { - // 注意:region字段现在是可选的,查询时需要处理NULL值 - // 如果region字段是NULL,使用IS NULL查询;否则使用等值查询 - // 这里简化处理,直接使用等值查询(如果数据库中有NULL值,需要特殊处理) - builder = builder.Where(squirrel.Eq{"region": *req.Region}) + + 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") @@ -58,6 +87,17 @@ func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListR 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)) for _, agent := range agents { @@ -117,17 +157,18 @@ func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListR } item := types.AgentListItem{ - Id: agent.Id, - UserId: agent.UserId, - Level: agent.Level, - LevelName: levelName, - Region: region, - Mobile: agent.Mobile, - RealName: realName, - IdCard: idCardPlain, - WechatId: wechatId, - TeamLeaderId: teamLeaderId, - AgentCode: agent.AgentCode, + Id: agent.Id, + UserId: agent.UserId, + Level: agent.Level, + LevelName: levelName, + Region: region, + Mobile: agent.Mobile, + RealName: realName, + IdCard: idCardPlain, + WechatId: wechatId, + TeamLeaderId: teamLeaderId, + SubordinateCount: subCountByLeader[agent.Id], + AgentCode: agent.AgentCode, Balance: 0, TotalEarnings: 0, FrozenBalance: 0, diff --git a/app/main/api/internal/logic/admin_agent/admingetagentorderlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentorderlistlogic.go index 8e12bb5..127bad1 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentorderlistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentorderlistlogic.go @@ -2,6 +2,8 @@ package admin_agent import ( "context" + "strconv" + "qnc-server/common/globalkey" "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) { builder := l.svcCtx.AgentOrderModel.SelectBuilder(). Where("del_state = ?", globalkey.DelStateNo) - if req.AgentId != nil { + if req.AgentId != nil && *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) } + + 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 { 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 if page <= 0 { page = 1 @@ -57,7 +95,6 @@ func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGet return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理订单列表失败, %v", err) } - // 批量查询产品名称 productIdSet := make(map[string]struct{}) for _, order := range orders { 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)) for _, order := range orders { items = append(items, types.AgentOrderListItem{ Id: order.Id, AgentId: order.AgentId, + AgentCode: agentCodeMap[order.AgentId], OrderId: order.OrderId, + OrderNo: orderNoMap[order.OrderId], ProductId: order.ProductId, ProductName: productNameMap[order.ProductId], OrderAmount: order.OrderAmount, @@ -89,6 +163,7 @@ func (l *AdminGetAgentOrderListLogic) AdminGetAgentOrderList(req *types.AdminGet PriceCost: order.PriceCost, AgentProfit: order.AgentProfit, ProcessStatus: order.ProcessStatus, + OrderStatus: orderStatusMap[order.OrderId], CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"), }) } diff --git a/app/main/api/internal/logic/admin_agent/admingetagentordersettlementlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentordersettlementlogic.go new file mode 100644 index 0000000..6191ca4 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentordersettlementlogic.go @@ -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 +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentrealnamelistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentrealnamelistlogic.go index 9f6c988..c272c7a 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentrealnamelistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentrealnamelistlogic.go @@ -60,6 +60,14 @@ func (l *AdminGetAgentRealNameListLogic) AdminGetAgentRealNameList(req *types.Ad 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)) for _, realName := range realNames { @@ -100,6 +108,7 @@ func (l *AdminGetAgentRealNameListLogic) AdminGetAgentRealNameList(req *types.Ad item := types.AgentRealNameListItem{ Id: realName.Id, AgentId: realName.AgentId, + AgentCode: agentCodeMap[realName.AgentId], Name: realName.Name, IdCard: idCard, Mobile: mobile, diff --git a/app/main/api/internal/logic/admin_agent/admingetagentrebatelistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentrebatelistlogic.go index a13aeeb..8717578 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentrebatelistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentrebatelistlogic.go @@ -2,6 +2,8 @@ package admin_agent import ( "context" + "strconv" + "qnc-server/common/globalkey" "qnc-server/common/xerr" @@ -35,9 +37,47 @@ func (l *AdminGetAgentRebateListLogic) AdminGetAgentRebateList(req *types.AdminG if req.AgentId != nil { 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 { builder = builder.Where("rebate_type = ?", *req.RebateType) } @@ -74,17 +114,48 @@ 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)) for _, rebate := range rebates { items = append(items, types.AgentRebateListItem{ - Id: rebate.Id, - AgentId: rebate.AgentId, - SourceAgentId: rebate.SourceAgentId, - OrderId: rebate.OrderId, - RebateType: rebate.RebateType, - Amount: rebate.RebateAmount, - CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"), + Id: rebate.Id, + AgentId: rebate.AgentId, + AgentCode: agentCodeMap[rebate.AgentId], + SourceAgentId: rebate.SourceAgentId, + SourceAgentCode: agentCodeMap[rebate.SourceAgentId], + OrderId: rebate.OrderId, + OrderNo: orderNoMap[rebate.OrderId], + RebateType: rebate.RebateType, + Amount: rebate.RebateAmount, + Status: rebate.Status, + CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"), }) } diff --git a/app/main/api/internal/logic/admin_agent/admingetagentteamtreelogic.go b/app/main/api/internal/logic/admin_agent/admingetagentteamtreelogic.go new file mode 100644 index 0000000..806264a --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentteamtreelogic.go @@ -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 +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentupgradelistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentupgradelistlogic.go index 0595c1d..e4d0105 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentupgradelistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentupgradelistlogic.go @@ -2,6 +2,8 @@ package admin_agent import ( "context" + "strconv" + "qnc-server/common/globalkey" "qnc-server/common/xerr" @@ -31,9 +33,26 @@ func (l *AdminGetAgentUpgradeListLogic) AdminGetAgentUpgradeList(req *types.Admi builder := l.svcCtx.AgentUpgradeModel.SelectBuilder(). 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) } + if req.UpgradeType != nil { 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) } + 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)) for _, upgrade := range upgrades { items = append(items, types.AgentUpgradeListItem{ Id: upgrade.Id, AgentId: upgrade.AgentId, + AgentCode: agentCodeMap[upgrade.AgentId], FromLevel: upgrade.FromLevel, ToLevel: upgrade.ToLevel, UpgradeType: upgrade.UpgradeType, diff --git a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go index eb2cad9..b7ac0e8 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go @@ -2,9 +2,13 @@ package admin_agent import ( "context" + "fmt" + "strconv" + "strings" "qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/types" + "qnc-server/common/globalkey" "github.com/Masterminds/squirrel" "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) { builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder() - if req.AgentId != nil { - builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + if req.AgentId != nil && strings.TrimSpace(*req.AgentId) != "" { + builder = builder.Where(squirrel.Eq{"agent_id": strings.TrimSpace(*req.AgentId)}) } - if req.Status != nil { - builder = builder.Where(squirrel.Eq{"status": *req.Status}) + if req.AgentCode != nil && strings.TrimSpace(*req.AgentCode) != "" { + 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") if err != nil { 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)) for _, v := range list { item := types.AgentWithdrawalListItem{} _ = copier.Copy(&item, v) + item.AgentCode = agentCodeMap[v.AgentId] item.Remark = "" if v.Remark.Valid { item.Remark = v.Remark.String diff --git a/app/main/api/internal/logic/admin_agent/admingetinvitecodelistlogic.go b/app/main/api/internal/logic/admin_agent/admingetinvitecodelistlogic.go index 1fdf7b3..29fd202 100644 --- a/app/main/api/internal/logic/admin_agent/admingetinvitecodelistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetinvitecodelistlogic.go @@ -2,6 +2,9 @@ package admin_agent import ( "context" + "strconv" + "strings" + "qnc-server/common/globalkey" "qnc-server/common/xerr" "qnc-server/pkg/lzkit/crypto" @@ -34,22 +37,18 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet builder := l.svcCtx.AgentInviteCodeModel.SelectBuilder(). Where("del_state = ?", globalkey.DelStateNo) - if req.Code != nil && *req.Code != "" { - builder = builder.Where("code = ?", *req.Code) - } - 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.Code != nil && strings.TrimSpace(*req.Code) != "" { + pat := "%" + strings.TrimSpace(*req.Code) + "%" + builder = builder.Where("code LIKE ?", pat) } if req.TargetLevel != nil { builder = builder.Where("target_level = ?", *req.TargetLevel) } - if req.Status != nil { - builder = builder.Where("status = ?", *req.Status) + if strings.TrimSpace(req.Status) != "" { + st, perr := strconv.ParseInt(strings.TrimSpace(req.Status), 10, 64) + if perr == nil { + builder = builder.Where("status = ?", st) + } } // 2. 分页查询 @@ -58,15 +57,19 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码列表失败, %v", err) } - // 3. 批量查询代理信息(用于显示代理手机号) + // 3. 批量查询代理信息(用于显示代理手机号、代理编号) agentIds := make(map[string]struct{}) for _, v := range list { if v.AgentId.Valid && v.AgentId.String != "" { agentIds[v.AgentId.String] = struct{}{} } + if v.UsedAgentId.Valid && v.UsedAgentId.String != "" { + agentIds[v.UsedAgentId.String] = struct{}{} + } } agentMobileMap := make(map[string]string) + agentCodeMap := make(map[string]int64) if len(agentIds) > 0 { agentIdList := make([]string, 0, len(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}), "") secretKey := l.svcCtx.Config.Encrypt.SecretKey for _, agent := range agents { + agentCodeMap[agent.Id] = agent.AgentCode mobile, decErr := crypto.DecryptMobile(agent.Mobile, secretKey) if decErr == nil { agentMobileMap[agent.Id] = mobile @@ -100,6 +104,7 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet if v.AgentId.Valid { item.AgentId = v.AgentId.String item.AgentMobile = agentMobileMap[v.AgentId.String] + item.AgentCode = agentCodeMap[v.AgentId.String] } if v.UsedUserId.Valid { @@ -107,6 +112,7 @@ func (l *AdminGetInviteCodeListLogic) AdminGetInviteCodeList(req *types.AdminGet } if v.UsedAgentId.Valid { item.UsedAgentId = v.UsedAgentId.String + item.UsedAgentCode = agentCodeMap[v.UsedAgentId.String] } if v.UsedTime.Valid { item.UsedTime = v.UsedTime.Time.Format("2006-01-02 15:04:05") diff --git a/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go b/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go index 2722f86..3473aa9 100644 --- a/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go +++ b/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go @@ -163,12 +163,9 @@ func (l *AdminUpdateAgentConfigLogic) AdminUpdateAgentConfig(req *types.AdminUpd if err := updateConfig("commission_freeze_threshold", &req.CommissionFreeze.Threshold); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结阈值失败, %v", err) } - // 更新解冻天数(整数类型) - if req.CommissionFreeze.Days > 0 { - daysFloat := float64(req.CommissionFreeze.Days) - if err := updateConfig("commission_freeze_days", &daysFloat); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结解冻天数失败, %v", err) - } + daysFloat := float64(req.CommissionFreeze.Days) + if err := updateConfig("commission_freeze_days", &daysFloat); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结解冻天数失败, %v", err) } } diff --git a/app/main/api/internal/logic/admin_agent/agentcodecollection.go b/app/main/api/internal/logic/admin_agent/agentcodecollection.go new file mode 100644 index 0000000..27e7f6c --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/agentcodecollection.go @@ -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 +} diff --git a/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go b/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go index 9158efa..16350a4 100644 --- a/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go +++ b/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go @@ -3,6 +3,7 @@ package admin_feature import ( "context" + "qnc-server/app/main/api/internal/logic/productcost" "qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/types" "qnc-server/common/xerr" @@ -60,6 +61,10 @@ func (l *AdminUpdateFeatureLogic) AdminUpdateFeature(req *types.AdminUpdateFeatu "更新功能失败, err: %v, id: %d", err, req.Id) } + if err := productcost.SyncAllProductsUsingFeature(l.ctx, l.svcCtx, req.Id); err != nil { + return nil, err + } + // 5. 返回成功结果 return &types.AdminUpdateFeatureResp{Success: true}, nil } diff --git a/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go b/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go index e12017d..104c17f 100644 --- a/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go +++ b/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go @@ -2,6 +2,7 @@ package admin_notification import ( "context" + "strings" "time" "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+"%") } 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 { builder = builder.Where("status = ?", *req.Status) diff --git a/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go b/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go index 838242a..d1824bd 100644 --- a/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go +++ b/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go @@ -3,6 +3,7 @@ package admin_notification import ( "context" "database/sql" + "strings" "time" "qnc-server/app/main/api/internal/svc" @@ -33,12 +34,28 @@ func (l *AdminUpdateNotificationLogic) AdminUpdateNotification(req *types.AdminU return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找通知失败, err: %v, id: %d", err, req.Id) } if req.StartDate != nil { - startDate, _ := time.Parse("2006-01-02", *req.StartDate) - notification.StartDate = sql.NullTime{Time: startDate, Valid: true} + 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} + } } if req.EndDate != nil { - endDate, _ := time.Parse("2006-01-02", *req.EndDate) - notification.EndDate = sql.NullTime{Time: endDate, Valid: true} + 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} + } } if req.Title != nil { notification.Title = *req.Title diff --git a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go index 1d76cba..cb1c016 100644 --- a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go +++ b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go @@ -2,6 +2,7 @@ package admin_order import ( "context" + "fmt" "sync" "qnc-server/app/main/api/internal/svc" @@ -54,6 +55,24 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR if 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 != "" { builder = builder.Where("create_time >= ?", req.CreateTimeStart) @@ -101,6 +120,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR queryStateMap := make(map[string]string) agentOrderMap := make(map[string]bool) // 代理订单映射 agentProcessStatusMap := make(map[string]string) // 代理处理状态映射 + agentCodeMap := make(map[string]string) // 代理编号映射 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 { 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] { item.IsAgentOrder = true item.AgentProcessStatus = agentProcessStatusMap[order.Id] + item.AgentCode = agentCodeMap[order.Id] } else { item.IsAgentOrder = false item.AgentProcessStatus = "not_agent" diff --git a/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go b/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go index 5e170ae..7be661a 100644 --- a/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go +++ b/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go @@ -156,6 +156,13 @@ func (l *AdminRefundOrderLogic) createRefundRecordAndUpdateOrder(order *model.Or 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 }) } diff --git a/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go b/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go index d068f82..0a190e5 100644 --- a/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go +++ b/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go @@ -30,8 +30,17 @@ func NewAdminGetPlatformUserListLogic(ctx context.Context, svcCtx *svc.ServiceCo func (l *AdminGetPlatformUserListLogic) AdminGetPlatformUserList(req *types.AdminGetPlatformUserListReq) (resp *types.AdminGetPlatformUserListResp, err error) { builder := l.svcCtx.UserModel.SelectBuilder() + secretKey := l.svcCtx.Config.Encrypt.SecretKey + if req.UserId != "" { + builder = builder.Where("id = ?", req.UserId) + } 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 != "" { 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) } var items []types.PlatformUserListItem - secretKey := l.svcCtx.Config.Encrypt.SecretKey for _, user := range users { mobile := user.Mobile if mobile.Valid { - encryptedMobile, err := crypto.DecryptMobile(mobile.String, secretKey) + DecryptMobile, err := crypto.DecryptMobile(mobile.String, secretKey) if err != nil { 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{ Id: user.Id, diff --git a/app/main/api/internal/logic/admin_product/admincreateproductlogic.go b/app/main/api/internal/logic/admin_product/admincreateproductlogic.go index c025a16..454f901 100644 --- a/app/main/api/internal/logic/admin_product/admincreateproductlogic.go +++ b/app/main/api/internal/logic/admin_product/admincreateproductlogic.go @@ -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) { // 1. 数据转换 data := &model.Product{ @@ -36,7 +37,7 @@ func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProdu ProductEn: req.ProductEn, Description: req.Description, Notes: sql.NullString{String: req.Notes, Valid: req.Notes != ""}, - CostPrice: req.CostPrice, + CostPrice: 0, // 成本由关联模块汇总,创建后为 0,保存模块关联后写入 SellPrice: req.SellPrice, } @@ -51,7 +52,14 @@ func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProdu } 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{ Id: uuid.NewString(), ProductId: productId, diff --git a/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go b/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go index 2c44b7f..6c93236 100644 --- a/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go +++ b/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go @@ -2,8 +2,10 @@ package admin_product import ( "context" + "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" @@ -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) { // 1. 查询记录是否存在 record, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), - "查找产品失败, err: %v, id: %d", err, req.Id) + "查找产品失败, err: %v, id: %s", err, req.Id) } // 2. 执行软删除(使用事务确保产品表和代理产品配置表同步) @@ -39,20 +42,22 @@ func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProdu err = l.svcCtx.ProductModel.DeleteSoft(ctx, session, record) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), - "删除产品失败, err: %v, id: %d", err, req.Id) + "删除产品失败, err: %v, id: %s", err, record.Id) } - // 2.2 同步软删除代理产品配置 - agentProductConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(ctx, req.Id) - if err != nil { - // 如果代理产品配置不存在,记录日志但不影响主流程 - l.Infof("同步删除代理产品配置失败:代理产品配置不存在, productId: %d, err: %v", req.Id, err) - } else { - err = l.svcCtx.AgentProductConfigModel.DeleteSoft(ctx, session, agentProductConfig) - if err != nil { + // 2.2 同步软删除代理产品配置(按产品主键 product_id) + agentProductConfig, findErr := l.svcCtx.AgentProductConfigModel.FindOneByProductId(ctx, record.Id) + switch { + case findErr == nil: + if err = l.svcCtx.AgentProductConfigModel.DeleteSoft(ctx, session, agentProductConfig); err != nil { 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 diff --git a/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go b/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go index ea30028..aa447c5 100644 --- a/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go +++ b/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go @@ -32,14 +32,21 @@ func (l *AdminGetProductDetailLogic) AdminGetProductDetail(req *types.AdminGetPr "查找产品失败, 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{ Id: record.Id, ProductName: record.ProductName, ProductEn: record.ProductEn, Description: record.Description, Notes: record.Notes.String, - CostPrice: record.CostPrice, + CostPrice: costSum, SellPrice: record.SellPrice, CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"), UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"), diff --git a/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go b/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go index 60573cc..6cc4982 100644 --- a/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go +++ b/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go @@ -105,6 +105,7 @@ func (l *AdminGetProductFeatureListLogic) AdminGetProductFeatureList(req *types. FeatureId: item.FeatureId, ApiId: feature.ApiId, Name: feature.Name, + CostPrice: feature.CostPrice, Sort: item.Sort, Enable: item.Enable, IsImportant: item.IsImportant, diff --git a/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go b/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go index 794b010..08e0cec 100644 --- a/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go +++ b/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go @@ -44,16 +44,26 @@ func (l *AdminGetProductListLogic) AdminGetProductList(req *types.AdminGetProduc "查询产品列表失败, err: %v, req: %+v", err, req) } - // 4. 构建响应列表 + // 4. 构建响应列表(成本价为关联模块 cost_price 之和,非库表字段直读) items := make([]types.ProductListItem, 0, len(list)) + productIds := make([]string, 0, len(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{ Id: item.Id, ProductName: item.ProductName, ProductEn: item.ProductEn, Description: item.Description, Notes: item.Notes.String, - CostPrice: item.CostPrice, + CostPrice: cost, SellPrice: item.SellPrice, CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), diff --git a/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go b/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go index 5b139da..88bd7f7 100644 --- a/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go +++ b/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go @@ -1,18 +1,20 @@ package admin_product import ( - "context" - "sync" - "qnc-server/app/main/api/internal/svc" - "qnc-server/app/main/api/internal/types" - "qnc-server/app/main/model" - "qnc-server/common/xerr" + "context" + "sync" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" - "github.com/zeromicro/go-zero/core/mr" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/google/uuid" + "qnc-server/app/main/api/internal/logic/productcost" + "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/google/uuid" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type AdminUpdateProductFeaturesLogic struct { @@ -120,15 +122,15 @@ func (l *AdminUpdateProductFeaturesLogic) AdminUpdateProductFeatures(req *types. return } } else { - // 新增关联 - newFeature := &model.ProductFeature{ - Id: uuid.NewString(), - ProductId: req.ProductId, - FeatureId: data.featureId, - Sort: data.newItem.Sort, - Enable: data.newItem.Enable, - IsImportant: data.newItem.IsImportant, - } + // 新增关联 + newFeature := &model.ProductFeature{ + Id: uuid.NewString(), + ProductId: req.ProductId, + FeatureId: data.featureId, + Sort: data.newItem.Sort, + Enable: data.newItem.Enable, + IsImportant: data.newItem.IsImportant, + } _, err = l.svcCtx.ProductFeatureModel.Insert(ctx, session, newFeature) if err != nil { updateErr = errors.Wrapf(err, "新增产品功能关联失败, product_id: %d, feature_id: %d", @@ -156,6 +158,10 @@ func (l *AdminUpdateProductFeaturesLogic) AdminUpdateProductFeatures(req *types. "更新产品功能关联失败, err: %v, req: %+v", err, req) } + if err := productcost.SyncProductCostFromModules(l.ctx, l.svcCtx, req.ProductId); err != nil { + return nil, err + } + // 5. 返回结果 return &types.AdminUpdateProductFeaturesResp{Success: true}, nil } diff --git a/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go b/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go index 98baf49..532d9aa 100644 --- a/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go +++ b/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go @@ -47,9 +47,6 @@ func (l *AdminUpdateProductLogic) AdminUpdateProduct(req *types.AdminUpdateProdu if req.Notes != nil { record.Notes = sql.NullString{String: *req.Notes, Valid: *req.Notes != ""} } - if req.CostPrice != nil { - record.CostPrice = *req.CostPrice - } if req.SellPrice != nil { record.SellPrice = *req.SellPrice } diff --git a/app/main/api/internal/logic/agent/applywithdrawallogic.go b/app/main/api/internal/logic/agent/applywithdrawallogic.go index 39619fc..c97e6df 100644 --- a/app/main/api/internal/logic/agent/applywithdrawallogic.go +++ b/app/main/api/internal/logic/agent/applywithdrawallogic.go @@ -88,26 +88,29 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r } } - // 5. 验证提现金额 - if req.Amount <= 0 { - return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "") - } - - // 6. 获取钱包信息 + // 5. 获取钱包信息 wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败, %v", err) } - // 7. 验证余额(包括检查是否为负数) - if wallet.Balance < 0 { - return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("账户存在欠款,请先补足欠款后再申请提现,当前余额:%.2f", wallet.Balance)), "") + // 6. 提现金额与可用余额:仅可提「可用余额」内金额;可用余额须大于 0 + withdrawAmount := lzUtils.RoundMoney(req.Amount) + if withdrawAmount <= 0 { + return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "") } - if wallet.Balance < req.Amount { - return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "") + bal := lzUtils.RoundMoney(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 { now := time.Now() 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( xerr.NewErrMsg(fmt.Sprintf("本月支付宝最高可提现 %.2f 元,请调整提现金额或使用银行卡提现", remainQuota)), "", @@ -148,7 +151,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r // 9. 计算税费 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 { return nil, errors.Wrapf(err, "计算税费失败") } @@ -164,9 +167,26 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r // 11. 使用事务处理提现申请 var withdrawalId string 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 冻结余额 - wallet.FrozenBalance += req.Amount - wallet.Balance -= req.Amount + wallet.FrozenBalance += withdrawAmount + wallet.Balance -= withdrawAmount if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil { return errors.Wrapf(err, "冻结余额失败") } @@ -179,7 +199,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r WithdrawalType: req.WithdrawalType, PayeeAccount: req.PayeeAccount, PayeeName: req.PayeeName, - Amount: req.Amount, + Amount: withdrawAmount, ActualAmount: taxInfo.ActualAmount, TaxAmount: taxInfo.TaxAmount, Status: 1, // 待审核 @@ -206,7 +226,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r AgentId: agent.Id, WithdrawalId: withdrawalId, YearMonth: yearMonth, - WithdrawalAmount: req.Amount, + WithdrawalAmount: withdrawAmount, TaxableAmount: taxInfo.TaxableAmount, TaxRate: taxInfo.TaxRate, TaxAmount: taxInfo.TaxAmount, @@ -293,8 +313,8 @@ func (l *ApplyWithdrawalLogic) calculateTax(ctx context.Context, agentId string, } // 计算税费 - taxAmount := taxableAmount * taxRate - actualAmount := amount - taxAmount + taxAmount := lzUtils.RoundMoney(taxableAmount * taxRate) + actualAmount := lzUtils.RoundMoney(amount - taxAmount) return &TaxInfo{ TaxableAmount: taxableAmount, diff --git a/app/main/api/internal/logic/agent/generatinglinklogic.go b/app/main/api/internal/logic/agent/generatinglinklogic.go index 5e5d898..bb7a2d2 100644 --- a/app/main/api/internal/logic/agent/generatinglinklogic.go +++ b/app/main/api/internal/logic/agent/generatinglinklogic.go @@ -15,6 +15,7 @@ import ( "qnc-server/common/tool" "qnc-server/common/xerr" "qnc-server/pkg/lzkit/crypto" + "qnc-server/pkg/lzkit/lzUtils" "github.com/Masterminds/squirrel" "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) { + // 将 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) if err != nil { 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 - actualBasePrice := basePrice + float64(levelBonus) + actualBasePrice := lzUtils.RoundMoney(basePrice + float64(levelBonus)) systemMaxPrice := productConfig.SystemMaxPrice upliftAmount, err := l.getLevelMaxUpliftAmount(agentModel.Level) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级上调金额失败, %v", err) } - levelMaxPrice := systemMaxPrice + upliftAmount - if req.SetPrice < actualBasePrice || req.SetPrice > levelMaxPrice { + levelMaxPrice := lzUtils.RoundMoney(systemMaxPrice + upliftAmount) + if setPrice < actualBasePrice || setPrice > 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{ squirrel.Eq{"agent_id": agentModel.Id}, squirrel.Eq{"product_id": req.ProductId}, - squirrel.Eq{"set_price": req.SetPrice}, + squirrel.Eq{"set_price": setPrice}, squirrel.Eq{"del_state": globalkey.DelStateNo}, }) @@ -115,7 +122,7 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) var agentIdentifier types.AgentIdentifier agentIdentifier.AgentID = agentModel.Id agentIdentifier.ProductID = req.ProductId - agentIdentifier.SetPrice = req.SetPrice + agentIdentifier.SetPrice = setPrice agentIdentifierByte, err := json.Marshal(agentIdentifier) if err != nil { 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, ProductId: req.ProductId, LinkIdentifier: encrypted, - SetPrice: req.SetPrice, + SetPrice: setPrice, ActualBasePrice: actualBasePrice, } diff --git a/app/main/api/internal/logic/agent/getagentproductconfiglogic.go b/app/main/api/internal/logic/agent/getagentproductconfiglogic.go index 5b45de9..2fb3349 100644 --- a/app/main/api/internal/logic/agent/getagentproductconfiglogic.go +++ b/app/main/api/internal/logic/agent/getagentproductconfiglogic.go @@ -6,6 +6,7 @@ import ( "qnc-server/app/main/model" "qnc-server/common/ctxdata" "qnc-server/common/xerr" + "qnc-server/pkg/lzkit/lzUtils" "github.com/pkg/errors" @@ -76,11 +77,11 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP productBasePrice := productConfig.BasePrice // 计算该产品的实际底价 - productActualBasePrice := productBasePrice + float64(levelBonus) + productActualBasePrice := lzUtils.RoundMoney(productBasePrice + float64(levelBonus)) priceRangeMin := productActualBasePrice upliftAmount, _ := l.getLevelMaxUpliftAmount(agentModel.Level) - priceRangeMax := productConfig.SystemMaxPrice + upliftAmount + priceRangeMax := lzUtils.RoundMoney(productConfig.SystemMaxPrice + upliftAmount) // 使用产品配置的提价阈值和手续费比例,如果为NULL则使用0 productPriceThreshold := 0.0 diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go index abea7c4..664900a 100644 --- a/app/main/api/internal/logic/pay/paymentlogic.go +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -292,7 +292,7 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses // 使用产品配置的底价计算实际底价 basePrice := productConfig.BasePrice - actualBasePrice := basePrice + float64(levelBonus) + actualBasePrice := lzUtils.RoundMoney(basePrice + float64(levelBonus)) // 计算提价成本(使用产品配置) priceThreshold := 0.0 @@ -306,11 +306,11 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses priceCost := 0.0 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{ diff --git a/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go b/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go index a4a0ccb..38b20a7 100644 --- a/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go +++ b/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go @@ -107,6 +107,12 @@ func (l *WechatPayRefundCallbackLogic) handleQueryOrderRefund(orderNo string, st 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 }) diff --git a/app/main/api/internal/logic/productcost/sync.go b/app/main/api/internal/logic/productcost/sync.go new file mode 100644 index 0000000..a883d9b --- /dev/null +++ b/app/main/api/internal/logic/productcost/sync.go @@ -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 +} diff --git a/app/main/api/internal/logic/user/bindmobilelogic.go b/app/main/api/internal/logic/user/bindmobilelogic.go index dc3b42d..032c221 100644 --- a/app/main/api/internal/logic/user/bindmobilelogic.go +++ b/app/main/api/internal/logic/user/bindmobilelogic.go @@ -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}) if err != nil && !errors.Is(err, model.ErrNotFound) { 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 { 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) + } // 发放token(userType会根据mobile字段动态计算) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID) if err != nil { diff --git a/app/main/api/internal/logic/user/mobilecodeloginlogic.go b/app/main/api/internal/logic/user/mobilecodeloginlogic.go index 4c27102..446d959 100644 --- a/app/main/api/internal/logic/user/mobilecodeloginlogic.go +++ b/app/main/api/internal/logic/user/mobilecodeloginlogic.go @@ -56,7 +56,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r var userID string user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) 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 { // 用户不存在,自动注册新用户 diff --git a/app/main/api/internal/queue/paySuccessNotify.go b/app/main/api/internal/queue/paySuccessNotify.go index c2c0387..38781c2 100644 --- a/app/main/api/internal/queue/paySuccessNotify.go +++ b/app/main/api/internal/queue/paySuccessNotify.go @@ -17,6 +17,7 @@ import ( "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type PaySuccessNotifyUserHandler struct { @@ -233,12 +234,16 @@ func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error } 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) + transErr := l.svcCtx.OrderModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { + order.Status = "refunded" + if err := l.svcCtx.OrderModel.UpdateWithVersion(transCtx, session, order); err != nil { + return fmt.Errorf("更新订单状态失败: %v", err) + } + return l.svcCtx.AgentService.ReverseAgentSettlementOnOrderRefund(transCtx, session, order.Id) + }) + if transErr != nil { + logx.Errorf("支付宝退款后更新订单或冲正代理分账失败, orderID: %s, err: %v", order.Id, transErr) + return fmt.Errorf("更新订单状态或冲正代理分账失败: %v", transErr) } return asynq.SkipRetry } else { diff --git a/app/main/api/internal/service/agentService.go b/app/main/api/internal/service/agentService.go index 300ca26..37f5117 100644 --- a/app/main/api/internal/service/agentService.go +++ b/app/main/api/internal/service/agentService.go @@ -11,6 +11,7 @@ import ( "qnc-server/common/globalkey" "qnc-server/pkg/lzkit/lzUtils" + "github.com/Masterminds/squirrel" "github.com/google/uuid" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" @@ -120,7 +121,7 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err // 5.2 使用产品配置的底价计算实际底价 basePrice := productConfig.BasePrice - actualBasePrice := basePrice + float64(levelBonus) + actualBasePrice := lzUtils.RoundMoney(basePrice + float64(levelBonus)) // 5.3 计算提价成本(使用产品配置) 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) // 5.4 计算代理收益 - agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost + agentProfit := lzUtils.RoundMoney(agentOrder.SetPrice - actualBasePrice - priceCost) // 5.5 更新代理订单记录 agentOrder.ProcessStatus = 1 @@ -200,7 +201,7 @@ func (s *AgentService) calculatePriceCost(setPrice, priceThreshold, priceFeeRate if setPrice <= priceThreshold { return 0 } - return (setPrice - priceThreshold) * priceFeeRate + return lzUtils.RoundMoney((setPrice - priceThreshold) * priceFeeRate) } // giveAgentCommission 发放代理佣金 @@ -244,7 +245,7 @@ func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Ses } // 计算冻结金额:订单单价的10% - freezeAmountByPrice := orderPrice * freezeRatio + freezeAmountByPrice := lzUtils.RoundMoney(orderPrice * freezeRatio) // 冻结金额不能超过佣金金额 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.FrozenBalance += freezeAmount @@ -431,7 +432,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s // ========== 步骤2:计算剩余金额并分配给钻石上级 ========== // 剩余金额 = 总等级加成 - 已给黄金上级的金额 // 例如:等级加成6元 - 给黄金上级3元 = 剩余3元 - remaining := amount - goldAmount + remaining := lzUtils.RoundMoney(amount - goldAmount) if remaining > 0 { // 从黄金上级开始向上查找钻石上级 // 场景示例: @@ -481,7 +482,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s // ========== 步骤2:计算剩余金额 ========== // 剩余金额 = 总等级加成 - 已给直接上级的金额 // 例如:等级加成6元 - 给直接上级2元 = 剩余4元 - remaining := amount - directAmount + remaining := lzUtils.RoundMoney(amount - directAmount) if remaining <= 0 { // 如果没有剩余,直接返回(所有金额已分配给直接上级) return nil @@ -524,8 +525,8 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s logx.Errorf("获取黄金等级加成配置失败,使用默认值3元: %v", err) goldBonus = 3 // 默认3元 } - // 计算等级加成的差 - bonusDiff := normalBonus - float64(goldBonus) // 例如:6元 - 3元 = 3元 + // 计算等级加成的差 + bonusDiff := lzUtils.RoundMoney(normalBonus - float64(goldBonus)) // 例如:6元 - 3元 = 3元 // 步骤A2:如果有黄金上级,给黄金上级一部分返佣 // 规则说明: @@ -536,7 +537,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s if goldParent != nil && bonusDiff > 0 { // 计算给黄金上级的金额 = 等级加成差 - 已给普通上级的金额 // 例如:等级加成差3元 - 已给普通2元 = 给黄金1元 - goldRebateAmount := bonusDiff - directAmount + goldRebateAmount := lzUtils.RoundMoney(bonusDiff - directAmount) // 如果计算出的金额小于等于0,说明差已经被普通代理全部占用了,不给黄金 // 例如:如果差是2元,已给普通2元,则 goldRebateAmount = 0,不给黄金 @@ -554,7 +555,7 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s // 更新剩余金额(用于后续分配给钻石) // 例如:剩余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 } + +// 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 +} diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 9b5ed05..555f293 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -146,7 +146,6 @@ type AdminCreateProductReq struct { ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes,optional"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 SellPrice float64 `json:"sell_price"` // 售价 } @@ -232,11 +231,12 @@ type AdminGenerateDiamondInviteCodeResp struct { } type AdminGetAgentCommissionListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *string `form:"agent_id,optional"` // 代理ID(可选) - OrderId *string `form:"order_id,optional"` // 订单ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) + OrderNo *string `form:"order_no,optional"` // 商户订单号(可选) + Status *int64 `form:"status,optional"` // 状态(可选) } type AdminGetAgentCommissionListResp struct { @@ -261,6 +261,7 @@ type AdminGetAgentLinkListReq struct { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) ProductId *string `form:"product_id,optional"` // 产品ID(可选) LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) } @@ -274,9 +275,12 @@ type AdminGetAgentListReq struct { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 Mobile *string `form:"mobile,optional"` // 手机号(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编码(可选) Region *string `form:"region,optional"` // 区域(可选) Level *int64 `form:"level,optional"` // 等级(可选) TeamLeaderId *string `form:"team_leader_id,optional"` // 团队首领ID(可选) + AgentId *string `form:"agent_id,optional"` // 代理主键UUID,精确查询;传入时不再按 team_leader 与其它条件组合筛选 + RealName *string `form:"real_name,optional"` // 实名姓名(可选,模糊匹配已三要素核验记录) } type AdminGetAgentListResp struct { @@ -288,7 +292,9 @@ type AdminGetAgentOrderListReq struct { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) OrderId *string `form:"order_id,optional"` // 订单ID(可选) + OrderNo *string `form:"order_no,optional"` // 商户订单号(可选) ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选) OrderStatus *string `form:"order_status,optional"` // 订单状态(可选):pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 } @@ -298,6 +304,15 @@ type AdminGetAgentOrderListResp struct { Items []AgentOrderListItem `json:"items"` // 列表数据 } +type AdminGetAgentOrderSettlementReq struct { + OrderNo string `form:"order_no"` // 商户订单号 +} + +type AdminGetAgentOrderSettlementResp struct { + Commissions []AgentCommissionListItem `json:"commissions"` // 本单销售佣金 + Rebates []AgentRebateListItem `json:"rebates"` // 本单返佣 +} + type AdminGetAgentProductConfigListReq struct { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 @@ -323,12 +338,14 @@ type AdminGetAgentRealNameListResp struct { } type AdminGetAgentRebateListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *string `form:"agent_id,optional"` // 代理ID(可选) - SourceAgentId *string `form:"source_agent_id,optional"` // 来源代理ID(可选) - RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选) - Status *int64 `form:"status,optional"` // 状态(可选):1=已发放,2=已冻结,3=已取消(已退款) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *string `form:"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"` // 返佣类型(可选) + Status *int64 `form:"status,optional"` // 状态(可选):1=已发放,2=已冻结,3=已取消(已退款) } type AdminGetAgentRebateListResp struct { @@ -336,10 +353,19 @@ type AdminGetAgentRebateListResp struct { Items []AgentRebateListItem `json:"items"` // 列表数据 } +type AdminGetAgentTeamTreeReq struct { + AgentId string `form:"agent_id"` // 锚点代理ID(UUID) +} + +type AdminGetAgentTeamTreeResp struct { + Root AgentTeamTreeNode `json:"root"` +} + type AdminGetAgentUpgradeListReq struct { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 AgentId *string `form:"agent_id,optional"` // 代理ID(可选) + AgentCode *string `form:"agent_code,optional"` // 代理编号(可选) UpgradeType *int64 `form:"upgrade_type,optional"` // 升级类型(可选) Status *int64 `form:"status,optional"` // 状态(可选) } @@ -350,11 +376,15 @@ type AdminGetAgentUpgradeListResp struct { } type AdminGetAgentWithdrawalListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *string `form:"agent_id,optional"` // 代理ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选) - WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *string `form:"agent_id,optional"` // 代理主键 UUID(弹窗内按代理筛选时传入) + AgentCode *string `form:"agent_code,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"` // 收款人(可选,模糊) } type AdminGetAgentWithdrawalListResp struct { @@ -442,10 +472,9 @@ type AdminGetFeatureListResp struct { type AdminGetInviteCodeListReq struct { Page int64 `form:"page"` // 页码 PageSize int64 `form:"pageSize"` // 每页数量 - Code *string `form:"code,optional"` // 邀请码(可选) - AgentId *string `form:"agent_id,optional"` // 发放代理ID(可选,NULL表示平台发放) + Code *string `form:"code,optional"` // 邀请码(可选,模糊匹配) TargetLevel *int64 `form:"target_level,optional"` // 目标等级(可选) - Status *int64 `form:"status,optional"` // 状态(可选) + Status string `form:"status,optional"` // 状态(可选):0=未使用,1=已使用,2=已失效 } type AdminGetInviteCodeListResp struct { @@ -518,6 +547,8 @@ type AdminGetOrderListReq struct { PaymentScene string `form:"payment_scene,optional"` // 支付平台 Amount float64 `form:"amount,optional"` // 金额 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"` // 创建时间开始 CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 @@ -548,6 +579,7 @@ type AdminGetPlatformUserDetailResp struct { type AdminGetPlatformUserListReq struct { Page int64 `form:"page,default=1"` // 页码 PageSize int64 `form:"pageSize,default=20"` // 每页数量 + UserId string `form:"user_id,optional"` // 平台用户主键 ID,精确筛选 Mobile string `form:"mobile,optional"` // 手机号 Nickname string `form:"nickname,optional"` // 昵称 Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 @@ -572,7 +604,7 @@ type AdminGetProductDetailResp struct { ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 + CostPrice float64 `json:"cost_price"` // 成本(由关联模块成本汇总,只读) SellPrice float64 `json:"sell_price"` // 售价 CreateTime string `json:"create_time"` // 创建时间 UpdateTime string `json:"update_time"` // 更新时间 @@ -583,16 +615,17 @@ type AdminGetProductFeatureListReq struct { } type AdminGetProductFeatureListResp struct { - Id string `json:"id"` // 关联ID - ProductId string `json:"product_id"` // 产品ID - FeatureId string `json:"feature_id"` // 功能ID - ApiId string `json:"api_id"` // API标识 - Name string `json:"name"` // 功能描述 - Sort int64 `json:"sort"` // 排序 - Enable int64 `json:"enable"` // 是否启用 - IsImportant int64 `json:"is_important"` // 是否重要 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 + Id string `json:"id"` // 关联ID + ProductId string `json:"product_id"` // 产品ID + FeatureId string `json:"feature_id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 + CostPrice float64 `json:"cost_price"` // 模块成本价(元,来自 feature) + Sort int64 `json:"sort"` // 排序 + Enable int64 `json:"enable"` // 是否启用 + IsImportant int64 `json:"is_important"` // 是否重要 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 } type AdminGetProductListReq struct { @@ -920,7 +953,6 @@ type AdminUpdateProductReq struct { ProductEn *string `json:"product_en,optional"` // 英文名 Description *string `json:"description,optional"` // 描述 Notes *string `json:"notes,optional"` // 备注 - CostPrice *float64 `json:"cost_price,optional"` // 成本 SellPrice *float64 `json:"sell_price,optional"` // 售价 } @@ -1005,7 +1037,9 @@ type AgentApplyResp struct { type AgentCommissionListItem struct { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 OrderId string `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 ProductName string `json:"product_name"` // 产品名称 Amount float64 `json:"amount"` // 金额 Status int64 `json:"status"` // 状态 @@ -1013,9 +1047,9 @@ type AgentCommissionListItem struct { } type AgentGeneratingLinkReq struct { - ProductId string `json:"product_id"` // 产品ID - SetPrice float64 `json:"set_price"` // 设定价格 - TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) + ProductId string `json:"product_id"` // 产品ID + SetPrice string `json:"set_price"` // 设定价格 + TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) } type AgentGeneratingLinkResp struct { @@ -1038,6 +1072,7 @@ type AgentInfoResp struct { type AgentLinkListItem struct { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 ProductId string `json:"product_id"` // 产品ID ProductName string `json:"product_name"` // 产品名称 SetPrice float64 `json:"set_price"` // 设定价格 @@ -1047,29 +1082,32 @@ type AgentLinkListItem struct { } type AgentListItem struct { - Id string `json:"id"` // 主键 - UserId string `json:"user_id"` // 用户ID - Level int64 `json:"level"` // 等级:1=普通,2=黄金,3=钻石 - LevelName string `json:"level_name"` // 等级名称 - Region string `json:"region"` // 区域 - Mobile string `json:"mobile"` // 手机号 - RealName string `json:"real_name"` // 姓名(来自实名表) - IdCard string `json:"id_card"` // 身份证(解密后明文返回) - WechatId string `json:"wechat_id"` // 微信号 - TeamLeaderId string `json:"team_leader_id"` // 团队首领ID - AgentCode int64 `json:"agent_code"` - Balance float64 `json:"balance"` // 钱包余额 - TotalEarnings float64 `json:"total_earnings"` // 累计收益 - FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 - WithdrawnAmount float64 `json:"withdrawn_amount"` // 提现总额 - IsRealName bool `json:"is_real_name"` // 是否已实名 - CreateTime string `json:"create_time"` // 创建时间 + Id string `json:"id"` // 主键 + UserId string `json:"user_id"` // 用户ID + Level int64 `json:"level"` // 等级:1=普通,2=黄金,3=钻石 + LevelName string `json:"level_name"` // 等级名称 + Region string `json:"region"` // 区域 + Mobile string `json:"mobile"` // 手机号 + RealName string `json:"real_name"` // 姓名(来自实名表) + IdCard string `json:"id_card"` // 身份证(解密后明文返回) + WechatId string `json:"wechat_id"` // 微信号 + TeamLeaderId string `json:"team_leader_id"` // 团队首领ID + SubordinateCount int64 `json:"subordinate_count"` // 直属下级代理数(team_leader_id 为本代理 id) + AgentCode int64 `json:"agent_code"` + Balance float64 `json:"balance"` // 钱包余额 + TotalEarnings float64 `json:"total_earnings"` // 累计收益 + FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 + WithdrawnAmount float64 `json:"withdrawn_amount"` // 提现总额 + IsRealName bool `json:"is_real_name"` // 是否已实名 + CreateTime string `json:"create_time"` // 创建时间 } type AgentOrderListItem struct { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 OrderId string `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 ProductId string `json:"product_id"` // 产品ID ProductName string `json:"product_name"` // 产品名称 OrderAmount float64 `json:"order_amount"` // 订单金额 @@ -1101,6 +1139,7 @@ type AgentProductConfigResp struct { type AgentRealNameListItem struct { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 Name string `json:"name"` // 姓名 IdCard string `json:"id_card"` // 身份证号 Mobile string `json:"mobile"` // 手机号 @@ -1110,19 +1149,33 @@ type AgentRealNameListItem struct { } type AgentRebateListItem struct { - Id string `json:"id"` // 主键 - AgentId string `json:"agent_id"` // 获得返佣的代理ID - SourceAgentId string `json:"source_agent_id"` // 来源代理ID - OrderId string `json:"order_id"` // 订单ID - RebateType int64 `json:"rebate_type"` // 返佣类型 - Amount float64 `json:"amount"` // 金额 - Status int64 `json:"status"` // 状态:1=已发放,2=已冻结,3=已取消(已退款) - CreateTime string `json:"create_time"` // 创建时间 + Id string `json:"id"` // 主键 + AgentId string `json:"agent_id"` // 获得返佣的代理ID + AgentCode int64 `json:"agent_code"` // 获得返佣代理编号 + SourceAgentId string `json:"source_agent_id"` // 来源代理ID + SourceAgentCode int64 `json:"source_agent_code"` // 来源代理编号 + OrderId string `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 + RebateType int64 `json:"rebate_type"` // 返佣类型 + Amount float64 `json:"amount"` // 金额 + Status int64 `json:"status"` // 状态:1=已发放,2=已冻结,3=已取消(已退款) + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentTeamTreeNode struct { + 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"` } type AgentUpgradeListItem struct { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 FromLevel int64 `json:"from_level"` // 原等级 ToLevel int64 `json:"to_level"` // 目标等级 UpgradeType int64 `json:"upgrade_type"` // 升级类型 @@ -1135,6 +1188,7 @@ type AgentUpgradeListItem struct { type AgentWithdrawalListItem struct { Id string `json:"id"` // 主键 AgentId string `json:"agent_id"` // 代理ID + AgentCode int64 `json:"agent_code"` // 代理编号 WithdrawNo string `json:"withdraw_no"` // 提现单号 Amount float64 `json:"amount"` // 金额 TaxAmount float64 `json:"tax_amount"` // 税费金额 @@ -1706,18 +1760,20 @@ type InviteCodeItem struct { } type InviteCodeListItem struct { - Id string `json:"id"` // 主键 - Code string `json:"code"` // 邀请码 - AgentId string `json:"agent_id"` // 发放代理ID(0表示平台发放) - AgentMobile string `json:"agent_mobile"` // 发放代理手机号 - TargetLevel int64 `json:"target_level"` // 目标等级 - Status int64 `json:"status"` // 状态:0=未使用,1=已使用,2=已失效 - UsedUserId string `json:"used_user_id"` // 使用用户ID - UsedAgentId string `json:"used_agent_id"` // 使用代理ID - UsedTime string `json:"used_time"` // 使用时间 - ExpireTime string `json:"expire_time"` // 过期时间 - Remark string `json:"remark"` // 备注 - CreateTime string `json:"create_time"` // 创建时间 + Id string `json:"id"` // 主键 + Code string `json:"code"` // 邀请码 + AgentId string `json:"agent_id"` // 发放代理ID(0表示平台发放) + AgentCode int64 `json:"agent_code"` // 发放代理编号(平台为0) + AgentMobile string `json:"agent_mobile"` // 发放代理手机号 + TargetLevel int64 `json:"target_level"` // 目标等级 + Status int64 `json:"status"` // 状态:0=未使用,1=已使用,2=已失效 + UsedUserId string `json:"used_user_id"` // 使用用户ID + UsedAgentId string `json:"used_agent_id"` // 使用代理ID + UsedAgentCode int64 `json:"used_agent_code"` // 使用代理编号 + UsedTime string `json:"used_time"` // 使用时间 + ExpireTime string `json:"expire_time"` // 过期时间 + Remark string `json:"remark"` // 备注 + CreateTime string `json:"create_time"` // 创建时间 } type InviteItem struct { @@ -1841,6 +1897,7 @@ type OrderListItem struct { RefundTime string `json:"refund_time"` // 退款时间 UpdateTime string `json:"update_time"` // 更新时间 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 + AgentCode string `json:"agent_code"` // 代理编号 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 } @@ -1924,7 +1981,7 @@ type ProductListItem struct { ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 + CostPrice float64 `json:"cost_price"` // 成本(由关联模块成本汇总,只读) SellPrice float64 `json:"sell_price"` // 售价 CreateTime string `json:"create_time"` // 创建时间 UpdateTime string `json:"update_time"` // 更新时间 diff --git a/app/main/model/agentModel.go b/app/main/model/agentModel.go index eea1285..ae90fc5 100644 --- a/app/main/model/agentModel.go +++ b/app/main/model/agentModel.go @@ -4,6 +4,9 @@ import ( "context" "fmt" + "qnc-server/common/globalkey" + + "github.com/Masterminds/squirrel" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/sqlx" @@ -18,6 +21,8 @@ type ( agentModel // UpdateInTransaction 在事务中更新刚插入的记录,避免 UpdateWithVersion 中的 FindOne 缓存问题 UpdateInTransaction(ctx context.Context, session sqlx.Session, data *Agent) error + // CountSubordinatesByTeamLeaderIds 按团队首领 id 统计未删除的直属代理数量(team_leader_id = 传入 id) + CountSubordinatesByTeamLeaderIds(ctx context.Context, leaderIds []string) (map[string]int64, error) } customAgentModel struct { @@ -63,3 +68,36 @@ func (m *customAgentModel) UpdateInTransaction(ctx context.Context, session sqlx return nil } + +type teamLeaderSubCountRow struct { + TeamLeaderId string `db:"team_leader_id"` + Cnt int64 `db:"cnt"` +} + +func (m *customAgentModel) CountSubordinatesByTeamLeaderIds(ctx context.Context, leaderIds []string) (map[string]int64, error) { + out := make(map[string]int64) + if len(leaderIds) == 0 { + return out, nil + } + // 不含「自己挂在自己名下」的记录,否则首领会在自己的下级列表里且下级数量虚高 + qb := squirrel.Select("team_leader_id", "COUNT(*) AS cnt"). + From(m.table). + Where(squirrel.Eq{"del_state": globalkey.DelStateNo}). + Where(squirrel.Eq{"team_leader_id": leaderIds}). + Where("`id` <> `team_leader_id`"). + GroupBy("team_leader_id") + query, args, err := qb.ToSql() + if err != nil { + return nil, err + } + var rows []teamLeaderSubCountRow + if err := m.QueryRowsNoCacheCtx(ctx, &rows, query, args...); err != nil { + return nil, err + } + for _, r := range rows { + if r.TeamLeaderId != "" { + out[r.TeamLeaderId] = r.Cnt + } + } + return out, nil +} diff --git a/app/main/model/productFeatureModel.go b/app/main/model/productFeatureModel.go index 5b77422..ce3cf14 100644 --- a/app/main/model/productFeatureModel.go +++ b/app/main/model/productFeatureModel.go @@ -1,6 +1,12 @@ package model import ( + "context" + "fmt" + "strings" + + "qnc-server/common/globalkey" + "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/sqlx" ) @@ -12,6 +18,10 @@ type ( // and implement the added methods in customProductFeatureModel. ProductFeatureModel interface { productFeatureModel + // SumFeatureCostPriceByProductId 汇总该产品下已关联且未删除的模块成本(feature.cost_price 之和) + SumFeatureCostPriceByProductId(ctx context.Context, productId string) (float64, error) + // SumFeatureCostPriceByProductIds 批量汇总,未出现在结果中的产品视为 0 + SumFeatureCostPriceByProductIds(ctx context.Context, productIds []string) (map[string]float64, error) } customProductFeatureModel struct { @@ -25,3 +35,48 @@ func NewProductFeatureModel(conn sqlx.SqlConn, c cache.CacheConf) ProductFeature defaultProductFeatureModel: newProductFeatureModel(conn, c), } } + +func (m *customProductFeatureModel) SumFeatureCostPriceByProductId(ctx context.Context, productId string) (float64, error) { + query := ` +SELECT COALESCE(SUM(f.cost_price), 0) +FROM product_feature pf +INNER JOIN feature f ON f.id = pf.feature_id AND f.del_state = ? +WHERE pf.product_id = ? AND pf.del_state = ?` + var sum float64 + err := m.QueryRowNoCacheCtx(ctx, &sum, query, globalkey.DelStateNo, productId, globalkey.DelStateNo) + if err != nil { + return 0, err + } + return sum, nil +} + +func (m *customProductFeatureModel) SumFeatureCostPriceByProductIds(ctx context.Context, productIds []string) (map[string]float64, error) { + out := make(map[string]float64) + if len(productIds) == 0 { + return out, nil + } + placeholders := strings.TrimSuffix(strings.Repeat("?,", len(productIds)), ",") + query := fmt.Sprintf(` +SELECT pf.product_id, COALESCE(SUM(f.cost_price), 0) AS total +FROM product_feature pf +INNER JOIN feature f ON f.id = pf.feature_id AND f.del_state = ? +WHERE pf.del_state = ? AND pf.product_id IN (%s) +GROUP BY pf.product_id`, placeholders) + args := make([]interface{}, 0, 2+len(productIds)) + args = append(args, globalkey.DelStateNo, globalkey.DelStateNo) + for _, id := range productIds { + args = append(args, id) + } + var rows []struct { + ProductId string `db:"product_id"` + Total float64 `db:"total"` + } + err := m.QueryRowsNoCacheCtx(ctx, &rows, query, args...) + if err != nil { + return nil, err + } + for i := range rows { + out[rows[i].ProductId] = rows[i].Total + } + return out, nil +} diff --git a/deploy/sql/agent_system_migration.sql b/deploy/sql/agent_system_migration.sql index 223f0f5..f7c6891 100644 --- a/deploy/sql/agent_system_migration.sql +++ b/deploy/sql/agent_system_migration.sql @@ -218,8 +218,11 @@ CREATE TABLE `agent_upgrade` ( CREATE TABLE `agent_withdrawal` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `agent_id` bigint NOT NULL COMMENT '代理ID', + `withdrawal_type` tinyint NOT NULL DEFAULT 1 COMMENT '提现方式:1=支付宝,2=银行卡', `withdraw_no` varchar(100) NOT NULL COMMENT '提现单号', `payee_account` varchar(100) NOT NULL COMMENT '收款账户', + `bank_card_no` varchar(100) DEFAULT NULL COMMENT '银行卡号(银行卡提现时使用)', + `bank_name` varchar(100) DEFAULT NULL COMMENT '开户行名称(银行卡提现时使用)', `payee_name` varchar(50) NOT NULL COMMENT '收款人姓名', `amount` decimal(10,2) NOT NULL COMMENT '提现金额', `actual_amount` decimal(10,2) NOT NULL COMMENT '实际到账金额(扣除税费后)', diff --git a/pkg/lzkit/lzUtils/utils.go b/pkg/lzkit/lzUtils/utils.go index 69a9aca..ab17639 100644 --- a/pkg/lzkit/lzUtils/utils.go +++ b/pkg/lzkit/lzUtils/utils.go @@ -1,15 +1,33 @@ package lzUtils -import "fmt" +import ( + "fmt" + "math" +) + +// RoundMoney 将金额四舍五入到指定小数位数(默认2位) +// 所有金额计算结果都应通过此函数处理,避免浮点精度问题 +func RoundMoney(v float64, precision ...int) float64 { + p := 2 + if len(precision) > 0 { + p = precision[0] + } + factor := math.Pow10(p) + return math.Round(v*factor) / factor +} + +// RoundMoney2 将金额四舍五入到2位小数并返回字符串 +// 用于需要字符串格式的场景(如 API 返回、日志打印) +func RoundMoney2(v float64) string { + return fmt.Sprintf("%.2f", RoundMoney(v)) +} // ToWechatAmount 将金额从元转换为微信支付 SDK 需要的分(int64 类型) func ToWechatAmount(amount float64) int64 { - // 将金额从元转换为分,并四舍五入 - return int64(amount*100 + 0.5) + return int64(RoundMoney(amount)*100 + 0.5) } // ToAlipayAmount 将金额从元转换为支付宝支付 SDK 需要的字符串格式,保留两位小数 func ToAlipayAmount(amount float64) string { - // 格式化为字符串,保留两位小数 - return fmt.Sprintf("%.2f", amount) + return RoundMoney2(amount) }