diff --git a/API调用失败退款时的代理处理确认.md b/API调用失败退款时的代理处理确认.md new file mode 100644 index 0000000..f9822f8 --- /dev/null +++ b/API调用失败退款时的代理处理确认.md @@ -0,0 +1,131 @@ +# API调用失败退款时的代理处理确认 + +## 执行流程分析 + +### 关键执行顺序 + +``` +1. 支付成功 → 创建代理订单(ProcessStatus = 0,未处理) + ↓ +2. 支付回调 → 发送查询任务 + ↓ +3. 查询任务执行: + ├─ 创建查询记录(query表,状态为 "pending") ✅ 第77-87行 + ├─ 生成授权书 ✅ 第108-142行 + ├─ 调用API(第164行)⚠️ 可能失败 + │ ├─ 如果成功 → 继续执行 + │ └─ 如果失败 → return handleError(第166行)⚠️ 直接返回 + │ + ├─ [API成功才会执行到这里] + ├─ 保存响应数据 ✅ 第177-182行 + ├─ 更新查询状态为 "success" ✅ 第184-189行 + └─ 发送代理处理任务 ✅ 第192行(关键!只有到这里才会发送) +``` + +### 关键代码位置 + +**第164-167行**: +```go +combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) +if err != nil { + return l.handleError(ctx, err, order, query) // ← 直接返回,不会继续执行 +} +``` + +**第192行(发送代理处理任务)**: +```go +// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程) +if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil { + logx.Errorf("发送代理处理任务失败,订单ID: %d, 错误: %v", order.Id, asyncErr) +} +``` + +--- + +## 确认结果 + +### ✅ API调用失败时的情况(第166-167行) + +**执行流程**: +1. 支付时创建代理订单(`ProcessStatus = 0`) +2. API调用失败(第164行) +3. **直接返回** `handleError`(第166行) +4. 进入退款流程 +5. **第192行不会执行**(因为已经return了) + +**代理状态**: +- ✅ 代理订单 `ProcessStatus = 0`(未处理) +- ✅ 代理**没有收到**佣金 +- ✅ 代理**没有收到**返佣 +- ✅ 代理处理任务**没有发送** + +--- + +## 业务逻辑分析 + +### ✅ 符合业务逻辑 + +**理由**: +1. **服务未完成交付** + - API调用失败,意味着无法生成查询报告 + - 用户支付了订单,但服务没有成功交付 + +2. **用户权益保护** + - 平台退款给用户是合理的 + - 用户没有获得服务,不应该承担费用 + +3. **代理收益逻辑** + - 代理的收益应该基于**服务成功交付** + - 而不是仅仅因为用户支付了订单 + - 如果服务未成功交付,代理不应该获得收益 + +4. **当前实现正确** + - 只有在查询成功后(第184行更新状态为 "success") + - 才会发送代理处理任务(第192行) + - API调用失败时,查询未成功,代理任务不会发送 + +--- + +## 结论 + +### ✅ 当前处理完全正确 + +**对于 API 调用失败退款的情况**: +- ✅ 代理订单未处理(`ProcessStatus = 0`) +- ✅ 代理没有收到佣金和返佣 +- ✅ 代理处理任务没有发送 +- ✅ **完全符合业务逻辑** + +**业务逻辑合理性**: +- ✅ 代理收益应该在服务成功交付后才发放 +- ✅ API调用失败意味着服务未交付,不应发放收益 +- ✅ 退款给用户是合理的,保护用户权益 + +--- + +## 不需要修改 + +当前逻辑已经正确处理了这种情况,**无需修改**。 + +**关键保护点**: +1. 代理处理任务只在查询成功后发送(第192行) +2. API调用失败会直接返回,不会执行到第192行 +3. 代理订单状态保持为0,代理没有收益 + +--- + +## 需要注意的其他场景 + +虽然当前场景处理正确,但需要注意以下场景: + +### ⚠️ 场景:代理已处理但订单被退款 + +如果: +1. API调用成功 +2. 查询状态更新为 "success" +3. 发送代理处理任务 +4. 代理处理任务执行,发放佣金和返佣(`ProcessStatus = 1`) +5. **之后**订单被退款(比如管理员手动退款) + +这种情况下需要撤销代理收益(需要另外处理,不是当前场景)。 + diff --git a/app/main/api/__debug_bin503801853.exe b/app/main/api/__debug_bin503801853.exe new file mode 100644 index 0000000..3a3cb11 Binary files /dev/null and b/app/main/api/__debug_bin503801853.exe differ diff --git a/app/main/api/__test_build.exe b/app/main/api/__test_build.exe new file mode 100644 index 0000000..139a511 Binary files /dev/null and b/app/main/api/__test_build.exe differ diff --git a/app/main/api/data/authorization_docs/2025/11/auth_1_7_20251130_151213.pdf b/app/main/api/data/authorization_docs/2025/11/auth_1_7_20251130_151213.pdf new file mode 100644 index 0000000..4a1600b Binary files /dev/null and b/app/main/api/data/authorization_docs/2025/11/auth_1_7_20251130_151213.pdf differ diff --git a/app/main/api/data/authorization_docs/2025/11/auth_1_8_20251130_153818.pdf b/app/main/api/data/authorization_docs/2025/11/auth_1_8_20251130_153818.pdf new file mode 100644 index 0000000..18ff8bc Binary files /dev/null and b/app/main/api/data/authorization_docs/2025/11/auth_1_8_20251130_153818.pdf differ diff --git a/app/main/api/data/authorization_docs/2025/12/auth_1_10_20251201_182957.pdf b/app/main/api/data/authorization_docs/2025/12/auth_1_10_20251201_182957.pdf new file mode 100644 index 0000000..54be00b Binary files /dev/null and b/app/main/api/data/authorization_docs/2025/12/auth_1_10_20251201_182957.pdf differ diff --git a/app/main/api/data/authorization_docs/2025/12/auth_1_11_20251201_184112.pdf b/app/main/api/data/authorization_docs/2025/12/auth_1_11_20251201_184112.pdf new file mode 100644 index 0000000..7ef1534 Binary files /dev/null and b/app/main/api/data/authorization_docs/2025/12/auth_1_11_20251201_184112.pdf differ diff --git a/app/main/api/data/authorization_docs/2025/12/auth_1_9_20251201_182434.pdf b/app/main/api/data/authorization_docs/2025/12/auth_1_9_20251201_182434.pdf new file mode 100644 index 0000000..2e707da Binary files /dev/null and b/app/main/api/data/authorization_docs/2025/12/auth_1_9_20251201_182434.pdf differ diff --git a/app/main/api/desc/admin/admin_agent.api b/app/main/api/desc/admin/admin_agent.api index ed37a32..e58f7f7 100644 --- a/app/main/api/desc/admin/admin_agent.api +++ b/app/main/api/desc/admin/admin_agent.api @@ -301,17 +301,16 @@ type ( AdminAuditRealNameResp { Success bool `json:"success"` } - // 系统配置查询 + // 系统配置查询(价格配置已移除,改为产品配置表管理) AdminGetAgentConfigResp { - BasePrice float64 `json:"base_price"` // 基础底价 - SystemMaxPrice float64 `json:"system_max_price"` // 系统价格上限 - PriceThreshold float64 `json:"price_threshold"` // 提价标准阈值 - PriceFeeRate float64 `json:"price_fee_rate"` // 提价手续费比例 - LevelBonus LevelBonusConfig `json:"level_bonus"` // 等级加成配置 - UpgradeFee UpgradeFeeConfig `json:"upgrade_fee"` // 升级费用配置 - UpgradeRebate UpgradeRebateConfig `json:"upgrade_rebate"` // 升级返佣配置 - TaxRate float64 `json:"tax_rate"` // 税率 - TaxExemptionAmount float64 `json:"tax_exemption_amount"` // 免税额度 + LevelBonus LevelBonusConfig `json:"level_bonus"` // 等级加成配置 + UpgradeFee UpgradeFeeConfig `json:"upgrade_fee"` // 升级费用配置 + UpgradeRebate UpgradeRebateConfig `json:"upgrade_rebate"` // 升级返佣配置 + DirectParentRebate DirectParentRebateConfig `json:"direct_parent_rebate"` // 直接上级返佣配置 + MaxGoldRebateAmount float64 `json:"max_gold_rebate_amount"` // 黄金代理最大返佣金额 + CommissionFreeze CommissionFreezeConfig `json:"commission_freeze"` // 佣金冻结配置 + TaxRate float64 `json:"tax_rate"` // 税率 + TaxExemptionAmount float64 `json:"tax_exemption_amount"` // 免税额度 } LevelBonusConfig { Diamond int64 `json:"diamond"` // 钻石加成:0 @@ -327,23 +326,36 @@ type ( NormalToGoldRebate float64 `json:"normal_to_gold_rebate"` // 普通→黄金返佣:139 ToDiamondRebate float64 `json:"to_diamond_rebate"` // 升级为钻石返佣:680 } - // 系统配置更新 + DirectParentRebateConfig { + Diamond float64 `json:"diamond"` // 直接上级是钻石的返佣金额(6元) + Gold float64 `json:"gold"` // 直接上级是黄金的返佣金额(3元) + Normal float64 `json:"normal"` // 直接上级是普通的返佣金额(2元) + } + CommissionFreezeConfig { + Ratio float64 `json:"ratio"` // 佣金冻结比例(例如:0.1表示10%) + Threshold float64 `json:"threshold"` // 佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元) + Days int64 `json:"days"` // 佣金冻结解冻天数(单位:天,例如:30表示30天后解冻) + } + // 系统配置更新(价格配置已移除,改为产品配置表管理) AdminUpdateAgentConfigReq { - BasePrice *float64 `json:"base_price,optional"` // 基础底价 - SystemMaxPrice *float64 `json:"system_max_price,optional"` // 系统价格上限 - PriceThreshold *float64 `json:"price_threshold,optional"` // 提价标准阈值 - PriceFeeRate *float64 `json:"price_fee_rate,optional"` // 提价手续费比例 - TaxRate *float64 `json:"tax_rate,optional"` // 税率 - TaxExemptionAmount *float64 `json:"tax_exemption_amount,optional"` // 免税额度 + LevelBonus *LevelBonusConfig `json:"level_bonus,optional"` // 等级加成配置 + UpgradeFee *UpgradeFeeConfig `json:"upgrade_fee,optional"` // 升级费用配置 + UpgradeRebate *UpgradeRebateConfig `json:"upgrade_rebate,optional"` // 升级返佣配置 + DirectParentRebate *DirectParentRebateConfig `json:"direct_parent_rebate,optional"` // 直接上级返佣配置 + MaxGoldRebateAmount *float64 `json:"max_gold_rebate_amount,optional"` // 黄金代理最大返佣金额 + CommissionFreeze *CommissionFreezeConfig `json:"commission_freeze,optional"` // 佣金冻结配置 + TaxRate *float64 `json:"tax_rate,optional"` // 税率 + TaxExemptionAmount *float64 `json:"tax_exemption_amount,optional"` // 免税额度 } AdminUpdateAgentConfigResp { Success bool `json:"success"` } // 产品配置分页查询 AdminGetAgentProductConfigListReq { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - ProductId *int64 `form:"product_id,optional"` // 产品ID(可选) + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductId *int64 `form:"product_id,optional"` // 产品ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名称(可选,用于搜索) } AgentProductConfigItem { Id int64 `json:"id"` // 主键 @@ -362,12 +374,11 @@ type ( } // 产品配置更新 AdminUpdateAgentProductConfigReq { - Id int64 `json:"id"` // 主键 - BasePrice float64 `json:"base_price"` // 基础底价 - PriceRangeMin float64 `json:"price_range_min"` // 最低定价 - PriceRangeMax float64 `json:"price_range_max"` // 最高定价 - PriceThreshold float64 `json:"price_threshold"` // 提价标准阈值 - PriceFeeRate float64 `json:"price_fee_rate"` // 提价手续费比例 + Id int64 `json:"id"` // 主键 + BasePrice float64 `json:"base_price"` // 基础底价 + PriceRangeMax float64 `json:"price_range_max"` // 最高定价(对应数据库 system_max_price) + PriceThreshold *float64 `json:"price_threshold,optional"` // 提价标准阈值(可选) + PriceFeeRate *float64 `json:"price_fee_rate,optional"` // 提价手续费比例(可选) } AdminUpdateAgentProductConfigResp { Success bool `json:"success"` diff --git a/app/main/api/desc/admin/order.api b/app/main/api/desc/admin/order.api index 84e606c..c511121 100644 --- a/app/main/api/desc/admin/order.api +++ b/app/main/api/desc/admin/order.api @@ -53,7 +53,6 @@ type ( PaymentScene string `form:"payment_scene,optional"` // 支付平台 Amount float64 `form:"amount,optional"` // 金额 Status string `form:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 - IsPromotion int64 `form:"is_promotion,optional,default=-1"` // 是否推广订单:0-否,1-是 CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 @@ -80,7 +79,6 @@ type ( CreateTime string `json:"create_time"` // 创建时间 PayTime string `json:"pay_time"` // 支付时间 RefundTime string `json:"refund_time"` // 退款时间 - IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 } @@ -102,7 +100,6 @@ type ( CreateTime string `json:"create_time"` // 创建时间 PayTime string `json:"pay_time"` // 支付时间 RefundTime string `json:"refund_time"` // 退款时间 - IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 UpdateTime string `json:"update_time"` // 更新时间 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 @@ -116,7 +113,6 @@ type ( PaymentScene string `json:"payment_scene"` // 支付平台 Amount float64 `json:"amount"` // 金额 Status string `json:"status,default=pending"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 - IsPromotion int64 `json:"is_promotion,default=0"` // 是否推广订单:0-否,1-是 } // 创建响应 AdminCreateOrderResp { @@ -134,7 +130,6 @@ type ( Status *string `json:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 PayTime *string `json:"pay_time,optional"` // 支付时间 RefundTime *string `json:"refund_time,optional"` // 退款时间 - IsPromotion *int64 `json:"is_promotion,optional"` // 是否推广订单:0-否,1-是 } // 更新响应 AdminUpdateOrderResp { diff --git a/app/main/api/desc/admin/promotion.api b/app/main/api/desc/admin/promotion.api deleted file mode 100644 index 9c0a03e..0000000 --- a/app/main/api/desc/admin/promotion.api +++ /dev/null @@ -1,182 +0,0 @@ -syntax = "v1" - -info ( - title: "推广服务" - desc: "推广服务" - version: "v1" -) - -@server ( - prefix: api/v1/admin/promotion/link - group: admin_promotion - middleware: AdminAuthInterceptor -) -service main { - @doc "获取推广链接列表" - @handler GetPromotionLinkList - get /list (GetPromotionLinkListReq) returns (GetPromotionLinkListResp) - - @doc "获取推广链接详情" - @handler GetPromotionLinkDetail - get /detail/:id (GetPromotionLinkDetailReq) returns (GetPromotionLinkDetailResp) - - @doc "创建推广链接" - @handler CreatePromotionLink - post /create (CreatePromotionLinkReq) returns (CreatePromotionLinkResp) - - @doc "更新推广链接" - @handler UpdatePromotionLink - put /update/:id (UpdatePromotionLinkReq) returns (UpdatePromotionLinkResp) - - @doc "删除推广链接" - @handler DeletePromotionLink - delete /delete/:id (DeletePromotionLinkReq) returns (DeletePromotionLinkResp) -} - -type ( - // 列表请求 - GetPromotionLinkListReq { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"pageSize,default=20"` // 每页数量 - Name string `form:"name,optional"` // 链接名称 - Url string `form:"url,optional"` // 推广链接URL - } - - // 列表响应 - GetPromotionLinkListResp { - Total int64 `json:"total"` // 总数 - Items []PromotionLinkItem `json:"items"` // 列表 - } - - // 列表项 - PromotionLinkItem { - Id int64 `json:"id"` // 链接ID - Name string `json:"name"` // 链接名称 - Url string `json:"url"` // 推广链接URL - ClickCount int64 `json:"click_count"` // 点击数 - PayCount int64 `json:"pay_count"` // 付费次数 - PayAmount string `json:"pay_amount"` // 付费金额 - CreateTime string `json:"create_time"` // 创建时间 - LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 - LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 - } - - // 详情请求 - GetPromotionLinkDetailReq { - Id int64 `path:"id"` // 链接ID - } - - // 详情响应 - GetPromotionLinkDetailResp { - Name string `json:"name"` // 链接名称 - Url string `json:"url"` // 推广链接URL - ClickCount int64 `json:"click_count"` // 点击数 - PayCount int64 `json:"pay_count"` // 付费次数 - PayAmount string `json:"pay_amount"` // 付费金额 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 - LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 - LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 - } - - // 创建请求 - CreatePromotionLinkReq { - Name string `json:"name"` // 链接名称 - } - - // 创建响应 - CreatePromotionLinkResp { - Id int64 `json:"id"` // 链接ID - Url string `json:"url"` // 生成的推广链接URL - } - - // 更新请求 - UpdatePromotionLinkReq { - Id int64 `path:"id"` // 链接ID - Name *string `json:"name,optional"` // 链接名称 - } - - // 更新响应 - UpdatePromotionLinkResp { - Success bool `json:"success"` // 是否成功 - } - - // 删除请求 - DeletePromotionLinkReq { - Id int64 `path:"id"` // 链接ID - } - - // 删除响应 - DeletePromotionLinkResp { - Success bool `json:"success"` // 是否成功 - } -) - -@server ( - prefix: api/v1/admin/promotion/link - group: admin_promotion - middleware: AdminAuthInterceptor -) -service main { - @doc "记录链接点击" - @handler RecordLinkClick - get /record/:path (RecordLinkClickReq) returns (RecordLinkClickResp) -} - -type ( - // 记录链接点击请求 - RecordLinkClickReq { - Path string `path:"path"` // 链接路径 - } - - // 记录链接点击响应 - RecordLinkClickResp { - Success bool `json:"success"` // 是否成功 - } -) -@server ( - prefix: api/v1/admin/promotion/stats - group: admin_promotion - middleware: AdminAuthInterceptor -) -service main { - @doc "获取推广历史记录" - @handler GetPromotionStatsHistory - get /history (GetPromotionStatsHistoryReq) returns ([]PromotionStatsHistoryItem) - - @doc "获取推广总统计" - @handler GetPromotionStatsTotal - get /total (GetPromotionStatsTotalReq) returns (GetPromotionStatsTotalResp) -} - -type ( - // 获取推广历史记录请求 - GetPromotionStatsHistoryReq { - StartDate string `form:"start_date"` // 开始日期,格式:YYYY-MM-DD - EndDate string `form:"end_date"` // 结束日期,格式:YYYY-MM-DD - } - - // 推广历史记录项 - PromotionStatsHistoryItem { - Id int64 `json:"id"` // 记录ID - LinkId int64 `json:"link_id"` // 链接ID - PayAmount float64 `json:"pay_amount"` // 金额 - ClickCount int64 `json:"click_count"` // 点击数 - PayCount int64 `json:"pay_count"` // 付费次数 - StatsDate string `json:"stats_date"` // 统计日期 - } - - // 获取推广总统计请求 - GetPromotionStatsTotalReq { - } - - // 获取推广总统计响应 - GetPromotionStatsTotalResp { - TodayPayAmount float64 `json:"today_pay_amount"` // 今日金额 - TodayClickCount int64 `json:"today_click_count"` // 今日点击数 - TodayPayCount int64 `json:"today_pay_count"` // 今日付费次数 - TotalPayAmount float64 `json:"total_pay_amount"` // 总金额 - TotalClickCount int64 `json:"total_click_count"` // 总点击数 - TotalPayCount int64 `json:"total_pay_count"` // 总付费次数 - } -) \ No newline at end of file diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index 2301dd4..32dd3ba 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -32,11 +32,15 @@ type ( LinkIdentifier string `form:"link_identifier"` // 推广链接标识 } GetLinkDataResp { - AgentId int64 `json:"agent_id"` // 代理ID - ProductId int64 `json:"product_id"` // 产品ID - SetPrice float64 `json:"set_price"` // 代理设定价格 - ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 - ProductName string `json:"product_name"` // 产品名称 + AgentId int64 `json:"agent_id"` // 代理ID + ProductId int64 `json:"product_id"` // 产品ID + SetPrice float64 `json:"set_price"` // 代理设定价格 + ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 + ProductName string `json:"product_name"` // 产品名称 + ProductEn string `json:"product_en"` // 产品英文标识 + SellPrice float64 `json:"sell_price"` // 销售价格(使用代理设定价格) + Description string `json:"description"` // 产品描述 + Features []Feature `json:"features"` // 产品功能列表 } AgentApplyReq { Region string `json:"region,optional"` // 区域(可选) @@ -94,10 +98,33 @@ type ( Remark string `json:"remark"` // 备注 CreateTime string `json:"create_time"` // 创建时间 } + // 删除邀请码 + DeleteInviteCodeReq { + Id int64 `json:"id"` // 邀请码ID + } + DeleteInviteCodeResp {} // 获取邀请链接 + GetInviteLinkReq { + InviteCode string `form:"invite_code"` // 邀请码 + TargetPath string `form:"target_path,optional"` // 目标地址(可选,默认为注册页面) + } GetInviteLinkResp { InviteLink string `json:"invite_link"` // 邀请链接 - QrCodeUrl string `json:"qr_code_url"` // 二维码URL + } + // 获取代理等级特权信息 + GetLevelPrivilegeResp { + Levels []LevelPrivilegeItem `json:"levels"` // 各等级特权信息列表 + } + LevelPrivilegeItem { + Level int64 `json:"level"` // 等级:1=普通,2=黄金,3=钻石 + LevelName string `json:"level_name"` // 等级名称 + LevelBonus float64 `json:"level_bonus"` // 等级加成(元) + PriceReduction float64 `json:"price_reduction"` // 底价降低(元,相对于当前等级) + PromoteRebate string `json:"promote_rebate"` // 推广返佣说明 + MaxPromoteRebate float64 `json:"max_promote_rebate"` // 最高推广返佣金额(元) + UpgradeRebate string `json:"upgrade_rebate"` // 下级升级返佣说明 + Privileges []string `json:"privileges"` // 特权列表 + CanUpgradeSubordinate bool `json:"can_upgrade_subordinate"` // 是否可以升级下级 } ) @@ -127,10 +154,22 @@ service main { @handler GetTeamStatistics get /team/statistics returns (TeamStatisticsResp) + // 获取转化率统计 + @handler GetConversionRate + get /conversion/rate returns (ConversionRateResp) + // 获取下级列表 @handler GetSubordinateList get /subordinate/list (GetSubordinateListReq) returns (GetSubordinateListResp) + // 获取下级贡献详情 + @handler GetSubordinateContributionDetail + get /subordinate/contribution/detail (GetSubordinateContributionDetailReq) returns (GetSubordinateContributionDetailResp) + + // 获取团队列表 + @handler GetTeamList + get /team/list (GetTeamListReq) returns (GetTeamListResp) + // 获取收益信息 @handler GetRevenueInfo get /revenue returns (GetRevenueInfoResp) @@ -139,10 +178,14 @@ service main { @handler GetCommissionList get /commission/list (GetCommissionListReq) returns (GetCommissionListResp) - // 获取返佣记录 + // 获取返佣记录(推广返佣) @handler GetRebateList get /rebate/list (GetRebateListReq) returns (GetRebateListResp) + // 获取升级返佣记录 + @handler GetUpgradeRebateList + get /rebate/upgrade/list (GetUpgradeRebateListReq) returns (GetUpgradeRebateListResp) + // 获取升级记录 @handler GetUpgradeList get /upgrade/list (GetUpgradeListReq) returns (GetUpgradeListResp) @@ -175,9 +218,17 @@ service main { @handler GetInviteCodeList get /invite_code/list (GetInviteCodeListReq) returns (GetInviteCodeListResp) - // 获取邀请链接 + // 删除邀请码 + @handler DeleteInviteCode + post /invite_code/delete (DeleteInviteCodeReq) returns (DeleteInviteCodeResp) + + // 获取邀请链接(根据邀请码生成) @handler GetInviteLink - get /invite_link returns (GetInviteLinkResp) + get /invite_link (GetInviteLinkReq) returns (GetInviteLinkResp) + + // 获取代理等级特权信息 + @handler GetLevelPrivilege + get /level/privilege returns (GetLevelPrivilegeResp) } type ( @@ -194,11 +245,13 @@ type ( } // 生成推广链接 AgentGeneratingLinkReq { - ProductId int64 `json:"product_id"` // 产品ID - SetPrice float64 `json:"set_price"` // 设定价格 + ProductId int64 `json:"product_id"` // 产品ID + SetPrice float64 `json:"set_price"` // 设定价格 + TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) } AgentGeneratingLinkResp { LinkIdentifier string `json:"link_identifier"` // 推广链接标识 + FullLink string `json:"full_link"` // 完整短链URL } // 产品配置 AgentProductConfigResp { @@ -207,8 +260,7 @@ type ( ProductConfigItem { ProductId int64 `json:"product_id"` // 产品ID ProductName string `json:"product_name"` // 产品名称 - BasePrice float64 `json:"base_price"` // 基础底价 - LevelBonus float64 `json:"level_bonus"` // 等级加成 + ProductEn string `json:"product_en"` // 产品英文标识 ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 PriceRangeMin float64 `json:"price_range_min"` // 最低价格 PriceRangeMax float64 `json:"price_range_max"` // 最高价格 @@ -217,15 +269,31 @@ type ( } // 团队统计 TeamStatisticsResp { - TotalCount int64 `json:"total_count"` // 团队总人数(包括自己) - DirectCount int64 `json:"direct_count"` // 直接下级数量 - IndirectCount int64 `json:"indirect_count"` // 间接下级数量 - ByLevel TeamLevelStats `json:"by_level"` // 按等级统计 + TotalCount int64 `json:"total_count"` // 团队总人数(不包括自己) + DirectCount int64 `json:"direct_count"` // 直接下级数量 + IndirectCount int64 `json:"indirect_count"` // 间接下级数量 + GoldCount int64 `json:"gold_count"` // 黄金代理数量 + NormalCount int64 `json:"normal_count"` // 普通代理数量 + TodayNewMembers int64 `json:"today_new_members"` // 今日新增成员 + MonthNewMembers int64 `json:"month_new_members"` // 本月新增成员 } - TeamLevelStats { - Diamond int64 `json:"diamond"` // 钻石数量 - Gold int64 `json:"gold"` // 黄金数量 - Normal int64 `json:"normal"` // 普通数量 + // 转化率统计 + ConversionRateResp { + MyConversionRate ConversionRateData `json:"my_conversion_rate"` // 我的转化率 + SubordinateConversionRate ConversionRateData `json:"subordinate_conversion_rate"` // 我的下级转化率 + } + ConversionRateData { + Daily []PeriodConversionData `json:"daily"` // 日统计(今日、昨日、前日) + Weekly []PeriodConversionData `json:"weekly"` // 周统计(这周、前一周、前两周) + Monthly []PeriodConversionData `json:"monthly"` // 月统计(本月、上月、前两月) + } + PeriodConversionData { + PeriodLabel string `json:"period_label"` // 时间段标签(如:今日、昨日、前日) + QueryUsers int64 `json:"query_users"` // 查询订单数(订单数量,不是用户数) + PaidUsers int64 `json:"paid_users"` // 付费订单数(订单数量,不是用户数) + TotalAmount float64 `json:"total_amount"` // 总金额(已支付订单的总金额) + QueryUserCount int64 `json:"query_user_count"` // 查询用户数(去重user_id后的用户数量) + PaidUserCount int64 `json:"paid_user_count"` // 付费用户数(去重user_id后的用户数量) } // 下级列表 GetSubordinateListReq { @@ -245,12 +313,103 @@ type ( TotalOrders int64 `json:"total_orders"` // 总订单数 TotalAmount float64 `json:"total_amount"` // 总金额 } + // 下级贡献详情 + GetSubordinateContributionDetailReq { + SubordinateId int64 `form:"subordinate_id"` // 下级代理ID + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + TabType string `form:"tab_type,optional"` // 标签页类型:order=订单列表,invite=邀请列表 + } + GetSubordinateContributionDetailResp { + Mobile string `json:"mobile"` // 手机号 + LevelName string `json:"level_name"` // 等级名称 + CreateTime string `json:"create_time"` // 创建时间 + OrderStats OrderStatistics `json:"order_stats"` // 订单统计(仅统计有返佣的订单) + RebateStats RebateStatistics `json:"rebate_stats"` // 返佣统计 + InviteStats InviteStatistics `json:"invite_stats"` // 邀请统计 + OrderList []OrderItem `json:"order_list"` // 订单列表(仅有贡献的订单) + InviteList []InviteItem `json:"invite_list"` // 邀请列表 + OrderListTotal int64 `json:"order_list_total"` // 订单列表总数 + InviteListTotal int64 `json:"invite_list_total"` // 邀请列表总数 + } + OrderStatistics { + TotalOrders int64 `json:"total_orders"` // 总订单量(仅统计有返佣的订单) + MonthOrders int64 `json:"month_orders"` // 月订单 + TodayOrders int64 `json:"today_orders"` // 今日订单 + } + RebateStatistics { + TotalRebateAmount float64 `json:"total_rebate_amount"` // 总贡献返佣金额 + TodayRebateAmount float64 `json:"today_rebate_amount"` // 今日贡献返佣金额 + MonthRebateAmount float64 `json:"month_rebate_amount"` // 月贡献返佣金额 + } + InviteStatistics { + TotalInvites int64 `json:"total_invites"` // 总的邀请 + TodayInvites int64 `json:"today_invites"` // 今日邀请 + MonthInvites int64 `json:"month_invites"` // 月邀请 + } + OrderItem { + OrderNo string `json:"order_no"` // 订单号 + ProductId int64 `json:"product_id"` // 产品ID + ProductName string `json:"product_name"` // 产品名称 + OrderAmount float64 `json:"order_amount"` // 订单金额 + RebateAmount float64 `json:"rebate_amount"` // 返佣金额 + CreateTime string `json:"create_time"` // 创建时间 + } + InviteItem { + AgentId int64 `json:"agent_id"` // 代理ID + Level int64 `json:"level"` // 等级 + LevelName string `json:"level_name"` // 等级名称 + Mobile string `json:"mobile"` // 手机号 + CreateTime string `json:"create_time"` // 邀请时间 + } + // 团队列表 + GetTeamListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + Mobile string `form:"mobile,optional"` // 手机号(可选,用于搜索) + } + GetTeamListResp { + Statistics TeamStatistics `json:"statistics"` // 统计数据 + Total int64 `json:"total"` // 总数 + List []TeamMemberItem `json:"list"` // 列表 + } + TeamStatistics { + TotalMembers int64 `json:"total_members"` // 团队成员总数 + TodayNewMembers int64 `json:"today_new_members"` // 团队人员今日新增 + MonthNewMembers int64 `json:"month_new_members"` // 团队人员月新增 + TotalQueries int64 `json:"total_queries"` // 团队总查询总量 + TodayPromotions int64 `json:"today_promotions"` // 团队今日推广量 + MonthPromotions int64 `json:"month_promotions"` // 团队月推广量 + TotalEarnings float64 `json:"total_earnings"` // 依靠团队得到的总收益 + TodayEarnings float64 `json:"today_earnings"` // 今日收益 + MonthEarnings float64 `json:"month_earnings"` // 月收益 + } + TeamMemberItem { + AgentId int64 `json:"agent_id"` // 代理ID + Level int64 `json:"level"` // 等级 + LevelName string `json:"level_name"` // 等级名称 + Mobile string `json:"mobile"` // 手机号 + CreateTime string `json:"create_time"` // 加入团队时间 + TodayQueries int64 `json:"today_queries"` // 当日查询量 + TotalQueries int64 `json:"total_queries"` // 总查询量 + TotalRebateAmount float64 `json:"total_rebate_amount"` // 返佣给我的总金额 + TodayRebateAmount float64 `json:"today_rebate_amount"` // 返佣给我的今日金额 + TotalInvites int64 `json:"total_invites"` // 邀请加入团队的总人数 + TodayInvites int64 `json:"today_invites"` // 邀请加入团队的今日人数 + IsDirect bool `json:"is_direct"` // 是否直接下级 + } // 收益信息 GetRevenueInfoResp { Balance float64 `json:"balance"` // 可用余额 FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 - TotalEarnings float64 `json:"total_earnings"` // 累计收益 + TotalEarnings float64 `json:"total_earnings"` // 累计收益(钱包总收益) WithdrawnAmount float64 `json:"withdrawn_amount"` // 累计提现 + CommissionTotal float64 `json:"commission_total"` // 佣金累计总收益(推广订单获得的佣金) + CommissionToday float64 `json:"commission_today"` // 佣金今日收益 + CommissionMonth float64 `json:"commission_month"` // 佣金本月收益 + RebateTotal float64 `json:"rebate_total"` // 返佣累计总收益(包括推广返佣和升级返佣) + RebateToday float64 `json:"rebate_today"` // 返佣今日收益 + RebateMonth float64 `json:"rebate_month"` // 返佣本月收益 } // 佣金记录 GetCommissionListReq { @@ -264,27 +423,51 @@ type ( CommissionItem { Id int64 `json:"id"` // 记录ID OrderId int64 `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 订单号 ProductName string `json:"product_name"` // 产品名称 Amount float64 `json:"amount"` // 佣金金额 Status int64 `json:"status"` // 状态:1=已发放,2=已冻结,3=已取消 CreateTime string `json:"create_time"` // 创建时间 } - // 返佣记录 + // 返佣记录(推广返佣) GetRebateListReq { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选):1=直接上级返佣,2=钻石上级返佣,3=黄金上级返佣 } GetRebateListResp { Total int64 `json:"total"` // 总数 List []RebateItem `json:"list"` // 列表 } RebateItem { - Id int64 `json:"id"` // 记录ID - SourceAgentId int64 `json:"source_agent_id"` // 来源代理ID - OrderId int64 `json:"order_id"` // 订单ID - RebateType int64 `json:"rebate_type"` // 返佣类型:1=直接上级,2=钻石上级,3=黄金上级 - Amount float64 `json:"amount"` // 返佣金额 - CreateTime string `json:"create_time"` // 创建时间 + Id int64 `json:"id"` // 记录ID + SourceAgentId int64 `json:"source_agent_id"` // 来源代理ID + SourceAgentMobile string `json:"source_agent_mobile"` // 来源代理手机号 + SourceAgentLevel int64 `json:"source_agent_level"` // 来源代理等级:1=普通,2=黄金,3=钻石 + OrderId int64 `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 订单号 + RebateType int64 `json:"rebate_type"` // 返佣类型:1=直接上级,2=钻石上级,3=黄金上级 + Amount float64 `json:"amount"` // 返佣金额 + CreateTime string `json:"create_time"` // 创建时间 + } + // 升级返佣记录 + GetUpgradeRebateListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + } + GetUpgradeRebateListResp { + Total int64 `json:"total"` // 总数 + List []UpgradeRebateItem `json:"list"` // 列表 + } + UpgradeRebateItem { + Id int64 `json:"id"` // 记录ID + SourceAgentId int64 `json:"source_agent_id"` // 来源代理ID(升级的代理) + SourceAgentMobile string `json:"source_agent_mobile"` // 来源代理手机号 + OrderNo string `json:"order_no"` // 订单号 + FromLevel int64 `json:"from_level"` // 原等级 + ToLevel int64 `json:"to_level"` // 目标等级 + Amount float64 `json:"amount"` // 返佣金额 + CreateTime string `json:"create_time"` // 创建时间 } // 升级记录 GetUpgradeListReq { @@ -365,3 +548,19 @@ type ( } ) +// ============================================ +// 短链重定向接口(公开访问,不需要认证和api前缀) +// ============================================ +@server ( + group: agent +) +service main { + // 短链重定向 + @handler ShortLinkRedirect + get /s/:shortCode returns (ShortLinkRedirectResp) +} + +type ( + ShortLinkRedirectResp {} +) + diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api index b322c46..3032223 100644 --- a/app/main/api/desc/front/pay.api +++ b/app/main/api/desc/front/pay.api @@ -46,8 +46,8 @@ service main { type ( PaymentReq { Id string `json:"id"` - PayMethod string `json:"pay_method"` - PayType string `json:"pay_type" validate:"required,oneof=query agent_vip"` + PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式) + PayType string `json:"pay_type" validate:"required,oneof=query agent_vip agent_upgrade"` } PaymentResp { PrepayData interface{} `json:"prepay_data"` diff --git a/app/main/api/desc/main.api b/app/main/api/desc/main.api index 2f15518..855660b 100644 --- a/app/main/api/desc/main.api +++ b/app/main/api/desc/main.api @@ -18,7 +18,6 @@ import "./front/authorization.api" import "./admin/auth.api" import "./admin/menu.api" import "./admin/role.api" -import "./admin/promotion.api" import "./admin/order.api" import "./admin/admin_user.api" import "./admin/platform_user.api" diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml index bbb601f..3bb6e37 100644 --- a/app/main/api/etc/main.dev.yaml +++ b/app/main/api/etc/main.dev.yaml @@ -41,13 +41,13 @@ Alipay: Wxpay: AppID: "wx442ee1ac1ee75917" - MchID: "1682635136" - MchCertificateSerialNumber: "5369B8AEEBDCF7AF274510252E6A8C0659C30F61" - MchApiv3Key: "e3ea4cf0765f1e71b01bb387dfcdbc9f" + MchID: "1687993434" + MchCertificateSerialNumber: "241E4BCF5B69AAAC48451DB2C7ED794EF8B3A3D3" + MchApiv3Key: "aB3cD5eF7gH9iJ1kL2mN4oP6qR8sT0uV" MchPrivateKeyPath: "etc/merchant/apiclient_key.pem" - MchPublicKeyID: "PUB_KEY_ID_0116826351362025060900382267001601" + MchPublicKeyID: "PUB_KEY_ID_0116879934342025120200181745004208" MchPublicKeyPath: "etc/merchant/pub_key.pem" - MchPlatformRAS: "1FFEC3F62E31885FAB4C91ADCB8D7557E9488781" + MchPlatformRAS: "5630D013C88EA348BF66E642B6C39AA0180D4B15" NotifyUrl: "https://6m4685017o.goho.co/api/v1/pay/wechat/callback" RefundNotifyUrl: "https://6m4685017o.goho.co/api/v1/wechat/refund_callback" Applepay: @@ -63,8 +63,8 @@ Ali: SystemConfig: ThreeVerify: false WechatH5: - AppID: "wxa581992dc74d860e" - AppSecret: "4de1fbf521712247542d49907fcd5dbf" + AppID: "wx442ee1ac1ee75917" + AppSecret: "c80474909db42f63913b7a307b3bee17" WechatMini: AppID: "wx781abb66b3368963" # 小程序的AppID AppSecret: "c7d02cdb0fc23c35c93187af9243b00d" # 小程序的AppSecret @@ -76,8 +76,6 @@ AdminConfig: AccessSecret: "jK8nP3qR7tV2xZ5aB9cD1eF6gH4iJ0kL8mN5oP6qR7sT" AccessExpire: 604800 RefreshAfter: 302400 -AdminPromotion: - URLDomain: "https://tianyuandb.com/p" TaxConfig: TaxRate: 0.06 TaxExemptionAmount: 0.00 @@ -87,4 +85,7 @@ Tianyuanapi: BaseURL: "https://api.tianyuanapi.com" Timeout: 60 Authorization: - FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL \ No newline at end of file + FileBaseURL: "https://www.onecha.cn/api/v1/auth-docs" # 授权书文件访问基础URL +Promotion: + PromotionDomain: "http://localhost:8888" # 推广域名(用于生成短链) + OfficialDomain: "http://localhost:5678" # 正式站点域名(短链重定向的目标域名) diff --git a/app/main/api/etc/main.yaml b/app/main/api/etc/main.yaml index 6c164df..8410ef3 100644 --- a/app/main/api/etc/main.yaml +++ b/app/main/api/etc/main.yaml @@ -29,19 +29,19 @@ Alipay: AlipayCertPath: "etc/merchant/alipayCertPublicKey_RSA2.crt" AlipayRootCertPath: "etc/merchant/alipayRootCert.crt" IsProduction: true - NotifyUrl: "https://www.quannengcha.com/api/v1/pay/alipay/callback" - ReturnURL: "https://www.quannengcha.com/payment/result" + NotifyUrl: "https://www.onecha.cn/api/v1/pay/alipay/callback" + ReturnURL: "https://www.onecha.cn/payment/result" Wxpay: AppID: "wx442ee1ac1ee75917" - MchID: "1682635136" - MchCertificateSerialNumber: "5369B8AEEBDCF7AF274510252E6A8C0659C30F61" - MchApiv3Key: "e3ea4cf0765f1e71b01bb387dfcdbc9f" + MchID: "1687993434" + MchCertificateSerialNumber: "241E4BCF5B69AAAC48451DB2C7ED794EF8B3A3D3" + MchApiv3Key: "aB3cD5eF7gH9iJ1kL2mN4oP6qR8sT0uV" MchPrivateKeyPath: "etc/merchant/apiclient_key.pem" - MchPublicKeyID: "PUB_KEY_ID_0116826351362025060900382267001601" + MchPublicKeyID: "PUB_KEY_ID_0116879934342025120200181745004208" MchPublicKeyPath: "etc/merchant/pub_key.pem" - MchPlatformRAS: "1FFEC3F62E31885FAB4C91ADCB8D7557E9488781" - NotifyUrl: "https://www.quannengcha.com/api/v1/pay/wechat/callback" - RefundNotifyUrl: "https://www.quannengcha.com/api/v1/wechat/refund_callback" + MchPlatformRAS: "5630D013C88EA348BF66E642B6C39AA0180D4B15" + NotifyUrl: "https://www.onecha.cn/api/v1/pay/wechat/callback" + RefundNotifyUrl: "https://www.onecha.cn/api/v1/wechat/refund_callback" Applepay: ProductionVerifyURL: "https://api.storekit.itunes.apple.com/inApps/v1/transactions/receipt" SandboxVerifyURL: "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/receipt" @@ -64,8 +64,6 @@ AdminConfig: AccessSecret: "jK8nP3qR7tV2xZ5aB9cD1eF6gH4iJ0kL8mN5oP6qR7sT" AccessExpire: 604800 RefreshAfter: 302400 -AdminPromotion: - URLDomain: "https://tianyuandb.com/p" TaxConfig: TaxRate: 0.06 TaxExemptionAmount: 0.00 @@ -75,4 +73,7 @@ Tianyuanapi: BaseURL: "https://api.tianyuanapi.com" Timeout: 60 Authorization: - FileBaseURL: "https://www.quannengcha.com/api/v1/auth-docs" # 授权书文件访问基础URL + FileBaseURL: "https://www.onecha.cn/api/v1/auth-docs" # 授权书文件访问基础URL +Promotion: + PromotionDomain: "https://p.onecha.cn" # 推广域名(用于生成短链) + OfficialDomain: "https://www.onecha.cn" # 正式站点域名(短链重定向的目标域名) diff --git a/app/main/api/etc/merchant/apiclient_key.pem b/app/main/api/etc/merchant/apiclient_key.pem index 246c1df..0e3575b 100644 --- a/app/main/api/etc/merchant/apiclient_key.pem +++ b/app/main/api/etc/merchant/apiclient_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCP6fWm1vXXybH -m3Ne6PjacGrN2+iMrzWZlzdHCZ31udDPqSUYaZ+78b441KZK/CJFQWeSJ/1h//A+ -BGsQDKvE/fj2QzN1KkOuQ8WJXNGpixE5uu5bv/QTN/ukurGdA1aO2aFCANumlOmB -HkB/B2so57ii8iQQjwK2xM4r3oOU/IfcFGKL+9/QjLGFFp9PJXCDBCgrxxlZGaj1 -3wowlfVOzlaX94gemQsCYVkuAFIYMAnFHs9cKNZQIU80somW/yy2Gy38N6n7NnbD -nvFSaq4GoDROqRgKbRZ5e706d/p7A3aS/2oRqq1jomUIugK8g++LmoHFTgfhfQkI -v1aG/nPzAgMBAAECggEAD2RN31J2J42xm/V0YdviBCUOQXugZK1peN8jkSxw6Myt -gBbuCo4sCw9vvD8VYjGyYXx6QXmLuV03YyKkfSQT5EsflBvlEu6jaEaUe3rwXhfX -6JQoWPrP00oHVZk5g7CFBlK2VW2N+hgonIOSJr6mvhoGZlr7gphiZasYjx9Vm9N3 -Pbnfru5ttzplYNniwH3DF6ph8VmdbD1nnbWSKLXvHCsXQT2wBcnsIagIH3vyq6K1 -pc5abWsQJrixOPebpI8jD5w0HxHAqVLx58H/OC2zW/roAw1WS2AkueJ1j7dQ7Z0C -mc9Xexz5gcAP0nMAQv+LP7iYqsa/niFhfcTFWfdxkQKBgQD5JkKNmInU2/IVYCwO -c483MCSv1+MnbRXlb7vut8T0IupHTU6hCge6C3q3HsjbKSBn8bRChtPUzvw9JFxK -QWKiQqQDPLDJ08AIKhfQD2JiLtoikkZN0bF6OTL+Soney1yGx51mlfHM194+PcCJ -jF7iWdMVbcBwHbgydNxxIS5cKQKBgQDHlvQ4lw6gvLILpGK494/vNYSJP/Jmd66V -3oSGYi84YRKTSwH4NlbBVVieb3Dv+pPugbsXEuFHBif7WsivbYgNTE9++8Yvt0gh -duB1G4yh7m/ylQeSuipgQU9tozrU/15cWwmcCRV50wWXBGoVEM0kf7mzEKSxmjYk -Qzko/zxSuwKBgQCY6Bc+SViFz3qSDdTcBaXma+CIHsmlH7ipd9px1kzEvEzl95cD -FGHLl1H34qfIgUQHJvrHPXHyEBoT+CW/2MMM7DM2XV/ubctT92ln4pkxwqlTQExv -Y/s1FLesAtj8Z/hgK0/5bprYab9WmZV5lTGCXzhB1XqeFE9AgCHuODv4iQKBgQC8 -g2uwd5ytXQydymokYk9klJvWNrvw5GHV1BJAC0Smb6lnzZTSqCBRAxdsrb1yLK7E -u2vGY2K7/qiM1DZw23eBd+4t9gg+0VIjqXBfq+GsoNTDvtckUwnrWER5PY831ut9 -N89fvYS3SAUjmlvIAdKBAtKWusWTqiAxJ/05J7oGOQKBgB5PSr5i0LlupIbKui9t -XtXnRqGPxxrZZUpTkyrGOAnlCz/zq2QiwFpBWo/NMHOp0KmxzJpQ8yEY2LWlRZ61 -Oc9m0J/HtPw3Ohi1treBosEVG/0NOI9Tq1Obny23N51MVibdW6zEIyGUp/DbFS8h -5DljdOYX9IYIHHn3Ig4GeTGe +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAh8YNl16EkVKW +IHDiPyx5Zz93osD4n2E7oJXPEOSGpumhsAMjXsRd32JulYDtD/B/phA/mxQiEf84 +Um4VKC16pNAEtpEyrO7ZZRhrPk2AMck6Jm81nXLoppttuS0B3VkOE/1UuvAbIz1y +VliRLDTiIbuSM8p1rMfpsJGo4CoLaerJgzXL6MHZA0+Fhn4PQLkLIt57jC0Jh2Dg +ayH7Ru/wgwgq6upXb7rXj0ZzMer5kkA446mLis9P6Nz1GiTUMyUy+tnORK1EpWLg +tUifsfXsKBLlUcOrNyG1+TyeMOuY2p592Au8J2eZbwyXKRDvK2oJzsDQW04pGyJc +oW1vObmLAgMBAAECggEAPc0XgRNezrULSo99TNK0hv/iepeu09/tSUOh8wbcJHD9 +u94RE9B+vhdPtGmfKfmc3IzE2HYCP3GBeGXVWks8VgsDjw+/igHC5duyu/IS1Jym +mFjwB8jTsuSQLedsEBYqWP+HqSQcoMluFv6qjWcgTpo/aI3hZmahAV2hVBEozeKR +Va+EssjI46k894Kr6s9rb9nk8hCORuLuqDXfWJdxT+UixMeYftrgmHXk6CCUb2Ct +EjMuxi66KyfVu9w5wS0DuE583mDIgTKmD+deJWxcVyJJMJDCULY4fotWhQb2ro9L +qndaCgBC+sOAB/PrO31E40hZhjgdToSq5SvUWgjUCQKBgQD6/zSzEGJYzjS3544l +PWF92WT3aFJxT3+Mgd/BrTWaY2LykbDoioM/Kp+5D1bB446k534xO6uwr3LuDCOE +jZGy/6+HQeDHSLfDZ+LgWQdEbakbniR57HXG293x3Mp5jTlZOXc8ryGURXaCP8Sy +xwIiZPUgpo4xA0Myt/CnjW9OhwKBgQDEXjkc4tyVTsgcVevxBjT4o3g2ihYouCdt +ClDr6iZ8Mi5A0vCcuy1A3uI5BZnax11AIwqapgjOdWgWEtyjQJ84bhesyI7km/Ya +AeaelsgSf+mAfFgTarWb+KpD5l0jxJAlX/1PAQU6vXuUPdA4PtBbKyUKHLY0kMXr +wE4vbPpZ3QKBgQDGvwpFt/WICFAqR0qBJmdqNZgDaDHP03lWEwjQ3YySYZxaIw8I +M5XVkLTE3uZ9xOiQn1WHOo6q62KAKFB/h3IVYOzmlz2bz3LjYgF+UEC26HZ9je2o +NZrVCghmmcQiF7ePdTd7b9mRBKfgXwor3fVMstB/OCNjoAe3w3rl0dKPRQKBgQC2 +oIbvdZH/DqkPqV1o6QPk5muMFbrsHfEU+U4vSrKGOUlJIqWCrpY0ydWNgMcJcPcq +Ciz3jUmNciXeuaYX8qbiHYnJOTGkLeShZXktrz/d7Lamt35WeJz0tTztL1caR9pj +2DVG/8T0T3uacC6x0MGIuMSW9gMDOk3Ipy5P70OaxQKBgQDFzgdJxU9Ra6AzbyBL +hcaKb+moNEo+peKCImwY1n3FeRM6XZ59qQaGV1lYu5KPUj9T/SVaJm6DCYFKqM0r +T1prq2LeR69nB7Dpsr2TKp57L86DoCqbxOBnWxZ/6Em65hoRYe7CAtn3yFQGKm9T ++EdUfn1gf7AWjQAgo3bis3TaMQ== -----END PRIVATE KEY----- diff --git a/app/main/api/etc/merchant/pub_key.pem b/app/main/api/etc/merchant/pub_key.pem index 9356d8d..0adf8e3 100644 --- a/app/main/api/etc/merchant/pub_key.pem +++ b/app/main/api/etc/merchant/pub_key.pem @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwSy7dS/ICZV38tI0HxM -SAIE7+Ug92qryuNlkNyaNDRjfsykHsrPCSsUUQEZblBNmZOLfLQxmAaWC+cQqWCv -zfy4rXGAHE1widWFkHGzQzaw6cB0VdDXatK9yAt1PgXdp5jzBRzOn9Z3u4t0s771 -2zjuxCnLxMq84DovNgh2y0LBiuorWbtuTFTd8SXUGk2Jyuojq/02U3KTuyh+7SmW -ffJXKrzhrKwSpGh59e/fFxqX2xGlVoJ1kdohMZPo/7k+e5jP7qjrf93l7JVeUKYa -V27hNVowJ4oho21WVCJ1AYo41IbPJWI+6WxlaVeoR4zKix0Mb2timaWayyLoN53y -aQIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4el5skKyhhV+lFP/lx2x +MIQ14WFzoywBmy7Jd/UnKp5i8g85rwsFKvkMD9QqdQgoUPdnKKpvKiJeQqwUeXUC +ogVZxedg+wCj4FuOmctHTJVwWaqZ07uom78nvDJhgRCsgWR7UBRq8v9MymbPC4p7 +IkGuq+lLkYPyJFMGpk33fAua4NkKxBseyLHbB9t3vSlUFh8x2JlIYxC531362qbS ++L2m0B2stMyQEdYxpYCtS3nsEG+ib2Du3GiT+5pAmXxZ6DGyr4jAlAWDnljwJZEf +xJPECXSlcAsHdI6ugkC+9DwPjLs1mrEQ/BevmTT2o0wPigCRNi9xZf178L1sptYy +DwIDAQAB -----END PUBLIC KEY----- diff --git a/app/main/api/internal/config/config.go b/app/main/api/internal/config/config.go index f4a5276..97aff3d 100644 --- a/app/main/api/internal/config/config.go +++ b/app/main/api/internal/config/config.go @@ -7,23 +7,23 @@ import ( type Config struct { rest.RestConf - DataSource string - CacheRedis cache.CacheConf - JwtAuth JwtAuth // JWT 鉴权相关配置 - VerifyCode VerifyCode - Encrypt Encrypt - Alipay AlipayConfig - Wxpay WxpayConfig - Applepay ApplepayConfig - Tianyuanapi TianyuanapiConfig - SystemConfig SystemConfig - WechatH5 WechatH5Config - Authorization AuthorizationConfig // 授权书配置 - WechatMini WechatMiniConfig - Query QueryConfig - AdminConfig AdminConfig - AdminPromotion AdminPromotion - TaxConfig TaxConfig + DataSource string + CacheRedis cache.CacheConf + JwtAuth JwtAuth // JWT 鉴权相关配置 + VerifyCode VerifyCode + Encrypt Encrypt + Alipay AlipayConfig + Wxpay WxpayConfig + Applepay ApplepayConfig + Tianyuanapi TianyuanapiConfig + SystemConfig SystemConfig + WechatH5 WechatH5Config + Authorization AuthorizationConfig // 授权书配置 + WechatMini WechatMiniConfig + Query QueryConfig + AdminConfig AdminConfig + TaxConfig TaxConfig + Promotion PromotionConfig // 推广链接配置 } // JwtAuth 用于 JWT 鉴权配置 @@ -83,8 +83,8 @@ type WechatH5Config struct { AppSecret string } type WechatMiniConfig struct { - AppID string - AppSecret string + AppID string + AppSecret string } type QueryConfig struct { ShareLinkExpire int64 @@ -95,9 +95,6 @@ type AdminConfig struct { RefreshAfter int64 } -type AdminPromotion struct { - URLDomain string -} type TaxConfig struct { TaxRate float64 TaxExemptionAmount float64 @@ -111,4 +108,10 @@ type TianyuanapiConfig struct { type AuthorizationConfig struct { FileBaseURL string // 授权书文件访问基础URL -} \ No newline at end of file +} + +// PromotionConfig 推广链接配置 +type PromotionConfig struct { + PromotionDomain string // 推广域名(用于生成短链) + OfficialDomain string // 正式站点域名(短链重定向的目标域名) +} diff --git a/app/main/api/internal/handler/admin_agent/adminauditagenthandler.go b/app/main/api/internal/handler/admin_agent/adminauditagenthandler.go index 11081e2..7a5ad92 100644 --- a/app/main/api/internal/handler/admin_agent/adminauditagenthandler.go +++ b/app/main/api/internal/handler/admin_agent/adminauditagenthandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminAuditAgentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminAuditAgentReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminAuditAgentLogic(r.Context(), svcCtx) resp, err := l.AdminAuditAgent(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/adminauditrealnamehandler.go b/app/main/api/internal/handler/admin_agent/adminauditrealnamehandler.go index 9eeda9a..1b91591 100644 --- a/app/main/api/internal/handler/admin_agent/adminauditrealnamehandler.go +++ b/app/main/api/internal/handler/admin_agent/adminauditrealnamehandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminAuditRealNameHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminAuditRealNameReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminAuditRealNameLogic(r.Context(), svcCtx) resp, err := l.AdminAuditRealName(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/adminauditwithdrawalhandler.go b/app/main/api/internal/handler/admin_agent/adminauditwithdrawalhandler.go index d63c574..a49f8fb 100644 --- a/app/main/api/internal/handler/admin_agent/adminauditwithdrawalhandler.go +++ b/app/main/api/internal/handler/admin_agent/adminauditwithdrawalhandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminAuditWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminAuditWithdrawalReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminAuditWithdrawalLogic(r.Context(), svcCtx) resp, err := l.AdminAuditWithdrawal(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/admingetagentconfighandler.go b/app/main/api/internal/handler/admin_agent/admingetagentconfighandler.go index 7b7e159..e8302c1 100644 --- a/app/main/api/internal/handler/admin_agent/admingetagentconfighandler.go +++ b/app/main/api/internal/handler/admin_agent/admingetagentconfighandler.go @@ -3,19 +3,15 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" + "ycc-server/common/result" ) func AdminGetAgentConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := admin_agent.NewAdminGetAgentConfigLogic(r.Context(), svcCtx) resp, err := l.AdminGetAgentConfig() - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/admingetagentorderlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentorderlisthandler.go index 633da35..423a653 100644 --- a/app/main/api/internal/handler/admin_agent/admingetagentorderlisthandler.go +++ b/app/main/api/internal/handler/admin_agent/admingetagentorderlisthandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminGetAgentOrderListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminGetAgentOrderListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminGetAgentOrderListLogic(r.Context(), svcCtx) resp, err := l.AdminGetAgentOrderList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/admingetagentproductconfiglisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentproductconfiglisthandler.go index 4a52fd1..803bc6b 100644 --- a/app/main/api/internal/handler/admin_agent/admingetagentproductconfiglisthandler.go +++ b/app/main/api/internal/handler/admin_agent/admingetagentproductconfiglisthandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminGetAgentProductConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminGetAgentProductConfigListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminGetAgentProductConfigListLogic(r.Context(), svcCtx) resp, err := l.AdminGetAgentProductConfigList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/admingetagentrealnamelisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentrealnamelisthandler.go index 899a3c3..5410626 100644 --- a/app/main/api/internal/handler/admin_agent/admingetagentrealnamelisthandler.go +++ b/app/main/api/internal/handler/admin_agent/admingetagentrealnamelisthandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminGetAgentRealNameListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminGetAgentRealNameListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminGetAgentRealNameListLogic(r.Context(), svcCtx) resp, err := l.AdminGetAgentRealNameList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/admingetagentrebatelisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentrebatelisthandler.go index ef7c1c2..51bfa8d 100644 --- a/app/main/api/internal/handler/admin_agent/admingetagentrebatelisthandler.go +++ b/app/main/api/internal/handler/admin_agent/admingetagentrebatelisthandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminGetAgentRebateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminGetAgentRebateListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminGetAgentRebateListLogic(r.Context(), svcCtx) resp, err := l.AdminGetAgentRebateList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/admingetagentupgradelisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentupgradelisthandler.go index f136cfa..eaa223d 100644 --- a/app/main/api/internal/handler/admin_agent/admingetagentupgradelisthandler.go +++ b/app/main/api/internal/handler/admin_agent/admingetagentupgradelisthandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminGetAgentUpgradeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminGetAgentUpgradeListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminGetAgentUpgradeListLogic(r.Context(), svcCtx) resp, err := l.AdminGetAgentUpgradeList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/adminupdateagentconfighandler.go b/app/main/api/internal/handler/admin_agent/adminupdateagentconfighandler.go index b609199..a6a0849 100644 --- a/app/main/api/internal/handler/admin_agent/adminupdateagentconfighandler.go +++ b/app/main/api/internal/handler/admin_agent/adminupdateagentconfighandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminUpdateAgentConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminUpdateAgentConfigReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminUpdateAgentConfigLogic(r.Context(), svcCtx) resp, err := l.AdminUpdateAgentConfig(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_agent/adminupdateagentproductconfighandler.go b/app/main/api/internal/handler/admin_agent/adminupdateagentproductconfighandler.go index fb23daf..f5cb952 100644 --- a/app/main/api/internal/handler/admin_agent/adminupdateagentproductconfighandler.go +++ b/app/main/api/internal/handler/admin_agent/adminupdateagentproductconfighandler.go @@ -3,26 +3,28 @@ package admin_agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/admin_agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func AdminUpdateAgentProductConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.AdminUpdateAgentProductConfigReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := admin_agent.NewAdminUpdateAgentProductConfigLogic(r.Context(), svcCtx) resp, err := l.AdminUpdateAgentProductConfig(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go b/app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go deleted file mode 100644 index f8772a6..0000000 --- a/app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go +++ /dev/null @@ -1,30 +0,0 @@ -package admin_promotion - -import ( - "net/http" - - "ycc-server/app/main/api/internal/logic/admin_promotion" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/result" - "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" -) - -func DeletePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.DeletePromotionLinkReq - 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_promotion.NewDeletePromotionLinkLogic(r.Context(), svcCtx) - err := l.DeletePromotionLink(&req) - result.HttpResult(r, w, nil, err) - } -} diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go b/app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go deleted file mode 100644 index 3d440e1..0000000 --- a/app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go +++ /dev/null @@ -1,30 +0,0 @@ -package admin_promotion - -import ( - "net/http" - - "ycc-server/app/main/api/internal/logic/admin_promotion" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/result" - "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" -) - -func GetPromotionLinkDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.GetPromotionLinkDetailReq - 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_promotion.NewGetPromotionLinkDetailLogic(r.Context(), svcCtx) - resp, err := l.GetPromotionLinkDetail(&req) - result.HttpResult(r, w, resp, err) - } -} diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go b/app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go deleted file mode 100644 index 8061abb..0000000 --- a/app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go +++ /dev/null @@ -1,30 +0,0 @@ -package admin_promotion - -import ( - "net/http" - - "ycc-server/app/main/api/internal/logic/admin_promotion" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/result" - "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" -) - -func GetPromotionStatsHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.GetPromotionStatsHistoryReq - 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_promotion.NewGetPromotionStatsHistoryLogic(r.Context(), svcCtx) - resp, err := l.GetPromotionStatsHistory(&req) - result.HttpResult(r, w, resp, err) - } -} diff --git a/app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go b/app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go deleted file mode 100644 index 1f6dbb7..0000000 --- a/app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go +++ /dev/null @@ -1,30 +0,0 @@ -package admin_promotion - -import ( - "net/http" - - "ycc-server/app/main/api/internal/logic/admin_promotion" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/result" - "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" -) - -func UpdatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.UpdatePromotionLinkReq - 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_promotion.NewUpdatePromotionLinkLogic(r.Context(), svcCtx) - err := l.UpdatePromotionLink(&req) - result.HttpResult(r, w, nil, err) - } -} diff --git a/app/main/api/internal/handler/agent/applyupgradehandler.go b/app/main/api/internal/handler/agent/applyupgradehandler.go index 3e8af0b..439677b 100644 --- a/app/main/api/internal/handler/agent/applyupgradehandler.go +++ b/app/main/api/internal/handler/agent/applyupgradehandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func ApplyUpgradeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.ApplyUpgradeReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewApplyUpgradeLogic(r.Context(), svcCtx) resp, err := l.ApplyUpgrade(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/applywithdrawalhandler.go b/app/main/api/internal/handler/agent/applywithdrawalhandler.go index c1e436f..8c3fd3e 100644 --- a/app/main/api/internal/handler/agent/applywithdrawalhandler.go +++ b/app/main/api/internal/handler/agent/applywithdrawalhandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func ApplyWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.ApplyWithdrawalReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewApplyWithdrawalLogic(r.Context(), svcCtx) resp, err := l.ApplyWithdrawal(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go b/app/main/api/internal/handler/agent/deleteinvitecodehandler.go similarity index 63% rename from app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go rename to app/main/api/internal/handler/agent/deleteinvitecodehandler.go index 4ab699d..1fdf144 100644 --- a/app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go +++ b/app/main/api/internal/handler/agent/deleteinvitecodehandler.go @@ -1,20 +1,19 @@ -package admin_promotion +package agent import ( "net/http" - "ycc-server/app/main/api/internal/logic/admin_promotion" + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/common/result" "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" ) -func RecordLinkClickHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { +func DeleteInviteCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.RecordLinkClickReq + var req types.DeleteInviteCodeReq if err := httpx.Parse(r, &req); err != nil { result.ParamErrorResult(r, w, err) return @@ -23,8 +22,8 @@ func RecordLinkClickHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { result.ParamValidateErrorResult(r, w, err) return } - l := admin_promotion.NewRecordLinkClickLogic(r.Context(), svcCtx) - resp, err := l.RecordLinkClick(&req) + l := agent.NewDeleteInviteCodeLogic(r.Context(), svcCtx) + resp, err := l.DeleteInviteCode(&req) result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getcommissionlisthandler.go b/app/main/api/internal/handler/agent/getcommissionlisthandler.go index d18ae5b..e8a012c 100644 --- a/app/main/api/internal/handler/agent/getcommissionlisthandler.go +++ b/app/main/api/internal/handler/agent/getcommissionlisthandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetCommissionListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.GetCommissionListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewGetCommissionListLogic(r.Context(), svcCtx) resp, err := l.GetCommissionList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getconversionratehandler.go b/app/main/api/internal/handler/agent/getconversionratehandler.go new file mode 100644 index 0000000..6769f07 --- /dev/null +++ b/app/main/api/internal/handler/agent/getconversionratehandler.go @@ -0,0 +1,17 @@ +package agent + +import ( + "net/http" + + "ycc-server/app/main/api/internal/logic/agent" + "ycc-server/app/main/api/internal/svc" + "ycc-server/common/result" +) + +func GetConversionRateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := agent.NewGetConversionRateLogic(r.Context(), svcCtx) + resp, err := l.GetConversionRate() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getinvitelinkhandler.go b/app/main/api/internal/handler/agent/getinvitelinkhandler.go index 7d5d4ad..2e0fc84 100644 --- a/app/main/api/internal/handler/agent/getinvitelinkhandler.go +++ b/app/main/api/internal/handler/agent/getinvitelinkhandler.go @@ -5,13 +5,26 @@ import ( "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetInviteLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + var req types.GetInviteLinkReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } l := agent.NewGetInviteLinkLogic(r.Context(), svcCtx) - resp, err := l.GetInviteLink() + resp, err := l.GetInviteLink(&req) result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getlevelprivilegehandler.go b/app/main/api/internal/handler/agent/getlevelprivilegehandler.go new file mode 100644 index 0000000..4192cd2 --- /dev/null +++ b/app/main/api/internal/handler/agent/getlevelprivilegehandler.go @@ -0,0 +1,18 @@ +package agent + +import ( + "net/http" + + "ycc-server/app/main/api/internal/logic/agent" + "ycc-server/app/main/api/internal/svc" + "ycc-server/common/result" +) + +func GetLevelPrivilegeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := agent.NewGetLevelPrivilegeLogic(r.Context(), svcCtx) + resp, err := l.GetLevelPrivilege() + result.HttpResult(r, w, resp, err) + } +} + diff --git a/app/main/api/internal/handler/agent/getrebatelisthandler.go b/app/main/api/internal/handler/agent/getrebatelisthandler.go index 83b8b1e..7251eb0 100644 --- a/app/main/api/internal/handler/agent/getrebatelisthandler.go +++ b/app/main/api/internal/handler/agent/getrebatelisthandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetRebateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.GetRebateListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewGetRebateListLogic(r.Context(), svcCtx) resp, err := l.GetRebateList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getrevenueinfohandler.go b/app/main/api/internal/handler/agent/getrevenueinfohandler.go index 026e4d4..cbabb67 100644 --- a/app/main/api/internal/handler/agent/getrevenueinfohandler.go +++ b/app/main/api/internal/handler/agent/getrevenueinfohandler.go @@ -3,19 +3,15 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" + "ycc-server/common/result" ) func GetRevenueInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := agent.NewGetRevenueInfoLogic(r.Context(), svcCtx) resp, err := l.GetRevenueInfo() - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go b/app/main/api/internal/handler/agent/getsubordinatecontributiondetailhandler.go similarity index 61% rename from app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go rename to app/main/api/internal/handler/agent/getsubordinatecontributiondetailhandler.go index 16c7400..a4a30c6 100644 --- a/app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go +++ b/app/main/api/internal/handler/agent/getsubordinatecontributiondetailhandler.go @@ -1,20 +1,19 @@ -package admin_promotion +package agent import ( "net/http" - "ycc-server/app/main/api/internal/logic/admin_promotion" + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/common/result" "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" ) -func GetPromotionStatsTotalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { +func GetSubordinateContributionDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.GetPromotionStatsTotalReq + var req types.GetSubordinateContributionDetailReq if err := httpx.Parse(r, &req); err != nil { result.ParamErrorResult(r, w, err) return @@ -23,8 +22,8 @@ func GetPromotionStatsTotalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc result.ParamValidateErrorResult(r, w, err) return } - l := admin_promotion.NewGetPromotionStatsTotalLogic(r.Context(), svcCtx) - resp, err := l.GetPromotionStatsTotal(&req) + l := agent.NewGetSubordinateContributionDetailLogic(r.Context(), svcCtx) + resp, err := l.GetSubordinateContributionDetail(&req) result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getsubordinatelisthandler.go b/app/main/api/internal/handler/agent/getsubordinatelisthandler.go index a1502d4..1e6b891 100644 --- a/app/main/api/internal/handler/agent/getsubordinatelisthandler.go +++ b/app/main/api/internal/handler/agent/getsubordinatelisthandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetSubordinateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.GetSubordinateListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewGetSubordinateListLogic(r.Context(), svcCtx) resp, err := l.GetSubordinateList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go b/app/main/api/internal/handler/agent/getteamlisthandler.go similarity index 62% rename from app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go rename to app/main/api/internal/handler/agent/getteamlisthandler.go index cf4eb3e..b468627 100644 --- a/app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go +++ b/app/main/api/internal/handler/agent/getteamlisthandler.go @@ -1,20 +1,19 @@ -package admin_promotion +package agent import ( "net/http" - "ycc-server/app/main/api/internal/logic/admin_promotion" + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/common/result" "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" ) -func CreatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { +func GetTeamListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.CreatePromotionLinkReq + var req types.GetTeamListReq if err := httpx.Parse(r, &req); err != nil { result.ParamErrorResult(r, w, err) return @@ -23,8 +22,8 @@ func CreatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { result.ParamValidateErrorResult(r, w, err) return } - l := admin_promotion.NewCreatePromotionLinkLogic(r.Context(), svcCtx) - resp, err := l.CreatePromotionLink(&req) + l := agent.NewGetTeamListLogic(r.Context(), svcCtx) + resp, err := l.GetTeamList(&req) result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getteamstatisticshandler.go b/app/main/api/internal/handler/agent/getteamstatisticshandler.go index 519b556..fbdd41c 100644 --- a/app/main/api/internal/handler/agent/getteamstatisticshandler.go +++ b/app/main/api/internal/handler/agent/getteamstatisticshandler.go @@ -3,19 +3,15 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" + "ycc-server/common/result" ) func GetTeamStatisticsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := agent.NewGetTeamStatisticsLogic(r.Context(), svcCtx) resp, err := l.GetTeamStatistics() - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getupgradelisthandler.go b/app/main/api/internal/handler/agent/getupgradelisthandler.go index d364adb..a3a0dec 100644 --- a/app/main/api/internal/handler/agent/getupgradelisthandler.go +++ b/app/main/api/internal/handler/agent/getupgradelisthandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetUpgradeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.GetUpgradeListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewGetUpgradeListLogic(r.Context(), svcCtx) resp, err := l.GetUpgradeList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go b/app/main/api/internal/handler/agent/getupgraderebatelisthandler.go similarity index 63% rename from app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go rename to app/main/api/internal/handler/agent/getupgraderebatelisthandler.go index 7ae08e2..983f5b6 100644 --- a/app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go +++ b/app/main/api/internal/handler/agent/getupgraderebatelisthandler.go @@ -1,20 +1,19 @@ -package admin_promotion +package agent import ( "net/http" - "ycc-server/app/main/api/internal/logic/admin_promotion" + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/common/result" "ycc-server/pkg/lzkit/validator" - - "github.com/zeromicro/go-zero/rest/httpx" ) -func GetPromotionLinkListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { +func GetUpgradeRebateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.GetPromotionLinkListReq + var req types.GetUpgradeRebateListReq if err := httpx.Parse(r, &req); err != nil { result.ParamErrorResult(r, w, err) return @@ -23,8 +22,8 @@ func GetPromotionLinkListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { result.ParamValidateErrorResult(r, w, err) return } - l := admin_promotion.NewGetPromotionLinkListLogic(r.Context(), svcCtx) - resp, err := l.GetPromotionLinkList(&req) + l := agent.NewGetUpgradeRebateListLogic(r.Context(), svcCtx) + resp, err := l.GetUpgradeRebateList(&req) result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/getwithdrawallisthandler.go b/app/main/api/internal/handler/agent/getwithdrawallisthandler.go index a2bbb59..259ca54 100644 --- a/app/main/api/internal/handler/agent/getwithdrawallisthandler.go +++ b/app/main/api/internal/handler/agent/getwithdrawallisthandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func GetWithdrawalListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.GetWithdrawalListReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewGetWithdrawalListLogic(r.Context(), svcCtx) resp, err := l.GetWithdrawalList(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/promotionredirecthandler.go b/app/main/api/internal/handler/agent/promotionredirecthandler.go new file mode 100644 index 0000000..694cf5e --- /dev/null +++ b/app/main/api/internal/handler/agent/promotionredirecthandler.go @@ -0,0 +1,21 @@ +package agent + +import ( + "net/http" + + "ycc-server/app/main/api/internal/logic/agent" + "ycc-server/app/main/api/internal/svc" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func PromotionRedirectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := agent.NewPromotionRedirectLogic(r.Context(), svcCtx) + err := l.PromotionRedirect(r, w) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } + } +} + diff --git a/app/main/api/internal/handler/agent/realnameauthhandler.go b/app/main/api/internal/handler/agent/realnameauthhandler.go index 919f001..9144ccf 100644 --- a/app/main/api/internal/handler/agent/realnameauthhandler.go +++ b/app/main/api/internal/handler/agent/realnameauthhandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func RealNameAuthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.RealNameAuthReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewRealNameAuthLogic(r.Context(), svcCtx) resp, err := l.RealNameAuth(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + result.HttpResult(r, w, resp, err) } } diff --git a/app/main/api/internal/handler/agent/shortlinkredirecthandler.go b/app/main/api/internal/handler/agent/shortlinkredirecthandler.go new file mode 100644 index 0000000..8fc46fa --- /dev/null +++ b/app/main/api/internal/handler/agent/shortlinkredirecthandler.go @@ -0,0 +1,29 @@ +package agent + +import ( + "net/http" + + "ycc-server/app/main/api/internal/logic/agent" + "ycc-server/app/main/api/internal/svc" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ShortLinkRedirectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 从URL.Path解析shortCode + // 路径格式:/s/{shortCode} + path := r.URL.Path + var shortCode string + if len(path) >= 3 && path[:3] == "/s/" { + shortCode = path[3:] + } + + l := agent.NewShortLinkRedirectLogic(r.Context(), svcCtx) + err := l.ShortLinkRedirect(shortCode, r, w) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } + } +} + diff --git a/app/main/api/internal/handler/agent/upgradesubordinatehandler.go b/app/main/api/internal/handler/agent/upgradesubordinatehandler.go index d39ec0a..7b23ea3 100644 --- a/app/main/api/internal/handler/agent/upgradesubordinatehandler.go +++ b/app/main/api/internal/handler/agent/upgradesubordinatehandler.go @@ -3,26 +3,28 @@ package agent import ( "net/http" - "github.com/zeromicro/go-zero/rest/httpx" "ycc-server/app/main/api/internal/logic/agent" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" ) func UpgradeSubordinateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.UpgradeSubordinateReq if err := httpx.Parse(r, &req); err != nil { - httpx.ErrorCtx(r.Context(), w, err) + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) return } - l := agent.NewUpgradeSubordinateLogic(r.Context(), svcCtx) resp, err := l.UpgradeSubordinate(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + 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 d4ee84e..bb978ee 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -13,7 +13,6 @@ import ( admin_order "ycc-server/app/main/api/internal/handler/admin_order" admin_platform_user "ycc-server/app/main/api/internal/handler/admin_platform_user" admin_product "ycc-server/app/main/api/internal/handler/admin_product" - admin_promotion "ycc-server/app/main/api/internal/handler/admin_promotion" admin_query "ycc-server/app/main/api/internal/handler/admin_query" admin_role "ycc-server/app/main/api/internal/handler/admin_role" admin_role_api "ycc-server/app/main/api/internal/handler/admin_role_api" @@ -425,81 +424,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithPrefix("/api/v1/admin/product"), ) - server.AddRoutes( - rest.WithMiddlewares( - []rest.Middleware{serverCtx.AdminAuthInterceptor}, - []rest.Route{ - { - // 创建推广链接 - Method: http.MethodPost, - Path: "/create", - Handler: admin_promotion.CreatePromotionLinkHandler(serverCtx), - }, - { - // 删除推广链接 - Method: http.MethodDelete, - Path: "/delete/:id", - Handler: admin_promotion.DeletePromotionLinkHandler(serverCtx), - }, - { - // 获取推广链接详情 - Method: http.MethodGet, - Path: "/detail/:id", - Handler: admin_promotion.GetPromotionLinkDetailHandler(serverCtx), - }, - { - // 获取推广链接列表 - Method: http.MethodGet, - Path: "/list", - Handler: admin_promotion.GetPromotionLinkListHandler(serverCtx), - }, - { - // 更新推广链接 - Method: http.MethodPut, - Path: "/update/:id", - Handler: admin_promotion.UpdatePromotionLinkHandler(serverCtx), - }, - }..., - ), - rest.WithPrefix("/api/v1/admin/promotion/link"), - ) - - server.AddRoutes( - rest.WithMiddlewares( - []rest.Middleware{serverCtx.AdminAuthInterceptor}, - []rest.Route{ - { - // 记录链接点击 - Method: http.MethodGet, - Path: "/record/:path", - Handler: admin_promotion.RecordLinkClickHandler(serverCtx), - }, - }..., - ), - rest.WithPrefix("/api/v1/admin/promotion/link"), - ) - - server.AddRoutes( - rest.WithMiddlewares( - []rest.Middleware{serverCtx.AdminAuthInterceptor}, - []rest.Route{ - { - // 获取推广历史记录 - Method: http.MethodGet, - Path: "/history", - Handler: admin_promotion.GetPromotionStatsHistoryHandler(serverCtx), - }, - { - // 获取推广总统计 - Method: http.MethodGet, - Path: "/total", - Handler: admin_promotion.GetPromotionStatsTotalHandler(serverCtx), - }, - }..., - ), - rest.WithPrefix("/api/v1/admin/promotion/stats"), - ) - server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.AdminAuthInterceptor}, @@ -693,6 +617,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/commission/list", Handler: agent.GetCommissionListHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/conversion/rate", + Handler: agent.GetConversionRateHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/generating_link", @@ -703,6 +632,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/info", Handler: agent.GetAgentInfoHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/invite_code/delete", + Handler: agent.DeleteInviteCodeHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/invite_code/generate", @@ -718,6 +652,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/invite_link", Handler: agent.GetInviteLinkHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/level/privilege", + Handler: agent.GetLevelPrivilegeHandler(serverCtx), + }, { Method: http.MethodGet, Path: "/product_config", @@ -733,16 +672,31 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/rebate/list", Handler: agent.GetRebateListHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/rebate/upgrade/list", + Handler: agent.GetUpgradeRebateListHandler(serverCtx), + }, { Method: http.MethodGet, Path: "/revenue", Handler: agent.GetRevenueInfoHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/subordinate/contribution/detail", + Handler: agent.GetSubordinateContributionDetailHandler(serverCtx), + }, { Method: http.MethodGet, Path: "/subordinate/list", Handler: agent.GetSubordinateListHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/team/list", + Handler: agent.GetTeamListHandler(serverCtx), + }, { Method: http.MethodGet, Path: "/team/statistics", @@ -779,6 +733,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithPrefix("/api/v1/agent"), ) + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/s/:shortCode", + Handler: agent.ShortLinkRedirectHandler(serverCtx), + }, + }, + ) + server.AddRoutes( []rest.Route{ { diff --git a/app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go b/app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go index 64062b7..423e7a7 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go @@ -34,6 +34,14 @@ func (l *AdminGetAgentConfigLogic) AdminGetAgentConfig() (resp *types.AdminGetAg value, _ := strconv.ParseFloat(config.ConfigValue, 64) return value } + getConfigInt := func(key string) int64 { + config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key) + if err != nil { + return 0 + } + value, _ := strconv.ParseInt(config.ConfigValue, 10, 64) + return value + } // 获取等级加成配置 level1Bonus := getConfigFloat("level_1_bonus") @@ -48,11 +56,20 @@ func (l *AdminGetAgentConfigLogic) AdminGetAgentConfig() (resp *types.AdminGetAg upgradeToGoldRebate := getConfigFloat("upgrade_to_gold_rebate") upgradeToDiamondRebate := getConfigFloat("upgrade_to_diamond_rebate") + // 获取直接上级返佣配置 + directParentAmountDiamond := getConfigFloat("direct_parent_amount_diamond") + directParentAmountGold := getConfigFloat("direct_parent_amount_gold") + directParentAmountNormal := getConfigFloat("direct_parent_amount_normal") + + // 获取黄金代理最大返佣金额 + maxGoldRebateAmount := getConfigFloat("max_gold_rebate_amount") + + // 获取佣金冻结配置 + commissionFreezeRatio := getConfigFloat("commission_freeze_ratio") + commissionFreezeThreshold := getConfigFloat("commission_freeze_threshold") + commissionFreezeDays := getConfigInt("commission_freeze_days") + return &types.AdminGetAgentConfigResp{ - BasePrice: getConfigFloat("base_price"), - SystemMaxPrice: getConfigFloat("system_max_price"), - PriceThreshold: getConfigFloat("price_threshold"), - PriceFeeRate: getConfigFloat("price_fee_rate"), LevelBonus: types.LevelBonusConfig{ Normal: int64(level1Bonus), Gold: int64(level2Bonus), @@ -67,6 +84,17 @@ func (l *AdminGetAgentConfigLogic) AdminGetAgentConfig() (resp *types.AdminGetAg NormalToGoldRebate: upgradeToGoldRebate, ToDiamondRebate: upgradeToDiamondRebate, }, + DirectParentRebate: types.DirectParentRebateConfig{ + Diamond: directParentAmountDiamond, + Gold: directParentAmountGold, + Normal: directParentAmountNormal, + }, + MaxGoldRebateAmount: maxGoldRebateAmount, + CommissionFreeze: types.CommissionFreezeConfig{ + Ratio: commissionFreezeRatio, + Threshold: commissionFreezeThreshold, + Days: commissionFreezeDays, + }, TaxRate: getConfigFloat("tax_rate"), TaxExemptionAmount: getConfigFloat("tax_exemption_amount"), }, nil diff --git a/app/main/api/internal/logic/admin_agent/admingetagentproductconfiglistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentproductconfiglistlogic.go index 3fcc3a8..dc07c27 100644 --- a/app/main/api/internal/logic/admin_agent/admingetagentproductconfiglistlogic.go +++ b/app/main/api/internal/logic/admin_agent/admingetagentproductconfiglistlogic.go @@ -31,10 +31,16 @@ func (l *AdminGetAgentProductConfigListLogic) AdminGetAgentProductConfigList(req builder := l.svcCtx.AgentProductConfigModel.SelectBuilder(). Where("del_state = ?", globalkey.DelStateNo) + // 如果提供了产品ID,直接过滤 if req.ProductId != nil { builder = builder.Where("product_id = ?", *req.ProductId) } + // 如果提供了产品名称,通过关联查询 product 表过滤 + if req.ProductName != nil && *req.ProductName != "" { + builder = builder.Where("product_id IN (SELECT id FROM product WHERE product_name LIKE ? AND del_state = ?)", "%"+*req.ProductName+"%", globalkey.DelStateNo) + } + // 分页查询 page := req.Page if page <= 0 { @@ -50,9 +56,19 @@ func (l *AdminGetAgentProductConfigListLogic) AdminGetAgentProductConfigList(req return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置列表失败, %v", err) } - // 组装响应 + // 组装响应(通过 product_id 查询产品名称) items := make([]types.AgentProductConfigItem, 0, len(configs)) for _, config := range configs { + // 通过 product_id 查询产品信息获取产品名称 + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, config.ProductId) + productName := "" + if err == nil { + productName = product.ProductName + } else { + // 如果产品不存在,记录日志但不影响主流程 + l.Infof("查询产品信息失败, productId: %d, err: %v", config.ProductId, err) + } + priceThreshold := 0.0 if config.PriceThreshold.Valid { priceThreshold = config.PriceThreshold.Float64 @@ -66,7 +82,7 @@ func (l *AdminGetAgentProductConfigListLogic) AdminGetAgentProductConfigList(req items = append(items, types.AgentProductConfigItem{ Id: config.Id, ProductId: config.ProductId, - ProductName: config.ProductName, + ProductName: productName, BasePrice: config.BasePrice, PriceRangeMin: config.BasePrice, // 最低定价等于基础底价 PriceRangeMax: config.SystemMaxPrice, diff --git a/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go b/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go index 8f470f9..3b43326 100644 --- a/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go +++ b/app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go @@ -41,19 +41,76 @@ func (l *AdminUpdateAgentConfigLogic) AdminUpdateAgentConfig(req *types.AdminUpd return l.svcCtx.AgentConfigModel.UpdateWithVersion(l.ctx, nil, config) } - // 更新各个配置项 - if err := updateConfig("base_price", req.BasePrice); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新基础底价失败, %v", err) + // 更新等级加成配置 + if req.LevelBonus != nil { + if err := updateConfig("level_1_bonus", func() *float64 { v := float64(req.LevelBonus.Normal); return &v }()); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通代理等级加成失败, %v", err) + } + if err := updateConfig("level_2_bonus", func() *float64 { v := float64(req.LevelBonus.Gold); return &v }()); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新黄金代理等级加成失败, %v", err) + } + if err := updateConfig("level_3_bonus", func() *float64 { v := float64(req.LevelBonus.Diamond); return &v }()); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新钻石代理等级加成失败, %v", err) + } } - if err := updateConfig("system_max_price", req.SystemMaxPrice); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新系统价格上限失败, %v", err) + + // 更新升级费用配置 + if req.UpgradeFee != nil { + if err := updateConfig("upgrade_to_gold_fee", &req.UpgradeFee.NormalToGold); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通→黄金升级费用失败, %v", err) + } + if err := updateConfig("upgrade_to_diamond_fee", &req.UpgradeFee.NormalToDiamond); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通→钻石升级费用失败, %v", err) + } + // gold_to_diamond 是计算得出的,不需要更新 } - if err := updateConfig("price_threshold", req.PriceThreshold); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新提价标准阈值失败, %v", err) + + // 更新升级返佣配置 + if req.UpgradeRebate != nil { + if err := updateConfig("upgrade_to_gold_rebate", &req.UpgradeRebate.NormalToGoldRebate); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通→黄金返佣失败, %v", err) + } + if err := updateConfig("upgrade_to_diamond_rebate", &req.UpgradeRebate.ToDiamondRebate); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新升级为钻石返佣失败, %v", err) + } } - if err := updateConfig("price_fee_rate", req.PriceFeeRate); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新提价手续费比例失败, %v", err) + + // 更新直接上级返佣配置 + if req.DirectParentRebate != nil { + if err := updateConfig("direct_parent_amount_diamond", &req.DirectParentRebate.Diamond); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新直接上级是钻石的返佣金额失败, %v", err) + } + if err := updateConfig("direct_parent_amount_gold", &req.DirectParentRebate.Gold); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新直接上级是黄金的返佣金额失败, %v", err) + } + if err := updateConfig("direct_parent_amount_normal", &req.DirectParentRebate.Normal); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新直接上级是普通的返佣金额失败, %v", err) + } } + + // 更新黄金代理最大返佣金额 + if err := updateConfig("max_gold_rebate_amount", req.MaxGoldRebateAmount); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新黄金代理最大返佣金额失败, %v", err) + } + + // 更新佣金冻结配置 + if req.CommissionFreeze != nil { + if err := updateConfig("commission_freeze_ratio", &req.CommissionFreeze.Ratio); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结比例失败, %v", err) + } + 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) + } + } + } + + // 更新税费配置 if err := updateConfig("tax_rate", req.TaxRate); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新税率失败, %v", err) } diff --git a/app/main/api/internal/logic/admin_agent/adminupdateagentproductconfiglogic.go b/app/main/api/internal/logic/admin_agent/adminupdateagentproductconfiglogic.go index a9001d9..368d977 100644 --- a/app/main/api/internal/logic/admin_agent/adminupdateagentproductconfiglogic.go +++ b/app/main/api/internal/logic/admin_agent/adminupdateagentproductconfiglogic.go @@ -37,8 +37,20 @@ func (l *AdminUpdateAgentProductConfigLogic) AdminUpdateAgentProductConfig(req * // 更新配置字段 config.BasePrice = req.BasePrice config.SystemMaxPrice = req.PriceRangeMax - config.PriceThreshold = sql.NullFloat64{Float64: req.PriceThreshold, Valid: true} - config.PriceFeeRate = sql.NullFloat64{Float64: req.PriceFeeRate, Valid: true} + + // 价格阈值(可选) + if req.PriceThreshold != nil { + config.PriceThreshold = sql.NullFloat64{Float64: *req.PriceThreshold, Valid: true} + } else { + config.PriceThreshold = sql.NullFloat64{Valid: false} + } + + // 提价手续费比例(可选) + if req.PriceFeeRate != nil { + config.PriceFeeRate = sql.NullFloat64{Float64: *req.PriceFeeRate, Valid: true} + } else { + config.PriceFeeRate = sql.NullFloat64{Valid: false} + } // 更新配置 if err := l.svcCtx.AgentProductConfigModel.UpdateWithVersion(l.ctx, nil, config); err != nil { diff --git a/app/main/api/internal/logic/admin_auth/adminloginlogic.go b/app/main/api/internal/logic/admin_auth/adminloginlogic.go index e47e83a..997996d 100644 --- a/app/main/api/internal/logic/admin_auth/adminloginlogic.go +++ b/app/main/api/internal/logic/admin_auth/adminloginlogic.go @@ -2,6 +2,7 @@ package admin_auth import ( "context" + "os" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" @@ -30,9 +31,11 @@ func NewAdminLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminL } func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.AdminLoginResp, err error) { - // 1. 验证验证码 - if !req.Captcha { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码错误"), "用户登录, 验证码错误, 验证码: %v", req.Captcha) + // 1. 验证验证码(开发环境下跳过验证码校验) + if os.Getenv("ENV") != "development" { + if !req.Captcha { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码错误"), "用户登录, 验证码错误, 验证码: %v", req.Captcha) + } } // 2. 验证用户名和密码 diff --git a/app/main/api/internal/logic/admin_order/admincreateorderlogic.go b/app/main/api/internal/logic/admin_order/admincreateorderlogic.go index 90d7d44..3a07f4b 100644 --- a/app/main/api/internal/logic/admin_order/admincreateorderlogic.go +++ b/app/main/api/internal/logic/admin_order/admincreateorderlogic.go @@ -72,20 +72,6 @@ func (l *AdminCreateOrderLogic) AdminCreateOrder(req *types.AdminCreateOrderReq) return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 获取订单ID失败 err: %v", err) } - // 如果是推广订单,创建推广订单记录 - if req.IsPromotion == 1 { - promotionOrder := &model.AdminPromotionOrder{ - OrderId: orderId, - Version: 1, - CreateTime: time.Now(), - UpdateTime: time.Now(), - } - _, err = l.svcCtx.AdminPromotionOrderModel.Insert(ctx, session, promotionOrder) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 创建推广订单失败 err: %v", err) - } - } - return nil }) diff --git a/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go b/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go index 8cfb44f..14b4657 100644 --- a/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go +++ b/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go @@ -41,15 +41,6 @@ func (l *AdminDeleteOrderLogic) AdminDeleteOrder(req *types.AdminDeleteOrderReq) return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 删除订单失败 err: %v", err) } - // 删除关联的推广订单记录 - promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(ctx, order.Id) - if err == nil && promotionOrder != nil { - err = l.svcCtx.AdminPromotionOrderModel.DeleteSoft(ctx, session, promotionOrder) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 删除推广订单失败 err: %v", err) - } - } - return nil }) diff --git a/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go b/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go index 3d6e6ac..7a787a8 100644 --- a/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go +++ b/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go @@ -40,13 +40,6 @@ func (l *AdminGetOrderDetailLogic) AdminGetOrderDetail(req *types.AdminGetOrderD return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderDetail, 查询产品失败 err: %v", err) } - // 判断是否为推广订单 - var isPromotion int64 - promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(l.ctx, order.Id) - if err == nil && promotionOrder != nil { - isPromotion = 1 - } - // 判断是否为代理订单并获取代理处理状态 var isAgentOrder bool var agentProcessStatus string @@ -118,7 +111,6 @@ func (l *AdminGetOrderDetailLogic) AdminGetOrderDetail(req *types.AdminGetOrderD Status: order.Status, CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"), UpdateTime: order.UpdateTime.Format("2006-01-02 15:04:05"), - IsPromotion: isPromotion, QueryState: queryState, IsAgentOrder: isAgentOrder, AgentProcessStatus: agentProcessStatus, diff --git a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go index e2edb91..37506ce 100644 --- a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go +++ b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go @@ -54,9 +54,6 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR if req.Status != "" { builder = builder.Where("status = ?", req.Status) } - if req.IsPromotion != -1 { - builder = builder.Where("id IN (SELECT order_id FROM admin_promotion_order WHERE del_state = 0)") - } // 时间范围查询 if req.CreateTimeStart != "" { builder = builder.Where("create_time >= ?", req.CreateTimeStart) @@ -274,11 +271,6 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR if order.RefundTime.Valid { item.RefundTime = order.RefundTime.Time.Format("2006-01-02 15:04:05") } - // 判断是否为推广订单 - promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(l.ctx, order.Id) - if err == nil && promotionOrder != nil { - item.IsPromotion = 1 - } // 设置代理订单相关字段 if agentOrderMap[order.Id] { diff --git a/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go b/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go index ae35c93..44fecba 100644 --- a/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go +++ b/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go @@ -7,7 +7,6 @@ import ( "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" "ycc-server/common/xerr" "github.com/pkg/errors" @@ -76,30 +75,6 @@ func (l *AdminUpdateOrderLogic) AdminUpdateOrder(req *types.AdminUpdateOrderReq) return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 更新订单失败 err: %v", err) } - // 处理推广订单状态 - if req.IsPromotion != nil { - promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(ctx, order.Id) - if err == nil && promotionOrder != nil { - // 如果存在推广订单记录但不需要推广,则删除 - if *req.IsPromotion == 0 { - err = l.svcCtx.AdminPromotionOrderModel.DeleteSoft(ctx, session, promotionOrder) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 删除推广订单失败 err: %v", err) - } - } - } else if *req.IsPromotion == 1 { - // 如果需要推广但不存在记录,则创建 - newPromotionOrder := &model.AdminPromotionOrder{ - OrderId: order.Id, - Version: 1, - } - _, err = l.svcCtx.AdminPromotionOrderModel.Insert(ctx, session, newPromotionOrder) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 创建推广订单失败 err: %v", err) - } - } - } - return nil }) diff --git a/app/main/api/internal/logic/admin_product/admincreateproductlogic.go b/app/main/api/internal/logic/admin_product/admincreateproductlogic.go index 45613b8..eb35031 100644 --- a/app/main/api/internal/logic/admin_product/admincreateproductlogic.go +++ b/app/main/api/internal/logic/admin_product/admincreateproductlogic.go @@ -2,14 +2,15 @@ package admin_product import ( "context" + "database/sql" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/app/main/model" "ycc-server/common/xerr" - "database/sql" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type AdminCreateProductLogic struct { @@ -37,14 +38,44 @@ func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProdu SellPrice: req.SellPrice, } - // 2. 数据库操作 - result, err := l.svcCtx.ProductModel.Insert(l.ctx, nil, data) + // 2. 数据库操作(使用事务确保产品表和代理产品配置表同步) + var productId int64 + err = l.svcCtx.ProductModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 2.1 插入产品 + result, err := l.svcCtx.ProductModel.Insert(ctx, session, data) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建产品失败, err: %v, req: %+v", err, req) + } + + productId, err = result.LastInsertId() + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "获取产品ID失败, err: %v", err) + } + + // 2.2 同步创建代理产品配置(使用默认值) + agentProductConfig := &model.AgentProductConfig{ + ProductId: productId, + BasePrice: 0.00, // 默认基础底价 + SystemMaxPrice: 9999.99, // 默认系统价格上限 + PriceThreshold: sql.NullFloat64{Valid: false}, // 默认不设阈值 + PriceFeeRate: sql.NullFloat64{Valid: false}, // 默认不收费 + } + + _, err = l.svcCtx.AgentProductConfigModel.Insert(ctx, session, agentProductConfig) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "创建代理产品配置失败, err: %v, productId: %d", err, productId) + } + + return nil + }) + + if err != nil { + return nil, err } // 3. 返回结果 - id, _ := result.LastInsertId() - return &types.AdminCreateProductResp{Id: id}, nil + return &types.AdminCreateProductResp{Id: productId}, nil } diff --git a/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go b/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go index af673ec..be3cda3 100644 --- a/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go +++ b/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type AdminDeleteProductLogic struct { @@ -32,11 +33,33 @@ func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProdu "查找产品失败, err: %v, id: %d", err, req.Id) } - // 2. 执行软删除 - err = l.svcCtx.ProductModel.DeleteSoft(l.ctx, nil, record) + // 2. 执行软删除(使用事务确保产品表和代理产品配置表同步) + err = l.svcCtx.ProductModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 2.1 软删除产品 + 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) + } + + // 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 { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "同步删除代理产品配置失败, err: %v, productId: %d", err, req.Id) + } + } + + return nil + }) + if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), - "删除产品失败, err: %v, id: %d", err, req.Id) + return nil, err } // 3. 返回结果 diff --git a/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go b/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go index a4ac404..e12280f 100644 --- a/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go +++ b/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type AdminUpdateProductLogic struct { @@ -53,11 +54,20 @@ func (l *AdminUpdateProductLogic) AdminUpdateProduct(req *types.AdminUpdateProdu record.SellPrice = *req.SellPrice } - // 3. 执行更新操作 - _, err = l.svcCtx.ProductModel.Update(l.ctx, nil, record) + // 3. 执行更新操作(使用事务确保产品表和代理产品配置表同步) + err = l.svcCtx.ProductModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 3.1 更新产品 + _, err = l.svcCtx.ProductModel.Update(ctx, session, record) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新产品失败, err: %v, req: %+v", err, req) + } + + return nil + }) + + if err != nil { + return nil, err } // 4. 返回结果 diff --git a/app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go b/app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go deleted file mode 100644 index f294b71..0000000 --- a/app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go +++ /dev/null @@ -1,136 +0,0 @@ -package admin_promotion - -import ( - "context" - "crypto/rand" - "fmt" - "math/big" - "time" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -type CreatePromotionLinkLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreatePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePromotionLinkLogic { - return &CreatePromotionLinkLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -// 生成6位随机字符串(大小写字母和数字) -func generateRandomString() (string, error) { - const ( - chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - length = 6 - ) - - result := make([]byte, length) - for i := 0; i < length; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) - if err != nil { - return "", err - } - result[i] = chars[num.Int64()] - } - return string(result), nil -} - -func (l *CreatePromotionLinkLogic) CreatePromotionLink(req *types.CreatePromotionLinkReq) (resp *types.CreatePromotionLinkResp, err error) { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接, 获取用户信息失败, %+v", getUidErr) - } - - // 生成唯一URL - var url string - maxRetries := 5 // 最大重试次数 - for i := 0; i < maxRetries; i++ { - // 生成6位随机字符串 - randomStr, err := generateRandomString() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接, 生成随机字符串失败, %+v", err) - } - - // 检查URL是否已存在 - existLink, err := l.svcCtx.AdminPromotionLinkModel.FindOneByUrl(l.ctx, randomStr) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建推广链接, 检查URL是否存在失败, %+v", err) - } - - if existLink != nil { - continue // URL已存在,继续尝试 - } - - // URL可用 - url = randomStr - break - } - - if url == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接失败, 多次尝试生成唯一URL均失败") - } - url = fmt.Sprintf("%s/%s", l.svcCtx.Config.AdminPromotion.URLDomain, url) - // 创建推广链接 - link := &model.AdminPromotionLink{ - Name: req.Name, - Url: url, - AdminUserId: adminUserId, - } - - var linkId int64 - err = l.svcCtx.AdminPromotionLinkModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { - result, err := l.svcCtx.AdminPromotionLinkModel.Insert(l.ctx, session, link) - if err != nil { - return fmt.Errorf("创建推广链接失败, %+v", err) - } - - linkId, err = result.LastInsertId() - if err != nil { - return fmt.Errorf("获取推广链接ID失败, %+v", err) - } - - // 创建总统计记录 - totalStats := &model.AdminPromotionLinkStatsTotal{ - LinkId: linkId, - } - _, err = l.svcCtx.AdminPromotionLinkStatsTotalModel.Insert(l.ctx, session, totalStats) - if err != nil { - return fmt.Errorf("创建推广链接总统计记录失败, %+v", err) - } - - // 创建统计历史记录 - historyStats := &model.AdminPromotionLinkStatsHistory{ - LinkId: linkId, - StatsDate: time.Now().Truncate(24 * time.Hour), - } - _, err = l.svcCtx.AdminPromotionLinkStatsHistoryModel.Insert(l.ctx, session, historyStats) - if err != nil { - return fmt.Errorf("创建推广链接统计历史记录失败, %+v", err) - } - return nil - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建推广链接失败, %+v", err) - } - - return &types.CreatePromotionLinkResp{ - Id: linkId, - Url: url, - }, nil -} diff --git a/app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go b/app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go deleted file mode 100644 index 00fb96d..0000000 --- a/app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go +++ /dev/null @@ -1,91 +0,0 @@ -package admin_promotion - -import ( - "context" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -type DeletePromotionLinkLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewDeletePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePromotionLinkLogic { - return &DeletePromotionLinkLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeletePromotionLinkLogic) DeletePromotionLink(req *types.DeletePromotionLinkReq) error { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "删除推广链接, 获取用户信息失败, %+v", getUidErr) - } - - // 获取链接信息 - link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id) - if err != nil { - return errors.Wrapf(err, "删除推广链接, 获取链接信息失败, %+v", err) - } - - // 验证用户权限 - if link.AdminUserId != adminUserId { - return errors.Wrapf(xerr.NewErrMsg("无权限删除此链接"), "删除推广链接, 无权限删除此链接, %+v", link) - } - - // 在事务中执行所有删除操作 - err = l.svcCtx.AdminPromotionLinkModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { - // 软删除链接 - err = l.svcCtx.AdminPromotionLinkModel.DeleteSoft(l.ctx, session, link) - if err != nil { - return errors.Wrapf(err, "删除推广链接, 软删除链接失败, %+v", err) - } - - // 软删除总统计记录 - totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, link.Id) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "删除推广链接, 获取总统计记录失败, %+v", err) - } - if totalStats != nil { - err = l.svcCtx.AdminPromotionLinkStatsTotalModel.DeleteSoft(l.ctx, session, totalStats) - if err != nil { - return errors.Wrapf(err, "删除推广链接, 软删除总统计记录失败, %+v", err) - } - } - - // 软删除历史统计记录 - builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder() - builder = builder.Where("link_id = ?", link.Id) - historyStats, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "") - if err != nil { - return errors.Wrapf(err, "删除推广链接, 获取历史统计记录失败, %+v", err) - } - for _, stat := range historyStats { - err = l.svcCtx.AdminPromotionLinkStatsHistoryModel.DeleteSoft(l.ctx, session, stat) - if err != nil { - return errors.Wrapf(err, "删除推广链接, 软删除历史统计记录失败, %+v", err) - } - } - - return nil - }) - - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除推广链接失败, %+v", err) - } - - return nil -} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go deleted file mode 100644 index 03c4418..0000000 --- a/app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go +++ /dev/null @@ -1,65 +0,0 @@ -package admin_promotion - -import ( - "context" - "fmt" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" -) - -type GetPromotionLinkDetailLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetPromotionLinkDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionLinkDetailLogic { - return &GetPromotionLinkDetailLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetPromotionLinkDetailLogic) GetPromotionLinkDetail(req *types.GetPromotionLinkDetailReq) (resp *types.GetPromotionLinkDetailResp, err error) { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) - } - - // 获取链接信息 - link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id) - if err != nil { - return nil, errors.Wrapf(err, "获取链接信息失败, %+v", err) - } - - // 验证用户权限 - if link.AdminUserId != adminUserId { - return nil, errors.Wrapf(xerr.NewErrMsg("无权限访问此链接"), "获取链接信息失败, 无权限访问此链接, %+v", link) - } - - // 获取总统计 - totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOne(l.ctx, link.Id) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return nil, errors.Wrapf(err, "获取总统计失败, %+v", err) - } - return &types.GetPromotionLinkDetailResp{ - Name: link.Name, - Url: link.Url, - ClickCount: totalStats.ClickCount, - PayCount: totalStats.PayCount, - PayAmount: fmt.Sprintf("%.2f", totalStats.PayAmount), - CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"), - UpdateTime: link.UpdateTime.Format("2006-01-02 15:04:05"), - LastClickTime: totalStats.LastClickTime.Time.Format("2006-01-02 15:04:05"), - LastPayTime: totalStats.LastPayTime.Time.Format("2006-01-02 15:04:05"), - }, nil -} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go deleted file mode 100644 index 205edcc..0000000 --- a/app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go +++ /dev/null @@ -1,104 +0,0 @@ -package admin_promotion - -import ( - "context" - "fmt" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" - "github.com/zeromicro/go-zero/core/mr" -) - -type GetPromotionLinkListLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetPromotionLinkListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionLinkListLogic { - return &GetPromotionLinkListLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetPromotionLinkListLogic) GetPromotionLinkList(req *types.GetPromotionLinkListReq) (resp *types.GetPromotionLinkListResp, err error) { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) - } - - // 构建查询条件 - builder := l.svcCtx.AdminPromotionLinkModel.SelectBuilder() - builder = builder.Where("admin_user_id = ?", adminUserId) - if req.Name != "" { - builder = builder.Where("name LIKE ?", "%"+req.Name+"%") - } - if req.Url != "" { - builder = builder.Where("url LIKE ?", "%"+req.Url+"%") - } - - // 获取列表和总数 - links, total, err := l.svcCtx.AdminPromotionLinkModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") - if err != nil { - return nil, errors.Wrapf(err, "获取推广链接列表失败, %+v", err) - } - - // 使用MapReduce并发获取统计数据 - items := make([]types.PromotionLinkItem, len(links)) - err = mr.MapReduceVoid(func(source chan<- interface{}) { - for _, link := range links { - source <- link - } - }, func(item interface{}, writer mr.Writer[types.PromotionLinkItem], cancel func(error)) { - link := item.(*model.AdminPromotionLink) - // 获取总统计 - totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, link.Id) - if err != nil && !errors.Is(err, model.ErrNotFound) { - cancel(errors.Wrapf(err, "获取总统计失败, linkId: %d, %+v", link.Id, err)) - return - } - writer.Write(types.PromotionLinkItem{ - Id: link.Id, - Name: link.Name, - Url: link.Url, - ClickCount: totalStats.ClickCount, - PayCount: totalStats.PayCount, - PayAmount: fmt.Sprintf("%.2f", totalStats.PayAmount), - CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"), - LastClickTime: func() string { - if totalStats.LastClickTime.Valid { - return totalStats.LastClickTime.Time.Format("2006-01-02 15:04:05") - } - return "" - }(), - LastPayTime: func() string { - if totalStats.LastPayTime.Valid { - return totalStats.LastPayTime.Time.Format("2006-01-02 15:04:05") - } - return "" - }(), - }) - }, func(pipe <-chan types.PromotionLinkItem, cancel func(error)) { - for i := 0; i < len(links); i++ { - item := <-pipe - items[i] = item - } - }) - if err != nil { - return nil, errors.Wrapf(err, "获取推广链接统计数据失败, %+v", err) - } - - return &types.GetPromotionLinkListResp{ - Total: total, - Items: items, - }, nil -} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go deleted file mode 100644 index 431e9bf..0000000 --- a/app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go +++ /dev/null @@ -1,83 +0,0 @@ -package admin_promotion - -import ( - "context" - "time" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" -) - -type GetPromotionStatsHistoryLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetPromotionStatsHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionStatsHistoryLogic { - return &GetPromotionStatsHistoryLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetPromotionStatsHistoryLogic) GetPromotionStatsHistory(req *types.GetPromotionStatsHistoryReq) (resp []types.PromotionStatsHistoryItem, err error) { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) - } - // 构建查询条件 - builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder() - - // 如果有日期范围,添加日期过滤 - if req.StartDate != "" && req.EndDate != "" { - startDate, err := time.Parse("2006-01-02", req.StartDate) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "开始日期格式错误") - } - endDate, err := time.Parse("2006-01-02", req.EndDate) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "结束日期格式错误") - } - // 将结束日期设置为当天的最后一刻 - endDate = endDate.Add(24*time.Hour - time.Second) - builder = builder.Where("stats_date BETWEEN ? AND ?", startDate, endDate) - } - - // 获取历史统计数据 - historyStats, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "stats_date DESC") - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取历史统计数据失败") - } - - // 转换为响应格式 - resp = make([]types.PromotionStatsHistoryItem, 0, len(historyStats)) - for _, stat := range historyStats { - // 验证链接是否属于当前用户 - link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, stat.LinkId) - if err != nil { - continue // 如果链接不存在,跳过该记录 - } - if link.AdminUserId != adminUserId { - continue // 如果链接不属于当前用户,跳过该记录 - } - - resp = append(resp, types.PromotionStatsHistoryItem{ - Id: stat.Id, - LinkId: stat.LinkId, - PayAmount: stat.PayAmount, - ClickCount: stat.ClickCount, - PayCount: stat.PayCount, - StatsDate: stat.StatsDate.Format("2006-01-02"), - }) - } - - return resp, nil -} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go deleted file mode 100644 index 8ca17ba..0000000 --- a/app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go +++ /dev/null @@ -1,166 +0,0 @@ -package admin_promotion - -import ( - "context" - "time" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" - "github.com/zeromicro/go-zero/core/mr" -) - -type GetPromotionStatsTotalLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetPromotionStatsTotalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionStatsTotalLogic { - return &GetPromotionStatsTotalLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetPromotionStatsTotalLogic) GetPromotionStatsTotal(req *types.GetPromotionStatsTotalReq) (resp *types.GetPromotionStatsTotalResp, err error) { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) - } - - // 获取用户的所有推广链接 - linkBuilder := l.svcCtx.AdminPromotionLinkModel.SelectBuilder() - linkBuilder = linkBuilder.Where("admin_user_id = ?", adminUserId) - links, err := l.svcCtx.AdminPromotionLinkModel.FindAll(l.ctx, linkBuilder, "") - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广链接列表失败, %+v", err) - } - - // 如果没有推广链接,返回空统计 - if len(links) == 0 { - return &types.GetPromotionStatsTotalResp{}, nil - } - - // 构建链接ID列表 - linkIds := make([]int64, len(links)) - for i, link := range links { - linkIds[i] = link.Id - } - - // 获取并计算总统计数据 - var totalClickCount, totalPayCount int64 - var totalPayAmount float64 - err = mr.MapReduceVoid(func(source chan<- interface{}) { - for _, linkId := range linkIds { - source <- linkId - } - }, func(item interface{}, writer mr.Writer[struct { - ClickCount int64 - PayCount int64 - PayAmount float64 - }], cancel func(error)) { - linkId := item.(int64) - stats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, linkId) - if err != nil && !errors.Is(err, model.ErrNotFound) { - cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取总统计数据失败, linkId: %d, %+v", linkId, err)) - return - } - if stats != nil { - writer.Write(struct { - ClickCount int64 - PayCount int64 - PayAmount float64 - }{ - ClickCount: stats.ClickCount, - PayCount: stats.PayCount, - PayAmount: stats.PayAmount, - }) - } - }, func(pipe <-chan struct { - ClickCount int64 - PayCount int64 - PayAmount float64 - }, cancel func(error)) { - for stats := range pipe { - totalClickCount += stats.ClickCount - totalPayCount += stats.PayCount - totalPayAmount += stats.PayAmount - } - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取总统计数据失败, %+v", err) - } - - // 获取今日统计数据 - now := time.Now() - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) - var todayClickCount, todayPayCount int64 - var todayPayAmount float64 - - err = mr.MapReduceVoid(func(source chan<- interface{}) { - for _, linkId := range linkIds { - source <- linkId - } - }, func(item interface{}, writer mr.Writer[struct { - ClickCount int64 - PayCount int64 - PayAmount float64 - }], cancel func(error)) { - linkId := item.(int64) - builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder() - builder = builder.Where("link_id = ? AND DATE(stats_date) = DATE(?)", linkId, today) - histories, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "") - if err != nil { - cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取今日统计数据失败, linkId: %d, %+v", linkId, err)) - return - } - - var clickCount, payCount int64 - var payAmount float64 - for _, history := range histories { - clickCount += history.ClickCount - payCount += history.PayCount - payAmount += history.PayAmount - } - - writer.Write(struct { - ClickCount int64 - PayCount int64 - PayAmount float64 - }{ - ClickCount: clickCount, - PayCount: payCount, - PayAmount: payAmount, - }) - }, func(pipe <-chan struct { - ClickCount int64 - PayCount int64 - PayAmount float64 - }, cancel func(error)) { - for stats := range pipe { - todayClickCount += stats.ClickCount - todayPayCount += stats.PayCount - todayPayAmount += stats.PayAmount - } - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取今日统计数据失败, %+v", err) - } - - return &types.GetPromotionStatsTotalResp{ - TodayClickCount: int64(todayClickCount), - TodayPayCount: int64(todayPayCount), - TodayPayAmount: todayPayAmount, - TotalClickCount: int64(totalClickCount), - TotalPayCount: int64(totalPayCount), - TotalPayAmount: totalPayAmount, - }, nil -} diff --git a/app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go b/app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go deleted file mode 100644 index 703bcc8..0000000 --- a/app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go +++ /dev/null @@ -1,57 +0,0 @@ -package admin_promotion - -import ( - "context" - "fmt" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" -) - -type RecordLinkClickLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewRecordLinkClickLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RecordLinkClickLogic { - return &RecordLinkClickLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RecordLinkClickLogic) RecordLinkClick(req *types.RecordLinkClickReq) (resp *types.RecordLinkClickResp, err error) { - // 校验路径格式 - if len(req.Path) != 6 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的推广链接路径") - } - - // 检查是否只包含大小写字母和数字 - for _, char := range req.Path { - if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的推广链接路径") - } - } - url := fmt.Sprintf("%s/%s", l.svcCtx.Config.AdminPromotion.URLDomain, req.Path) - - link, err := l.svcCtx.AdminPromotionLinkModel.FindOneByUrl(l.ctx, url) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "无效的推广链接路径") - } - - // 使用 statsService 更新点击统计 - err = l.svcCtx.AdminPromotionLinkStatsService.UpdateLinkStats(l.ctx, link.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新点击统计失败: %+v", err) - } - - return &types.RecordLinkClickResp{ - Success: true, - }, nil -} diff --git a/app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go b/app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go deleted file mode 100644 index 1472fd2..0000000 --- a/app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go +++ /dev/null @@ -1,57 +0,0 @@ -package admin_promotion - -import ( - "context" - "time" - - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/common/ctxdata" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" -) - -type UpdatePromotionLinkLogic struct { - logx.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewUpdatePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePromotionLinkLogic { - return &UpdatePromotionLinkLogic{ - Logger: logx.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdatePromotionLinkLogic) UpdatePromotionLink(req *types.UpdatePromotionLinkReq) error { - // 获取当前用户ID - adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) - if getUidErr != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "更新推广链接, 获取用户信息失败, %+v", getUidErr) - } - - // 获取链接信息 - link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id) - if err != nil { - return errors.Wrapf(err, "更新推广链接, 获取链接信息失败, %+v", err) - } - - // 验证用户权限 - if link.AdminUserId != adminUserId { - return errors.Wrapf(xerr.NewErrMsg("无权限修改此链接"), "更新推广链接, 无权限修改此链接, %+v", link) - } - - // 更新链接信息 - link.Name = *req.Name - link.UpdateTime = time.Now() - - _, err = l.svcCtx.AdminPromotionLinkModel.Update(l.ctx, nil, link) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新推广链接, 更新链接信息失败, %+v", err) - } - return nil -} diff --git a/app/main/api/internal/logic/agent/applyforagentlogic.go b/app/main/api/internal/logic/agent/applyforagentlogic.go index da1ca4e..62423aa 100644 --- a/app/main/api/internal/logic/agent/applyforagentlogic.go +++ b/app/main/api/internal/logic/agent/applyforagentlogic.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "os" "time" "ycc-server/app/main/model" "ycc-server/common/ctxdata" @@ -51,8 +52,8 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type return nil, errors.Wrapf(xerr.NewErrMsg("必须提供邀请码才能成为代理,请联系平台或代理获取邀请码"), "") } - // 2. 校验验证码 - if req.Mobile != "18889793585" { + // 2. 校验验证码(开发环境下跳过验证码校验) + if os.Getenv("ENV") != "development" { redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) if err != nil { @@ -138,9 +139,9 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type // 4.5 创建代理记录 newAgent := &model.Agent{ - UserId: userID, - Level: targetLevel, - Mobile: encryptedMobile, + UserId: userID, + Level: targetLevel, + Mobile: encryptedMobile, } if req.Region != "" { newAgent.Region = sql.NullString{String: req.Region, Valid: true} @@ -179,7 +180,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type // 建立关系 relation := &model.AgentRelation{ ParentId: parentAgent.Id, - ChildId: agentId, + ChildId: agentId, RelationType: 1, // 直接关系 } if _, err := l.svcCtx.AgentRelationModel.Insert(transCtx, session, relation); err != nil { @@ -197,7 +198,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type // 设置自己为团队首领 newAgent.TeamLeaderId = sql.NullInt64{Int64: agentId, Valid: true} - if err := l.svcCtx.AgentModel.UpdateWithVersion(transCtx, session, newAgent); err != nil { + if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil { return errors.Wrapf(err, "更新团队首领失败") } } else { @@ -232,6 +233,19 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type return errors.Wrapf(err, "更新邀请码状态失败") } + // 4.9 记录邀请码使用历史(用于统计和查询) + usage := &model.AgentInviteCodeUsage{ + InviteCodeId: inviteCodeModel.Id, + Code: inviteCodeModel.Code, + UserId: userID, + AgentId: newAgent.Id, + AgentLevel: targetLevel, + UsedTime: time.Now(), + } + if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil { + return errors.Wrapf(err, "记录邀请码使用历史失败") + } + return nil }) diff --git a/app/main/api/internal/logic/agent/applyupgradelogic.go b/app/main/api/internal/logic/agent/applyupgradelogic.go index 2d67dc6..685cbbb 100644 --- a/app/main/api/internal/logic/agent/applyupgradelogic.go +++ b/app/main/api/internal/logic/agent/applyupgradelogic.go @@ -6,7 +6,6 @@ import ( "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" - "ycc-server/pkg/lzkit/lzUtils" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/sqlx" @@ -55,8 +54,14 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type } // 3. 计算升级费用和返佣 - upgradeFee := l.svcCtx.AgentService.GetUpgradeFee(fromLevel, toLevel) - rebateAmount := l.svcCtx.AgentService.GetUpgradeRebate(fromLevel, toLevel) + upgradeFee, err := l.svcCtx.AgentService.GetUpgradeFee(l.ctx, fromLevel, toLevel) + if err != nil { + return nil, errors.Wrapf(err, "获取升级费用失败") + } + rebateAmount, err := l.svcCtx.AgentService.GetUpgradeRebate(l.ctx, fromLevel, toLevel) + if err != nil { + return nil, errors.Wrapf(err, "获取升级返佣金额失败") + } // 4. 查找原直接上级(用于返佣) var rebateAgentId int64 @@ -68,18 +73,18 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type rebateAgentId = parent.Id } - // 5. 使用事务处理升级 + // 5. 创建升级记录(待支付状态) var upgradeId int64 err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { - // 5.1 创建升级记录 + // 5.1 创建升级记录(状态为待支付) upgradeRecord := &model.AgentUpgrade{ - AgentId: agent.Id, - FromLevel: fromLevel, - ToLevel: toLevel, - UpgradeType: 1, // 自主付费 - UpgradeFee: upgradeFee, - RebateAmount: rebateAmount, - Status: 1, // 待处理 + AgentId: agent.Id, + FromLevel: fromLevel, + ToLevel: toLevel, + UpgradeType: 1, // 自主付费 + UpgradeFee: upgradeFee, + RebateAmount: rebateAmount, + Status: 1, // 待支付(1=待支付,2=已支付,3=已完成,4=已取消) } if rebateAgentId > 0 { upgradeRecord.RebateAgentId = sql.NullInt64{Int64: rebateAgentId, Valid: true} @@ -91,23 +96,7 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type } upgradeId, _ = upgradeResult.LastInsertId() - // 5.2 处理支付(这里假设支付已在外层处理,只记录订单号) - // 实际支付应该在创建升级记录之前完成 - // 注意:支付订单号需要从支付回调中获取,这里暂时留空 - - // 5.3 执行升级操作 - if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, agent.Id, toLevel, 1, upgradeFee, rebateAmount, "", 0); err != nil { - return errors.Wrapf(err, "执行升级操作失败") - } - - // 5.4 更新升级记录状态 - upgradeRecord.Id = upgradeId - upgradeRecord.Status = 2 // 已完成 - upgradeRecord.Remark = lzUtils.StringToNullString("升级成功") - if err := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); err != nil { - return errors.Wrapf(err, "更新升级记录失败") - } - + // 注意:升级操作将在支付成功后通过支付回调完成 return nil }) @@ -115,10 +104,10 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type return nil, err } - // 返回响应(订单号需要从支付回调中获取,这里暂时返回空) + // 返回响应(订单号将在支付接口中生成) return &types.ApplyUpgradeResp{ UpgradeId: upgradeId, - OrderNo: "", // 需要从支付回调中获取 + OrderNo: "", // 将在支付接口中生成 }, nil } diff --git a/app/main/api/internal/logic/agent/deleteinvitecodelogic.go b/app/main/api/internal/logic/agent/deleteinvitecodelogic.go new file mode 100644 index 0000000..410deab --- /dev/null +++ b/app/main/api/internal/logic/agent/deleteinvitecodelogic.go @@ -0,0 +1,72 @@ +package agent + +import ( + "context" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/xerr" + + "github.com/pkg/errors" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteInviteCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteInviteCodeLogic { + return &DeleteInviteCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteInviteCodeLogic) DeleteInviteCode(req *types.DeleteInviteCodeReq) (resp *types.DeleteInviteCodeResp, err error) { + // 1. 获取当前代理信息 + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + + // 2. 查询邀请码是否存在且属于当前代理 + inviteCode, err := l.svcCtx.AgentInviteCodeModel.FindOne(l.ctx, req.Id) + 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) + } + + // 3. 验证邀请码是否属于当前代理 + if !inviteCode.AgentId.Valid || inviteCode.AgentId.Int64 != agent.Id { + return nil, errors.Wrapf(xerr.NewErrMsg("无权删除此邀请码"), "") + } + + // 4. 检查邀请码是否已被使用 + if inviteCode.Status == 1 { + return nil, errors.Wrapf(xerr.NewErrMsg("已使用的邀请码无法删除"), "") + } + + // 5. 执行软删除 + err = l.svcCtx.AgentInviteCodeModel.DeleteSoft(l.ctx, nil, inviteCode) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除邀请码失败, %v", err) + } + + return &types.DeleteInviteCodeResp{}, nil +} diff --git a/app/main/api/internal/logic/agent/generatinglinklogic.go b/app/main/api/internal/logic/agent/generatinglinklogic.go index 18402a8..74c1205 100644 --- a/app/main/api/internal/logic/agent/generatinglinklogic.go +++ b/app/main/api/internal/logic/agent/generatinglinklogic.go @@ -2,12 +2,17 @@ package agent import ( "context" + "database/sql" "encoding/hex" "encoding/json" + "fmt" + "net/url" "strconv" + "strings" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/globalkey" + "ycc-server/common/tool" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/crypto" @@ -49,19 +54,25 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) } - // 2. 获取系统配置 - basePrice, err := l.getConfigFloat("base_price") + // 2. 获取产品配置(必须存在) + productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, req.ProductId) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取基础底价配置失败, %v", err) - } - systemMaxPrice, err := l.getConfigFloat("system_max_price") - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取系统价格上限配置失败, %v", err) + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品配置不存在, productId: %d,请先在后台配置产品价格参数", req.ProductId) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, productId: %d, %v", req.ProductId, err) } - // 4. 计算实际底价(基础底价 + 等级加成) - levelBonus := l.getLevelBonus(agentModel.Level) + // 3. 获取等级加成配置 + levelBonus, err := l.getLevelBonus(agentModel.Level) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err) + } + + // 4. 计算实际底价(产品基础底价 + 等级加成) + basePrice := productConfig.BasePrice actualBasePrice := basePrice + float64(levelBonus) + systemMaxPrice := productConfig.SystemMaxPrice // 5. 验证设定价格范围 if req.SetPrice < actualBasePrice || req.SetPrice > systemMaxPrice { @@ -82,9 +93,18 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) } if len(existingLinks) > 0 { - // 已存在,直接返回 + // 已存在,检查是否有短链,如果没有则生成 + targetPath := req.TargetPath + if targetPath == "" { + targetPath = "/agent/promotionInquire/" + } + shortLink, err := l.getOrCreateShortLink(1, existingLinks[0].Id, 0, existingLinks[0].LinkIdentifier, "", targetPath) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取或创建短链失败, %v", err) + } return &types.AgentGeneratingLinkResp{ LinkIdentifier: existingLinks[0].LinkIdentifier, + FullLink: shortLink, }, nil } @@ -120,39 +140,187 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) ActualBasePrice: actualBasePrice, } - _, err = l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink) + result, err := l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存推广链接失败, %v", err) } + // 获取插入的ID + linkId, err := result.LastInsertId() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广链接ID失败, %v", err) + } + + // 使用默认target_path(如果未提供) + targetPath := req.TargetPath + if targetPath == "" { + targetPath = "/agent/promotionInquire/" + } + + // 生成短链(类型:1=推广报告) + shortLink, err := l.createShortLink(1, linkId, 0, encrypted, "", targetPath) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err) + } + return &types.AgentGeneratingLinkResp{ LinkIdentifier: encrypted, + FullLink: shortLink, }, nil } -// getLevelBonus 获取等级加成 -func (l *GeneratingLinkLogic) getLevelBonus(level int64) int64 { +// getLevelBonus 获取等级加成(从配置表读取) +func (l *GeneratingLinkLogic) getLevelBonus(level int64) (int64, error) { + var configKey string switch level { case 1: // 普通 - return 6 + configKey = "level_1_bonus" case 2: // 黄金 - return 3 + configKey = "level_2_bonus" case 3: // 钻石 - return 0 + configKey = "level_3_bonus" default: - return 0 + return 0, nil } -} -// getConfigFloat 获取配置值(浮点数) -func (l *GeneratingLinkLogic) getConfigFloat(configKey string) (float64, error) { config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey) if err != nil { - return 0, err + // 配置不存在时返回默认值 + l.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v,使用默认值", level, configKey, err) + switch level { + case 1: + return 6, nil + case 2: + return 3, nil + case 3: + return 0, nil + } + return 0, nil } + value, err := strconv.ParseFloat(config.ConfigValue, 64) if err != nil { return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue) } - return value, nil + return int64(value), nil +} + +// getOrCreateShortLink 获取或创建短链 +// type: 1=推广报告(promotion), 2=邀请好友(invite) +// linkId: 推广链接ID(仅推广报告使用) +// inviteCodeId: 邀请码ID(仅邀请好友使用) +// linkIdentifier: 推广链接标识(仅推广报告使用) +// inviteCode: 邀请码(仅邀请好友使用) +// targetPath: 目标地址(前端传入) +func (l *GeneratingLinkLogic) getOrCreateShortLink(linkType int64, linkId, inviteCodeId int64, linkIdentifier, inviteCode, targetPath string) (string, error) { + // 先查询是否已存在短链 + var existingShortLink *model.AgentShortLink + var err error + + if linkType == 1 { + // 推广报告类型,使用link_id查询 + if linkId > 0 { + existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByLinkIdTypeDelState(l.ctx, sql.NullInt64{Int64: linkId, Valid: true}, linkType, globalkey.DelStateNo) + } + } else { + // 邀请好友类型,使用invite_code_id查询 + if inviteCodeId > 0 { + existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullInt64{Int64: inviteCodeId, Valid: true}, linkType, globalkey.DelStateNo) + } + } + + if err == nil && existingShortLink != nil { + // 已存在短链,直接返回 + return l.buildShortLinkURL(existingShortLink.ShortCode), nil + } + + if err != nil && !errors.Is(err, model.ErrNotFound) { + return "", errors.Wrapf(err, "查询短链失败") + } + + // 不存在,创建新的短链 + return l.createShortLink(linkType, linkId, inviteCodeId, linkIdentifier, inviteCode, targetPath) +} + +// createShortLink 创建短链 +// type: 1=推广报告(promotion), 2=邀请好友(invite) +func (l *GeneratingLinkLogic) createShortLink(linkType int64, linkId, inviteCodeId int64, linkIdentifier, inviteCode, targetPath string) (string, error) { + promotionConfig := l.svcCtx.Config.Promotion + + // 如果没有配置推广域名,返回空字符串(保持向后兼容) + if promotionConfig.PromotionDomain == "" { + l.Errorf("推广域名未配置,返回空链接") + return "", nil + } + + // 验证target_path + if targetPath == "" { + return "", errors.Wrapf(xerr.NewErrMsg("目标地址不能为空"), "") + } + + // 对于推广报告类型,将 linkIdentifier 拼接到 target_path + if linkType == 1 && linkIdentifier != "" { + // 如果 target_path 以 / 结尾,直接拼接 linkIdentifier + if strings.HasSuffix(targetPath, "/") { + targetPath = targetPath + url.QueryEscape(linkIdentifier) + } else { + // 否则在末尾添加 / 再拼接 + targetPath = targetPath + "/" + url.QueryEscape(linkIdentifier) + } + } + + // 生成短链标识(6位随机字符串,大小写字母+数字) + var shortCode string + maxRetries := 10 // 最大重试次数 + for retry := 0; retry < maxRetries; retry++ { + shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL) + // 检查短链标识是否已存在 + _, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + // 短链标识不存在,可以使用 + break + } + return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err) + } + // 短链标识已存在,继续生成 + if retry == maxRetries-1 { + return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "") + } + } + + // 创建短链记录 + shortLink := &model.AgentShortLink{ + Type: linkType, + ShortCode: shortCode, + TargetPath: targetPath, + PromotionDomain: promotionConfig.PromotionDomain, + } + + // 根据类型设置对应字段 + if linkType == 1 { + // 推广报告类型 + shortLink.LinkId = sql.NullInt64{Int64: linkId, Valid: linkId > 0} + if linkIdentifier != "" { + shortLink.LinkIdentifier = sql.NullString{String: linkIdentifier, Valid: true} + } + } else if linkType == 2 { + // 邀请好友类型 + shortLink.InviteCodeId = sql.NullInt64{Int64: inviteCodeId, Valid: inviteCodeId > 0} + if inviteCode != "" { + shortLink.InviteCode = sql.NullString{String: inviteCode, Valid: true} + } + } + _, err := l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink) + if err != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err) + } + + return l.buildShortLinkURL(shortCode), nil +} + +// buildShortLinkURL 构建短链URL +func (l *GeneratingLinkLogic) buildShortLinkURL(shortCode string) string { + promotionConfig := l.svcCtx.Config.Promotion + return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode) } diff --git a/app/main/api/internal/logic/agent/getagentproductconfiglogic.go b/app/main/api/internal/logic/agent/getagentproductconfiglogic.go index 8359839..ee083d0 100644 --- a/app/main/api/internal/logic/agent/getagentproductconfiglogic.go +++ b/app/main/api/internal/logic/agent/getagentproductconfiglogic.go @@ -44,68 +44,58 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) } - // 2. 获取系统配置 - basePrice, err := l.getConfigFloat("base_price") + // 2. 获取等级加成配置(从系统配置表读取) + levelBonus, err := l.getLevelBonus(agentModel.Level) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取基础底价配置失败, %v", err) - } - systemMaxPrice, err := l.getConfigFloat("system_max_price") - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取系统价格上限配置失败, %v", err) - } - priceThreshold, err := l.getConfigFloat("price_threshold") - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取提价标准阈值配置失败, %v", err) - } - priceFeeRate, err := l.getConfigFloat("price_fee_rate") - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取提价手续费比例配置失败, %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err) } - // 3. 计算等级加成 - levelBonus := l.getLevelBonus(agentModel.Level) - - // 4. 查询所有产品配置 + // 3. 查询所有产品配置 builder := l.svcCtx.AgentProductConfigModel.SelectBuilder() productConfigs, err := l.svcCtx.AgentProductConfigModel.FindAll(l.ctx, builder, "") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, %v", err) } - // 5. 组装响应数据 + // 4. 组装响应数据(通过 product_id 查询产品名称和英文标识) var respList []types.ProductConfigItem for _, productConfig := range productConfigs { - // 使用产品配置中的基础底价,如果没有则使用系统配置的基础底价 - productBasePrice := basePrice - if productConfig.BasePrice > 0 { - productBasePrice = productConfig.BasePrice + // 通过 product_id 查询产品信息获取产品名称和英文标识 + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, productConfig.ProductId) + productName := "" + productEn := "" + if err == nil { + productName = product.ProductName + productEn = product.ProductEn + } else { + // 如果产品不存在,记录日志但不影响主流程 + l.Infof("查询产品信息失败, productId: %d, err: %v", productConfig.ProductId, err) } + // 使用产品配置的基础底价(必须配置) + productBasePrice := productConfig.BasePrice + // 计算该产品的实际底价 productActualBasePrice := productBasePrice + float64(levelBonus) - // 价格范围:实际底价 ≤ 设定价格 ≤ 系统价格上限(或产品配置的最高价格) + // 价格范围:实际底价 ≤ 设定价格 ≤ 产品配置的最高价格 priceRangeMin := productActualBasePrice - priceRangeMax := systemMaxPrice - if productConfig.SystemMaxPrice > 0 && productConfig.SystemMaxPrice < systemMaxPrice { - priceRangeMax = productConfig.SystemMaxPrice - } + priceRangeMax := productConfig.SystemMaxPrice - // 使用产品配置的提价阈值和手续费比例,如果没有则使用系统配置 - productPriceThreshold := priceThreshold - if productConfig.PriceThreshold.Valid && productConfig.PriceThreshold.Float64 > 0 { + // 使用产品配置的提价阈值和手续费比例,如果为NULL则使用0 + productPriceThreshold := 0.0 + if productConfig.PriceThreshold.Valid { productPriceThreshold = productConfig.PriceThreshold.Float64 } - productPriceFeeRate := priceFeeRate - if productConfig.PriceFeeRate.Valid && productConfig.PriceFeeRate.Float64 > 0 { + productPriceFeeRate := 0.0 + if productConfig.PriceFeeRate.Valid { productPriceFeeRate = productConfig.PriceFeeRate.Float64 } respList = append(respList, types.ProductConfigItem{ ProductId: productConfig.ProductId, - ProductName: productConfig.ProductName, - BasePrice: productBasePrice, - LevelBonus: float64(levelBonus), + ProductName: productName, + ProductEn: productEn, ActualBasePrice: productActualBasePrice, PriceRangeMin: priceRangeMin, PriceRangeMax: priceRangeMax, @@ -119,29 +109,38 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP }, nil } -// getLevelBonus 获取等级加成 -func (l *GetAgentProductConfigLogic) getLevelBonus(level int64) int64 { +// getLevelBonus 获取等级加成(从配置表读取) +func (l *GetAgentProductConfigLogic) getLevelBonus(level int64) (int64, error) { + var configKey string switch level { case 1: // 普通 - return 6 + configKey = "level_1_bonus" case 2: // 黄金 - return 3 + configKey = "level_2_bonus" case 3: // 钻石 - return 0 + configKey = "level_3_bonus" default: - return 0 + return 0, nil } -} -// getConfigFloat 获取配置值(浮点数) -func (l *GetAgentProductConfigLogic) getConfigFloat(configKey string) (float64, error) { config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey) if err != nil { - return 0, err + // 配置不存在时返回默认值 + l.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v,使用默认值", level, configKey, err) + switch level { + case 1: + return 6, nil + case 2: + return 3, nil + case 3: + return 0, nil + } + return 0, nil } + value, err := strconv.ParseFloat(config.ConfigValue, 64) if err != nil { return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue) } - return value, nil + return int64(value), nil } diff --git a/app/main/api/internal/logic/agent/getcommissionlistlogic.go b/app/main/api/internal/logic/agent/getcommissionlistlogic.go index 1f27a86..7d831ab 100644 --- a/app/main/api/internal/logic/agent/getcommissionlistlogic.go +++ b/app/main/api/internal/logic/agent/getcommissionlistlogic.go @@ -61,7 +61,7 @@ func (l *GetCommissionListLogic) GetCommissionList(req *types.GetCommissionListR offset := (page - 1) * pageSize // 4. 查询总数 - total, err := l.svcCtx.AgentCommissionModel.FindCount(l.ctx, builder, "") + total, err := l.svcCtx.AgentCommissionModel.FindCount(l.ctx, builder, "id") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询佣金总数失败, %v", err) } @@ -85,9 +85,19 @@ func (l *GetCommissionListLogic) GetCommissionList(req *types.GetCommissionListR } } + // 查询订单号 + orderNo := "" + if commission.OrderId > 0 { + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, commission.OrderId) + if err == nil { + orderNo = order.OrderNo + } + } + list = append(list, types.CommissionItem{ Id: commission.Id, OrderId: commission.OrderId, + OrderNo: orderNo, ProductName: productName, Amount: commission.Amount, Status: commission.Status, diff --git a/app/main/api/internal/logic/agent/getconversionratelogic.go b/app/main/api/internal/logic/agent/getconversionratelogic.go new file mode 100644 index 0000000..9e712fd --- /dev/null +++ b/app/main/api/internal/logic/agent/getconversionratelogic.go @@ -0,0 +1,492 @@ +package agent + +import ( + "context" + "time" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" + "ycc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetConversionRateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetConversionRateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetConversionRateLogic { + return &GetConversionRateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetConversionRateLogic) GetConversionRate() (resp *types.ConversionRateResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + // 1. 获取代理信息 + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + + // 2. 统计我的转化率 + myConversionRate := l.calculateConversionRate(agent.Id, nil) + + // 3. 统计下级转化率(按时间段动态查询历史下级关系) + subordinateConversionRate := l.calculateSubordinateConversionRate(agent.Id) + + return &types.ConversionRateResp{ + MyConversionRate: myConversionRate, + SubordinateConversionRate: subordinateConversionRate, + }, nil +} + +// calculateSubordinateConversionRate 计算下级转化率(考虑历史关系) +func (l *GetConversionRateLogic) calculateSubordinateConversionRate(parentAgentId int64) types.ConversionRateData { + // 使用Asia/Shanghai时区,与数据库保持一致 + loc, _ := time.LoadLocation("Asia/Shanghai") + now := time.Now().In(loc) + + // 日统计:今日、昨日、前日 + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + todayEnd := todayStart.AddDate(0, 0, 1) + yesterdayStart := todayStart.AddDate(0, 0, -1) + yesterdayEnd := todayStart + dayBeforeStart := todayStart.AddDate(0, 0, -2) + dayBeforeEnd := yesterdayStart + + daily := []types.PeriodConversionData{ + l.calculateSubordinatePeriodConversion(parentAgentId, "今日", todayStart, todayEnd), + l.calculateSubordinatePeriodConversion(parentAgentId, "昨日", yesterdayStart, yesterdayEnd), + l.calculateSubordinatePeriodConversion(parentAgentId, "前日", dayBeforeStart, dayBeforeEnd), + } + + // 周统计:本周、上周、上上周 + weekdayOffset := int(now.Weekday()) + if weekdayOffset == 0 { + weekdayOffset = 7 // 周日算作第7天 + } + thisWeekStart := now.AddDate(0, 0, -(weekdayOffset - 1)) + thisWeekStart = time.Date(thisWeekStart.Year(), thisWeekStart.Month(), thisWeekStart.Day(), 0, 0, 0, 0, loc) + thisWeekEnd := now + lastWeekStart := thisWeekStart.AddDate(0, 0, -7) + lastWeekEnd := thisWeekStart + lastTwoWeekStart := thisWeekStart.AddDate(0, 0, -14) + lastTwoWeekEnd := lastWeekStart + + weekly := []types.PeriodConversionData{ + l.calculateSubordinatePeriodConversion(parentAgentId, "本周", thisWeekStart, thisWeekEnd), + l.calculateSubordinatePeriodConversion(parentAgentId, "上周", lastWeekStart, lastWeekEnd), + l.calculateSubordinatePeriodConversion(parentAgentId, "上上周", lastTwoWeekStart, lastTwoWeekEnd), + } + + // 月统计:本月、上月、前两月 + thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc) + thisMonthEnd := now + lastMonthStart := thisMonthStart.AddDate(0, -1, 0) + lastMonthEnd := thisMonthStart + lastTwoMonthStart := thisMonthStart.AddDate(0, -2, 0) + lastTwoMonthEnd := lastMonthStart + + monthly := []types.PeriodConversionData{ + l.calculateSubordinatePeriodConversion(parentAgentId, "本月", thisMonthStart, thisMonthEnd), + l.calculateSubordinatePeriodConversion(parentAgentId, "上月", lastMonthStart, lastMonthEnd), + l.calculateSubordinatePeriodConversion(parentAgentId, "前两月", lastTwoMonthStart, lastTwoMonthEnd), + } + + return types.ConversionRateData{ + Daily: daily, + Weekly: weekly, + Monthly: monthly, + } +} + +// calculateConversionRate 计算转化率 +// agentId > 0 时统计该代理的转化率,否则统计 subordinateIds 列表的转化率 +func (l *GetConversionRateLogic) calculateConversionRate(agentId int64, subordinateIds []int64) types.ConversionRateData { + // 使用Asia/Shanghai时区,与数据库保持一致 + loc, _ := time.LoadLocation("Asia/Shanghai") + now := time.Now().In(loc) + + // 日统计:今日、昨日、前日 + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + todayEnd := todayStart.AddDate(0, 0, 1) + yesterdayStart := todayStart.AddDate(0, 0, -1) + yesterdayEnd := todayStart + dayBeforeStart := todayStart.AddDate(0, 0, -2) + dayBeforeEnd := yesterdayStart + + daily := []types.PeriodConversionData{ + l.calculatePeriodConversion(agentId, subordinateIds, "今日", todayStart, todayEnd), + l.calculatePeriodConversion(agentId, subordinateIds, "昨日", yesterdayStart, yesterdayEnd), + l.calculatePeriodConversion(agentId, subordinateIds, "前日", dayBeforeStart, dayBeforeEnd), + } + + // 周统计:本周、上周、上上周 + // 本周:本周一00:00:00 到现在 + weekdayOffset := int(now.Weekday()) + if weekdayOffset == 0 { + weekdayOffset = 7 // 周日算作第7天 + } + // 计算本周一:当前日期减去(weekdayOffset-1)天 + thisWeekStart := now.AddDate(0, 0, -(weekdayOffset - 1)) + thisWeekStart = time.Date(thisWeekStart.Year(), thisWeekStart.Month(), thisWeekStart.Day(), 0, 0, 0, 0, loc) + thisWeekEnd := now + lastWeekStart := thisWeekStart.AddDate(0, 0, -7) + lastWeekEnd := thisWeekStart + lastTwoWeekStart := thisWeekStart.AddDate(0, 0, -14) + lastTwoWeekEnd := lastWeekStart + + weekly := []types.PeriodConversionData{ + l.calculatePeriodConversion(agentId, subordinateIds, "本周", thisWeekStart, thisWeekEnd), + l.calculatePeriodConversion(agentId, subordinateIds, "上周", lastWeekStart, lastWeekEnd), + l.calculatePeriodConversion(agentId, subordinateIds, "上上周", lastTwoWeekStart, lastTwoWeekEnd), + } + + // 月统计:本月、上月、前两月 + thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc) + thisMonthEnd := now + lastMonthStart := thisMonthStart.AddDate(0, -1, 0) + lastMonthEnd := thisMonthStart + lastTwoMonthStart := thisMonthStart.AddDate(0, -2, 0) + lastTwoMonthEnd := lastMonthStart + + monthly := []types.PeriodConversionData{ + l.calculatePeriodConversion(agentId, subordinateIds, "本月", thisMonthStart, thisMonthEnd), + l.calculatePeriodConversion(agentId, subordinateIds, "上月", lastMonthStart, lastMonthEnd), + l.calculatePeriodConversion(agentId, subordinateIds, "前两月", lastTwoMonthStart, lastTwoMonthEnd), + } + + return types.ConversionRateData{ + Daily: daily, + Weekly: weekly, + Monthly: monthly, + } +} + +// calculatePeriodConversion 计算指定时间段的转化率数据 +func (l *GetConversionRateLogic) calculatePeriodConversion(agentId int64, subordinateIds []int64, periodLabel string, startTime, endTime time.Time) types.PeriodConversionData { + // 构建 agent_order 查询条件 + agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder(). + Where("del_state = ?", globalkey.DelStateNo). + Where("create_time >= ? AND create_time < ?", startTime, endTime) + + if agentId > 0 { + // 统计我的转化率 + agentOrderBuilder = agentOrderBuilder.Where("agent_id = ?", agentId) + } else if len(subordinateIds) > 0 { + // 统计下级转化率 + agentOrderBuilder = agentOrderBuilder.Where(squirrel.Eq{"agent_id": subordinateIds}) + } else { + // 没有数据 + l.Infof("calculatePeriodConversion: 没有代理ID或下级ID,periodLabel=%s", periodLabel) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + // 添加调试日志 + if agentId == 0 && len(subordinateIds) > 0 { + l.Infof("calculatePeriodConversion: 统计下级转化率,periodLabel=%s, startTime=%v, endTime=%v, subordinateIds数量=%d", + periodLabel, startTime, endTime, len(subordinateIds)) + } + + // 查询所有代理订单 + agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, agentOrderBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + l.Errorf("查询代理订单失败: periodLabel=%s, err=%v", periodLabel, err) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + if len(agentOrders) == 0 { + if agentId == 0 && len(subordinateIds) > 0 { + l.Infof("calculatePeriodConversion: 未找到代理订单,periodLabel=%s, startTime=%v, endTime=%v", + periodLabel, startTime, endTime) + } + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + l.Infof("calculatePeriodConversion: 找到代理订单数量=%d, periodLabel=%s", len(agentOrders), periodLabel) + + // 收集订单ID + orderIds := make([]int64, 0, len(agentOrders)) + for _, ao := range agentOrders { + orderIds = append(orderIds, ao.OrderId) + } + + // 查询订单信息 + orderBuilder := l.svcCtx.OrderModel.SelectBuilder(). + Where(squirrel.Eq{"id": orderIds}). + Where("del_state = ?", globalkey.DelStateNo) + + orders, err := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + l.Errorf("查询订单失败: %v", err) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + // 统计查询订单数、付费订单数、用户数和总金额 + var totalAmount float64 + paidOrderCount := 0 + queryUserSet := make(map[int64]bool) + paidUserSet := make(map[int64]bool) + + for _, order := range orders { + // 查询用户数(所有订单的用户,去重) + queryUserSet[order.UserId] = true + + // 付费订单数和总金额(只统计已支付的订单) + if order.Status == "paid" { + paidOrderCount++ + paidUserSet[order.UserId] = true + totalAmount += order.Amount + } + } + + // 查询订单数 = 所有订单数量 + queryOrderCount := len(orders) + + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: int64(queryOrderCount), // 订单数量(保持向后兼容) + PaidUsers: int64(paidOrderCount), // 订单数量(保持向后兼容) + TotalAmount: totalAmount, + QueryUserCount: int64(len(queryUserSet)), // 用户数量(新增) + PaidUserCount: int64(len(paidUserSet)), // 用户数量(新增) + } +} + +// calculateSubordinatePeriodConversion 计算指定时间段内下级转化率 +// 结合使用agent_rebate表和agent_order表: +// 1. 查询量:通过agent_order表统计所有查询(包括未付费的) +// 2. 付费量和金额:通过agent_rebate表统计(只有付费的订单才会产生返佣) +func (l *GetConversionRateLogic) calculateSubordinatePeriodConversion(parentAgentId int64, periodLabel string, startTime, endTime time.Time) types.PeriodConversionData { + // 1. 查询agent_rebate表:获取所有曾经给当前用户产生返佣的source_agent_id(这些代理在某个时间点是下级) + // 不限制时间,获取所有历史返佣记录,用于确定哪些代理曾经是下级 + rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ?", parentAgentId, globalkey.DelStateNo). + Where("source_agent_id != ?", parentAgentId) + + allRebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, rebateBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + l.Errorf("查询返佣记录失败: periodLabel=%s, err=%v", periodLabel, err) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + // 收集所有曾经产生返佣的source_agent_id(这些代理在某个时间点是下级) + sourceAgentIdSet := make(map[int64]bool) + paidOrderIdSet := make(map[int64]bool) // 已付费的订单ID(有返佣的订单) + paidOrderIdToAmount := make(map[int64]float64) // 已付费订单的金额 + + for _, rebate := range allRebates { + sourceAgentIdSet[rebate.SourceAgentId] = true + // 如果返佣记录的创建时间在时间段内,说明该订单在时间段内已付费 + if rebate.CreateTime.After(startTime) || rebate.CreateTime.Equal(startTime) { + if rebate.CreateTime.Before(endTime) { + paidOrderIdSet[rebate.OrderId] = true + } + } + } + + if len(sourceAgentIdSet) == 0 { + l.Infof("calculateSubordinatePeriodConversion: 未找到返佣记录,periodLabel=%s, startTime=%v, endTime=%v", + periodLabel, startTime, endTime) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + sourceAgentIds := make([]int64, 0, len(sourceAgentIdSet)) + for agentId := range sourceAgentIdSet { + sourceAgentIds = append(sourceAgentIds, agentId) + } + + l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 曾经产生返佣的代理数量=%d", periodLabel, len(sourceAgentIds)) + + // 2. 查询agent_order表:统计这些代理在时间段内的所有订单(包括未付费的) + agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder(). + Where("del_state = ?", globalkey.DelStateNo). + Where("create_time >= ? AND create_time < ?", startTime, endTime). + Where(squirrel.Eq{"agent_id": sourceAgentIds}) + + agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, agentOrderBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + l.Errorf("查询代理订单失败: periodLabel=%s, err=%v", periodLabel, err) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 查询到代理订单数量=%d", periodLabel, len(agentOrders)) + + if len(agentOrders) == 0 { + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + // 3. 通过order_id去重,获取所有订单ID(用于查询订单详情) + orderIdSet := make(map[int64]bool) + orderIdToAgentOrder := make(map[int64]*model.AgentOrder) + + for _, ao := range agentOrders { + orderIdSet[ao.OrderId] = true + // 如果同一个订单有多个agent_order记录,保留金额更大的 + if existing, exists := orderIdToAgentOrder[ao.OrderId]; exists { + if ao.OrderAmount > existing.OrderAmount { + orderIdToAgentOrder[ao.OrderId] = ao + } + } else { + orderIdToAgentOrder[ao.OrderId] = ao + } + } + + orderIds := make([]int64, 0, len(orderIdSet)) + for orderId := range orderIdSet { + orderIds = append(orderIds, orderId) + } + + // 4. 查询订单信息 + orderBuilder := l.svcCtx.OrderModel.SelectBuilder(). + Where(squirrel.Eq{"id": orderIds}). + Where("del_state = ?", globalkey.DelStateNo) + + orders, err := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + l.Errorf("查询订单失败: %v", err) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + // 5. 查询时间段内的返佣记录,获取已付费订单的金额 + rebateBuilder = l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ?", parentAgentId, globalkey.DelStateNo). + Where("source_agent_id != ?", parentAgentId). + Where("create_time >= ? AND create_time < ?", startTime, endTime). + Where(squirrel.Eq{"order_id": orderIds}) + + rebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, rebateBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + l.Errorf("查询返佣记录失败: %v", err) + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: 0, + PaidUsers: 0, + TotalAmount: 0, + QueryUserCount: 0, + PaidUserCount: 0, + } + } + + // 记录已付费订单的金额(使用agent_order的order_amount) + for _, rebate := range rebates { + if ao, exists := orderIdToAgentOrder[rebate.OrderId]; exists { + paidOrderIdToAmount[rebate.OrderId] = ao.OrderAmount + } + } + + // 6. 统计查询订单数、付费订单数、用户数和总金额 + var totalAmount float64 + paidOrderCount := 0 + queryUserSet := make(map[int64]bool) + paidUserSet := make(map[int64]bool) + + for _, order := range orders { + // 查询用户数(所有订单的用户,去重) + queryUserSet[order.UserId] = true + + // 付费订单数和总金额(只统计已付费的订单,即order_id在paidOrderIdToAmount中) + if _, isPaid := paidOrderIdToAmount[order.Id]; isPaid { + paidOrderCount++ + paidUserSet[order.UserId] = true + // 使用agent_order的order_amount(用户实际支付金额) + totalAmount += paidOrderIdToAmount[order.Id] + } + } + + // 查询订单数 = 所有订单数量 + queryOrderCount := len(orders) + + l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 查询订单数=%d, 付费订单数=%d, 查询用户数=%d, 付费用户数=%d, 总金额=%.2f", + periodLabel, queryOrderCount, paidOrderCount, len(queryUserSet), len(paidUserSet), totalAmount) + + return types.PeriodConversionData{ + PeriodLabel: periodLabel, + QueryUsers: int64(queryOrderCount), // 订单数量(保持向后兼容) + PaidUsers: int64(paidOrderCount), // 订单数量(保持向后兼容) + TotalAmount: totalAmount, + QueryUserCount: int64(len(queryUserSet)), // 用户数量(新增) + PaidUserCount: int64(len(paidUserSet)), // 用户数量(新增) + } +} diff --git a/app/main/api/internal/logic/agent/getinvitelinklogic.go b/app/main/api/internal/logic/agent/getinvitelinklogic.go index 45962da..c4b6129 100644 --- a/app/main/api/internal/logic/agent/getinvitelinklogic.go +++ b/app/main/api/internal/logic/agent/getinvitelinklogic.go @@ -4,13 +4,14 @@ import ( "context" "database/sql" "fmt" + "time" "ycc-server/app/main/model" "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" "ycc-server/common/tool" "ycc-server/common/xerr" "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/sqlx" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" @@ -32,7 +33,7 @@ func NewGetInviteLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } } -func (l *GetInviteLinkLogic) GetInviteLink() (resp *types.GetInviteLinkResp, err error) { +func (l *GetInviteLinkLogic) GetInviteLink(req *types.GetInviteLinkReq) (resp *types.GetInviteLinkResp, err error) { // 1. 获取当前代理信息 userID, err := ctxdata.GetUidFromCtx(l.ctx) if err != nil { @@ -47,63 +48,109 @@ func (l *GetInviteLinkLogic) GetInviteLink() (resp *types.GetInviteLinkResp, err return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) } - // 2. 为邀请链接生成一个邀请码(每次调用都生成新的,不过期) - // 这样代理可以分享这个链接,用户通过链接注册时会使用这个邀请码 - var inviteCode string - err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { - // 生成8位随机邀请码 - code := tool.Krand(8, tool.KC_RAND_KIND_ALL) - maxRetries := 10 - for retry := 0; retry < maxRetries; retry++ { - _, err := l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, code) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - break - } - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查邀请码失败, %v", err) - } - code = tool.Krand(8, tool.KC_RAND_KIND_ALL) - if retry == maxRetries-1 { - return errors.Wrapf(xerr.NewErrMsg("生成邀请码失败,请重试"), "") - } - } - - // 创建邀请码记录(用于链接,不过期) - inviteCodeRecord := &model.AgentInviteCode{ - Code: code, - AgentId: sql.NullInt64{Int64: agent.Id, Valid: true}, - TargetLevel: 1, // 代理发放的邀请码,目标等级为普通代理 - Status: 0, // 未使用 - ExpireTime: sql.NullTime{Valid: false}, // 不过期 - Remark: sql.NullString{String: "邀请链接生成", Valid: true}, - } - - _, err := l.svcCtx.AgentInviteCodeModel.Insert(transCtx, session, inviteCodeRecord) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建邀请码失败, %v", err) - } - - inviteCode = code - return nil - }) + // 2. 验证邀请码参数 + if req.InviteCode == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不能为空"), "") + } + // 3. 查询邀请码是否存在且属于当前代理 + inviteCodeRecord, err := l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, req.InviteCode) if err != nil { - return nil, err + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不存在"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err) } - // 3. 生成邀请链接 - // 使用配置中的前端域名,如果没有则使用默认值 - frontendDomain := l.svcCtx.Config.AdminPromotion.URLDomain - if frontendDomain == "" { - frontendDomain = "https://example.com" // 默认值,需要配置 + // 4. 验证邀请码是否属于当前代理 + if !inviteCodeRecord.AgentId.Valid || inviteCodeRecord.AgentId.Int64 != agent.Id { + return nil, errors.Wrapf(xerr.NewErrMsg("无权使用此邀请码"), "") } - inviteLink := fmt.Sprintf("%s/register?invite_code=%s", frontendDomain, inviteCode) - // 4. 生成二维码URL(使用ImageService) - qrCodeUrl := fmt.Sprintf("%s/api/v1/image/qrcode?type=invitation&content=%s", frontendDomain, inviteLink) + // 5. 验证邀请码状态 + if inviteCodeRecord.Status != 0 { + if inviteCodeRecord.Status == 1 { + return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "") + } + return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "") + } + + // 6. 验证邀请码是否过期 + if inviteCodeRecord.ExpireTime.Valid && inviteCodeRecord.ExpireTime.Time.Before(time.Now()) { + return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "") + } + + // 7. 使用默认target_path(如果未提供) + targetPath := req.TargetPath + if targetPath == "" { + targetPath = fmt.Sprintf("/register?invite_code=%s", req.InviteCode) + } + + // 8. 生成短链(类型:2=邀请好友) + shortLink, err := l.createInviteShortLink(inviteCodeRecord.Id, req.InviteCode, targetPath) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err) + } return &types.GetInviteLinkResp{ - InviteLink: inviteLink, - QrCodeUrl: qrCodeUrl, + InviteLink: shortLink, }, nil } + +// createInviteShortLink 创建邀请好友短链 +func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId int64, inviteCode, targetPath string) (string, error) { + promotionConfig := l.svcCtx.Config.Promotion + + // 如果没有配置推广域名,返回空字符串(保持向后兼容) + if promotionConfig.PromotionDomain == "" { + l.Errorf("推广域名未配置,返回空链接") + return "", nil + } + + // 先查询是否已存在短链 + existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullInt64{Int64: inviteCodeId, Valid: true}, 2, globalkey.DelStateNo) + if err == nil && existingShortLink != nil { + // 已存在短链,直接返回 + return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil + } + + if err != nil && !errors.Is(err, model.ErrNotFound) { + return "", errors.Wrapf(err, "查询短链失败") + } + + // 生成短链标识(6位随机字符串,大小写字母+数字) + var shortCode string + maxRetries := 10 // 最大重试次数 + for retry := 0; retry < maxRetries; retry++ { + shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL) + // 检查短链标识是否已存在 + _, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + // 短链标识不存在,可以使用 + break + } + return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err) + } + // 短链标识已存在,继续生成 + if retry == maxRetries-1 { + return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "") + } + } + + // 创建短链记录(类型:2=邀请好友) + shortLink := &model.AgentShortLink{ + Type: 2, // 邀请好友 + InviteCodeId: sql.NullInt64{Int64: inviteCodeId, Valid: inviteCodeId > 0}, + InviteCode: sql.NullString{String: inviteCode, Valid: inviteCode != ""}, + ShortCode: shortCode, + TargetPath: targetPath, + PromotionDomain: promotionConfig.PromotionDomain, + } + _, err = l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink) + if err != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err) + } + + return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode), nil +} diff --git a/app/main/api/internal/logic/agent/getlevelprivilegelogic.go b/app/main/api/internal/logic/agent/getlevelprivilegelogic.go new file mode 100644 index 0000000..93f2c0a --- /dev/null +++ b/app/main/api/internal/logic/agent/getlevelprivilegelogic.go @@ -0,0 +1,188 @@ +package agent + +import ( + "context" + "strconv" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/xerr" + + "github.com/pkg/errors" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetLevelPrivilegeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetLevelPrivilegeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLevelPrivilegeLogic { + return &GetLevelPrivilegeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetLevelPrivilegeLogic) GetLevelPrivilege() (resp *types.GetLevelPrivilegeResp, err error) { + // 1. 获取当前代理等级 + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + var currentLevel int64 = 1 // 默认普通代理 + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + if agent != nil { + currentLevel = agent.Level + } + + // 获取配置值的辅助函数 + getConfigFloat := func(key string, defaultValue float64) float64 { + config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key) + if err != nil { + return defaultValue + } + value, err := strconv.ParseFloat(config.ConfigValue, 64) + if err != nil { + return defaultValue + } + return value + } + + // 获取等级加成配置 + level1Bonus := getConfigFloat("level_1_bonus", 6.0) + level2Bonus := getConfigFloat("level_2_bonus", 3.0) + level3Bonus := getConfigFloat("level_3_bonus", 0.0) + + // 获取当前等级的加成 + var currentBonus float64 + switch currentLevel { + case 1: + currentBonus = level1Bonus + case 2: + currentBonus = level2Bonus + case 3: + currentBonus = level3Bonus + default: + currentBonus = level1Bonus + } + + // 获取升级返佣配置 + upgradeToGoldRebate := getConfigFloat("upgrade_to_gold_rebate", 139.0) + upgradeToDiamondRebate := getConfigFloat("upgrade_to_diamond_rebate", 680.0) + + // 获取直接上级返佣配置 + directParentAmountDiamond := getConfigFloat("direct_parent_amount_diamond", 6.0) + directParentAmountGold := getConfigFloat("direct_parent_amount_gold", 3.0) + directParentAmountNormal := getConfigFloat("direct_parent_amount_normal", 2.0) + + // 构建各等级特权信息 + levels := []types.LevelPrivilegeItem{ + { + Level: 1, + LevelName: "普通代理", + LevelBonus: level1Bonus, + PriceReduction: l.calculatePriceReduction(currentBonus, level1Bonus), + PromoteRebate: l.buildPromoteRebateDesc(1, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal), + MaxPromoteRebate: directParentAmountDiamond, // 普通代理最高返佣是直接上级是钻石时的返佣 + UpgradeRebate: l.buildUpgradeRebateDesc(1, upgradeToGoldRebate, upgradeToDiamondRebate), + Privileges: []string{ + "基础代理特权", + "可生成推广链接", + "享受推广返佣", + "可邀请下级代理", + }, + CanUpgradeSubordinate: false, + }, + { + Level: 2, + LevelName: "黄金代理", + LevelBonus: level2Bonus, + PriceReduction: l.calculatePriceReduction(currentBonus, level2Bonus), + PromoteRebate: l.buildPromoteRebateDesc(2, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal), + MaxPromoteRebate: level2Bonus, // 黄金代理最高返佣是等级加成全部返佣给钻石上级 + UpgradeRebate: l.buildUpgradeRebateDesc(2, upgradeToGoldRebate, upgradeToDiamondRebate), + Privileges: []string{ + "高级代理特权", + "更高的返佣比例", + "享受推广返佣", + "可邀请下级代理", + "更多推广权益", + }, + CanUpgradeSubordinate: false, + }, + { + Level: 3, + LevelName: "钻石代理", + LevelBonus: level3Bonus, + PriceReduction: l.calculatePriceReduction(currentBonus, level3Bonus), + PromoteRebate: l.buildPromoteRebateDesc(3, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal), + MaxPromoteRebate: 0, // 钻石代理无返佣 + UpgradeRebate: l.buildUpgradeRebateDesc(3, upgradeToGoldRebate, upgradeToDiamondRebate), + Privileges: []string{ + "尊享代理特权", + "最高返佣比例", + "独立团队管理", + "可调整下级级别", + "可免费升级下级为黄金代理", + }, + CanUpgradeSubordinate: true, + }, + } + + return &types.GetLevelPrivilegeResp{ + Levels: levels, + }, nil +} + +// buildPromoteRebateDesc 构建推广返佣说明 +func (l *GetLevelPrivilegeLogic) buildPromoteRebateDesc(level int64, diamondAmount, goldAmount, normalAmount float64) string { + switch level { + case 1: // 普通代理:等级加成6元 + return "更高的推广返佣,最高可达¥" + l.formatFloat(diamondAmount) + case 2: // 黄金代理:等级加成3元 + return "更高的推广返佣,最高可达¥" + l.formatFloat(3.0) + case 3: // 钻石代理:等级加成0元 + return "无等级加成,享受最低底价" + default: + return "" + } +} + +// buildUpgradeRebateDesc 构建下级升级返佣说明 +func (l *GetLevelPrivilegeLogic) buildUpgradeRebateDesc(level int64, goldRebate, diamondRebate float64) string { + switch level { + case 1: // 普通代理 + return "下级升级为黄金代理返佣¥" + l.formatFloat(goldRebate) + ",升级为钻石代理返佣¥" + l.formatFloat(diamondRebate) + case 2: // 黄金代理 + return "下级升级为钻石代理返佣¥" + l.formatFloat(diamondRebate) + case 3: // 钻石代理 + return "可免费升级下级为黄金代理" + default: + return "" + } +} + +// calculatePriceReduction 计算底价降低(相对于当前等级) +func (l *GetLevelPrivilegeLogic) calculatePriceReduction(currentBonus, targetBonus float64) float64 { + reduction := currentBonus - targetBonus + if reduction < 0 { + return 0 + } + return reduction +} + +// formatFloat 格式化浮点数,去掉末尾的0 +func (l *GetLevelPrivilegeLogic) formatFloat(f float64) string { + s := strconv.FormatFloat(f, 'f', -1, 64) + return s +} diff --git a/app/main/api/internal/logic/agent/getlinkdatalogic.go b/app/main/api/internal/logic/agent/getlinkdatalogic.go index af182d7..20a7870 100644 --- a/app/main/api/internal/logic/agent/getlinkdatalogic.go +++ b/app/main/api/internal/logic/agent/getlinkdatalogic.go @@ -2,9 +2,14 @@ package agent import ( "context" + "sort" + "ycc-server/app/main/model" "ycc-server/common/xerr" + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/mr" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" @@ -37,11 +42,61 @@ func (l *GetLinkDataLogic) GetLinkData(req *types.GetLinkDataReq) (resp *types.G return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取产品信息失败, %v", err) } + // 获取产品功能列表 + build := l.svcCtx.ProductFeatureModel.SelectBuilder().Where(squirrel.Eq{ + "product_id": productModel.Id, + }) + productFeatureAll, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, build, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取产品功能列表失败, %v", err) + } + + // 创建featureId到sort的映射,用于后续排序 + featureSortMap := make(map[int64]int64) + for _, productFeature := range productFeatureAll { + featureSortMap[productFeature.FeatureId] = productFeature.Sort + } + + var features []types.Feature + mr.MapReduceVoid(func(source chan<- interface{}) { + for _, productFeature := range productFeatureAll { + source <- productFeature.FeatureId + } + }, func(item interface{}, writer mr.Writer[*model.Feature], cancel func(error)) { + id := item.(int64) + + feature, findFeatureErr := l.svcCtx.FeatureModel.FindOne(l.ctx, id) + if findFeatureErr != nil { + logx.WithContext(l.ctx).Errorf("获取产品功能失败: %d, err:%v", id, findFeatureErr) + return + } + if feature != nil && feature.Id > 0 { + writer.Write(feature) + } + }, func(pipe <-chan *model.Feature, cancel func(error)) { + for item := range pipe { + var feature types.Feature + _ = copier.Copy(&feature, item) + features = append(features, feature) + } + }) + + // 按照productFeature.Sort字段对features进行排序 + sort.Slice(features, func(i, j int) bool { + sortI := featureSortMap[features[i].ID] + sortJ := featureSortMap[features[j].ID] + return sortI < sortJ + }) + return &types.GetLinkDataResp{ - AgentId: agentLinkModel.AgentId, - ProductId: agentLinkModel.ProductId, - SetPrice: agentLinkModel.SetPrice, + AgentId: agentLinkModel.AgentId, + ProductId: agentLinkModel.ProductId, + SetPrice: agentLinkModel.SetPrice, ActualBasePrice: agentLinkModel.ActualBasePrice, - ProductName: productModel.ProductName, + ProductName: productModel.ProductName, + ProductEn: productModel.ProductEn, + SellPrice: agentLinkModel.SetPrice, // 使用代理设定价格作为销售价格 + Description: productModel.Description, + Features: features, }, nil } diff --git a/app/main/api/internal/logic/agent/getrebatelistlogic.go b/app/main/api/internal/logic/agent/getrebatelistlogic.go index 8c04091..556986c 100644 --- a/app/main/api/internal/logic/agent/getrebatelistlogic.go +++ b/app/main/api/internal/logic/agent/getrebatelistlogic.go @@ -6,6 +6,7 @@ import ( "ycc-server/common/ctxdata" "ycc-server/common/globalkey" "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/crypto" "github.com/pkg/errors" @@ -46,8 +47,14 @@ func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *t // 2. 构建查询条件 builder := l.svcCtx.AgentRebateModel.SelectBuilder(). - Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo). - OrderBy("create_time DESC") + Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo) + + // 如果指定了返佣类型,则按类型筛选(1, 2, 3) + if req.RebateType != nil { + builder = builder.Where("rebate_type = ?", *req.RebateType) + } + + builder = builder.OrderBy("create_time DESC") // 3. 分页查询 page := req.Page @@ -61,7 +68,7 @@ func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *t offset := (page - 1) * pageSize // 4. 查询总数 - total, err := l.svcCtx.AgentRebateModel.FindCount(l.ctx, builder, "") + total, err := l.svcCtx.AgentRebateModel.FindCount(l.ctx, builder, "id") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询返佣总数失败, %v", err) } @@ -76,13 +83,41 @@ func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *t // 6. 组装响应 var list []types.RebateItem for _, rebate := range rebates { + // 查询订单号 + orderNo := "" + if rebate.OrderId > 0 { + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, rebate.OrderId) + if err == nil { + orderNo = order.OrderNo + } + } + + // 查询来源代理手机号和等级 + sourceAgentMobile := "" + sourceAgentLevel := int64(0) + if rebate.SourceAgentId > 0 { + sourceAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, rebate.SourceAgentId) + if err == nil { + if sourceAgent.Mobile != "" { + decrypted, err := crypto.DecryptMobile(sourceAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err == nil { + sourceAgentMobile = decrypted + } + } + sourceAgentLevel = sourceAgent.Level + } + } + list = append(list, types.RebateItem{ - Id: rebate.Id, - 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, + SourceAgentId: rebate.SourceAgentId, + SourceAgentMobile: sourceAgentMobile, + SourceAgentLevel: sourceAgentLevel, + OrderId: rebate.OrderId, + OrderNo: orderNo, + RebateType: rebate.RebateType, + Amount: rebate.RebateAmount, + CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"), }) } diff --git a/app/main/api/internal/logic/agent/getrevenueinfologic.go b/app/main/api/internal/logic/agent/getrevenueinfologic.go index 3c65f30..050051d 100644 --- a/app/main/api/internal/logic/agent/getrevenueinfologic.go +++ b/app/main/api/internal/logic/agent/getrevenueinfologic.go @@ -2,8 +2,10 @@ package agent import ( "context" + "time" "ycc-server/app/main/model" "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" "ycc-server/common/xerr" "github.com/pkg/errors" @@ -49,10 +51,77 @@ func (l *GetRevenueInfoLogic) GetRevenueInfo() (resp *types.GetRevenueInfoResp, return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包信息失败, %v", err) } + // 获取当前时间 + now := time.Now() + // 今日开始时间(00:00:00) + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + // 本月开始时间(1号 00:00:00) + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + // 3. 统计佣金总额(从 agent_commission 表统计) + commissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo) + commissionTotal, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionBuilder, "amount") + + // 3.1 统计佣金今日收益 + commissionTodayBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart) + commissionToday, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionTodayBuilder, "amount") + + // 3.2 统计佣金本月收益 + commissionMonthBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart) + commissionMonth, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionMonthBuilder, "amount") + + // 4. 统计返佣总额(包括推广返佣和升级返佣) + // 4.1 统计推广返佣(从 agent_rebate 表) + rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo) + promoteRebateTotal, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount") + + // 4.2 统计升级返佣(从 agent_upgrade 表,查询 rebate_agent_id = 当前代理ID 且 status = 2(已完成)且 upgrade_type = 1(自主付费)的记录) + // 注意:只要返佣给自己的都要统计,不管升级后是否脱离关系(rebate_agent_id 记录的是升级时的原直接上级) + upgradeRebateBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ?", + agent.Id, 2, 1, globalkey.DelStateNo) + upgradeRebateTotal, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateBuilder, "rebate_amount") + + rebateTotal := promoteRebateTotal + upgradeRebateTotal + + // 4.3 统计返佣今日收益 + // 推广返佣今日 + promoteRebateTodayBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart) + promoteRebateToday, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, promoteRebateTodayBuilder, "rebate_amount") + // 升级返佣今日 + upgradeRebateTodayBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ? AND create_time >= ?", + agent.Id, 2, 1, globalkey.DelStateNo, todayStart) + upgradeRebateToday, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateTodayBuilder, "rebate_amount") + rebateToday := promoteRebateToday + upgradeRebateToday + + // 4.4 统计返佣本月收益 + // 推广返佣本月 + promoteRebateMonthBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart) + promoteRebateMonth, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, promoteRebateMonthBuilder, "rebate_amount") + // 升级返佣本月 + upgradeRebateMonthBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ? AND create_time >= ?", + agent.Id, 2, 1, globalkey.DelStateNo, monthStart) + upgradeRebateMonth, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateMonthBuilder, "rebate_amount") + rebateMonth := promoteRebateMonth + upgradeRebateMonth + return &types.GetRevenueInfoResp{ - Balance: wallet.Balance, - FrozenBalance: wallet.FrozenBalance, - TotalEarnings: wallet.TotalEarnings, + Balance: wallet.Balance, + FrozenBalance: wallet.FrozenBalance, + TotalEarnings: wallet.TotalEarnings, WithdrawnAmount: wallet.WithdrawnAmount, + CommissionTotal: commissionTotal, // 佣金累计总收益(推广订单获得的佣金) + CommissionToday: commissionToday, // 佣金今日收益 + CommissionMonth: commissionMonth, // 佣金本月收益 + RebateTotal: rebateTotal, // 返佣累计总收益(包括推广返佣和升级返佣) + RebateToday: rebateToday, // 返佣今日收益 + RebateMonth: rebateMonth, // 返佣本月收益 }, nil } diff --git a/app/main/api/internal/logic/agent/getsubordinatecontributiondetaillogic.go b/app/main/api/internal/logic/agent/getsubordinatecontributiondetaillogic.go new file mode 100644 index 0000000..7bd64b3 --- /dev/null +++ b/app/main/api/internal/logic/agent/getsubordinatecontributiondetaillogic.go @@ -0,0 +1,404 @@ +package agent + +import ( + "context" + "sort" + "time" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" + "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/crypto" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetSubordinateContributionDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetSubordinateContributionDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubordinateContributionDetailLogic { + return &GetSubordinateContributionDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubordinateContributionDetailLogic) GetSubordinateContributionDetail(req *types.GetSubordinateContributionDetailReq) (resp *types.GetSubordinateContributionDetailResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + // 1. 获取当前代理信息 + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + + // 2. 验证下级代理是否存在,并且确实是当前代理的下级(直接或间接) + subordinate, err := l.svcCtx.AgentModel.FindOne(l.ctx, req.SubordinateId) + 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) + } + + // 3. 验证下级关系(递归检查是否是下级) + isSubordinate := l.isSubordinate(agent.Id, req.SubordinateId) + if !isSubordinate { + return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的下级"), "") + } + + // 4. 解密手机号 + mobile := "" + if subordinate.Mobile != "" { + decrypted, err := crypto.DecryptMobile(subordinate.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err == nil { + mobile = decrypted + } + } + + // 5. 获取等级名称 + levelName := "" + switch subordinate.Level { + case 1: + levelName = "普通" + case 2: + levelName = "黄金" + case 3: + levelName = "钻石" + } + + // 6. 计算时间范围 + now := time.Now() + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + // 7. 统计订单数据(仅统计有返佣给当前用户的订单) + // 通过 agent_rebate 表统计 DISTINCT order_id + rebateBaseBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agent.Id, req.SubordinateId, globalkey.DelStateNo) + + // 总订单量(去重订单ID) + totalOrders := l.countDistinctOrders(l.ctx, rebateBaseBuilder) + + // 今日订单量 + todayRebateBuilder := rebateBaseBuilder.Where("create_time >= ?", todayStart) + todayOrders := l.countDistinctOrders(l.ctx, todayRebateBuilder) + + // 月订单量 + monthRebateBuilder := rebateBaseBuilder.Where("create_time >= ?", monthStart) + monthOrders := l.countDistinctOrders(l.ctx, monthRebateBuilder) + + // 8. 统计返佣金额 + totalRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBaseBuilder, "rebate_amount") + todayRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, todayRebateBuilder, "rebate_amount") + monthRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, monthRebateBuilder, "rebate_amount") + + // 9. 统计邀请数据 + inviteBaseBuilder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", req.SubordinateId, 1, globalkey.DelStateNo) + totalInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBaseBuilder, "id") + + todayInviteBuilder := inviteBaseBuilder.Where("create_time >= ?", todayStart) + todayInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, todayInviteBuilder, "id") + + monthInviteBuilder := inviteBaseBuilder.Where("create_time >= ?", monthStart) + monthInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, monthInviteBuilder, "id") + + // 10. 分页查询订单列表或邀请列表 + page := req.Page + if page <= 0 { + page = 1 + } + pageSize := req.PageSize + if pageSize <= 0 { + pageSize = 20 + } + + var orderList []types.OrderItem + var inviteList []types.InviteItem + var orderListTotal int64 + var inviteListTotal int64 + + tabType := req.TabType + if tabType == "" { + tabType = "order" // 默认显示订单列表 + } + + if tabType == "order" { + // 查询订单列表(仅显示有返佣的订单) + orderList, orderListTotal, err = l.getOrderList(l.ctx, agent.Id, req.SubordinateId, page, pageSize) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单列表失败, %v", err) + } + } else if tabType == "invite" { + // 查询邀请列表 + inviteList, inviteListTotal, err = l.getInviteList(l.ctx, req.SubordinateId, page, pageSize) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请列表失败, %v", err) + } + } + + // 11. 组装响应 + resp = &types.GetSubordinateContributionDetailResp{ + Mobile: mobile, + LevelName: levelName, + CreateTime: subordinate.CreateTime.Format("2006-01-02 15:04:05"), + OrderStats: types.OrderStatistics{ + TotalOrders: totalOrders, + MonthOrders: monthOrders, + TodayOrders: todayOrders, + }, + RebateStats: types.RebateStatistics{ + TotalRebateAmount: totalRebateAmount, + TodayRebateAmount: todayRebateAmount, + MonthRebateAmount: monthRebateAmount, + }, + InviteStats: types.InviteStatistics{ + TotalInvites: totalInvites, + TodayInvites: todayInvites, + MonthInvites: monthInvites, + }, + OrderList: orderList, + InviteList: inviteList, + OrderListTotal: orderListTotal, + InviteListTotal: inviteListTotal, + } + + return resp, nil +} + +// isSubordinate 递归检查 targetId 是否是 parentId 的下级(直接或间接) +func (l *GetSubordinateContributionDetailLogic) isSubordinate(parentId, targetId int64) bool { + // 查询直接下级 + builder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo) + relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "") + if err != nil { + return false + } + + for _, relation := range relations { + // 如果是直接下级,返回 true + if relation.ChildId == targetId { + return true + } + // 递归检查间接下级 + if l.isSubordinate(relation.ChildId, targetId) { + return true + } + } + + return false +} + +// countDistinctOrders 统计去重的订单数量(通过返佣记录) +func (l *GetSubordinateContributionDetailLogic) countDistinctOrders(ctx context.Context, builder squirrel.SelectBuilder) int64 { + // 查询所有返佣记录,在内存中去重订单ID + rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "") + if err != nil { + return 0 + } + + orderIdSet := make(map[int64]bool) + for _, rebate := range rebates { + orderIdSet[rebate.OrderId] = true + } + + return int64(len(orderIdSet)) +} + +// getOrderList 获取订单列表(仅显示有返佣的订单) +func (l *GetSubordinateContributionDetailLogic) getOrderList(ctx context.Context, agentId, subordinateId int64, page, pageSize int64) ([]types.OrderItem, int64, error) { + // 1. 查询所有返佣记录 + rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, subordinateId, globalkey.DelStateNo). + OrderBy("create_time DESC") + + // 查询总数(去重订单ID) + totalBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, subordinateId, globalkey.DelStateNo) + total := l.countDistinctOrders(ctx, totalBuilder) + + // 查询所有返佣记录 + allRebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, rebateBuilder, "") + if err != nil { + return nil, 0, err + } + + if len(allRebates) == 0 { + return []types.OrderItem{}, total, nil + } + + // 2. 在内存中去重订单ID,并按创建时间排序 + type OrderRebateInfo struct { + OrderId int64 + RebateId int64 + ProductId int64 + RebateAmount float64 + CreateTime time.Time + } + + orderMap := make(map[int64]*OrderRebateInfo) // orderId -> 最新的返佣信息 + for _, rebate := range allRebates { + if existing, ok := orderMap[rebate.OrderId]; ok { + // 如果已存在,保留创建时间最新的 + if rebate.CreateTime.After(existing.CreateTime) { + orderMap[rebate.OrderId] = &OrderRebateInfo{ + OrderId: rebate.OrderId, + RebateId: rebate.Id, + ProductId: rebate.ProductId, + RebateAmount: rebate.RebateAmount, + CreateTime: rebate.CreateTime, + } + } + } else { + orderMap[rebate.OrderId] = &OrderRebateInfo{ + OrderId: rebate.OrderId, + RebateId: rebate.Id, + ProductId: rebate.ProductId, + RebateAmount: rebate.RebateAmount, + CreateTime: rebate.CreateTime, + } + } + } + + // 3. 转换为切片并按时间排序 + orderList := make([]*OrderRebateInfo, 0, len(orderMap)) + for _, info := range orderMap { + orderList = append(orderList, info) + } + + // 按创建时间倒序排序 + sort.Slice(orderList, func(i, j int) bool { + return orderList[i].CreateTime.After(orderList[j].CreateTime) + }) + + // 4. 分页 + offset := (page - 1) * pageSize + end := offset + pageSize + if end > int64(len(orderList)) { + end = int64(len(orderList)) + } + if offset >= int64(len(orderList)) { + return []types.OrderItem{}, total, nil + } + + pagedOrderList := orderList[offset:end] + + // 5. 组装订单列表 + var resultList []types.OrderItem + productCache := make(map[int64]string) // 产品ID -> 产品名称缓存 + + for _, orderInfo := range pagedOrderList { + // 查询订单信息 + order, err := l.svcCtx.OrderModel.FindOne(ctx, orderInfo.OrderId) + if err != nil { + continue + } + + // 查询agent_order信息(用于获取订单金额) + agentOrder, err := l.svcCtx.AgentOrderModel.FindOneByOrderId(ctx, orderInfo.OrderId) + if err != nil { + continue + } + + // 查询产品名称(使用缓存) + productName := "" + if name, ok := productCache[orderInfo.ProductId]; ok { + productName = name + } else { + product, err := l.svcCtx.ProductModel.FindOne(ctx, orderInfo.ProductId) + if err == nil { + productName = product.ProductName + productCache[orderInfo.ProductId] = productName + } + } + + resultList = append(resultList, types.OrderItem{ + OrderNo: order.OrderNo, + ProductId: orderInfo.ProductId, + ProductName: productName, + OrderAmount: agentOrder.OrderAmount, + RebateAmount: orderInfo.RebateAmount, + CreateTime: orderInfo.CreateTime.Format("2006-01-02 15:04:05"), + }) + } + + return resultList, total, nil +} + +// getInviteList 获取邀请列表 +func (l *GetSubordinateContributionDetailLogic) getInviteList(ctx context.Context, subordinateId int64, page, pageSize int64) ([]types.InviteItem, int64, error) { + // 查询总数 + builder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", subordinateId, 1, globalkey.DelStateNo) + total, err := l.svcCtx.AgentRelationModel.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + // 分页查询 + offset := (page - 1) * pageSize + builder = builder.OrderBy("create_time DESC"). + Limit(uint64(pageSize)).Offset(uint64(offset)) + relations, err := l.svcCtx.AgentRelationModel.FindAll(ctx, builder, "") + if err != nil { + return nil, 0, err + } + + // 组装邀请列表 + var inviteList []types.InviteItem + for _, relation := range relations { + // 查询被邀请的代理信息 + invitedAgent, err := l.svcCtx.AgentModel.FindOne(ctx, relation.ChildId) + if err != nil { + continue + } + + // 获取等级名称 + levelName := "" + switch invitedAgent.Level { + case 1: + levelName = "普通" + case 2: + levelName = "黄金" + case 3: + levelName = "钻石" + } + + // 解密手机号 + mobile := "" + if invitedAgent.Mobile != "" { + decrypted, err := crypto.DecryptMobile(invitedAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err == nil { + mobile = decrypted + } + } + + inviteList = append(inviteList, types.InviteItem{ + AgentId: invitedAgent.Id, + Level: invitedAgent.Level, + LevelName: levelName, + Mobile: mobile, + CreateTime: relation.CreateTime.Format("2006-01-02 15:04:05"), + }) + } + + return inviteList, total, nil +} diff --git a/app/main/api/internal/logic/agent/getsubordinatelistlogic.go b/app/main/api/internal/logic/agent/getsubordinatelistlogic.go index 32014aa..5008ea4 100644 --- a/app/main/api/internal/logic/agent/getsubordinatelistlogic.go +++ b/app/main/api/internal/logic/agent/getsubordinatelistlogic.go @@ -62,7 +62,7 @@ func (l *GetSubordinateListLogic) GetSubordinateList(req *types.GetSubordinateLi offset := (page - 1) * pageSize // 4. 查询总数 - total, err := l.svcCtx.AgentRelationModel.FindCount(l.ctx, builder, "") + total, err := l.svcCtx.AgentRelationModel.FindCount(l.ctx, builder, "id") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级总数失败, %v", err) } diff --git a/app/main/api/internal/logic/agent/getteamlistlogic.go b/app/main/api/internal/logic/agent/getteamlistlogic.go new file mode 100644 index 0000000..c4b76bf --- /dev/null +++ b/app/main/api/internal/logic/agent/getteamlistlogic.go @@ -0,0 +1,341 @@ +package agent + +import ( + "context" + "sort" + "strings" + "time" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" + "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/crypto" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetTeamListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetTeamListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTeamListLogic { + return &GetTeamListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetTeamListLogic) GetTeamList(req *types.GetTeamListReq) (resp *types.GetTeamListResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + // 1. 获取代理信息 + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + + // 2. 递归查询所有下级(直接+间接) + allSubordinateIds := make(map[int64]bool) + directSubordinateIds := make(map[int64]bool) + + // 递归函数:收集所有下级ID + var collectSubordinates func(int64) error + collectSubordinates = func(parentId int64) error { + // 查询直接下级 + builder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo) + relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "") + if err != nil { + return err + } + + for _, relation := range relations { + // 如果是第一层,标记为直接下级 + if parentId == agent.Id { + directSubordinateIds[relation.ChildId] = true + } + // 添加到所有下级集合 + allSubordinateIds[relation.ChildId] = true + // 递归查询下级的下级 + if err := collectSubordinates(relation.ChildId); err != nil { + return err + } + } + return nil + } + + // 开始递归收集所有下级 + if err := collectSubordinates(agent.Id); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级关系失败, %v", err) + } + + // 3. 如果没有下级,返回空数据 + if len(allSubordinateIds) == 0 { + return &types.GetTeamListResp{ + Statistics: types.TeamStatistics{}, + Total: 0, + List: []types.TeamMemberItem{}, + }, nil + } + + // 4. 将下级ID转换为切片用于查询 + subordinateIds := make([]int64, 0, len(allSubordinateIds)) + for id := range allSubordinateIds { + subordinateIds = append(subordinateIds, id) + } + + // 5. 计算时间范围 + now := time.Now() + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + // 6. 统计顶部数据 + statistics := l.calculateTeamStatistics(agent.Id, subordinateIds, todayStart, monthStart) + + // 7. 查询所有下级代理信息(不进行手机号过滤,因为需要模糊搜索) + builder := l.svcCtx.AgentModel.SelectBuilder(). + Where(squirrel.Eq{"id": subordinateIds}). + Where("del_state = ?", globalkey.DelStateNo) + + allTeamMembers, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败, %v", err) + } + + // 8. 如果有手机号搜索条件,进行模糊匹配过滤(在内存中) + var filteredMembers []*model.Agent + searchMobile := strings.TrimSpace(req.Mobile) + if searchMobile != "" { + // 解密所有手机号并进行模糊匹配 + for _, member := range allTeamMembers { + if member.Mobile != "" { + decryptedMobile, err := crypto.DecryptMobile(member.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err == nil && strings.Contains(decryptedMobile, searchMobile) { + filteredMembers = append(filteredMembers, member) + } + } + } + } else { + // 没有搜索条件,使用所有成员 + filteredMembers = allTeamMembers + } + + // 9. 按创建时间倒序排序 + sort.Slice(filteredMembers, func(i, j int) bool { + return filteredMembers[i].CreateTime.After(filteredMembers[j].CreateTime) + }) + + // 10. 分页处理 + total := int64(len(filteredMembers)) + page := req.Page + if page <= 0 { + page = 1 + } + pageSize := req.PageSize + if pageSize <= 0 { + pageSize = 20 + } + offset := (page - 1) * pageSize + + // 计算分页范围 + start := int(offset) + end := start + int(pageSize) + if start > len(filteredMembers) { + start = len(filteredMembers) + } + if end > len(filteredMembers) { + end = len(filteredMembers) + } + + var teamMembers []*model.Agent + if start < end { + teamMembers = filteredMembers[start:end] + } + + // 11. 组装响应列表 + var list []types.TeamMemberItem + for _, member := range teamMembers { + memberItem := l.buildTeamMemberItem(agent.Id, member, directSubordinateIds, todayStart) + list = append(list, memberItem) + } + + return &types.GetTeamListResp{ + Statistics: statistics, + Total: total, + List: list, + }, nil +} + +// calculateTeamStatistics 计算团队统计数据 +// 注意:所有统计都基于 subordinateIds(下级ID列表),不包含自己的数据 +func (l *GetTeamListLogic) calculateTeamStatistics(agentId int64, subordinateIds []int64, todayStart, monthStart time.Time) types.TeamStatistics { + // 团队成员总数:只统计下级,不包括自己 + stats := types.TeamStatistics{ + TotalMembers: int64(len(subordinateIds)), + } + + // 统计今日和本月新增成员(只统计下级,不包括自己) + todayNewCount := int64(0) + monthNewCount := int64(0) + for _, id := range subordinateIds { + member, err := l.svcCtx.AgentModel.FindOne(l.ctx, id) + if err != nil { + continue + } + if member.CreateTime.After(todayStart) { + todayNewCount++ + } + if member.CreateTime.After(monthStart) { + monthNewCount++ + } + } + stats.TodayNewMembers = todayNewCount + stats.MonthNewMembers = monthNewCount + + // 统计团队总查询量(仅统计有返佣给当前用户的订单,通过agent_rebate表去重统计order_id) + if len(subordinateIds) > 0 { + rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ?", agentId, globalkey.DelStateNo). + Where("source_agent_id != ?", agentId). // 明确排除自己 + Where(squirrel.Eq{"source_agent_id": subordinateIds}) + + // 统计去重的订单数量 + totalQueries := l.countDistinctOrders(l.ctx, rebateBuilder) + stats.TotalQueries = totalQueries + + // 今日推广量(仅统计有返佣的订单) + todayRebateBuilder := rebateBuilder.Where("create_time >= ?", todayStart) + todayPromotions := l.countDistinctOrders(l.ctx, todayRebateBuilder) + stats.TodayPromotions = todayPromotions + + // 本月推广量(仅统计有返佣的订单) + monthRebateBuilder := rebateBuilder.Where("create_time >= ?", monthStart) + monthPromotions := l.countDistinctOrders(l.ctx, monthRebateBuilder) + stats.MonthPromotions = monthPromotions + } + + // 统计收益:只统计从下级获得的返佣(依靠团队得到的收益) + // 返佣:从agent_rebate表统计(从下级获得的返佣) + // agent_id = 当前代理ID(获得返佣的代理) + // source_agent_id IN 下级ID列表(来源代理,即产生订单的下级代理) + // 明确排除自己:source_agent_id != agentId(确保不统计自己的数据) + if len(subordinateIds) > 0 { + rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND del_state = ?", agentId, globalkey.DelStateNo). + Where("source_agent_id != ?", agentId). // 明确排除自己 + Where(squirrel.Eq{"source_agent_id": subordinateIds}) + totalRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount") + todayRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", todayStart), "rebate_amount") + monthRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", monthStart), "rebate_amount") + + stats.TotalEarnings = totalRebate + stats.TodayEarnings = todayRebate + stats.MonthEarnings = monthRebate + } + + return stats +} + +// countDistinctOrders 统计去重的订单数量(通过返佣记录) +func (l *GetTeamListLogic) countDistinctOrders(ctx context.Context, builder squirrel.SelectBuilder) int64 { + // 查询所有返佣记录,在内存中去重订单ID + rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "") + if err != nil { + return 0 + } + + orderIdSet := make(map[int64]bool) + for _, rebate := range rebates { + orderIdSet[rebate.OrderId] = true + } + + return int64(len(orderIdSet)) +} + +// countDistinctOrdersForMember 统计单个成员的去重订单数量(通过返佣记录) +func (l *GetTeamListLogic) countDistinctOrdersForMember(ctx context.Context, builder squirrel.SelectBuilder) int64 { + // 查询所有返佣记录,在内存中去重订单ID + rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "") + if err != nil { + return 0 + } + + orderIdSet := make(map[int64]bool) + for _, rebate := range rebates { + orderIdSet[rebate.OrderId] = true + } + + return int64(len(orderIdSet)) +} + +// buildTeamMemberItem 构建团队成员项 +func (l *GetTeamListLogic) buildTeamMemberItem(agentId int64, member *model.Agent, directSubordinateIds map[int64]bool, todayStart time.Time) types.TeamMemberItem { + levelName := "" + switch member.Level { + case 1: + levelName = "普通" + case 2: + levelName = "黄金" + case 3: + levelName = "钻石" + } + + // 解密手机号 + mobile := "" + if member.Mobile != "" { + decrypted, err := crypto.DecryptMobile(member.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err == nil { + mobile = decrypted + } + } + + // 统计查询量(仅统计有返佣给当前用户的订单,通过agent_rebate表去重统计order_id) + rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder(). + Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, member.Id, globalkey.DelStateNo) + totalQueries := l.countDistinctOrdersForMember(l.ctx, rebateBuilder) + todayRebateBuilder := rebateBuilder.Where("create_time >= ?", todayStart) + todayQueries := l.countDistinctOrdersForMember(l.ctx, todayRebateBuilder) + + // 统计返佣给我的金额(从agent_rebate表,source_agent_id = member.Id, agent_id = agentId) + totalRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount") + todayRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", todayStart), "rebate_amount") + + // 统计邀请人数(从agent_relation表,parent_id = member.Id) + inviteBuilder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", member.Id, 1, globalkey.DelStateNo) + totalInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBuilder, "id") + todayInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBuilder.Where("create_time >= ?", todayStart), "id") + + // 判断是否直接下级 + isDirect := directSubordinateIds[member.Id] + + return types.TeamMemberItem{ + AgentId: member.Id, + Level: member.Level, + LevelName: levelName, + Mobile: mobile, + CreateTime: member.CreateTime.Format("2006-01-02 15:04:05"), + TodayQueries: todayQueries, + TotalQueries: totalQueries, + TotalRebateAmount: totalRebateAmount, + TodayRebateAmount: todayRebateAmount, + TotalInvites: totalInvites, + TodayInvites: todayInvites, + IsDirect: isDirect, + } +} diff --git a/app/main/api/internal/logic/agent/getteamstatisticslogic.go b/app/main/api/internal/logic/agent/getteamstatisticslogic.go index 760ba9f..01cdb44 100644 --- a/app/main/api/internal/logic/agent/getteamstatisticslogic.go +++ b/app/main/api/internal/logic/agent/getteamstatisticslogic.go @@ -2,6 +2,7 @@ package agent import ( "context" + "time" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/globalkey" @@ -45,21 +46,68 @@ func (l *GetTeamStatisticsLogic) GetTeamStatistics() (resp *types.TeamStatistics return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) } - // 2. 确定团队首领ID - teamLeaderId := agent.Id - if agent.TeamLeaderId.Valid { - teamLeaderId = agent.TeamLeaderId.Int64 + // 2. 递归查询所有下级(直接+间接) + allSubordinateIds := make(map[int64]bool) + directSubordinateIds := make(map[int64]bool) + + // 递归函数:收集所有下级ID + var collectSubordinates func(int64) error + collectSubordinates = func(parentId int64) error { + // 查询直接下级 + builder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo) + relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "") + if err != nil { + return err + } + + for _, relation := range relations { + // 如果是第一层,标记为直接下级 + if parentId == agent.Id { + directSubordinateIds[relation.ChildId] = true + } + // 添加到所有下级集合 + allSubordinateIds[relation.ChildId] = true + // 递归查询下级的下级 + if err := collectSubordinates(relation.ChildId); err != nil { + return err + } + } + return nil } - // 3. 查询团队所有成员(通过 team_leader_id) + // 开始递归收集所有下级 + if err := collectSubordinates(agent.Id); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级关系失败, %v", err) + } + + // 3. 获取当前时间用于统计今日和本月新增 + now := time.Now() + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + // 4. 如果没有下级,返回空数据 + if len(allSubordinateIds) == 0 { + return &types.TeamStatisticsResp{ + TotalCount: 0, + DirectCount: 0, + IndirectCount: 0, + GoldCount: 0, + NormalCount: 0, + TodayNewMembers: 0, + MonthNewMembers: 0, + }, nil + } + + // 5. 将下级ID转换为切片用于查询 + subordinateIds := make([]int64, 0, len(allSubordinateIds)) + for id := range allSubordinateIds { + subordinateIds = append(subordinateIds, id) + } + + // 6. 查询所有下级代理信息 builder := l.svcCtx.AgentModel.SelectBuilder(). - Where(squirrel.Or{ - squirrel.Eq{"team_leader_id": teamLeaderId}, - squirrel.And{ - squirrel.Eq{"id": teamLeaderId}, - squirrel.Eq{"level": 3}, // 钻石代理自己也是团队首领 - }, - }). + Where(squirrel.Eq{"id": subordinateIds}). Where("del_state = ?", globalkey.DelStateNo) teamMembers, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "") @@ -67,51 +115,43 @@ func (l *GetTeamStatisticsLogic) GetTeamStatistics() (resp *types.TeamStatistics return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败, %v", err) } - // 4. 统计 - totalCount := int64(len(teamMembers)) + // 7. 统计 + totalCount := int64(len(teamMembers)) // 下级总数(不包括自己) + directCount := int64(len(directSubordinateIds)) + indirectCount := totalCount - directCount + level1Count := int64(0) // 普通 level2Count := int64(0) // 黄金 - level3Count := int64(0) // 钻石 + // 不再统计钻石,因为下级不可能是钻石 - // 统计直接下级 - directCount := int64(0) - builder = l.svcCtx.AgentRelationModel.SelectBuilder(). - Where("parent_id = ? AND relation_type = ? AND del_state = ?", agent.Id, 1, globalkey.DelStateNo) - directRelations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "") - if err == nil { - directCount = int64(len(directRelations)) - } + todayNewCount := int64(0) + monthNewCount := int64(0) for _, member := range teamMembers { - // 排除自己 - if member.Id == agent.Id { - continue - } - + // 统计等级(只统计普通和黄金) switch member.Level { case 1: level1Count++ case 2: level2Count++ - case 3: - level3Count++ + } + + // 统计今日和本月新增 + if member.CreateTime.After(todayStart) { + todayNewCount++ + } + if member.CreateTime.After(monthStart) { + monthNewCount++ } } - // 间接下级 = 总人数 - 直接下级 - 自己 - indirectCount := totalCount - directCount - 1 - if indirectCount < 0 { - indirectCount = 0 - } - return &types.TeamStatisticsResp{ - TotalCount: totalCount - 1, // 排除自己 - DirectCount: directCount, - IndirectCount: indirectCount, - ByLevel: types.TeamLevelStats{ - Normal: level1Count, - Gold: level2Count, - Diamond: level3Count, - }, + TotalCount: totalCount, // 下级总数(不包括自己) + DirectCount: directCount, + IndirectCount: indirectCount, + GoldCount: level2Count, + NormalCount: level1Count, + TodayNewMembers: todayNewCount, + MonthNewMembers: monthNewCount, }, nil } diff --git a/app/main/api/internal/logic/agent/getupgradelistlogic.go b/app/main/api/internal/logic/agent/getupgradelistlogic.go index 9d17387..37933ae 100644 --- a/app/main/api/internal/logic/agent/getupgradelistlogic.go +++ b/app/main/api/internal/logic/agent/getupgradelistlogic.go @@ -61,7 +61,7 @@ func (l *GetUpgradeListLogic) GetUpgradeList(req *types.GetUpgradeListReq) (resp offset := (page - 1) * pageSize // 4. 查询总数 - total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "") + total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "id") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录总数失败, %v", err) } diff --git a/app/main/api/internal/logic/agent/getupgraderebatelistlogic.go b/app/main/api/internal/logic/agent/getupgraderebatelistlogic.go new file mode 100644 index 0000000..82f7235 --- /dev/null +++ b/app/main/api/internal/logic/agent/getupgraderebatelistlogic.go @@ -0,0 +1,119 @@ +package agent + +import ( + "context" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" + "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUpgradeRebateListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetUpgradeRebateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUpgradeRebateListLogic { + return &GetUpgradeRebateListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUpgradeRebateListLogic) GetUpgradeRebateList(req *types.GetUpgradeRebateListReq) (resp *types.GetUpgradeRebateListResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + // 1. 获取代理信息 + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + + // 2. 构建查询条件:查询 rebate_agent_id = 当前代理ID 且 status = 2(已完成)且 upgrade_type = 1(自主付费)的记录 + // 注意:rebate_agent_id 是 NullInt64 类型,需要同时检查 IS NOT NULL + // 只要返佣给自己的都要显示,不管升级后是否脱离关系(rebate_agent_id 记录的是升级时的原直接上级) + builder := l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ?", + agent.Id, 2, 1, globalkey.DelStateNo). + OrderBy("create_time DESC") + + // 3. 分页查询 + page := req.Page + if page <= 0 { + page = 1 + } + pageSize := req.PageSize + if pageSize <= 0 { + pageSize = 20 + } + offset := (page - 1) * pageSize + + // 4. 查询总数 + total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "id") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级返佣总数失败, %v", err) + } + + // 5. 查询列表 + builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset)) + upgrades, err := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, builder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级返佣列表失败, %v", err) + } + + // 6. 组装响应 + var list []types.UpgradeRebateItem + for _, upgrade := range upgrades { + // 查询来源代理手机号(升级的代理) + sourceAgentMobile := "" + if upgrade.AgentId > 0 { + sourceAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, upgrade.AgentId) + if err == nil { + if sourceAgent.Mobile != "" { + decrypted, err := crypto.DecryptMobile(sourceAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err == nil { + sourceAgentMobile = decrypted + } + } + } + } + + // 获取订单号 + orderNo := "" + if upgrade.OrderNo.Valid { + orderNo = upgrade.OrderNo.String + } + + list = append(list, types.UpgradeRebateItem{ + Id: upgrade.Id, + SourceAgentId: upgrade.AgentId, + SourceAgentMobile: sourceAgentMobile, + OrderNo: orderNo, + FromLevel: upgrade.FromLevel, + ToLevel: upgrade.ToLevel, + Amount: upgrade.RebateAmount, + CreateTime: upgrade.CreateTime.Format("2006-01-02 15:04:05"), + }) + } + + return &types.GetUpgradeRebateListResp{ + Total: total, + List: list, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getwithdrawallistlogic.go b/app/main/api/internal/logic/agent/getwithdrawallistlogic.go index 41e6a5f..adf8456 100644 --- a/app/main/api/internal/logic/agent/getwithdrawallistlogic.go +++ b/app/main/api/internal/logic/agent/getwithdrawallistlogic.go @@ -61,7 +61,7 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR offset := (page - 1) * pageSize // 4. 查询总数 - total, err := l.svcCtx.AgentWithdrawalModel.FindCount(l.ctx, builder, "") + total, err := l.svcCtx.AgentWithdrawalModel.FindCount(l.ctx, builder, "id") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录总数失败, %v", err) } diff --git a/app/main/api/internal/logic/agent/promotionredirectlogic.go b/app/main/api/internal/logic/agent/promotionredirectlogic.go new file mode 100644 index 0000000..7f39165 --- /dev/null +++ b/app/main/api/internal/logic/agent/promotionredirectlogic.go @@ -0,0 +1,58 @@ +package agent + +import ( + "context" + "fmt" + "net/http" + "net/url" + "ycc-server/app/main/model" + "ycc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + + "ycc-server/app/main/api/internal/svc" +) + +type PromotionRedirectLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewPromotionRedirectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PromotionRedirectLogic { + return &PromotionRedirectLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// PromotionRedirect 推广链接重定向 +// 从推广域名重定向到正式域名的推广页面 +func (l *PromotionRedirectLogic) PromotionRedirect(r *http.Request, w http.ResponseWriter) error { + // 1. 获取link参数 + linkIdentifier := r.URL.Query().Get("link") + if linkIdentifier == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "缺少link参数") + } + + // 2. 验证linkIdentifier是否存在(可选,用于确保链接有效) + _, err := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, linkIdentifier) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "推广链接不存在或已失效") + } + l.Errorf("查询推广链接失败: %v", err) + // 即使查询失败,也继续重定向,避免影响用户体验 + } + + // 3. 构建重定向URL(使用相对路径,由服务器配置处理域名) + redirectURL := fmt.Sprintf("/agent/promotionInquire/%s", url.QueryEscape(linkIdentifier)) + + // 5. 执行重定向(302临时重定向) + l.Infof("推广链接重定向: linkIdentifier=%s, redirectURL=%s", linkIdentifier, redirectURL) + http.Redirect(w, r, redirectURL, http.StatusFound) + + return nil +} diff --git a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go index 553cb27..86ab87b 100644 --- a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go +++ b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "os" "time" "ycc-server/app/main/model" "ycc-server/common/xerr" @@ -40,8 +41,8 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) } - // 校验验证码 - if req.Mobile != "18889793585" { + // 校验验证码(开发环境下跳过验证码校验) + if os.Getenv("ENV") != "development" { redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) if err != nil { @@ -181,7 +182,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn // 设置自己为团队首领 newAgent.TeamLeaderId = sql.NullInt64{Int64: agentID, Valid: true} - if err := l.svcCtx.AgentModel.UpdateWithVersion(transCtx, session, newAgent); err != nil { + if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil { return errors.Wrapf(err, "更新团队首领失败") } } else { @@ -217,6 +218,19 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn return errors.Wrapf(err, "更新邀请码状态失败") } + // 4.7 记录邀请码使用历史(用于统计和查询) + usage := &model.AgentInviteCodeUsage{ + InviteCodeId: inviteCodeModel.Id, + Code: inviteCodeModel.Code, + UserId: userID, + AgentId: agentID, + AgentLevel: targetLevel, + UsedTime: time.Now(), + } + if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil { + return errors.Wrapf(err, "记录邀请码使用历史失败") + } + agentLevel = targetLevel return nil }) diff --git a/app/main/api/internal/logic/agent/shortlinkredirectlogic.go b/app/main/api/internal/logic/agent/shortlinkredirectlogic.go new file mode 100644 index 0000000..2f80d9e --- /dev/null +++ b/app/main/api/internal/logic/agent/shortlinkredirectlogic.go @@ -0,0 +1,114 @@ +package agent + +import ( + "context" + "fmt" + "net/http" + "strings" + "ycc-server/app/main/model" + "ycc-server/common/globalkey" + "ycc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + + "ycc-server/app/main/api/internal/svc" +) + +type ShortLinkRedirectLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewShortLinkRedirectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ShortLinkRedirectLogic { + return &ShortLinkRedirectLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// ShortLinkRedirect 短链重定向 +// 从短链重定向到推广页面 +func (l *ShortLinkRedirectLogic) ShortLinkRedirect(shortCode string, r *http.Request, w http.ResponseWriter) error { + // 1. 验证短链标识 + if shortCode == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "缺少短链标识") + } + + // 2. 查询短链记录 + shortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "短链不存在或已失效") + } + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询短链失败, %v", err) + } + + // 3. 根据类型验证链接有效性 + if shortLink.Type == 1 { + // 推广报告类型:验证linkIdentifier是否存在 + if shortLink.LinkIdentifier.Valid && shortLink.LinkIdentifier.String != "" { + _, err = l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, shortLink.LinkIdentifier.String) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "推广链接不存在或已失效") + } + l.Errorf("查询推广链接失败: %v", err) + // 即使查询失败,也继续重定向,避免影响用户体验 + } + } + } else if shortLink.Type == 2 { + // 邀请好友类型:验证邀请码是否存在 + if shortLink.InviteCode.Valid && shortLink.InviteCode.String != "" { + _, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, shortLink.InviteCode.String) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "邀请码不存在或已失效") + } + l.Errorf("查询邀请码失败: %v", err) + // 即使查询失败,也继续重定向,避免影响用户体验 + } + } + } + + // 4. 构建重定向URL + redirectURL := shortLink.TargetPath + if redirectURL == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短链目标地址为空") + } + + // 如果 target_path 是相对路径,需要拼接正式域名 + // 如果 target_path 已经是完整URL,则直接使用 + if !strings.HasPrefix(redirectURL, "http://") && !strings.HasPrefix(redirectURL, "https://") { + // 相对路径,需要拼接正式域名 + officialDomain := l.svcCtx.Config.Promotion.OfficialDomain + if officialDomain == "" { + // 如果没有配置正式域名,使用当前请求的域名(向后兼容) + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + officialDomain = fmt.Sprintf("%s://%s", scheme, r.Host) + } + // 确保正式域名不以 / 结尾 + officialDomain = strings.TrimSuffix(officialDomain, "/") + // 确保 target_path 以 / 开头 + if !strings.HasPrefix(redirectURL, "/") { + redirectURL = "/" + redirectURL + } + redirectURL = officialDomain + redirectURL + } + + // 5. 执行重定向(302临时重定向) + linkIdentifierStr := "" + if shortLink.LinkIdentifier.Valid { + linkIdentifierStr = shortLink.LinkIdentifier.String + } + l.Infof("短链重定向: shortCode=%s, type=%d, linkIdentifier=%s, redirectURL=%s", shortCode, shortLink.Type, linkIdentifierStr, redirectURL) + http.Redirect(w, r, redirectURL, http.StatusFound) + + return nil +} + diff --git a/app/main/api/internal/logic/agent/upgradesubordinatelogic.go b/app/main/api/internal/logic/agent/upgradesubordinatelogic.go index 14ca4f3..8f7b19a 100644 --- a/app/main/api/internal/logic/agent/upgradesubordinatelogic.go +++ b/app/main/api/internal/logic/agent/upgradesubordinatelogic.go @@ -5,6 +5,7 @@ import ( "database/sql" "ycc-server/app/main/model" "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/lzUtils" @@ -62,17 +63,10 @@ func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordina return nil, errors.Wrapf(xerr.NewErrMsg("只能升级普通代理为黄金代理"), "") } - // 5. 验证关系:必须是直接下级 - parent, err := l.svcCtx.AgentService.FindDirectParent(l.ctx, subordinateAgent.Id) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的直接下级"), "") - } - return nil, errors.Wrapf(err, "查询关系失败") - } - - if parent.Id != operatorAgent.Id { - return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的直接下级"), "") + // 5. 验证关系:必须是下级(直接或间接) + isSubordinate := l.isSubordinate(operatorAgent.Id, subordinateAgent.Id) + if !isSubordinate { + return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的下级"), "") } // 6. 验证目标等级:只能升级为黄金 @@ -87,12 +81,12 @@ func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordina upgradeRecord := &model.AgentUpgrade{ AgentId: subordinateAgent.Id, FromLevel: 1, // 普通 - ToLevel: toLevel, - UpgradeType: 2, // 钻石升级下级 - UpgradeFee: 0, // 免费 - RebateAmount: 0, // 无返佣 - OperatorAgentId: sql.NullInt64{Int64: operatorAgent.Id, Valid: true}, - Status: 1, // 待处理 + ToLevel: toLevel, + UpgradeType: 2, // 钻石升级下级 + UpgradeFee: 0, // 免费 + RebateAmount: 0, // 无返佣 + OperatorAgentId: sql.NullInt64{Int64: operatorAgent.Id, Valid: true}, + Status: 1, // 待处理 } upgradeResult, err := l.svcCtx.AgentUpgradeModel.Insert(transCtx, session, upgradeRecord) @@ -125,3 +119,27 @@ func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordina Success: true, }, nil } + +// isSubordinate 递归检查 targetId 是否是 parentId 的下级(直接或间接) +func (l *UpgradeSubordinateLogic) isSubordinate(parentId, targetId int64) bool { + // 查询直接下级 + builder := l.svcCtx.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo) + relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "") + if err != nil { + return false + } + + for _, relation := range relations { + // 如果是直接下级,返回 true + if relation.ChildId == targetId { + return true + } + // 递归检查间接下级 + if l.isSubordinate(relation.ChildId, targetId) { + return true + } + } + + return false +} diff --git a/app/main/api/internal/logic/app/getappversionlogic.go b/app/main/api/internal/logic/app/getappversionlogic.go index bbbee23..6a2b11a 100644 --- a/app/main/api/internal/logic/app/getappversionlogic.go +++ b/app/main/api/internal/logic/app/getappversionlogic.go @@ -26,6 +26,6 @@ func NewGetAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get func (l *GetAppVersionLogic) GetAppVersion() (resp *types.GetAppVersionResp, err error) { return &types.GetAppVersionResp{ Version: "1.0.0", - WgtUrl: "https://www.quannengcha.com/app_version/ycc_1.0.0.wgt", + WgtUrl: "https://www.onecha.cn/app_version/ycc_1.0.0.wgt", }, nil } diff --git a/app/main/api/internal/logic/pay/alipaycallbacklogic.go b/app/main/api/internal/logic/pay/alipaycallbacklogic.go index d7560ce..6a088c8 100644 --- a/app/main/api/internal/logic/pay/alipaycallbacklogic.go +++ b/app/main/api/internal/logic/pay/alipaycallbacklogic.go @@ -7,11 +7,13 @@ import ( "time" "ycc-server/pkg/lzkit/lzUtils" + "github.com/pkg/errors" "github.com/smartwalle/alipay/v3" "ycc-server/app/main/api/internal/svc" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type AlipayCallbackLogic struct { @@ -40,6 +42,9 @@ func (l *AlipayCallbackLogic) AlipayCallback(w http.ResponseWriter, r *http.Requ if strings.HasPrefix(orderNo, "Q_") { // 查询订单处理 return l.handleQueryOrderPayment(w, notification) + } else if strings.HasPrefix(orderNo, "U_") { + // 代理升级订单处理 + return l.handleAgentUpgradeOrderPayment(w, notification) } else if strings.HasPrefix(orderNo, "A_") { // 旧系统会员充值订单(已废弃,新系统使用升级功能) // return l.handleAgentVipOrderPayment(w, notification) @@ -105,6 +110,104 @@ func (l *AlipayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, not return nil } +// 处理代理升级订单支付 +func (l *AlipayCallbackLogic) handleAgentUpgradeOrderPayment(w http.ResponseWriter, notification *alipay.Notification) error { + orderNo := notification.OutTradeNo + + // 1. 查找订单 + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo) + if findOrderErr != nil { + logx.Errorf("支付宝支付回调,查找升级订单失败: %+v", findOrderErr) + alipay.ACKNotification(w) + return nil + } + + // 2. 验证金额 + amount := lzUtils.ToAlipayAmount(order.Amount) + user, err := l.svcCtx.UserModel.FindOne(l.ctx, order.UserId) + if err == nil && user.Inside != 1 { + if amount != notification.TotalAmount { + logx.Errorf("支付宝支付回调,升级订单金额不一致,订单号: %s", orderNo) + alipay.ACKNotification(w) + return nil + } + } + + // 3. 检查订单状态 + if order.Status != "pending" { + alipay.ACKNotification(w) + return nil + } + + // 4. 查找升级记录 + upgradeRecords, findUpgradeErr := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("order_no = ?", orderNo). + Limit(1), "") + if findUpgradeErr != nil || len(upgradeRecords) == 0 { + logx.Errorf("支付宝支付回调,查找升级记录失败,订单号: %s, 错误: %+v", orderNo, findUpgradeErr) + alipay.ACKNotification(w) + return nil + } + upgradeRecord := upgradeRecords[0] + + // 5. 检查升级记录状态 + if upgradeRecord.Status != 1 { + // 升级记录状态不是待支付,直接返回成功 + alipay.ACKNotification(w) + return nil + } + + // 6. 处理支付状态 + switch notification.TradeStatus { + case alipay.TradeStatusSuccess: + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + order.PlatformOrderId = lzUtils.StringToNullString(notification.TradeNo) + case alipay.TradeStatusClosed: + order.Status = "closed" + order.CloseTime = lzUtils.TimeToNullTime(time.Now()) + default: + alipay.ACKNotification(w) + return nil + } + + // 7. 更新订单状态 + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("支付宝支付回调,更新升级订单状态失败: %+v", updateErr) + alipay.ACKNotification(w) + return nil + } + + // 8. 如果支付成功,执行升级操作 + if order.Status == "paid" { + err := l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { + // 8.1 执行升级操作 + if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, upgradeRecord.AgentId, upgradeRecord.ToLevel, upgradeRecord.UpgradeType, upgradeRecord.UpgradeFee, upgradeRecord.RebateAmount, orderNo, 0); err != nil { + return errors.Wrapf(err, "执行升级操作失败") + } + + // 8.2 更新升级记录状态为已完成 + upgradeRecord.Status = 2 // 已完成(status: 1=待处理,2=已完成,3=已失败) + upgradeRecord.Remark = lzUtils.StringToNullString("支付成功,升级完成") + if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); updateErr != nil { + return errors.Wrapf(updateErr, "更新升级记录状态失败") + } + + return nil + }) + + if err != nil { + logx.Errorf("支付宝支付回调,处理升级订单失败,订单号: %s, 错误: %+v", orderNo, err) + // 即使升级失败,也返回成功给支付宝,避免重复回调 + } else { + logx.Infof("支付宝支付回调,代理升级成功,订单号: %s, 代理ID: %d, 从等级 %d 升级到等级 %d", orderNo, upgradeRecord.AgentId, upgradeRecord.FromLevel, upgradeRecord.ToLevel) + } + } + + alipay.ACKNotification(w) + return nil +} + // 处理代理会员订单支付(已废弃,新系统使用升级功能) /* func (l *AlipayCallbackLogic) handleAgentVipOrderPayment(w http.ResponseWriter, notification *alipay.Notification) error { diff --git a/app/main/api/internal/logic/pay/paymentchecklogic.go b/app/main/api/internal/logic/pay/paymentchecklogic.go index 4533bd9..f6d43cb 100644 --- a/app/main/api/internal/logic/pay/paymentchecklogic.go +++ b/app/main/api/internal/logic/pay/paymentchecklogic.go @@ -2,6 +2,7 @@ package pay import ( "context" + "strings" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/common/xerr" @@ -25,17 +26,18 @@ func NewPaymentCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Paym } func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) { - // 旧系统会员充值订单(已废弃,新系统使用升级功能) - // if strings.HasPrefix(req.OrderNo, "A_") { - // order, err := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) - // if err != nil { - // return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err) - // } - // return &types.PaymentCheckResp{ - // Type: "agent_vip", - // Status: order.Status, - // }, nil - // } + // 根据订单号前缀判断订单类型 + if strings.HasPrefix(req.OrderNo, "U_") { + // 升级订单 + order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", err) + } + return &types.PaymentCheckResp{ + Type: "agent_upgrade", + Status: order.Status, + }, nil + } // 查询订单(包括代理订单) order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go index 747b872..936a696 100644 --- a/app/main/api/internal/logic/pay/paymentlogic.go +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -2,14 +2,19 @@ package pay import ( "context" + "database/sql" "encoding/json" "fmt" + "os" "strconv" + "strings" + "time" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/lzUtils" "github.com/pkg/errors" "github.com/redis/go-redis/v9" @@ -26,6 +31,7 @@ type PaymentTypeResp struct { amount float64 outTradeNo string description string + orderID int64 // 订单ID,用于开发环境测试支付模式 } func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic { @@ -39,6 +45,13 @@ func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLo func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, err error) { var paymentTypeResp *PaymentTypeResp var prepayData interface{} + var orderID int64 + + // 检查是否为开发环境的测试支付模式 + env := os.Getenv("ENV") + isDevTestPayment := env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty") + isEmptyReportMode := env == "development" && req.PayMethod == "test_empty" + l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { switch req.PayType { case "agent_vip": @@ -52,8 +65,33 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, if err != nil { return err } + + case "agent_upgrade": + paymentTypeResp, err = l.AgentUpgradeOrderPayment(req, session) + if err != nil { + return err + } } + // 开发环境测试支付模式:跳过实际支付流程 + // 注意:订单状态更新在事务外进行,避免在事务中查询不到订单的问题 + if isDevTestPayment { + // 获取订单ID(从 QueryOrderPayment 返回的 orderID) + if paymentTypeResp.orderID <= 0 { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "开发测试模式,订单ID无效") + } + orderID = paymentTypeResp.orderID + + // 在事务中只记录订单ID,不更新订单状态 + // 订单状态的更新和后续流程在事务提交后处理 + logx.Infof("开发环境测试支付模式:订单 %s (ID: %d) 将在事务提交后更新状态", paymentTypeResp.outTradeNo, orderID) + + // 返回测试支付标识 + prepayData = "test_payment_success" + return nil + } + + // 正常支付流程 var createOrderErr error if req.PayMethod == "wechat" { prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) @@ -70,6 +108,90 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, if err != nil { return nil, err } + + // 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程 + if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID > 0 { + // 使用 goroutine 异步处理,确保事务已完全提交 + go func() { + // 短暂延迟,确保事务已完全提交到数据库 + time.Sleep(200 * time.Millisecond) + + finalOrderID := paymentTypeResp.orderID + + // 查找订单并更新状态为已支付 + order, findOrderErr := l.svcCtx.OrderModel.FindOne(context.Background(), finalOrderID) + if findOrderErr != nil { + logx.Errorf("开发测试模式,查找订单失败,订单ID: %d, 错误: %v", finalOrderID, findOrderErr) + return + } + + // 更新订单状态为已支付 + order.Status = "paid" + now := time.Now() + order.PayTime = sql.NullTime{Time: now, Valid: true} + + // 空报告模式:在 PaymentPlatform 字段中标记,用于后续生成空报告 + if isEmptyReportMode { + order.PaymentPlatform = "test_empty" + logx.Infof("开发环境空报告模式:订单 %s (ID: %d) 已标记为空报告模式", paymentTypeResp.outTradeNo, finalOrderID) + } + + // 更新订单状态(在事务外执行) + updateErr := l.svcCtx.OrderModel.UpdateWithVersion(context.Background(), nil, order) + if updateErr != nil { + logx.Errorf("开发测试模式,更新订单状态失败,订单ID: %d, 错误: %+v", finalOrderID, updateErr) + return + } + + logx.Infof("开发环境测试支付模式:订单 %s (ID: %d) 已自动标记为已支付", paymentTypeResp.outTradeNo, finalOrderID) + + // 再次短暂延迟,确保订单状态更新已提交 + time.Sleep(100 * time.Millisecond) + + // 根据订单类型处理后续流程 + if strings.HasPrefix(paymentTypeResp.outTradeNo, "U_") { + // 升级订单:直接执行升级操作 + upgradeRecords, findUpgradeErr := l.svcCtx.AgentUpgradeModel.FindAll(context.Background(), l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("order_no = ?", paymentTypeResp.outTradeNo). + Limit(1), "") + if findUpgradeErr != nil || len(upgradeRecords) == 0 { + logx.Errorf("开发测试模式,查找升级记录失败,订单号: %s, 错误: %+v", paymentTypeResp.outTradeNo, findUpgradeErr) + return + } + upgradeRecord := upgradeRecords[0] + + // 执行升级操作 + err := l.svcCtx.AgentWalletModel.Trans(context.Background(), func(transCtx context.Context, session sqlx.Session) error { + if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, upgradeRecord.AgentId, upgradeRecord.ToLevel, upgradeRecord.UpgradeType, upgradeRecord.UpgradeFee, upgradeRecord.RebateAmount, paymentTypeResp.outTradeNo, 0); err != nil { + return errors.Wrapf(err, "执行升级操作失败") + } + + // 更新升级记录状态为已完成 + upgradeRecord.Status = 2 // 已完成(status: 1=待处理,2=已完成,3=已失败) + upgradeRecord.Remark = lzUtils.StringToNullString("测试支付成功,升级完成") + if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); updateErr != nil { + return errors.Wrapf(updateErr, "更新升级记录状态失败") + } + + return nil + }) + + if err != nil { + logx.Errorf("开发测试模式,处理升级订单失败,订单号: %s, 错误: %+v", paymentTypeResp.outTradeNo, err) + } else { + logx.Infof("开发测试模式,代理升级成功,订单号: %s, 代理ID: %d", paymentTypeResp.outTradeNo, upgradeRecord.AgentId) + } + } else { + // 查询订单:发送支付成功通知任务,触发后续流程(生成报告和代理处理) + if sendErr := l.svcCtx.AsynqService.SendQueryTask(finalOrderID); sendErr != nil { + logx.Errorf("开发测试模式,发送支付成功通知任务失败,订单ID: %d, 错误: %+v", finalOrderID, sendErr) + } else { + logx.Infof("开发测试模式,已发送支付成功通知任务,订单ID: %d", finalOrderID) + } + } + }() + } + switch v := prepayData.(type) { case string: // 如果 prepayData 是字符串类型,直接返回 @@ -153,19 +275,35 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 查询代理信息失败: %+v", err) } - // 获取系统配置 - basePrice, err := l.getConfigFloat("base_price") + // 获取产品配置(必须存在) + productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, product.Id) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取基础底价配置失败: %+v", err) + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单失败,产品配置不存在, productId: %d,请先在后台配置产品价格参数", product.Id) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 查询产品配置失败: %+v", err) } - priceThreshold, _ := l.getConfigFloat("price_threshold") - priceFeeRate, _ := l.getConfigFloat("price_fee_rate") - // 计算实际底价(基础底价+等级加成) - levelBonus := l.getLevelBonus(agent.Level) + // 获取等级加成(需要从系统配置读取) + levelBonus, err := l.getLevelBonus(agent.Level) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取等级加成配置失败: %+v", err) + } + + // 使用产品配置的底价计算实际底价 + basePrice := productConfig.BasePrice actualBasePrice := basePrice + float64(levelBonus) - // 计算提价成本 + // 计算提价成本(使用产品配置) + priceThreshold := 0.0 + priceFeeRate := 0.0 + if productConfig.PriceThreshold.Valid { + priceThreshold = productConfig.PriceThreshold.Float64 + } + if productConfig.PriceFeeRate.Valid { + priceFeeRate = productConfig.PriceFeeRate.Float64 + } + priceCost := 0.0 if agentLinkModel.SetPrice > priceThreshold { priceCost = (agentLinkModel.SetPrice - priceThreshold) * priceFeeRate @@ -191,25 +329,138 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert) } } - return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName}, nil + return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName, orderID: orderID}, nil } + // AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代) func (l *PaymentLogic) AgentVipOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) { // 新代理系统已废弃会员充值功能,请使用升级功能 return nil, errors.Wrapf(xerr.NewErrMsg("该功能已废弃,请使用代理升级功能"), "") } -// getLevelBonus 获取等级加成 -func (l *PaymentLogic) getLevelBonus(level int64) int64 { + +// AgentUpgradeOrderPayment 代理升级订单支付 +func (l *PaymentLogic) AgentUpgradeOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err) + } + + // 1. 解析升级记录ID + upgradeId, err := strconv.ParseInt(req.Id, 10, 64) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的升级记录ID: %s", req.Id) + } + + // 2. 查找升级记录 + upgradeRecord, err := l.svcCtx.AgentUpgradeModel.FindOne(l.ctx, upgradeId) + 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) + } + + // 3. 验证升级记录状态(必须是待支付状态) + if upgradeRecord.Status != 1 { + return nil, errors.Wrapf(xerr.NewErrMsg("升级记录状态不正确,无法支付"), "") + } + + // 4. 验证代理ID是否匹配 + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err) + } + if agent.Id != upgradeRecord.AgentId { + return nil, errors.Wrapf(xerr.NewErrMsg("无权支付此升级订单"), "") + } + + // 5. 生成订单号(使用 U_ 前缀表示升级订单) + outTradeNo := fmt.Sprintf("U_%d_%d", upgradeId, time.Now().Unix()) + + // 6. 获取用户信息(用于内部用户判断) + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取用户信息失败: %v", err) + } + + // 7. 计算支付金额 + amount := upgradeRecord.UpgradeFee + if user.Inside == 1 { + amount = 0.01 // 内部用户测试金额 + } + + // 8. 创建订单记录 + order := model.Order{ + OrderNo: outTradeNo, + UserId: userID, + ProductId: 0, // 升级订单没有产品ID + PaymentPlatform: req.PayMethod, + PaymentScene: "app", + Amount: amount, + Status: "pending", + } + orderInsertResult, insertOrderErr := l.svcCtx.OrderModel.Insert(l.ctx, session, &order) + if insertOrderErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建订单失败: %+v", insertOrderErr) + } + orderID, lastInsertIdErr := orderInsertResult.LastInsertId() + if lastInsertIdErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取订单ID失败: %+v", lastInsertIdErr) + } + + // 9. 更新升级记录的订单号 + upgradeRecord.OrderNo = lzUtils.StringToNullString(outTradeNo) + if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(l.ctx, session, upgradeRecord); updateErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新升级记录订单号失败: %+v", updateErr) + } + + // 10. 生成描述信息 + levelNames := map[int64]string{ + 1: "普通代理", + 2: "黄金代理", + 3: "钻石代理", + } + fromLevelName := levelNames[upgradeRecord.FromLevel] + toLevelName := levelNames[upgradeRecord.ToLevel] + description := fmt.Sprintf("代理升级:%s → %s", fromLevelName, toLevelName) + + return &PaymentTypeResp{ + amount: amount, + outTradeNo: outTradeNo, + description: description, + orderID: orderID, + }, nil +} + +// getLevelBonus 获取等级加成(从配置表读取) +func (l *PaymentLogic) getLevelBonus(level int64) (int64, error) { + var configKey string switch level { case 1: // 普通 - return 6 + configKey = "level_1_bonus" case 2: // 黄金 - return 3 + configKey = "level_2_bonus" case 3: // 钻石 - return 0 + configKey = "level_3_bonus" default: - return 0 + return 0, nil } + + bonus, err := l.getConfigFloat(configKey) + if err != nil { + // 配置不存在时返回默认值 + l.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v,使用默认值", level, configKey, err) + switch level { + case 1: + return 6, nil + case 2: + return 3, nil + case 3: + return 0, nil + } + return 0, nil + } + return int64(bonus), nil } // getConfigFloat 获取配置值(浮点数) diff --git a/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go b/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go index c41fc4d..bb608fc 100644 --- a/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go +++ b/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go @@ -10,8 +10,10 @@ import ( "ycc-server/app/main/api/internal/svc" + "github.com/pkg/errors" "github.com/wechatpay-apiv3/wechatpay-go/services/payments" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type WechatPayCallbackLogic struct { @@ -40,6 +42,9 @@ func (l *WechatPayCallbackLogic) WechatPayCallback(w http.ResponseWriter, r *htt if strings.HasPrefix(orderNo, "Q_") { // 查询订单处理 return l.handleQueryOrderPayment(w, notification) + } else if strings.HasPrefix(orderNo, "U_") { + // 代理升级订单处理 + return l.handleAgentUpgradeOrderPayment(w, notification) } else if strings.HasPrefix(orderNo, "A_") { // 旧系统会员充值订单(已废弃,新系统使用升级功能) // return l.handleAgentVipOrderPayment(w, notification) @@ -104,6 +109,101 @@ func (l *WechatPayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, return nil } +// 处理代理升级订单支付 +func (l *WechatPayCallbackLogic) handleAgentUpgradeOrderPayment(w http.ResponseWriter, notification *payments.Transaction) error { + orderNo := *notification.OutTradeNo + + // 1. 查找订单 + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo) + if findOrderErr != nil { + logx.Errorf("微信支付回调,查找升级订单失败: %+v", findOrderErr) + return nil + } + + // 2. 验证金额 + amount := lzUtils.ToWechatAmount(order.Amount) + if amount != *notification.Amount.Total { + logx.Errorf("微信支付回调,升级订单金额不一致,订单号: %s", orderNo) + return nil + } + + // 3. 检查订单状态 + if order.Status != "pending" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil + } + + // 4. 查找升级记录 + upgradeRecords, findUpgradeErr := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, l.svcCtx.AgentUpgradeModel.SelectBuilder(). + Where("order_no = ?", orderNo). + Limit(1), "") + if findUpgradeErr != nil || len(upgradeRecords) == 0 { + logx.Errorf("微信支付回调,查找升级记录失败,订单号: %s, 错误: %+v", orderNo, findUpgradeErr) + return nil + } + upgradeRecord := upgradeRecords[0] + + // 5. 检查升级记录状态 + if upgradeRecord.Status != 1 { + // 升级记录状态不是待支付,直接返回成功 + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil + } + + // 6. 处理支付状态 + switch *notification.TradeState { + case service.TradeStateSuccess: + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + order.PlatformOrderId = lzUtils.StringToNullString(*notification.TransactionId) + case service.TradeStateClosed: + order.Status = "closed" + order.CloseTime = lzUtils.TimeToNullTime(time.Now()) + case service.TradeStateRevoked: + order.Status = "failed" + default: + return nil + } + + // 7. 更新订单状态 + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("微信支付回调,更新升级订单状态失败: %+v", updateErr) + return nil + } + + // 8. 如果支付成功,执行升级操作 + if order.Status == "paid" { + err := l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { + // 8.1 执行升级操作 + if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, upgradeRecord.AgentId, upgradeRecord.ToLevel, upgradeRecord.UpgradeType, upgradeRecord.UpgradeFee, upgradeRecord.RebateAmount, orderNo, 0); err != nil { + return errors.Wrapf(err, "执行升级操作失败") + } + + // 8.2 更新升级记录状态为已完成 + upgradeRecord.Status = 2 // 已完成(status: 1=待处理,2=已完成,3=已失败) + upgradeRecord.Remark = lzUtils.StringToNullString("支付成功,升级完成") + if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); updateErr != nil { + return errors.Wrapf(updateErr, "更新升级记录状态失败") + } + + return nil + }) + + if err != nil { + logx.Errorf("微信支付回调,处理升级订单失败,订单号: %s, 错误: %+v", orderNo, err) + // 即使升级失败,也返回成功给微信,避免重复回调 + } else { + logx.Infof("微信支付回调,代理升级成功,订单号: %s, 代理ID: %d, 从等级 %d 升级到等级 %d", orderNo, upgradeRecord.AgentId, upgradeRecord.FromLevel, upgradeRecord.ToLevel) + } + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil +} + // 处理代理会员订单支付(已废弃,新系统使用升级功能) /* func (l *WechatPayCallbackLogic) handleAgentVipOrderPayment(w http.ResponseWriter, notification *payments.Transaction) error { diff --git a/app/main/api/internal/logic/query/queryservicelogic.go b/app/main/api/internal/logic/query/queryservicelogic.go index 47e7a34..9162d11 100644 --- a/app/main/api/internal/logic/query/queryservicelogic.go +++ b/app/main/api/internal/logic/query/queryservicelogic.go @@ -5,13 +5,14 @@ import ( "encoding/hex" "encoding/json" "fmt" + "os" + "time" "ycc-server/app/main/api/internal/service" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/crypto" "ycc-server/pkg/lzkit/validator" - "time" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/redis" @@ -582,7 +583,7 @@ func (l *QueryServiceLogic) ProcessConsumerFinanceReportLogic(req *types.QuerySe if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) } - cacheNo, cacheDataErr := l.CacheData(params, "personalData", userID) + cacheNo, cacheDataErr := l.CacheData(params, "consumerFinanceReport", userID) if cacheDataErr != nil { return nil, cacheDataErr } @@ -616,6 +617,10 @@ func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) { // 校验验证码 func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error { + // 开发环境下跳过验证码校验 + if os.Getenv("ENV") == "development" { + return nil + } secretKey := l.svcCtx.Config.Encrypt.SecretKey encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey) if err != nil { @@ -637,6 +642,10 @@ func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error { // 二、三要素验证 func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error { + // 开发环境下跳过二/三要素验证 + if os.Getenv("ENV") == "development" { + return nil + } if !l.svcCtx.Config.SystemConfig.ThreeVerify { twoVerification := service.TwoFactorVerificationRequest{ Name: Name, diff --git a/app/main/api/internal/logic/user/bindmobilelogic.go b/app/main/api/internal/logic/user/bindmobilelogic.go index 38a7915..386dd22 100644 --- a/app/main/api/internal/logic/user/bindmobilelogic.go +++ b/app/main/api/internal/logic/user/bindmobilelogic.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "os" "time" "ycc-server/app/main/api/internal/svc" @@ -42,7 +43,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 加密手机号失败: %v", err) } - if req.Mobile != "18889793585" { + // 开发环境下跳过验证码校验 + if os.Getenv("ENV") != "development" { // 检查手机号是否在一分钟内已发送过验证码 redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) diff --git a/app/main/api/internal/logic/user/mobilecodeloginlogic.go b/app/main/api/internal/logic/user/mobilecodeloginlogic.go index 3ad636d..3ea0dc3 100644 --- a/app/main/api/internal/logic/user/mobilecodeloginlogic.go +++ b/app/main/api/internal/logic/user/mobilecodeloginlogic.go @@ -2,14 +2,15 @@ package user import ( "context" + "database/sql" + "fmt" + "os" + "time" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/app/main/model" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/crypto" - "database/sql" - "fmt" - "time" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/redis" @@ -37,17 +38,20 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 加密手机号失败: %+v", err) } - // 检查手机号是否在一分钟内已发送过验证码 - redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile) - cacheCode, err := l.svcCtx.Redis.Get(redisKey) - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机登录, 验证码过期: %s", encryptedMobile) + // 开发环境下跳过验证码校验 + if os.Getenv("ENV") != "development" { + // 检查手机号是否在一分钟内已发送过验证码 + redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(redisKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机登录, 验证码过期: %s", encryptedMobile) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err) + } + if cacheCode != req.Code { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机登录, 验证码不正确: %s", encryptedMobile) } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err) - } - if cacheCode != req.Code { - return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机登录, 验证码不正确: %s", encryptedMobile) } var userID int64 user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) diff --git a/app/main/api/internal/queue/agentProcess.go b/app/main/api/internal/queue/agentProcess.go new file mode 100644 index 0000000..9418b86 --- /dev/null +++ b/app/main/api/internal/queue/agentProcess.go @@ -0,0 +1,63 @@ +package queue + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/app/main/model" + + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" +) + +type AgentProcessHandler struct { + svcCtx *svc.ServiceContext +} + +func NewAgentProcessHandler(svcCtx *svc.ServiceContext) *AgentProcessHandler { + return &AgentProcessHandler{ + svcCtx: svcCtx, + } +} + +func (l *AgentProcessHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + var payload types.MsgAgentProcessPayload + if err := json.Unmarshal(t.Payload(), &payload); err != nil { + return fmt.Errorf("解析代理处理任务负载失败: %w", err) + } + + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOne(ctx, payload.OrderID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + logx.Errorf("代理处理任务失败,订单不存在: orderID=%d", payload.OrderID) + return asynq.SkipRetry // 订单不存在,跳过重试 + } + return fmt.Errorf("查询订单失败: orderID=%d, err=%w", payload.OrderID, err) + } + + // 检查订单状态 + if order.Status != "paid" { + logx.Infof("代理处理任务跳过,订单未支付: orderID=%d, status=%s", payload.OrderID, order.Status) + return nil // 订单未支付,不处理,不重试 + } + + // 调用代理处理服务 + err = l.svcCtx.AgentService.AgentProcess(ctx, order) + if err != nil { + // 记录错误日志,但不阻塞报告流程 + logx.Errorf("代理处理失败,订单ID: %d, 错误: %v", payload.OrderID, err) + // 返回错误以触发重试机制 + return fmt.Errorf("代理处理失败: orderID=%d, err=%w", payload.OrderID, err) + } + + // 注意:解冻任务现在通过定时任务扫描处理,不再需要发送延迟任务 + // 定时任务每5分钟扫描一次待解冻的任务,更加可靠 + logx.Infof("代理处理成功,订单ID: %d,冻结任务(如有)将由定时任务自动处理", payload.OrderID) + + logx.Infof("代理处理成功,订单ID: %d", payload.OrderID) + return nil +} diff --git a/app/main/api/internal/queue/cleanQueryData.go b/app/main/api/internal/queue/cleanQueryData.go index 7b1d1ec..ee9abb6 100644 --- a/app/main/api/internal/queue/cleanQueryData.go +++ b/app/main/api/internal/queue/cleanQueryData.go @@ -2,13 +2,13 @@ package queue import ( "context" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/model" - "ycc-server/common/globalkey" "database/sql" "fmt" "strconv" "time" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/model" + "ycc-server/common/globalkey" "github.com/hibiken/asynq" "github.com/zeromicro/go-zero/core/logx" @@ -48,11 +48,16 @@ func (l *CleanQueryDataHandler) getConfigValue(ctx context.Context, key string) } func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + // 添加超时控制:最多运行1小时 + taskCtx, cancel := context.WithTimeout(ctx, 1*time.Hour) + defer cancel() + + startTime := time.Now() now := time.Now() logx.Infof("%s - 开始执行查询数据清理任务", now.Format("2006-01-02 15:04:05")) // 1. 检查是否启用清理 - enableCleanup, err := l.getConfigValue(ctx, "enable_cleanup") + enableCleanup, err := l.getConfigValue(taskCtx, "enable_cleanup") if err != nil { return err } @@ -62,29 +67,53 @@ func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task) } // 2. 获取保留天数 - retentionDaysStr, err := l.getConfigValue(ctx, "retention_days") + retentionDaysStr, err := l.getConfigValue(taskCtx, "retention_days") if err != nil { return err } retentionDays, err := strconv.Atoi(retentionDaysStr) if err != nil { - return err + return fmt.Errorf("保留天数配置无效: %v", err) + } + if retentionDays < 0 { + return fmt.Errorf("保留天数不能为负数: %d", retentionDays) } // 3. 获取批次大小 - batchSizeStr, err := l.getConfigValue(ctx, "batch_size") + batchSizeStr, err := l.getConfigValue(taskCtx, "batch_size") if err != nil { return err } batchSize, err := strconv.Atoi(batchSizeStr) if err != nil { - return err + return fmt.Errorf("批次大小配置无效: %v", err) + } + if batchSize <= 0 || batchSize > 10000 { + return fmt.Errorf("批次大小必须在1-10000之间: %d", batchSize) } // 计算清理截止时间 cleanupBefore := now.AddDate(0, 0, -retentionDays) - // 创建清理日志记录 + // 先快速检查是否有数据需要清理(避免创建无用的日志记录) + checkBuilder := l.svcCtx.QueryModel.SelectBuilder(). + Where("create_time < ?", cleanupBefore). + Where("del_state = ?", globalkey.DelStateNo). + Limit(1) // 只查询1条,用于判断是否有数据 + + checkQueries, checkErr := l.svcCtx.QueryModel.FindAll(taskCtx, checkBuilder, "") + if checkErr != nil { + logx.Errorf("检查是否有数据需要清理失败: %v", checkErr) + return checkErr + } + + // 如果没有数据需要清理,直接返回,不创建日志记录 + if len(checkQueries) == 0 { + logx.Infof("%s - 没有需要清理的数据(清理截止时间: %s)", now.Format("2006-01-02 15:04:05"), cleanupBefore.Format("2006-01-02 15:04:05")) + return nil + } + + // 创建清理日志记录(只创建一次) cleanupLog := &model.QueryCleanupLog{ CleanupTime: now, CleanupBefore: cleanupBefore, @@ -92,52 +121,75 @@ func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task) Remark: sql.NullString{String: "定时清理数据", Valid: true}, } - // 使用事务处理清理操作和日志记录 - err = l.svcCtx.QueryModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { - // 分批处理 - for { - // 1. 查询一批要删除的记录 + // 先创建清理日志记录 + var cleanupLogId int64 + err = l.svcCtx.QueryCleanupLogModel.Trans(taskCtx, func(logCtx context.Context, logSession sqlx.Session) error { + cleanupLogInsertResult, insertErr := l.svcCtx.QueryCleanupLogModel.Insert(logCtx, logSession, cleanupLog) + if insertErr != nil { + return insertErr + } + cleanupLogId, insertErr = cleanupLogInsertResult.LastInsertId() + return insertErr + }) + if err != nil { + logx.Errorf("创建清理日志记录失败: %v", err) + return err + } + + logx.Infof("创建清理日志记录成功,日志ID: %d", cleanupLogId) + + // 分批处理,每个批次使用独立事务 + batchCount := 0 + lastProcessedId := int64(0) + + for { + // 检查是否被取消(优雅关闭支持) + select { + case <-taskCtx.Done(): + logx.Infof("清理任务被取消,已处理 %d 批次,共删除 %d 条记录", batchCount, cleanupLog.AffectedRows) + // 更新清理日志状态 + l.updateCleanupLogStatus(taskCtx, cleanupLogId, cleanupLog, fmt.Errorf("任务被取消")) + return taskCtx.Err() + default: + // 继续处理 + } + + // 每个批次使用独立事务 + var batchQueries []*model.Query + batchErr := l.svcCtx.QueryModel.Trans(taskCtx, func(batchCtx context.Context, batchSession sqlx.Session) error { + // 1. 查询一批要删除的记录(添加排序确保一致性) builder := l.svcCtx.QueryModel.SelectBuilder(). Where("create_time < ?", cleanupBefore). Where("del_state = ?", globalkey.DelStateNo). + OrderBy("id ASC"). // 添加排序,确保处理顺序一致 Limit(uint64(batchSize)) - queries, err := l.svcCtx.QueryModel.FindAll(ctx, builder, "") - if err != nil { - cleanupLog.Status = 2 - cleanupLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true} - return err + // 如果已处理过,从上次处理的ID之后继续 + if lastProcessedId > 0 { + builder = builder.Where("id > ?", lastProcessedId) } - if len(queries) == 0 { - break // 没有更多数据需要清理 + var queryErr error + batchQueries, queryErr = l.svcCtx.QueryModel.FindAll(batchCtx, builder, "") + if queryErr != nil { + return queryErr + } + + if len(batchQueries) == 0 { + // 没有更多数据需要清理,标记为完成 + return nil } // 2. 执行清理 - for _, query := range queries { - err = l.svcCtx.QueryModel.DeleteSoft(ctx, session, query) - if err != nil { - cleanupLog.Status = 2 - cleanupLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true} - return err + for _, query := range batchQueries { + deleteErr := l.svcCtx.QueryModel.DeleteSoft(batchCtx, batchSession, query) + if deleteErr != nil { + return deleteErr } } - // 3. 更新影响行数 - cleanupLog.AffectedRows += int64(len(queries)) - - // 4. 保存清理日志(每批次都记录) - cleanupLogInsertResult, err := l.svcCtx.QueryCleanupLogModel.Insert(ctx, session, cleanupLog) - if err != nil { - return err - } - cleanupLogId, err := cleanupLogInsertResult.LastInsertId() - if err != nil { - return err - } - - // 5. 保存清理明细 - for _, query := range queries { + // 3. 保存清理明细 + for _, query := range batchQueries { detail := &model.QueryCleanupDetail{ CleanupLogId: cleanupLogId, QueryId: query.Id, @@ -147,21 +199,76 @@ func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task) QueryState: query.QueryState, CreateTimeOld: query.CreateTime, } - _, err = l.svcCtx.QueryCleanupDetailModel.Insert(ctx, session, detail) - if err != nil { - return err + _, insertErr := l.svcCtx.QueryCleanupDetailModel.Insert(batchCtx, batchSession, detail) + if insertErr != nil { + return insertErr } } + + // 4. 记录最后处理的ID(用于下次查询) + lastProcessedId = batchQueries[len(batchQueries)-1].Id + + return nil + }) + + if batchErr != nil { + // 批次失败,更新清理日志状态 + logx.Errorf("批次处理失败(批次 %d): %v", batchCount+1, batchErr) + l.updateCleanupLogStatus(taskCtx, cleanupLogId, cleanupLog, batchErr) + return batchErr } - return nil + // 如果查询结果为空,说明没有更多数据 + if len(batchQueries) == 0 { + logx.Infof("所有数据已处理完成") + break + } + + // 更新影响行数(在事务外更新,避免重复计算) + actualBatchSize := int64(len(batchQueries)) + cleanupLog.AffectedRows += actualBatchSize + batchCount++ + logx.Infof("批次 %d 处理完成,本批次删除 %d 条记录,累计删除 %d 条记录", batchCount, actualBatchSize, cleanupLog.AffectedRows) + + // 如果本批次查询到的数据少于批次大小,说明已经处理完所有数据 + if actualBatchSize < int64(batchSize) { + logx.Infof("所有数据已处理完成(本批次数据量少于批次大小)") + break + } + } + + // 更新清理日志状态为成功 + l.updateCleanupLogStatus(taskCtx, cleanupLogId, cleanupLog, nil) + + duration := time.Since(startTime) + logx.Infof("%s - 查询数据清理完成,共处理 %d 批次,删除 %d 条记录,耗时 %v", + now.Format("2006-01-02 15:04:05"), batchCount, cleanupLog.AffectedRows, duration) + return nil +} + +// updateCleanupLogStatus 更新清理日志状态 +func (l *CleanQueryDataHandler) updateCleanupLogStatus(ctx context.Context, logId int64, cleanupLog *model.QueryCleanupLog, err error) { + err = l.svcCtx.QueryCleanupLogModel.Trans(ctx, func(updateCtx context.Context, updateSession sqlx.Session) error { + // 查询当前日志记录 + currentLog, findErr := l.svcCtx.QueryCleanupLogModel.FindOne(updateCtx, logId) + if findErr != nil { + return findErr + } + + // 更新状态和影响行数 + currentLog.AffectedRows = cleanupLog.AffectedRows + if err != nil { + currentLog.Status = 2 // 失败 + currentLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true} + } else { + currentLog.Status = 1 // 成功 + } + + _, updateErr := l.svcCtx.QueryCleanupLogModel.Update(updateCtx, updateSession, currentLog) + return updateErr }) if err != nil { - logx.Errorf("%s - 清理查询数据失败: %v", now.Format("2006-01-02 15:04:05"), err) - return err + logx.Errorf("更新清理日志状态失败: %v", err) } - - logx.Infof("%s - 查询数据清理完成,共删除 %d 条记录", now.Format("2006-01-02 15:04:05"), cleanupLog.AffectedRows) - return nil } diff --git a/app/main/api/internal/queue/paySuccessNotify.go b/app/main/api/internal/queue/paySuccessNotify.go index 7263e1b..d8d20da 100644 --- a/app/main/api/internal/queue/paySuccessNotify.go +++ b/app/main/api/internal/queue/paySuccessNotify.go @@ -2,17 +2,17 @@ package queue import ( "context" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/pkg/lzkit/crypto" - "ycc-server/pkg/lzkit/lzUtils" "encoding/hex" "encoding/json" "fmt" "os" "regexp" "strings" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/app/main/model" + "ycc-server/pkg/lzkit/crypto" + "ycc-server/pkg/lzkit/lzUtils" "github.com/hibiken/asynq" "github.com/zeromicro/go-zero/core/logx" @@ -40,7 +40,9 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. order, err := l.svcCtx.OrderModel.FindOne(ctx, payload.OrderID) if err != nil { - return fmt.Errorf("无效的订单ID: %d, %v", payload.OrderID, err) + // 订单不存在,记录详细日志并跳过重试 + logx.Errorf("支付成功通知任务失败:订单不存在,订单ID: %d, 错误: %v", payload.OrderID, err) + return asynq.SkipRetry // 订单不存在时跳过重试,避免重复失败 } env := os.Getenv("ENV") if order.Status != "paid" && env != "development" { @@ -139,17 +141,39 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. } } - // 调用API请求服务 - combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) - if err != nil { - return l.handleError(ctx, err, order, query) - } - // 加密返回响应 - encryptData, aesEncryptErr := crypto.AesEncrypt(combinedResponse, key) - if aesEncryptErr != nil { - err = fmt.Errorf("加密响应信息失败: %v", aesEncryptErr) - return l.handleError(ctx, err, order, query) + // 检查是否为空报告模式(开发环境) + isEmptyReportMode := env == "development" && order.PaymentPlatform == "test" + + var encryptData string + if isEmptyReportMode { + // 空报告模式:生成空的报告数据,跳过API调用 + logx.Infof("空报告模式:订单 %s (ID: %d) 跳过API调用,生成空报告", order.OrderNo, order.Id) + + // 生成空报告数据结构(根据实际报告格式生成) + emptyReportData := []byte(`[]`) // 空数组,表示没有数据 + + // 加密空报告数据 + encryptedEmptyData, aesEncryptErr := crypto.AesEncrypt(emptyReportData, key) + if aesEncryptErr != nil { + err = fmt.Errorf("加密空报告数据失败: %v", aesEncryptErr) + return l.handleError(ctx, err, order, query) + } + encryptData = encryptedEmptyData + } else { + // 正常模式:调用API请求服务 + combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) + if err != nil { + return l.handleError(ctx, err, order, query) + } + // 加密返回响应 + encryptedResponse, aesEncryptErr := crypto.AesEncrypt(combinedResponse, key) + if aesEncryptErr != nil { + err = fmt.Errorf("加密响应信息失败: %v", aesEncryptErr) + return l.handleError(ctx, err, order, query) + } + encryptData = encryptedResponse } + query.QueryData = lzUtils.StringToNullString(encryptData) updateErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) if updateErr != nil { @@ -164,9 +188,10 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. return l.handleError(ctx, updateQueryErr, order, query) } - err = l.svcCtx.AgentService.AgentProcess(ctx, order) - if err != nil { - return l.handleError(ctx, err, order, query) + // 报告生成成功后,发送代理处理异步任务(不阻塞报告流程) + if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil { + // 代理处理任务发送失败,只记录日志,不影响报告流程 + logx.Errorf("发送代理处理任务失败,订单ID: %d, 错误: %v", order.Id, asyncErr) } _, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey) diff --git a/app/main/api/internal/queue/routes.go b/app/main/api/internal/queue/routes.go index 93c22ec..a31611f 100644 --- a/app/main/api/internal/queue/routes.go +++ b/app/main/api/internal/queue/routes.go @@ -2,9 +2,9 @@ package queue import ( "context" + "fmt" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" - "fmt" "github.com/hibiken/asynq" ) @@ -24,17 +24,30 @@ func NewCronJob(ctx context.Context, svcCtx *svc.ServiceContext) *CronJob { func (l *CronJob) Register() *asynq.ServeMux { redisClientOpt := asynq.RedisClientOpt{Addr: l.svcCtx.Config.CacheRedis[0].Host, Password: l.svcCtx.Config.CacheRedis[0].Pass} scheduler := asynq.NewScheduler(redisClientOpt, nil) + + // 注册数据清理定时任务(每天凌晨3点) task := asynq.NewTask(types.MsgCleanQueryData, nil, nil) _, err := scheduler.Register(TASKTIME, task) if err != nil { panic(fmt.Sprintf("定时任务注册失败:%v", err)) } + + // 注册解冻佣金扫描定时任务(每2小时执行一次) + unfreezeScanTask := asynq.NewTask(types.MsgUnfreezeCommissionScan, nil, nil) + _, err = scheduler.Register("0 */2 * * *", unfreezeScanTask) // 每2小时执行一次(每小时的第0分钟) + if err != nil { + panic(fmt.Sprintf("解冻佣金扫描定时任务注册失败:%v", err)) + } + scheduler.Start() fmt.Println("定时任务启动!!!") mux := asynq.NewServeMux() mux.Handle(types.MsgPaySuccessQuery, NewPaySuccessNotifyUserHandler(l.svcCtx)) mux.Handle(types.MsgCleanQueryData, NewCleanQueryDataHandler(l.svcCtx)) + mux.Handle(types.MsgAgentProcess, NewAgentProcessHandler(l.svcCtx)) + mux.Handle(types.MsgUnfreezeCommission, NewUnfreezeCommissionHandler(l.svcCtx)) + mux.Handle(types.MsgUnfreezeCommissionScan, NewUnfreezeCommissionScanHandler(l.svcCtx)) return mux } diff --git a/app/main/api/internal/queue/unfreezeCommission.go b/app/main/api/internal/queue/unfreezeCommission.go new file mode 100644 index 0000000..9e533ca --- /dev/null +++ b/app/main/api/internal/queue/unfreezeCommission.go @@ -0,0 +1,94 @@ +package queue + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + "ycc-server/app/main/model" + "ycc-server/pkg/lzkit/lzUtils" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + + "github.com/hibiken/asynq" + pkgerrors "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type UnfreezeCommissionHandler struct { + svcCtx *svc.ServiceContext +} + +func NewUnfreezeCommissionHandler(svcCtx *svc.ServiceContext) *UnfreezeCommissionHandler { + return &UnfreezeCommissionHandler{ + svcCtx: svcCtx, + } +} + +func (l *UnfreezeCommissionHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + var payload types.MsgUnfreezeCommissionPayload + if err := json.Unmarshal(t.Payload(), &payload); err != nil { + return fmt.Errorf("解析解冻任务负载失败: %w", err) + } + + // 1. 查询冻结任务 + freezeTask, err := l.svcCtx.AgentFreezeTaskModel.FindOne(ctx, payload.FreezeTaskId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + logx.Errorf("解冻任务失败,冻结任务不存在: freezeTaskId=%d", payload.FreezeTaskId) + return asynq.SkipRetry // 任务不存在,跳过重试 + } + return fmt.Errorf("查询冻结任务失败: freezeTaskId=%d, err=%w", payload.FreezeTaskId, err) + } + + // 2. 检查任务状态 + if freezeTask.Status != 1 { + logx.Infof("解冻任务跳过,任务已处理: freezeTaskId=%d, status=%d", payload.FreezeTaskId, freezeTask.Status) + return nil // 任务已处理,不重试 + } + + // 3. 检查解冻时间是否已到 + if time.Now().Before(freezeTask.UnfreezeTime) { + logx.Infof("解冻任务跳过,未到解冻时间: freezeTaskId=%d, unfreezeTime=%v", payload.FreezeTaskId, freezeTask.UnfreezeTime) + // 重新发送延迟任务 + if err := l.svcCtx.AsynqService.SendUnfreezeTask(payload.FreezeTaskId, freezeTask.UnfreezeTime); err != nil { + logx.Errorf("重新发送解冻任务失败: freezeTaskId=%d, err=%v", payload.FreezeTaskId, err) + } + return nil + } + + // 4. 使用事务处理解冻 + err = l.svcCtx.AgentFreezeTaskModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { + // 4.1 更新冻结任务状态 + freezeTask.Status = 2 // 已解冻 + freezeTask.ActualUnfreezeTime = lzUtils.TimeToNullTime(time.Now()) + if updateErr := l.svcCtx.AgentFreezeTaskModel.UpdateWithVersion(transCtx, session, freezeTask); updateErr != nil { + return pkgerrors.Wrapf(updateErr, "更新冻结任务状态失败") + } + + // 4.2 更新钱包(解冻余额) + wallet, walletErr := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, freezeTask.AgentId) + if walletErr != nil { + return pkgerrors.Wrapf(walletErr, "查询钱包失败, agentId: %d", freezeTask.AgentId) + } + + wallet.FrozenBalance -= freezeTask.FreezeAmount + wallet.Balance += freezeTask.FreezeAmount + if updateWalletErr := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); updateWalletErr != nil { + return pkgerrors.Wrapf(updateWalletErr, "更新钱包失败") + } + + return nil + }) + + if err != nil { + logx.Errorf("解冻任务处理失败: freezeTaskId=%d, err=%v", payload.FreezeTaskId, err) + return fmt.Errorf("解冻任务处理失败: freezeTaskId=%d, err=%w", payload.FreezeTaskId, err) + } + + logx.Infof("解冻任务处理成功: freezeTaskId=%d, agentId=%d, amount=%.2f", payload.FreezeTaskId, freezeTask.AgentId, freezeTask.FreezeAmount) + return nil +} diff --git a/app/main/api/internal/queue/unfreezeCommissionScan.go b/app/main/api/internal/queue/unfreezeCommissionScan.go new file mode 100644 index 0000000..b82cf00 --- /dev/null +++ b/app/main/api/internal/queue/unfreezeCommissionScan.go @@ -0,0 +1,233 @@ +package queue + +import ( + "context" + "strings" + "sync" + "time" + "ycc-server/app/main/model" + "ycc-server/pkg/lzkit/lzUtils" + + "ycc-server/app/main/api/internal/svc" + + "github.com/hibiken/asynq" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// UnfreezeCommissionScanHandler 定时扫描解冻任务处理器 +type UnfreezeCommissionScanHandler struct { + svcCtx *svc.ServiceContext +} + +func NewUnfreezeCommissionScanHandler(svcCtx *svc.ServiceContext) *UnfreezeCommissionScanHandler { + return &UnfreezeCommissionScanHandler{ + svcCtx: svcCtx, + } +} + +// ProcessTask 定时扫描需要解冻的任务 +func (l *UnfreezeCommissionScanHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + scanStartTime := time.Now() + now := time.Now() + logx.Infof("开始扫描需要解冻的佣金任务,当前时间: %v", now) + + // 1. 查询所有待解冻且解冻时间已到的任务 + // 使用索引 idx_status 和 idx_unfreeze_time 优化查询 + // 不限制查询数量,找到所有需要解冻的任务 + builder := l.svcCtx.AgentFreezeTaskModel.SelectBuilder(). + Where("status = ? AND unfreeze_time <= ? AND del_state = ?", 1, now, 0). // 1=待解冻,0=未删除 + OrderBy("unfreeze_time ASC") // 按解冻时间升序,优先处理最早的任务 + + freezeTasks, err := l.svcCtx.AgentFreezeTaskModel.FindAll(ctx, builder, "") + if err != nil { + logx.Errorf("查询待解冻任务失败: %v", err) + return errors.Wrapf(err, "查询待解冻任务失败") + } + + // 如果没有需要解冻的任务,直接返回(不创建任何记录,只记录日志) + if len(freezeTasks) == 0 { + scanDuration := time.Since(scanStartTime) + logx.Infof("没有需要解冻的任务,扫描耗时: %v", scanDuration) + return nil + } + + // 2. 批次大小限制:如果任务量过大,分批处理 + const maxBatchSize = 1000 + originalCount := len(freezeTasks) + if len(freezeTasks) > maxBatchSize { + logx.Errorf("任务数量过多(%d),本次只处理前%d个,剩余将在下次扫描处理", len(freezeTasks), maxBatchSize) + freezeTasks = freezeTasks[:maxBatchSize] + } + + logx.Infof("找到 %d 个需要解冻的任务(原始数量: %d),开始处理(最多同时处理2个)", len(freezeTasks), originalCount) + + // 3. 并发控制:使用信号量限制最多同时处理2个任务 + const maxConcurrency = 2 // 最多同时处理2个任务 + const taskTimeout = 30 * time.Second // 每个任务30秒超时 + semaphore := make(chan struct{}, maxConcurrency) // 信号量通道 + var wg sync.WaitGroup + var mu sync.Mutex // 保护计数器的互斥锁 + successCount := 0 + failCount := 0 + skipCount := 0 // 跳过的任务数(已处理、时间未到等) + + // 4. 并发处理所有任务,但最多同时处理2个 + for _, freezeTask := range freezeTasks { + // 检查是否被取消(优雅关闭支持) + select { + case <-ctx.Done(): + logx.Infof("扫描任务被取消,已处理: 成功=%d, 失败=%d, 跳过=%d", successCount, failCount, skipCount) + return ctx.Err() + default: + // 继续处理 + } + + wg.Add(1) + semaphore <- struct{}{} // 获取信号量,如果已满2个则阻塞 + + go func(task *model.AgentFreezeTask) { + defer wg.Done() + defer func() { <-semaphore }() // 释放信号量 + + taskStartTime := time.Now() + + // 为每个任务设置超时控制 + taskCtx, cancel := context.WithTimeout(ctx, taskTimeout) + defer cancel() + + // 使用事务处理每个任务,确保原子性 + err := l.svcCtx.AgentFreezeTaskModel.Trans(taskCtx, func(transCtx context.Context, session sqlx.Session) error { + // 4.1 重新查询任务(使用乐观锁,确保并发安全) + currentTask, err := l.svcCtx.AgentFreezeTaskModel.FindOne(transCtx, task.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + logx.Infof("冻结任务不存在,可能已被处理: freezeTaskId=%d", task.Id) + return nil // 任务不存在,跳过 + } + return errors.Wrapf(err, "查询冻结任务失败, freezeTaskId: %d", task.Id) + } + + // 4.2 幂等性增强:检查是否已经解冻过(通过 actual_unfreeze_time) + if currentTask.ActualUnfreezeTime.Valid { + logx.Infof("任务已解冻,跳过: freezeTaskId=%d, actualUnfreezeTime=%v", task.Id, currentTask.ActualUnfreezeTime.Time) + return nil // 已解冻,跳过 + } + + // 4.3 检查任务状态(双重检查,防止并发处理) + if currentTask.Status != 1 { + logx.Infof("冻结任务状态已变更,跳过处理: freezeTaskId=%d, status=%d", task.Id, currentTask.Status) + return nil // 状态已变更,跳过 + } + + // 4.4 再次检查解冻时间(防止时间判断误差) + nowTime := time.Now() + if nowTime.Before(currentTask.UnfreezeTime) { + logx.Infof("冻结任务解冻时间未到,跳过处理: freezeTaskId=%d, unfreezeTime=%v", task.Id, currentTask.UnfreezeTime) + return nil // 时间未到,跳过 + } + + // 4.5 计算延迟时间(便于监控) + delay := nowTime.Sub(currentTask.UnfreezeTime) + if delay > 1*time.Hour { + logx.Errorf("解冻任务延迟处理: freezeTaskId=%d, 延迟=%v, unfreezeTime=%v", task.Id, delay, currentTask.UnfreezeTime) + } + + // 4.6 更新冻结任务状态 + currentTask.Status = 2 // 已解冻 + currentTask.ActualUnfreezeTime = lzUtils.TimeToNullTime(nowTime) + if updateErr := l.svcCtx.AgentFreezeTaskModel.UpdateWithVersion(transCtx, session, currentTask); updateErr != nil { + return errors.Wrapf(updateErr, "更新冻结任务状态失败, freezeTaskId: %d", task.Id) + } + + // 4.7 更新钱包(解冻余额) + wallet, walletErr := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, currentTask.AgentId) + if walletErr != nil { + return errors.Wrapf(walletErr, "查询钱包失败, agentId: %d", currentTask.AgentId) + } + + // 检查冻结余额是否足够(防止数据异常) + if wallet.FrozenBalance < currentTask.FreezeAmount { + logx.Errorf("钱包冻结余额不足,数据异常: freezeTaskId=%d, agentId=%d, frozenBalance=%.2f, freezeAmount=%.2f", + task.Id, currentTask.AgentId, wallet.FrozenBalance, currentTask.FreezeAmount) + return errors.Errorf("钱包冻结余额不足: agentId=%d, frozenBalance=%.2f, freezeAmount=%.2f", + currentTask.AgentId, wallet.FrozenBalance, currentTask.FreezeAmount) + } + + wallet.FrozenBalance -= currentTask.FreezeAmount + wallet.Balance += currentTask.FreezeAmount + if updateWalletErr := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); updateWalletErr != nil { + return errors.Wrapf(updateWalletErr, "更新钱包失败, agentId: %d", currentTask.AgentId) + } + + // 更详细的日志(包含更多上下文信息) + logx.Infof("解冻任务处理成功: freezeTaskId=%d, agentId=%d, amount=%.2f, orderPrice=%.2f, freezeTime=%v, unfreezeTime=%v, delay=%v", + task.Id, currentTask.AgentId, currentTask.FreezeAmount, currentTask.OrderPrice, + currentTask.FreezeTime, currentTask.UnfreezeTime, delay) + return nil + }) + + // 记录处理时间 + taskDuration := time.Since(taskStartTime) + if taskDuration > 5*time.Second { + logx.Errorf("解冻任务处理耗时较长: freezeTaskId=%d, duration=%v", task.Id, taskDuration) + } + + // 更新计数器(需要加锁保护) + mu.Lock() + if err != nil { + // 错误分类处理 + if isTemporaryError(err) { + // 临时错误(如超时、网络问题),记录但继续处理其他任务 + failCount++ + logx.Errorf("解冻任务临时失败,将在下次扫描重试: freezeTaskId=%d, duration=%v, err=%v", task.Id, taskDuration, err) + } else { + // 永久错误(如数据异常),记录详细日志 + failCount++ + logx.Errorf("解冻任务永久失败: freezeTaskId=%d, duration=%v, err=%v", task.Id, taskDuration, err) + } + } else { + successCount++ + logx.Infof("解冻任务处理完成: freezeTaskId=%d, duration=%v", task.Id, taskDuration) + } + mu.Unlock() + + }(freezeTask) + } + + // 5. 等待所有任务完成 + wg.Wait() + + // 6. 记录扫描统计信息 + scanDuration := time.Since(scanStartTime) + logx.Infof("解冻任务扫描完成: 成功=%d, 失败=%d, 跳过=%d, 总计=%d, 扫描耗时=%v", + successCount, failCount, skipCount, len(freezeTasks), scanDuration) + + return nil +} + +// isTemporaryError 判断是否为临时错误(可以重试的错误) +func isTemporaryError(err error) bool { + if err == nil { + return false + } + + errStr := err.Error() + // 超时错误 + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + return true + } + // 网络相关错误 + errStrLower := strings.ToLower(errStr) + if strings.Contains(errStrLower, "timeout") || strings.Contains(errStrLower, "connection") || strings.Contains(errStrLower, "network") { + return true + } + // 数据库连接错误 + if strings.Contains(errStrLower, "connection pool") || strings.Contains(errStrLower, "too many connections") { + return true + } + + // 其他错误视为永久错误(如数据异常、业务逻辑错误等) + return false +} diff --git a/app/main/api/internal/service/adminPromotionLinkStatsService.go b/app/main/api/internal/service/adminPromotionLinkStatsService.go deleted file mode 100644 index be792df..0000000 --- a/app/main/api/internal/service/adminPromotionLinkStatsService.go +++ /dev/null @@ -1,211 +0,0 @@ -package service - -import ( - "context" - "database/sql" - "time" - - "ycc-server/app/main/model" - "ycc-server/common/xerr" - - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -type AdminPromotionLinkStatsService struct { - logx.Logger - AdminPromotionLinkModel model.AdminPromotionLinkModel - AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel - AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel -} - -func NewAdminPromotionLinkStatsService( - AdminPromotionLinkModel model.AdminPromotionLinkModel, - AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel, - AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel, -) *AdminPromotionLinkStatsService { - return &AdminPromotionLinkStatsService{ - Logger: logx.WithContext(context.Background()), - AdminPromotionLinkModel: AdminPromotionLinkModel, - AdminPromotionLinkStatsTotalModel: AdminPromotionLinkStatsTotalModel, - AdminPromotionLinkStatsHistoryModel: AdminPromotionLinkStatsHistoryModel, - } -} - -// ensureTotalStats 确保总统计记录存在,如果不存在则创建 -func (s *AdminPromotionLinkStatsService) ensureTotalStats(ctx context.Context, session sqlx.Session, linkId int64) (*model.AdminPromotionLinkStatsTotal, error) { - totalStats, err := s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - // 如果记录不存在,创建新记录 - totalStats = &model.AdminPromotionLinkStatsTotal{ - LinkId: linkId, - ClickCount: 0, - PayCount: 0, - PayAmount: 0, - } - _, err = s.AdminPromotionLinkStatsTotalModel.Insert(ctx, session, totalStats) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建总统计记录失败: %+v", err) - } - // 重新获取创建后的记录 - totalStats, err = s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的总统计记录失败: %+v", err) - } - } else { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询总统计失败: %+v", err) - } - } - return totalStats, nil -} - -// ensureHistoryStats 确保历史统计记录存在,如果不存在则创建 -func (s *AdminPromotionLinkStatsService) ensureHistoryStats(ctx context.Context, session sqlx.Session, linkId int64, today time.Time) (*model.AdminPromotionLinkStatsHistory, error) { - historyStats, err := s.AdminPromotionLinkStatsHistoryModel.FindOneByLinkIdStatsDate(ctx, linkId, today) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - // 如果记录不存在,创建新记录 - historyStats = &model.AdminPromotionLinkStatsHistory{ - LinkId: linkId, - StatsDate: today, - ClickCount: 0, - PayCount: 0, - PayAmount: 0, - } - _, err = s.AdminPromotionLinkStatsHistoryModel.Insert(ctx, session, historyStats) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建今日统计记录失败: %+v", err) - } - // 重新获取创建后的记录 - historyStats, err = s.AdminPromotionLinkStatsHistoryModel.FindOneByLinkIdStatsDate(ctx, linkId, today) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的今日统计记录失败: %+v", err) - } - } else { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询今日统计记录失败: %+v", err) - } - } - return historyStats, nil -} - -// UpdateLinkStats 更新推广链接统计 -func (s *AdminPromotionLinkStatsService) UpdateLinkStats(ctx context.Context, linkId int64) error { - return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { - // 确保总统计记录存在 - totalStats, err := s.ensureTotalStats(ctx, session, linkId) - if err != nil { - return err - } - - // 更新总统计 - totalStats.ClickCount++ - totalStats.LastClickTime = sql.NullTime{Time: time.Now(), Valid: true} - err = s.AdminPromotionLinkStatsTotalModel.UpdateWithVersion(ctx, session, totalStats) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新总统计失败: %+v", err) - } - - // 确保历史统计记录存在 - now := time.Now() - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) - historyStats, err := s.ensureHistoryStats(ctx, session, linkId, today) - if err != nil { - return err - } - - // 更新历史统计 - historyStats.ClickCount++ - historyStats.LastClickTime = sql.NullTime{Time: time.Now(), Valid: true} - err = s.AdminPromotionLinkStatsHistoryModel.UpdateWithVersion(ctx, session, historyStats) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新历史统计失败: %+v", err) - } - - return nil - }) -} - -// UpdatePaymentStats 更新付费统计 -func (s *AdminPromotionLinkStatsService) UpdatePaymentStats(ctx context.Context, linkId int64, amount float64) error { - return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { - // 确保总统计记录存在 - totalStats, err := s.ensureTotalStats(ctx, session, linkId) - if err != nil { - return err - } - - // 更新总统计 - totalStats.PayCount++ - totalStats.PayAmount += amount - totalStats.LastPayTime = sql.NullTime{Time: time.Now(), Valid: true} - err = s.AdminPromotionLinkStatsTotalModel.UpdateWithVersion(ctx, session, totalStats) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新总统计失败: %+v", err) - } - - // 确保历史统计记录存在 - now := time.Now() - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) - historyStats, err := s.ensureHistoryStats(ctx, session, linkId, today) - if err != nil { - return err - } - - // 更新历史统计 - historyStats.PayCount++ - historyStats.PayAmount += amount - historyStats.LastPayTime = sql.NullTime{Time: time.Now(), Valid: true} - err = s.AdminPromotionLinkStatsHistoryModel.UpdateWithVersion(ctx, session, historyStats) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新历史统计失败: %+v", err) - } - - return nil - }) -} - -// CreateLinkStats 创建新的推广链接统计记录 -func (s *AdminPromotionLinkStatsService) CreateLinkStats(ctx context.Context, linkId int64) error { - return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { - // 检查总统计记录是否已存在 - _, err := s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId) - if err == nil { - // 记录已存在,不需要创建 - return nil - } - if err != model.ErrNotFound { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询总统计记录失败: %+v", err) - } - - // 创建总统计记录 - totalStats := &model.AdminPromotionLinkStatsTotal{ - LinkId: linkId, - ClickCount: 0, - PayCount: 0, - PayAmount: 0, - } - _, err = s.AdminPromotionLinkStatsTotalModel.Insert(ctx, session, totalStats) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建总统计记录失败: %+v", err) - } - - // 创建今日历史统计记录 - now := time.Now() - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) - historyStats := &model.AdminPromotionLinkStatsHistory{ - LinkId: linkId, - StatsDate: today, - ClickCount: 0, - PayCount: 0, - PayAmount: 0, - } - _, err = s.AdminPromotionLinkStatsHistoryModel.Insert(ctx, session, historyStats) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建历史统计记录失败: %+v", err) - } - - return nil - }) -} diff --git a/app/main/api/internal/service/agentService.go b/app/main/api/internal/service/agentService.go index 5d39d23..c30b42a 100644 --- a/app/main/api/internal/service/agentService.go +++ b/app/main/api/internal/service/agentService.go @@ -3,6 +3,7 @@ package service import ( "context" "database/sql" + "fmt" "strconv" "time" "ycc-server/app/main/api/internal/config" @@ -17,21 +18,22 @@ import ( // AgentService 新代理系统服务 type AgentService struct { - config config.Config - OrderModel model.OrderModel - AgentModel model.AgentModel - AgentWalletModel model.AgentWalletModel - AgentRelationModel model.AgentRelationModel - AgentLinkModel model.AgentLinkModel - AgentOrderModel model.AgentOrderModel - AgentCommissionModel model.AgentCommissionModel - AgentRebateModel model.AgentRebateModel - AgentUpgradeModel model.AgentUpgradeModel - AgentWithdrawalModel model.AgentWithdrawalModel - AgentConfigModel model.AgentConfigModel + config config.Config + OrderModel model.OrderModel + AgentModel model.AgentModel + AgentWalletModel model.AgentWalletModel + AgentRelationModel model.AgentRelationModel + AgentLinkModel model.AgentLinkModel + AgentOrderModel model.AgentOrderModel + AgentCommissionModel model.AgentCommissionModel + AgentRebateModel model.AgentRebateModel + AgentUpgradeModel model.AgentUpgradeModel + AgentWithdrawalModel model.AgentWithdrawalModel + AgentConfigModel model.AgentConfigModel AgentProductConfigModel model.AgentProductConfigModel - AgentRealNameModel model.AgentRealNameModel + AgentRealNameModel model.AgentRealNameModel AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel + AgentFreezeTaskModel model.AgentFreezeTaskModel // 冻结任务模型(需要先运行SQL并生成model) } // NewAgentService 创建新的代理服务 @@ -51,23 +53,25 @@ func NewAgentService( agentProductConfigModel model.AgentProductConfigModel, agentRealNameModel model.AgentRealNameModel, agentWithdrawalTaxModel model.AgentWithdrawalTaxModel, + agentFreezeTaskModel model.AgentFreezeTaskModel, // 冻结任务模型(需要先运行SQL并生成model) ) *AgentService { return &AgentService{ - config: c, - OrderModel: orderModel, - AgentModel: agentModel, - AgentWalletModel: agentWalletModel, - AgentRelationModel: agentRelationModel, - AgentLinkModel: agentLinkModel, - AgentOrderModel: agentOrderModel, - AgentCommissionModel: agentCommissionModel, - AgentRebateModel: agentRebateModel, - AgentUpgradeModel: agentUpgradeModel, - AgentWithdrawalModel: agentWithdrawalModel, - AgentConfigModel: agentConfigModel, + config: c, + OrderModel: orderModel, + AgentModel: agentModel, + AgentWalletModel: agentWalletModel, + AgentRelationModel: agentRelationModel, + AgentLinkModel: agentLinkModel, + AgentOrderModel: agentOrderModel, + AgentCommissionModel: agentCommissionModel, + AgentRebateModel: agentRebateModel, + AgentUpgradeModel: agentUpgradeModel, + AgentWithdrawalModel: agentWithdrawalModel, + AgentConfigModel: agentConfigModel, AgentProductConfigModel: agentProductConfigModel, - AgentRealNameModel: agentRealNameModel, + AgentRealNameModel: agentRealNameModel, AgentWithdrawalTaxModel: agentWithdrawalTaxModel, + AgentFreezeTaskModel: agentFreezeTaskModel, } } @@ -96,27 +100,42 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err return errors.Wrapf(err, "查询代理信息失败, agentId: %d", agentOrder.AgentId) } - // 4. 获取系统配置 - basePrice, err := s.getConfigFloat(ctx, "base_price") + // 4. 获取产品配置(必须存在) + productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId) if err != nil { - return errors.Wrapf(err, "获取基础底价配置失败") + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "产品配置不存在, productId: %d,请先在后台配置产品价格参数", order.ProductId) + } + return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId) } - // 6. 使用事务处理订单 - return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { - // 6.1 计算实际底价和代理收益 - levelBonus := s.getLevelBonus(agent.Level) + // 5. 使用事务处理订单 + err = s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { + // 5.1 获取等级加成 + levelBonus, err := s.getLevelBonus(transCtx, agent.Level) + if err != nil { + return errors.Wrapf(err, "获取等级加成配置失败") + } + + // 5.2 使用产品配置的底价计算实际底价 + basePrice := productConfig.BasePrice actualBasePrice := basePrice + float64(levelBonus) - // 6.2 计算提价成本 - priceThreshold, _ := s.getConfigFloat(ctx, "price_threshold") - priceFeeRate, _ := s.getConfigFloat(ctx, "price_fee_rate") + // 5.3 计算提价成本(使用产品配置) + priceThreshold := 0.0 + priceFeeRate := 0.0 + if productConfig.PriceThreshold.Valid { + priceThreshold = productConfig.PriceThreshold.Float64 + } + if productConfig.PriceFeeRate.Valid { + priceFeeRate = productConfig.PriceFeeRate.Float64 + } priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate) - // 6.3 计算代理收益 + // 5.4 计算代理收益 agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost - // 6.4 更新代理订单记录 + // 5.5 更新代理订单记录 agentOrder.ProcessStatus = 1 agentOrder.ProcessTime = lzUtils.TimeToNullTime(time.Now()) agentOrder.ProcessRemark = lzUtils.StringToNullString("处理成功") @@ -124,12 +143,14 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err return errors.Wrapf(err, "更新代理订单失败") } - // 6.5 发放代理佣金 - if err := s.giveAgentCommission(transCtx, session, agentOrder.AgentId, order.Id, order.ProductId, agentProfit); err != nil { - return errors.Wrapf(err, "发放代理佣金失败") + // 5.6 发放代理佣金(传入订单单价用于冻结判断) + // 注意:冻结任务会在 agentProcess.go 中通过查询订单的冻结任务来发送解冻任务 + _, commissionErr := s.giveAgentCommission(transCtx, session, agentOrder.AgentId, order.Id, order.ProductId, agentProfit, agentOrder.SetPrice) + if commissionErr != nil { + return errors.Wrapf(commissionErr, "发放代理佣金失败") } - // 6.6 分配等级加成返佣给上级链 + // 5.7 分配等级加成返佣给上级链 if levelBonus > 0 { if err := s.distributeLevelBonus(transCtx, session, agent, order.Id, order.ProductId, float64(levelBonus), levelBonus); err != nil { return errors.Wrapf(err, "分配等级加成返佣失败") @@ -138,20 +159,39 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err return nil }) + + return err } -// getLevelBonus 获取等级加成 -func (s *AgentService) getLevelBonus(level int64) int64 { +// getLevelBonus 获取等级加成(从配置表读取) +func (s *AgentService) getLevelBonus(ctx context.Context, level int64) (int64, error) { + var configKey string switch level { case 1: // 普通 - return 6 + configKey = "level_1_bonus" case 2: // 黄金 - return 3 + configKey = "level_2_bonus" case 3: // 钻石 - return 0 + configKey = "level_3_bonus" default: - return 0 + return 0, nil } + + bonus, err := s.getConfigFloat(ctx, configKey) + if err != nil { + // 配置不存在时返回默认值 + logx.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v,使用默认值", level, configKey, err) + switch level { + case 1: + return 6, nil + case 2: + return 3, nil + case 3: + return 0, nil + } + return 0, nil + } + return int64(bonus), nil } // calculatePriceCost 计算提价成本 @@ -163,7 +203,9 @@ func (s *AgentService) calculatePriceCost(setPrice, priceThreshold, priceFeeRate } // giveAgentCommission 发放代理佣金 -func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Session, agentId, orderId, productId int64, amount float64) error { +// orderPrice: 订单单价,用于判断是否需要冻结 +// 返回:freezeTaskId(如果有冻结任务),error +func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Session, agentId, orderId, productId int64, amount float64, orderPrice float64) (int64, error) { // 1. 创建佣金记录 commission := &model.AgentCommission{ AgentId: agentId, @@ -172,23 +214,94 @@ func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Ses Amount: amount, Status: 1, // 已发放 } - if _, err := s.AgentCommissionModel.Insert(ctx, session, commission); err != nil { - return errors.Wrapf(err, "创建佣金记录失败") + commissionResult, err := s.AgentCommissionModel.Insert(ctx, session, commission) + if err != nil { + return 0, errors.Wrapf(err, "创建佣金记录失败") + } + commissionId, _ := commissionResult.LastInsertId() + + // 2. 判断是否需要冻结 + // 2.1 获取冻结阈值配置(默认100元) + freezeThreshold, err := s.getConfigFloat(ctx, "commission_freeze_threshold") + if err != nil { + // 配置不存在时使用默认值100元 + freezeThreshold = 100.0 + logx.Errorf("获取冻结阈值配置失败,使用默认值100元, orderId: %d, err: %v", orderId, err) } - // 2. 更新钱包余额 + // 2.2 判断订单单价是否达到冻结阈值 + freezeAmount := 0.0 + var freezeTaskId int64 = 0 + if orderPrice >= freezeThreshold { + // 2.3 获取冻结比例配置(默认10%) + freezeRatio, err := s.getConfigFloat(ctx, "commission_freeze_ratio") + if err != nil { + // 配置不存在时使用默认值0.1(10%) + freezeRatio = 0.1 + logx.Errorf("获取冻结比例配置失败,使用默认值10%%, orderId: %d, err: %v", orderId, err) + } + + // 计算冻结金额:订单单价的10% + freezeAmountByPrice := orderPrice * freezeRatio + + // 冻结金额不能超过佣金金额 + if freezeAmountByPrice > amount { + freezeAmount = amount + } else { + freezeAmount = freezeAmountByPrice + } + + // 如果冻结金额大于0,创建冻结任务 + if freezeAmount > 0 { + // 2.4 获取解冻天数配置(默认30天,即1个月) + unfreezeDays, err := s.getConfigInt(ctx, "commission_freeze_days") + if err != nil { + // 配置不存在时使用默认值30天 + unfreezeDays = 30 + logx.Errorf("获取解冻天数配置失败,使用默认值30天, orderId: %d, err: %v", orderId, err) + } + // 计算解冻时间(从配置读取的天数后) + // 注意:配置只在创建任务时读取,已创建的任务不受后续配置修改影响 + unfreezeTime := time.Now().AddDate(0, 0, int(unfreezeDays)) + + // 创建冻结任务记录 + freezeTask := &model.AgentFreezeTask{ + AgentId: agentId, + OrderId: orderId, + CommissionId: commissionId, + FreezeAmount: freezeAmount, + OrderPrice: orderPrice, + FreezeRatio: freezeRatio, + Status: 1, // 待解冻 + FreezeTime: time.Now(), + UnfreezeTime: unfreezeTime, + Remark: lzUtils.StringToNullString(fmt.Sprintf("订单单价%.2f元,冻结比例%.2f%%,解冻天数%d天", orderPrice, freezeRatio*100, unfreezeDays)), + } + freezeTaskResult, err := s.AgentFreezeTaskModel.Insert(ctx, session, freezeTask) + if err != nil { + return 0, errors.Wrapf(err, "创建冻结任务失败") + } + freezeTaskId, _ = freezeTaskResult.LastInsertId() + } + } + + // 3. 更新钱包余额 wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, agentId) if err != nil { - return errors.Wrapf(err, "查询钱包失败, agentId: %d", agentId) + return 0, errors.Wrapf(err, "查询钱包失败, agentId: %d", agentId) } - wallet.Balance += amount - wallet.TotalEarnings += amount + // 实际到账金额 = 佣金金额 - 冻结金额 + actualAmount := amount - freezeAmount + + wallet.Balance += actualAmount + wallet.FrozenBalance += freezeAmount + wallet.TotalEarnings += amount // 累计收益包含冻结部分 if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil { - return errors.Wrapf(err, "更新钱包失败") + return 0, errors.Wrapf(err, "更新钱包失败") } - return nil + return freezeTaskId, nil } // distributeLevelBonus 分配等级加成返佣给上级链 @@ -219,7 +332,51 @@ func (s *AgentService) distributeLevelBonus(ctx context.Context, session sqlx.Se return nil } -// distributeNormalAgentBonus 普通代理的等级加成返佣分配(6元) +// distributeNormalAgentBonus 普通代理的等级加成返佣分配 +// +// 功能说明:根据普通代理的直接上级等级,按照规则分配等级加成返佣 +// +// 参数说明: +// - amount: 等级加成总额(例如:6元) +// - levelBonusInt: 等级加成整数(用于记录) +// +// 分配规则总览: +// 1. 直接上级是钻石:等级加成全部给钻石 +// 2. 直接上级是黄金:一部分给黄金(配置:direct_parent_amount_gold,默认3元),剩余给钻石上级 +// 3. 直接上级是普通:一部分给直接上级(配置:direct_parent_amount_normal,默认2元),剩余给钻石/黄金上级 +// +// 覆盖的所有情况: +// +// 情况1:普通(推广人) -> 钻石(直接上级) +// => 全部给钻石 +// +// 情况2:普通(推广人) -> 黄金(直接上级) -> 钻石 +// => 一部分给黄金,剩余给钻石 +// +// 情况3:普通(推广人) -> 黄金(直接上级) -> 无钻石上级 +// => 一部分给黄金,剩余归平台 +// +// 情况4:普通(推广人) -> 普通(直接上级) -> 钻石 +// => 一部分给直接上级普通,剩余全部给钻石 +// +// 情况5:普通(推广人) -> 普通(直接上级) -> 黄金 -> 钻石 +// => 一部分给直接上级普通(例如2元),一部分给黄金(等级加成差减去给普通的,例如3-2=1元),剩余给钻石(例如3元) +// +// 情况6:普通(推广人) -> 普通(直接上级) -> 黄金(无钻石) +// => 一部分给直接上级普通,剩余一部分给黄金(最多3元),超出归平台 +// +// 情况7:普通(推广人) -> 普通(直接上级) -> 普通 -> 钻石 +// => 一部分给直接上级普通,剩余全部给钻石(跳过中间普通代理) +// +// 情况8:普通(推广人) -> 普通(直接上级) -> 普通 -> 黄金(无钻石) +// => 一部分给直接上级普通,剩余一部分给黄金(最多3元),超出归平台(跳过中间普通代理) +// +// 情况9:普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(全部是普通) +// => 一部分给直接上级普通,剩余归平台 +// +// 注意:findDiamondParent 和 findGoldParent 会自动跳过中间的所有普通代理, +// +// 直接向上查找到第一个钻石或黄金代理 func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId int64, amount float64, levelBonusInt int64) error { // 1. 查找直接上级 parent, err := s.findDirectParent(ctx, agent.Id) @@ -232,67 +389,240 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s return nil } - // 2. 给直接上级分配固定金额 - var directParentAmount float64 + // 2. 根据直接上级等级分配 switch parent.Level { - case 3: // 钻石 - directParentAmount = 6 - case 2: // 黄金 - directParentAmount = 3 - case 1: // 普通 - directParentAmount = 2 - default: - directParentAmount = 0 - } + case 3: // 直接上级是钻石代理的情况 + // ========== 直接上级是钻石:等级加成全部给钻石上级 ========== + // 场景示例: + // - 普通(推广人) -> 钻石(直接上级):等级加成6元全部给钻石 + // 说明:如果直接上级就是钻石,不需要再向上查找,全部返佣给直接上级钻石 + // rebateType = 2:表示钻石上级返佣 + return s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, amount, levelBonusInt, 2) - if directParentAmount > 0 { - if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, directParentAmount, levelBonusInt, 1); err != nil { - return errors.Wrapf(err, "给直接上级返佣失败") + case 2: // 直接上级是黄金代理的情况 + // ========== 步骤1:给直接上级黄金代理返佣 ========== + // 配置键:direct_parent_amount_gold(普通代理给直接上级黄金代理的返佣金额) + // 默认值:3.0元 + // 说明:这部分金额给直接上级黄金代理,剩余部分继续向上分配给钻石上级 + goldRebateAmount, err := s.getRebateConfigFloat(ctx, "direct_parent_amount_gold", 3.0) + if err != nil { + logx.Errorf("获取黄金返佣配置失败,使用默认值3元: %v", err) + goldRebateAmount = 3.0 // 配置读取失败时使用默认值3元 } - } - remaining := amount - directParentAmount - if remaining <= 0 { - return nil - } - - // 3. 分配剩余金额 - // 确定查找起点:直接上级是普通时从直接上级开始查找,否则从直接上级的上级开始查找 - searchStart := parent - if parent.Level != 1 { - searchStartParent, err := s.findDirectParent(ctx, parent.Id) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(err, "查找上级的上级失败") + // 计算给直接上级黄金代理的返佣金额(不能超过总等级加成金额) + goldAmount := goldRebateAmount // 默认3元 + if goldAmount > amount { + // 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数) + goldAmount = amount } - if searchStartParent != nil { - searchStart = searchStartParent - } - } - if searchStart != nil { - // 查找上级链中的钻石和黄金 - diamondParent, _ := s.findDiamondParent(ctx, searchStart.Id) - goldParent, _ := s.findGoldParent(ctx, searchStart.Id) - - // 按优先级分配剩余金额 - if diamondParent != nil { - // 优先级1:有钻石,剩余金额全部给钻石 - return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2) - } else if goldParent != nil { - // 优先级2:只有黄金,最多3元给黄金,剩余归平台 - goldAmount := remaining - if goldAmount > 3 { - goldAmount = 3 - } - if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 3); err != nil { + // 发放返佣给直接上级黄金代理 + // rebateType = 1:表示直接上级返佣 + if goldAmount > 0 { + if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 1); err != nil { return errors.Wrapf(err, "给黄金上级返佣失败") } - // 剩余归平台(不需要记录) } - // 优先级3:都没有,剩余金额归平台(不需要记录) - } - return nil + // ========== 步骤2:计算剩余金额并分配给钻石上级 ========== + // 剩余金额 = 总等级加成 - 已给黄金上级的金额 + // 例如:等级加成6元 - 给黄金上级3元 = 剩余3元 + remaining := amount - goldAmount + if remaining > 0 { + // 从黄金上级开始向上查找钻石上级 + // 场景示例: + // - 普通(推广人) -> 黄金(直接上级) -> 钻石:剩余3元给钻石 + // - 普通(推广人) -> 黄金(直接上级) -> 普通 -> 钻石:剩余3元给钻石(跳过中间普通代理) + // - 普通(推广人) -> 黄金(直接上级) -> 无上级:剩余3元归平台(没有钻石上级) + diamondParent, err := s.findDiamondParent(ctx, parent.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查找钻石上级失败") + } + if diamondParent != nil { + // 找到钻石上级,剩余金额全部给钻石上级 + // rebateType = 2:表示钻石上级返佣 + return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2) + } + // 找不到钻石上级,剩余金额归平台(不需要记录) + // 例如:等级加成6元,给黄金3元,剩余3元但找不到钻石上级,则剩余3元归平台 + } + return nil + + case 1: // 直接上级是普通代理的情况 + // ========== 步骤1:给直接上级普通代理返佣 ========== + // 配置键:direct_parent_amount_normal(普通代理给直接上级普通代理的返佣金额) + // 默认值:2.0元 + // 说明:无论后续层级有多少普通代理,这部分金额只给推广人的直接上级 + normalRebateAmount, err := s.getRebateConfigFloat(ctx, "direct_parent_amount_normal", 2.0) + if err != nil { + logx.Errorf("获取普通返佣配置失败,使用默认值2元: %v", err) + normalRebateAmount = 2.0 // 配置读取失败时使用默认值2元 + } + + // 计算给直接上级的返佣金额(不能超过总等级加成金额) + directAmount := normalRebateAmount // 默认2元 + if directAmount > amount { + // 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数) + directAmount = amount + } + + // 发放返佣给直接上级普通代理 + // rebateType = 1:表示直接上级返佣 + if directAmount > 0 { + if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, directAmount, levelBonusInt, 1); err != nil { + return errors.Wrapf(err, "给直接上级返佣失败") + } + } + + // ========== 步骤2:计算剩余金额 ========== + // 剩余金额 = 总等级加成 - 已给直接上级的金额 + // 例如:等级加成6元 - 给直接上级2元 = 剩余4元 + remaining := amount - directAmount + if remaining <= 0 { + // 如果没有剩余,直接返回(所有金额已分配给直接上级) + return nil + } + + // ========== 步骤3:从直接上级开始向上查找钻石和黄金代理 ========== + // 注意:findDiamondParent 和 findGoldParent 会自动跳过中间的所有普通代理 + // 例如: + // - 普通 -> 普通 -> 普通 -> 钻石:会跳过中间的普通代理,直接找到钻石 + // - 普通 -> 普通 -> 黄金 -> 钻石:会找到钻石(优先级更高) + // - 普通 -> 黄金:会找到黄金 + diamondParent, _ := s.findDiamondParent(ctx, parent.Id) // 向上查找钻石上级(跳过所有普通和黄金) + goldParent, _ := s.findGoldParent(ctx, parent.Id) // 向上查找黄金上级(跳过所有普通) + + // ========== 步骤4:按优先级分配剩余金额 ========== + // 优先级规则: + // 1. 优先给钻石上级(如果存在) + // 2. 其次给黄金上级(如果钻石不存在) + // 3. 最后归平台(如果钻石和黄金都不存在) + + if diamondParent != nil { + // ========== 情况A:找到钻石上级 ========== + // 场景示例: + // - 普通(推广人) -> 普通(直接上级) -> 钻石:给普通2元,剩余4元给钻石 + // - 普通(推广人) -> 普通(直接上级) -> 黄金 -> 钻石:给普通2元,给黄金1元,剩余3元给钻石 + // - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通 -> 钻石:给普通2元,剩余4元给钻石(跳过中间普通代理) + // + // 分配规则:当有钻石上级时,需要给黄金上级一部分返佣 + // 1. 等级加成的差 = 普通等级加成 - 黄金等级加成(例如:6元 - 3元 = 3元) + // 2. 从这个差中,先给直接上级普通代理(已给,例如2元) + // 3. 差减去给普通的部分,剩余给黄金代理(例如:3元 - 2元 = 1元) + // 4. 最后剩余部分给钻石(例如:6元 - 2元 - 1元 = 3元) + + // 步骤A1:计算等级加成的差 + // 获取普通代理等级加成(当前代理的等级加成,即amount) + normalBonus := amount // 例如:6元 + // 获取黄金代理等级加成 + goldBonus, err := s.getLevelBonus(ctx, 2) // 黄金等级加成 + if err != nil { + logx.Errorf("获取黄金等级加成配置失败,使用默认值3元: %v", err) + goldBonus = 3 // 默认3元 + } + // 计算等级加成的差 + bonusDiff := normalBonus - float64(goldBonus) // 例如:6元 - 3元 = 3元 + + // 步骤A2:如果有黄金上级,给黄金上级一部分返佣 + // 规则说明: + // - 等级加成的差(bonusDiff)代表普通代理和黄金代理的等级加成差异 + // - 从这个差中,先分配给直接上级普通代理(directAmount,已分配) + // - 剩余的差分配给黄金代理:goldRebateAmount = bonusDiff - directAmount + // - 如果差小于等于已给普通的部分,则不给黄金(这种情况理论上不应该发生,因为差应该>=给普通的部分) + if goldParent != nil && bonusDiff > 0 { + // 计算给黄金上级的金额 = 等级加成差 - 已给普通上级的金额 + // 例如:等级加成差3元 - 已给普通2元 = 给黄金1元 + goldRebateAmount := bonusDiff - directAmount + + // 如果计算出的金额小于等于0,说明差已经被普通代理全部占用了,不给黄金 + // 例如:如果差是2元,已给普通2元,则 goldRebateAmount = 0,不给黄金 + if goldRebateAmount > 0 { + // 边界检查:确保不超过剩余金额(理论上应该不会超过,但保险起见) + if goldRebateAmount > remaining { + goldRebateAmount = remaining + } + + // 发放返佣给黄金上级 + // rebateType = 3:表示黄金上级返佣 + if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldRebateAmount, levelBonusInt, 3); err != nil { + return errors.Wrapf(err, "给黄金上级返佣失败") + } + + // 更新剩余金额(用于后续分配给钻石) + // 例如:剩余4元 - 给黄金1元 = 剩余3元(给钻石) + remaining = remaining - goldRebateAmount + } + } + + // 步骤A3:剩余金额全部给钻石上级 + // rebateType = 2:表示钻石上级返佣 + if remaining > 0 { + return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2) + } + return nil + } else if goldParent != nil { + // ========== 情况B:没有钻石上级,但有黄金上级 ========== + // 场景示例: + // - 普通(推广人) -> 普通(直接上级) -> 黄金(没有钻石):给黄金一部分,剩余归平台 + // - 普通(推广人) -> 普通(直接上级) -> 普通 -> 黄金(没有钻石):给黄金一部分,剩余归平台(跳过中间普通代理) + + // 配置键:max_gold_rebate_amount(普通代理给黄金上级的最大返佣金额) + // 默认值:3.0元 + // 说明:即使剩余金额超过这个值,也只能给黄金上级最多3元,超出部分归平台 + maxGoldRebate, err := s.getRebateConfigFloat(ctx, "max_gold_rebate_amount", 3.0) + if err != nil { + logx.Errorf("获取黄金最大返佣配置失败,使用默认值3元: %v", err) + maxGoldRebate = 3.0 // 配置读取失败时使用默认值3元 + } + + // 计算给黄金上级的返佣金额 + goldAmount := remaining // 剩余金额 + if goldAmount > maxGoldRebate { + // 如果剩余金额超过最大限额,则只给最大限额(例如:剩余4元,但最多只能给3元) + goldAmount = maxGoldRebate + } + // 例如:剩余4元,最大限额3元,则给黄金3元,剩余1元归平台 + + // 发放返佣给黄金上级 + // rebateType = 3:表示黄金上级返佣 + if goldAmount > 0 { + if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 3); err != nil { + return errors.Wrapf(err, "给黄金上级返佣失败") + } + } + // 超出最大限额的部分归平台(不需要记录) + // 例如:剩余4元,给黄金3元,剩余1元归平台 + return nil + } + + // ========== 情况C:既没有钻石上级,也没有黄金上级 ========== + // 场景示例: + // - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(整个链路都是普通代理) + // - 普通(推广人) -> 普通(直接上级) -> 无上级(已经是最顶层) + // 剩余金额全部归平台(不需要记录) + return nil + + default: + // 未知等级,全部归平台 + return nil + } +} + +// getRebateConfigFloat 获取返佣配置值(浮点数),如果配置不存在则返回默认值 +func (s *AgentService) getRebateConfigFloat(ctx context.Context, configKey string, defaultValue float64) (float64, error) { + config, err := s.AgentConfigModel.FindOneByConfigKey(ctx, configKey) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return defaultValue, nil + } + return defaultValue, err + } + value, err := strconv.ParseFloat(config.ConfigValue, 64) + if err != nil { + return defaultValue, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue) + } + return value, nil } // giveRebate 发放返佣 @@ -350,6 +680,18 @@ func (s *AgentService) findDirectParent(ctx context.Context, agentId int64) (*mo } // findDiamondParent 向上查找钻石上级 +// +// 功能说明:从指定代理开始,向上逐级查找第一个钻石代理(Level == 3) +// +// 查找规则: +// - 自动跳过所有普通代理(Level == 1)和黄金代理(Level == 2) +// - 只返回第一个找到的钻石代理 +// - 如果没有找到钻石代理,返回 ErrNotFound +// +// 示例场景: +// - 普通 -> 普通 -> 钻石:会找到钻石(跳过中间的普通代理) +// - 普通 -> 黄金 -> 钻石:会找到钻石(跳过黄金代理) +// - 普通 -> 普通 -> 黄金:返回 ErrNotFound(没有钻石) func (s *AgentService) findDiamondParent(ctx context.Context, agentId int64) (*model.Agent, error) { currentId := agentId maxDepth := 100 // 防止无限循环 @@ -376,6 +718,20 @@ func (s *AgentService) findDiamondParent(ctx context.Context, agentId int64) (*m } // findGoldParent 向上查找黄金上级 +// +// 功能说明:从指定代理开始,向上逐级查找第一个黄金代理(Level == 2) +// +// 查找规则: +// - 自动跳过所有普通代理(Level == 1) +// - 如果先遇到钻石代理(Level == 3),不会跳过,但会继续查找黄金代理 +// 注意:在实际使用中,应该先调用 findDiamondParent,如果没找到钻石再调用此方法 +// - 只返回第一个找到的黄金代理 +// - 如果没有找到黄金代理,返回 ErrNotFound +// +// 示例场景: +// - 普通 -> 普通 -> 黄金:会找到黄金(跳过中间的普通代理) +// - 普通 -> 黄金:会找到黄金 +// - 普通 -> 普通 -> 钻石:返回 ErrNotFound(跳过钻石,继续查找黄金,但找不到) func (s *AgentService) findGoldParent(ctx context.Context, agentId int64) (*model.Agent, error) { currentId := agentId maxDepth := 100 // 防止无限循环 @@ -446,7 +802,7 @@ func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId, toLevel int6 if parent != nil && rebateAmount > 0 { // 返佣给原直接上级 - if err := s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount); err != nil { + if err := s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount, orderNo); err != nil { return errors.Wrapf(err, "返佣给上级失败") } } @@ -462,10 +818,28 @@ func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId, toLevel int6 } if needDetach { + // 脱离前先获取原直接上级及其上级的信息(用于后续重新连接) + oldParent, oldParentErr := s.findDirectParent(transCtx, agentId) + var grandparentId int64 = 0 + if oldParentErr == nil && oldParent != nil { + // 查找原上级的上级 + grandparent, grandparentErr := s.findDirectParent(transCtx, oldParent.Id) + if grandparentErr == nil && grandparent != nil { + grandparentId = grandparent.Id + } + } + // 脱离直接上级关系 if err := s.detachFromParent(transCtx, session, agentId); err != nil { return errors.Wrapf(err, "脱离直接上级关系失败") } + + // 脱离后,尝试连接到原上级的上级 + if grandparentId > 0 { + if err := s.reconnectToGrandparent(transCtx, session, agentId, toLevel, grandparentId); err != nil { + return errors.Wrapf(err, "重新连接上级关系失败") + } + } } // 5. 如果升级为钻石,独立成新团队 @@ -515,14 +889,10 @@ func (s *AgentService) needDetachFromParent(ctx context.Context, agent *model.Ag // 规则2:同级不能作为上下级(除了普通代理) if newLevel == parent.Level { - if newLevel == 2 || newLevel == 3 { // 黄金或钻石 + if newLevel == 2 || newLevel == 3 { // 黄金或钻石同级需要脱离 return true, nil } - } - - // 规则3:钻石 → 黄金禁止(特殊规则) - if newLevel == 2 && parent.Level == 3 { - return true, nil + // 普通代理同级(newLevel == 1 && parent.Level == 1)不需要脱离 } return false, nil @@ -555,6 +925,60 @@ func (s *AgentService) detachFromParent(ctx context.Context, session sqlx.Sessio return nil } +// reconnectToGrandparent 重新连接到原上级的上级(如果存在且符合条件) +func (s *AgentService) reconnectToGrandparent(ctx context.Context, session sqlx.Session, agentId int64, newLevel int64, grandparentId int64) error { + // 获取原上级的上级信息 + grandparent, err := s.AgentModel.FindOne(ctx, grandparentId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + // 原上级的上级不存在,不需要重新连接 + return nil + } + return errors.Wrapf(err, "查询原上级的上级失败") + } + + // 验证是否可以连接到原上级的上级 + // 规则:新等级必须低于或等于原上级的上级等级,且不能同级(除了普通代理) + if newLevel > grandparent.Level { + // 新等级高于原上级的上级,不能连接 + return nil + } + + // 同级不能作为上下级(除了普通代理) + if newLevel == grandparent.Level { + if newLevel == 2 || newLevel == 3 { + // 黄金或钻石同级不能连接 + return nil + } + // 普通代理同级可以连接(虽然这种情况不太可能发生) + } + + // 检查是否已经存在关系 + builder := s.AgentRelationModel.SelectBuilder(). + Where("parent_id = ? AND child_id = ? AND relation_type = ? AND del_state = ?", grandparent.Id, agentId, 1, globalkey.DelStateNo) + existingRelations, err := s.AgentRelationModel.FindAll(ctx, builder, "") + if err != nil { + return errors.Wrapf(err, "查询现有关系失败") + } + + if len(existingRelations) > 0 { + // 关系已存在,不需要重复创建 + return nil + } + + // 创建新的关系连接到原上级的上级 + relation := &model.AgentRelation{ + ParentId: grandparent.Id, + ChildId: agentId, + RelationType: 1, // 直接关系 + } + if _, err := s.AgentRelationModel.Insert(ctx, session, relation); err != nil { + return errors.Wrapf(err, "创建新关系失败") + } + + return nil +} + // updateChildrenTeamLeader 更新所有下级的团队首领 func (s *AgentService) updateChildrenTeamLeader(ctx context.Context, session sqlx.Session, agentId, teamLeaderId int64) error { // 递归更新所有下级 @@ -604,7 +1028,8 @@ func (s *AgentService) findTeamLeaderId(ctx context.Context, agentId int64) (int } // giveRebateForUpgrade 发放升级返佣 -func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Session, parentAgentId, upgradeAgentId int64, amount float64) error { +// 注意:升级返佣信息记录在 agent_upgrade 表中(rebate_agent_id 和 rebate_amount),不需要在 agent_rebate 表中创建记录 +func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Session, parentAgentId, upgradeAgentId int64, amount float64, orderNo string) error { // 更新钱包余额 wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, parentAgentId) if err != nil { @@ -621,21 +1046,25 @@ func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Se } // GetUpgradeFee 获取升级费用 -func (s *AgentService) GetUpgradeFee(fromLevel, toLevel int64) float64 { +func (s *AgentService) GetUpgradeFee(ctx context.Context, fromLevel, toLevel int64) (float64, error) { if fromLevel == 1 && toLevel == 2 { - return 199 // 普通→黄金 + // 普通→黄金:从配置获取 + return s.getRebateConfigFloat(ctx, "upgrade_to_gold_fee", 199) } else if toLevel == 3 { - return 980 // 升级为钻石 + // 升级为钻石:从配置获取 + return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_fee", 980) } - return 0 + return 0, nil } // GetUpgradeRebate 获取升级返佣金额 -func (s *AgentService) GetUpgradeRebate(fromLevel, toLevel int64) float64 { +func (s *AgentService) GetUpgradeRebate(ctx context.Context, fromLevel, toLevel int64) (float64, error) { if fromLevel == 1 && toLevel == 2 { - return 139 // 普通→黄金返佣 + // 普通→黄金返佣:从配置获取 + return s.getRebateConfigFloat(ctx, "upgrade_to_gold_rebate", 139) } else if toLevel == 3 { - return 680 // 升级为钻石返佣 + // 升级为钻石返佣:从配置获取 + return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_rebate", 680) } - return 0 + return 0, nil } diff --git a/app/main/api/internal/service/apiRegistryService.go b/app/main/api/internal/service/apiRegistryService.go index 5fb8625..bf722ef 100644 --- a/app/main/api/internal/service/apiRegistryService.go +++ b/app/main/api/internal/service/apiRegistryService.go @@ -160,7 +160,6 @@ func (s *ApiRegistryService) generateApiName(path string) string { "order": "订单管理", "platform_user": "平台用户", "product": "产品管理", - "promotion": "推广管理", "query": "查询管理", "role": "角色管理", "user": "用户管理", diff --git a/app/main/api/internal/service/asynqService.go b/app/main/api/internal/service/asynqService.go index 85930aa..5a33149 100644 --- a/app/main/api/internal/service/asynqService.go +++ b/app/main/api/internal/service/asynqService.go @@ -3,6 +3,7 @@ package service import ( + "time" "ycc-server/app/main/api/internal/config" "ycc-server/app/main/api/internal/types" "encoding/json" @@ -58,3 +59,65 @@ func (s *AsynqService) SendQueryTask(orderID int64) error { logx.Infof("发送异步任务成功,任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID) return nil } + +// SendAgentProcessTask 发送代理处理任务 +func (s *AsynqService) SendAgentProcessTask(orderID int64) error { + // 准备任务的 payload + payload := types.MsgAgentProcessPayload{ + OrderID: orderID, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + logx.Errorf("发送代理处理任务失败 (无法编码 payload): %v, 订单号: %d", err, orderID) + return err + } + + options := []asynq.Option{ + asynq.MaxRetry(5), // 设置最大重试次数 + } + // 创建任务 + task := asynq.NewTask(types.MsgAgentProcess, payloadBytes, options...) + + // 将任务加入队列并获取任务信息 + info, err := s.client.Enqueue(task) + if err != nil { + logx.Errorf("发送代理处理任务失败 (加入队列失败): %+v, 订单号: %d", err, orderID) + return err + } + + // 记录成功日志,带上任务 ID 和队列信息 + logx.Infof("发送代理处理任务成功,任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID) + return nil +} + +// SendUnfreezeTask 发送解冻任务(延迟执行) +func (s *AsynqService) SendUnfreezeTask(freezeTaskId int64, processAt time.Time) error { + // 准备任务的 payload + payload := types.MsgUnfreezeCommissionPayload{ + FreezeTaskId: freezeTaskId, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + logx.Errorf("发送解冻任务失败 (无法编码 payload): %v, 冻结任务ID: %d", err, freezeTaskId) + return err + } + + options := []asynq.Option{ + asynq.MaxRetry(5), // 设置最大重试次数 + asynq.ProcessAt(processAt), // 延迟到指定时间执行 + asynq.Queue("critical"), // 使用关键队列 + } + // 创建任务 + task := asynq.NewTask(types.MsgUnfreezeCommission, payloadBytes, options...) + + // 将任务加入队列并获取任务信息 + info, err := s.client.Enqueue(task) + if err != nil { + logx.Errorf("发送解冻任务失败 (加入队列失败): %+v, 冻结任务ID: %d", err, freezeTaskId) + return err + } + + // 记录成功日志,带上任务 ID 和队列信息 + logx.Infof("发送解冻任务成功,任务ID: %s, 队列: %s, 冻结任务ID: %d, 执行时间: %v", info.ID, info.Queue, freezeTaskId, processAt) + return nil +} \ No newline at end of file diff --git a/app/main/api/internal/service/authorizationService.go b/app/main/api/internal/service/authorizationService.go index 1bb4c7d..61fd403 100644 --- a/app/main/api/internal/service/authorizationService.go +++ b/app/main/api/internal/service/authorizationService.go @@ -28,8 +28,8 @@ func NewAuthorizationService(c config.Config, authDocModel model.AuthorizationDo return &AuthorizationService{ config: c, authDocModel: authDocModel, - fileStoragePath: "data/authorization_docs", // 使用相对路径,兼容开发环境 - fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取 + fileStoragePath: "data/authorization_docs", // 使用相对路径,兼容开发环境 + fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取 } } @@ -113,7 +113,7 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ "/app/static/SIMHEI.TTF", // Docker容器内的字体文件 "app/main/api/static/SIMHEI.TTF", // 开发环境备用路径 } - + // 尝试添加字体 fontAdded := false for _, fontPath := range fontPaths { @@ -150,19 +150,19 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ pdf.SetFont("Arial", "", 20) } pdf.CellFormat(0, 15, "授权书", "", 1, "C", false, 0, "") - + // 添加空行 pdf.Ln(5) - + // 设置正文样式 - 正常字体 if fontAdded { pdf.SetFont("ChineseFont", "", 12) } else { pdf.SetFont("Arial", "", 12) } - + // 构建授权书内容(去掉标题部分) - content := fmt.Sprintf(`海南省学宇思网络科技有限公司: + content := fmt.Sprintf(`海南海宇大数据有限公司: 本人%s拟向贵司申请大数据分析报告查询业务,贵司需要了解本人相关状况,用于查询大数据分析报告,因此本人同意向贵司提供本人的姓名和手机号等个人信息,并同意贵司向第三方(包括但不限于西部数据交易有限公司)传送上述信息。第三方将使用上述信息核实信息真实情况,查询信用记录,并生成报告。 授权内容如下: @@ -185,8 +185,8 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ 附加说明: 本人在授权的相关数据将依据法律法规及贵司内部数据管理规范妥善存储,存储期限为法律要求的最短必要时间。超过存储期限或在数据使用目的达成后,贵司将对相关数据进行销毁或匿名化处理。 本人有权随时撤回本授权书中的授权,但撤回前的授权行为及其法律后果仍具有法律效力。若需撤回授权,本人可通过贵司官方渠道提交书面申请,贵司将在收到申请后依法停止对本人数据的使用。 -你通过"一查查",自愿支付相应费用,用于购买海南省学宇思网络科技有限公司的大数据报告产品。如若对产品内容存在异议,可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈,贵司将在收到异议之日起20日内进行核查和处理,并将结果答复。 -你向海南省学宇思网络科技有限公司的支付方式为:海南省学宇思网络科技有限公司及其经官方授权的相关企业的支付宝账户。 +你通过"一查查",自愿支付相应费用,用于购买海南海宇大数据有限公司的大数据报告产品。如若对产品内容存在异议,可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈,贵司将在收到异议之日起20日内进行核查和处理,并将结果答复。 +你向海南海宇大数据有限公司的支付方式为:海南海宇大数据有限公司及其经官方授权的相关企业的支付宝账户。 争议解决机制: 若因本授权书引发争议,双方应友好协商解决;协商不成的,双方同意将争议提交至授权书签署地(海南省)有管辖权的人民法院解决。 diff --git a/app/main/api/internal/service/authorizationService_test.go b/app/main/api/internal/service/authorizationService_test.go deleted file mode 100644 index fa21263..0000000 --- a/app/main/api/internal/service/authorizationService_test.go +++ /dev/null @@ -1,670 +0,0 @@ -package service - -import ( - "context" - "ycc-server/app/main/api/internal/config" - "ycc-server/app/main/model" - "database/sql" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/Masterminds/squirrel" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -// mockResult 模拟sql.Result -type mockResult struct { - lastInsertId int64 - rowsAffected int64 -} - -func (m *mockResult) LastInsertId() (int64, error) { - return m.lastInsertId, nil -} - -func (m *mockResult) RowsAffected() (int64, error) { - return m.rowsAffected, nil -} - -// MockAuthorizationDocumentModel 模拟授权书模型 -type MockAuthorizationDocumentModel struct { - mock.Mock -} - -func (m *MockAuthorizationDocumentModel) Insert(ctx context.Context, session sqlx.Session, data *model.AuthorizationDocument) (sql.Result, error) { - args := m.Called(ctx, session, data) - return args.Get(0).(sql.Result), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) FindOne(ctx context.Context, id int64) (*model.AuthorizationDocument, error) { - args := m.Called(ctx, id) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*model.AuthorizationDocument), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) Update(ctx context.Context, session sqlx.Session, data *model.AuthorizationDocument) (sql.Result, error) { - args := m.Called(ctx, session, data) - return args.Get(0).(sql.Result), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, data *model.AuthorizationDocument) error { - args := m.Called(ctx, session, data) - return args.Error(0) -} - -func (m *MockAuthorizationDocumentModel) Trans(ctx context.Context, fn func(context.Context, sqlx.Session) error) error { - args := m.Called(ctx, fn) - return args.Error(0) -} - -func (m *MockAuthorizationDocumentModel) SelectBuilder() squirrel.SelectBuilder { - args := m.Called() - return args.Get(0).(squirrel.SelectBuilder) -} - -func (m *MockAuthorizationDocumentModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *model.AuthorizationDocument) error { - args := m.Called(ctx, session, data) - return args.Error(0) -} - -func (m *MockAuthorizationDocumentModel) FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) { - args := m.Called(ctx, sumBuilder, field) - return args.Get(0).(float64), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) { - args := m.Called(ctx, countBuilder, field) - return args.Get(0).(int64), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*model.AuthorizationDocument, error) { - args := m.Called(ctx, rowBuilder, orderBy) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*model.AuthorizationDocument), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*model.AuthorizationDocument, error) { - args := m.Called(ctx, rowBuilder, page, pageSize, orderBy) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*model.AuthorizationDocument), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*model.AuthorizationDocument, int64, error) { - args := m.Called(ctx, rowBuilder, page, pageSize, orderBy) - if args.Get(0) == nil { - return nil, args.Get(1).(int64), args.Error(2) - } - return args.Get(0).([]*model.AuthorizationDocument), args.Get(1).(int64), args.Error(2) -} - -func (m *MockAuthorizationDocumentModel) FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*model.AuthorizationDocument, error) { - args := m.Called(ctx, rowBuilder, preMinId, pageSize) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*model.AuthorizationDocument), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*model.AuthorizationDocument, error) { - args := m.Called(ctx, rowBuilder, preMaxId, pageSize) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*model.AuthorizationDocument), args.Error(1) -} - -func (m *MockAuthorizationDocumentModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - args := m.Called(ctx, session, id) - return args.Error(0) -} - -func (m *MockAuthorizationDocumentModel) FindByOrderId(ctx context.Context, orderId int64) ([]*model.AuthorizationDocument, error) { - args := m.Called(ctx, orderId) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*model.AuthorizationDocument), args.Error(1) -} - -// MockResult 模拟数据库结果 -type MockResult struct { - lastInsertId int64 -} - -func (m *MockResult) LastInsertId() (int64, error) { - return m.lastInsertId, nil -} - -func (m *MockResult) RowsAffected() (int64, error) { - return 1, nil -} - -// TestNewAuthorizationService 测试创建授权书服务 -func TestNewAuthorizationService(t *testing.T) { - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.com/api/v1/auth-docs", - }, - } - mockModel := &MockAuthorizationDocumentModel{} - - service := NewAuthorizationService(config, mockModel) - - assert.NotNil(t, service) - assert.Equal(t, "data/authorization_docs", service.fileStoragePath) - assert.Equal(t, "https://test.com/api/v1/auth-docs", service.fileBaseURL) -} - -// TestGetFullFileURL 测试获取完整文件URL -func TestGetFullFileURL(t *testing.T) { - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.com/api/v1/auth-docs", - }, - } - mockModel := &MockAuthorizationDocumentModel{} - service := NewAuthorizationService(config, mockModel) - - tests := []struct { - name string - relativePath string - expected string - }{ - { - name: "正常相对路径", - relativePath: "2025/09/auth_123_456_20250913_160800.pdf", - expected: "https://test.com/api/v1/auth-docs/2025/09/auth_123_456_20250913_160800.pdf", - }, - { - name: "空路径", - relativePath: "", - expected: "", - }, - { - name: "只有文件名", - relativePath: "test.pdf", - expected: "https://test.com/api/v1/auth-docs/test.pdf", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := service.GetFullFileURL(tt.relativePath) - assert.Equal(t, tt.expected, result) - }) - } -} - -// TestGenerateAuthorizationDocument 测试生成授权书 -func TestGenerateAuthorizationDocument(t *testing.T) { - // 创建测试配置 - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.example.com/api/v1/auth-docs", - }, - } - - // 创建模拟的数据库模型 - mockModel := &MockAuthorizationDocumentModel{} - - // 创建授权书服务 - service := NewAuthorizationService(config, mockModel) - - // 准备测试数据 - userInfo := map[string]interface{}{ - "name": "张三", - "id_card": "110101199001011234", - "mobile": "13800138000", - } - - // 模拟数据库插入成功 - mockModel.On("Insert", mock.Anything, mock.Anything, mock.Anything).Return( - &mockResult{lastInsertId: 1, rowsAffected: 1}, nil) - - // 执行测试 - authDoc, err := service.GenerateAuthorizationDocument( - context.Background(), - 1, // userID - 2, // orderID - 3, // queryID - userInfo, - ) - - // 验证结果 - assert.NoError(t, err) - assert.NotNil(t, authDoc) - assert.Equal(t, int64(1), authDoc.UserId) - assert.Equal(t, int64(2), authDoc.OrderId) - assert.Equal(t, int64(3), authDoc.QueryId) - assert.Equal(t, "pdf", authDoc.FileType) - assert.Equal(t, "active", authDoc.Status) - assert.False(t, authDoc.ExpireTime.Valid) // 永久保留,不设置过期时间 - - // 验证文件路径格式(兼容Windows和Unix路径分隔符) - assert.True(t, strings.Contains(authDoc.FilePath, "data/authorization_docs") || - strings.Contains(authDoc.FilePath, "data\\authorization_docs")) - assert.Contains(t, authDoc.FileName, "auth_") - assert.Contains(t, authDoc.FileName, ".pdf") - - // 验证相对路径格式 - assert.Regexp(t, `^\d{4}/\d{2}/auth_\d+_\d+_\d{8}_\d{6}\.pdf$`, authDoc.FileUrl) - - // 验证文件大小 - assert.Greater(t, authDoc.FileSize, int64(0)) - - // 验证数据库调用 - mockModel.AssertExpectations(t) - - // 验证文件是否真的被创建 - if _, err := os.Stat(authDoc.FilePath); err == nil { - t.Logf("✅ 授权书文件已创建: %s", authDoc.FilePath) - t.Logf("📊 文件大小: %d 字节", authDoc.FileSize) - } else { - t.Logf("⚠️ 文件未找到: %s", authDoc.FilePath) - } -} - -// TestGenerateAuthorizationDocument_DatabaseError 测试数据库错误 -func TestGenerateAuthorizationDocument_DatabaseError(t *testing.T) { - // 创建测试配置 - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.example.com/api/v1/auth-docs", - }, - } - - // 创建模拟的数据库模型 - mockModel := &MockAuthorizationDocumentModel{} - - // 创建授权书服务 - service := NewAuthorizationService(config, mockModel) - - // 准备测试数据 - userInfo := map[string]interface{}{ - "name": "李四", - "id_card": "110101199001011235", - "mobile": "13800138001", - } - - // 模拟数据库插入失败 - mockModel.On("Insert", mock.Anything, mock.Anything, mock.Anything).Return( - (*mockResult)(nil), errors.New("数据库连接失败")) - - // 执行测试 - authDoc, err := service.GenerateAuthorizationDocument( - context.Background(), - 1, // userID - 2, // orderID - 3, // queryID - userInfo, - ) - - // 验证结果 - assert.Error(t, err) - assert.Nil(t, authDoc) - assert.Contains(t, err.Error(), "数据库连接失败") - - // 验证数据库调用 - mockModel.AssertExpectations(t) -} - -// TestGeneratePDFContent 测试生成PDF内容 -func TestGeneratePDFContent(t *testing.T) { - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.com/api/v1/auth-docs", - }, - } - mockModel := &MockAuthorizationDocumentModel{} - service := NewAuthorizationService(config, mockModel) - - userInfo := map[string]interface{}{ - "name": "张三", - "id_card": "110101199001011234", - } - - pdfBytes, err := service.generatePDFContent(userInfo) - - // 验证结果 - assert.NoError(t, err) - assert.NotNil(t, pdfBytes) - assert.Greater(t, len(pdfBytes), 0) - - // 验证PDF内容(PDF是二进制格式,只验证基本结构) - assert.Contains(t, string(pdfBytes), "%PDF") // PDF文件头 - - // 按照 GenerateAuthorizationDocument 的方式保存文件到本地 - // 1. 创建文件存储目录 - year := time.Now().Format("2006") - month := time.Now().Format("01") - dirPath := filepath.Join("data", "authorization_docs", year, month) - if err := os.MkdirAll(dirPath, 0755); err != nil { - t.Fatalf("创建存储目录失败: %v", err) - } - - // 2. 生成文件名和路径 - fileName := fmt.Sprintf("test_auth_%s.pdf", time.Now().Format("20060102_150405")) - filePath := filepath.Join(dirPath, fileName) - relativePath := fmt.Sprintf("%s/%s/%s", year, month, fileName) - - // 3. 保存PDF文件 - if err := os.WriteFile(filePath, pdfBytes, 0644); err != nil { - t.Fatalf("保存PDF文件失败: %v", err) - } - - // 4. 验证文件是否保存成功 - if _, err := os.Stat(filePath); err == nil { - t.Logf("✅ PDF文件已保存到本地") - t.Logf("📄 文件名: %s", fileName) - t.Logf("📁 文件路径: %s", filePath) - t.Logf("🔗 相对路径: %s", relativePath) - t.Logf("📊 文件大小: %d 字节", len(pdfBytes)) - - // 获取绝对路径 - absPath, _ := filepath.Abs(filePath) - t.Logf("📍 绝对路径: %s", absPath) - } else { - t.Errorf("❌ PDF文件保存失败: %v", err) - } -} - -// TestSavePDFToLocal 专门测试保存PDF文件到本地 -func TestSavePDFToLocal(t *testing.T) { - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.com/api/v1/auth-docs", - }, - } - mockModel := &MockAuthorizationDocumentModel{} - service := NewAuthorizationService(config, mockModel) - - // 准备测试数据 - userInfo := map[string]interface{}{ - "name": "何志勇", - "id_card": "452528197907133014", - "mobile": "18276151590", - } - - // 生成PDF内容 - pdfBytes, err := service.generatePDFContent(userInfo) - assert.NoError(t, err) - assert.NotNil(t, pdfBytes) - assert.Greater(t, len(pdfBytes), 0) - - // 按照 GenerateAuthorizationDocument 的方式保存文件到本地 - // 1. 创建文件存储目录 - year := time.Now().Format("2006") - month := time.Now().Format("01") - dirPath := filepath.Join("data", "authorization_docs", year, month) - if err := os.MkdirAll(dirPath, 0755); err != nil { - t.Fatalf("创建存储目录失败: %v", err) - } - - // 2. 生成文件名和路径 - fileName := fmt.Sprintf("local_test_auth_%s.pdf", time.Now().Format("20060102_150405")) - filePath := filepath.Join(dirPath, fileName) - relativePath := fmt.Sprintf("%s/%s/%s", year, month, fileName) - - // 3. 保存PDF文件 - if err := os.WriteFile(filePath, pdfBytes, 0644); err != nil { - t.Fatalf("保存PDF文件失败: %v", err) - } - - // 4. 验证文件是否保存成功 - if _, err := os.Stat(filePath); err == nil { - t.Logf("✅ PDF文件已保存到本地") - t.Logf("📄 文件名: %s", fileName) - t.Logf("📁 文件路径: %s", filePath) - t.Logf("🔗 相对路径: %s", relativePath) - t.Logf("📊 文件大小: %d 字节", len(pdfBytes)) - - // 获取绝对路径 - absPath, _ := filepath.Abs(filePath) - t.Logf("📍 绝对路径: %s", absPath) - - // 验证文件内容 - fileInfo, err := os.Stat(filePath) - assert.NoError(t, err) - assert.Greater(t, fileInfo.Size(), int64(1000)) // 文件应该大于1KB - - t.Logf("🎉 文件保存验证通过!") - } else { - t.Errorf("❌ PDF文件保存失败: %v", err) - } -} - -// TestGeneratePDFContent_EmptyUserInfo 测试空用户信息 -func TestGeneratePDFContent_EmptyUserInfo(t *testing.T) { - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.com/api/v1/auth-docs", - }, - } - mockModel := &MockAuthorizationDocumentModel{} - service := NewAuthorizationService(config, mockModel) - - userInfo := map[string]interface{}{} - - pdfBytes, err := service.generatePDFContent(userInfo) - - // 验证结果 - assert.NoError(t, err) - assert.NotNil(t, pdfBytes) - assert.Greater(t, len(pdfBytes), 0) - - // 验证PDF内容(PDF是二进制格式,只验证基本结构) - assert.Contains(t, string(pdfBytes), "%PDF") // PDF文件头 -} - -// TestGetUserInfoString 测试获取用户信息字符串 -func TestGetUserInfoString(t *testing.T) { - userInfo := map[string]interface{}{ - "name": "张三", - "id_card": "110101199001011234", - "age": 30, - "empty": "", - } - - tests := []struct { - name string - key string - expected string - }{ - { - name: "正常字符串", - key: "name", - expected: "张三", - }, - { - name: "身份证号", - key: "id_card", - expected: "110101199001011234", - }, - { - name: "空字符串", - key: "empty", - expected: "", - }, - { - name: "不存在的键", - key: "not_exist", - expected: "", - }, - { - name: "非字符串类型", - key: "age", - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := getUserInfoString(userInfo, tt.key) - assert.Equal(t, tt.expected, result) - }) - } -} - -// BenchmarkGeneratePDFContent 性能测试 -func BenchmarkGeneratePDFContent(b *testing.B) { - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.com/api/v1/auth-docs", - }, - } - mockModel := &MockAuthorizationDocumentModel{} - service := NewAuthorizationService(config, mockModel) - - userInfo := map[string]interface{}{ - "name": "张三", - "id_card": "110101199001011234", - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := service.generatePDFContent(userInfo) - if err != nil { - b.Fatal(err) - } - } -} - -// TestAuthorizationService_Integration 集成测试(需要真实文件系统) -func TestAuthorizationService_Integration(t *testing.T) { - // 创建测试配置 - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.example.com/api/v1/auth-docs", - }, - } - - // 创建模拟的数据库模型 - mockModel := &MockAuthorizationDocumentModel{} - - // 创建授权书服务 - service := NewAuthorizationService(config, mockModel) - - // 准备测试数据 - userInfo := map[string]interface{}{ - "name": "王五", - "id_card": "110101199001011236", - "mobile": "13800138002", - } - - // 模拟数据库插入成功 - mockModel.On("Insert", mock.Anything, mock.Anything, mock.Anything).Return( - &mockResult{lastInsertId: 1, rowsAffected: 1}, nil) - - // 执行测试 - authDoc, err := service.GenerateAuthorizationDocument( - context.Background(), - 1, // userID - 2, // orderID - 3, // queryID - userInfo, - ) - - // 验证结果 - assert.NoError(t, err) - assert.NotNil(t, authDoc) - - // 测试GetFullFileURL方法 - fullURL := service.GetFullFileURL(authDoc.FileUrl) - expectedURL := fmt.Sprintf("%s/%s", config.Authorization.FileBaseURL, authDoc.FileUrl) - assert.Equal(t, expectedURL, fullURL) - - // 验证文件是否真的被创建 - if _, err := os.Stat(authDoc.FilePath); err == nil { - t.Logf("✅ 集成测试成功 - 授权书文件已创建") - t.Logf("📄 文件名: %s", authDoc.FileName) - t.Logf("📁 文件路径: %s", authDoc.FilePath) - t.Logf("🔗 相对路径: %s", authDoc.FileUrl) - t.Logf("🌐 完整URL: %s", fullURL) - t.Logf("📊 文件大小: %d 字节", authDoc.FileSize) - } else { - t.Logf("⚠️ 集成测试 - 文件未找到: %s", authDoc.FilePath) - } - - // 验证数据库调用 - mockModel.AssertExpectations(t) -} - -// TestGeneratePDFFile 专门测试PDF文件生成 -func TestGeneratePDFFile(t *testing.T) { - // 创建测试配置 - config := config.Config{ - Authorization: config.AuthorizationConfig{ - FileBaseURL: "https://test.example.com/api/v1/auth-docs", - }, - } - - // 创建模拟的数据库模型 - mockModel := &MockAuthorizationDocumentModel{} - - // 创建授权书服务 - service := NewAuthorizationService(config, mockModel) - - // 准备测试数据 - userInfo := map[string]interface{}{ - "name": "测试用户", - "id_card": "110101199001011237", - "mobile": "13800138003", - } - - // 模拟数据库插入成功 - mockModel.On("Insert", mock.Anything, mock.Anything, mock.Anything).Return( - &mockResult{lastInsertId: 1, rowsAffected: 1}, nil) - - // 执行测试 - authDoc, err := service.GenerateAuthorizationDocument( - context.Background(), - 999, // userID - 888, // orderID - 777, // queryID - userInfo, - ) - - // 验证结果 - assert.NoError(t, err) - assert.NotNil(t, authDoc) - - // 验证文件是否真的被创建 - if _, err := os.Stat(authDoc.FilePath); err == nil { - t.Logf("✅ PDF文件生成成功!") - t.Logf("📄 文件名: %s", authDoc.FileName) - t.Logf("📁 文件路径: %s", authDoc.FilePath) - t.Logf("🔗 相对路径: %s", authDoc.FileUrl) - t.Logf("📊 文件大小: %d 字节", authDoc.FileSize) - - // 验证文件内容 - fileInfo, err := os.Stat(authDoc.FilePath) - assert.NoError(t, err) - assert.Greater(t, fileInfo.Size(), int64(1000)) // 文件应该大于1KB - - // 验证文件名格式 - assert.Regexp(t, `^auth_999_888_\d{8}_\d{6}\.pdf$`, authDoc.FileName) - - // 验证路径格式 - assert.Regexp(t, `^\d{4}/\d{2}/auth_999_888_\d{8}_\d{6}\.pdf$`, authDoc.FileUrl) - - t.Logf("🎉 所有验证通过!") - } else { - t.Errorf("❌ PDF文件未创建: %s", authDoc.FilePath) - } - - // 验证数据库调用 - mockModel.AssertExpectations(t) -} diff --git a/app/main/api/internal/svc/servicecontext.go b/app/main/api/internal/svc/servicecontext.go index ce58096..6278ffb 100644 --- a/app/main/api/internal/svc/servicecontext.go +++ b/app/main/api/internal/svc/servicecontext.go @@ -21,9 +21,9 @@ type ServiceContext struct { Redis *redis.Redis // 中间件 - AuthInterceptor rest.Middleware - UserAuthInterceptor rest.Middleware - AdminAuthInterceptor rest.Middleware + AuthInterceptor rest.Middleware + UserAuthInterceptor rest.Middleware + AdminAuthInterceptor rest.Middleware // 用户相关模型 UserModel model.UserModel @@ -44,56 +44,53 @@ type ServiceContext struct { QueryCleanupConfigModel model.QueryCleanupConfigModel // 代理相关模型(新系统) - AgentModel model.AgentModel - AgentWalletModel model.AgentWalletModel - AgentRelationModel model.AgentRelationModel - AgentLinkModel model.AgentLinkModel - AgentOrderModel model.AgentOrderModel - AgentCommissionModel model.AgentCommissionModel - AgentRebateModel model.AgentRebateModel - AgentUpgradeModel model.AgentUpgradeModel - AgentWithdrawalModel model.AgentWithdrawalModel - AgentConfigModel model.AgentConfigModel - AgentProductConfigModel model.AgentProductConfigModel - AgentRealNameModel model.AgentRealNameModel - AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel - AgentInviteCodeModel model.AgentInviteCodeModel + AgentModel model.AgentModel + AgentWalletModel model.AgentWalletModel + AgentRelationModel model.AgentRelationModel + AgentLinkModel model.AgentLinkModel + AgentOrderModel model.AgentOrderModel + AgentCommissionModel model.AgentCommissionModel + AgentRebateModel model.AgentRebateModel + AgentUpgradeModel model.AgentUpgradeModel + AgentWithdrawalModel model.AgentWithdrawalModel + AgentConfigModel model.AgentConfigModel + AgentProductConfigModel model.AgentProductConfigModel + AgentRealNameModel model.AgentRealNameModel + AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel + AgentInviteCodeModel model.AgentInviteCodeModel + AgentInviteCodeUsageModel model.AgentInviteCodeUsageModel + AgentFreezeTaskModel model.AgentFreezeTaskModel + AgentShortLinkModel model.AgentShortLinkModel // 管理后台相关模型 - AdminApiModel model.AdminApiModel - AdminMenuModel model.AdminMenuModel - AdminRoleModel model.AdminRoleModel - AdminRoleApiModel model.AdminRoleApiModel - AdminRoleMenuModel model.AdminRoleMenuModel - AdminUserModel model.AdminUserModel - AdminUserRoleModel model.AdminUserRoleModel - AdminDictDataModel model.AdminDictDataModel - AdminDictTypeModel model.AdminDictTypeModel - AdminPromotionLinkModel model.AdminPromotionLinkModel - AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel - AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel - AdminPromotionOrderModel model.AdminPromotionOrderModel + AdminApiModel model.AdminApiModel + AdminMenuModel model.AdminMenuModel + AdminRoleModel model.AdminRoleModel + AdminRoleApiModel model.AdminRoleApiModel + AdminRoleMenuModel model.AdminRoleMenuModel + AdminUserModel model.AdminUserModel + AdminUserRoleModel model.AdminUserRoleModel + AdminDictDataModel model.AdminDictDataModel + AdminDictTypeModel model.AdminDictTypeModel // 其他模型 - ExampleModel model.ExampleModel - GlobalNotificationsModel model.GlobalNotificationsModel + ExampleModel model.ExampleModel + GlobalNotificationsModel model.GlobalNotificationsModel AuthorizationDocumentModel model.AuthorizationDocumentModel // 服务 - AlipayService *service.AliPayService - WechatPayService *service.WechatPayService - ApplePayService *service.ApplePayService - ApiRequestService *service.ApiRequestService - AsynqServer *asynq.Server - AsynqService *service.AsynqService - VerificationService *service.VerificationService - AgentService *service.AgentService - UserService *service.UserService - DictService *service.DictService - AdminPromotionLinkStatsService *service.AdminPromotionLinkStatsService - ImageService *service.ImageService - AuthorizationService *service.AuthorizationService - + AlipayService *service.AliPayService + WechatPayService *service.WechatPayService + ApplePayService *service.ApplePayService + ApiRequestService *service.ApiRequestService + AsynqServer *asynq.Server + AsynqService *service.AsynqService + VerificationService *service.VerificationService + AgentService *service.AgentService + UserService *service.UserService + DictService *service.DictService + ImageService *service.ImageService + AuthorizationService *service.AuthorizationService } // NewServiceContext 创建服务上下文 @@ -143,6 +140,9 @@ func NewServiceContext(c config.Config) *ServiceContext { agentRealNameModel := model.NewAgentRealNameModel(db, cacheConf) agentWithdrawalTaxModel := model.NewAgentWithdrawalTaxModel(db, cacheConf) agentInviteCodeModel := model.NewAgentInviteCodeModel(db, cacheConf) + agentInviteCodeUsageModel := model.NewAgentInviteCodeUsageModel(db, cacheConf) + agentFreezeTaskModel := model.NewAgentFreezeTaskModel(db, cacheConf) + agentShortLinkModel := model.NewAgentShortLinkModel(db, cacheConf) // ============================== 管理后台相关模型 ============================== adminApiModel := model.NewAdminApiModel(db, cacheConf) adminMenuModel := model.NewAdminMenuModel(db, cacheConf) @@ -153,17 +153,12 @@ func NewServiceContext(c config.Config) *ServiceContext { adminUserRoleModel := model.NewAdminUserRoleModel(db, cacheConf) adminDictDataModel := model.NewAdminDictDataModel(db, cacheConf) adminDictTypeModel := model.NewAdminDictTypeModel(db, cacheConf) - adminPromotionLinkModel := model.NewAdminPromotionLinkModel(db, cacheConf) - adminPromotionLinkStatsTotalModel := model.NewAdminPromotionLinkStatsTotalModel(db, cacheConf) - adminPromotionLinkStatsHistoryModel := model.NewAdminPromotionLinkStatsHistoryModel(db, cacheConf) - adminPromotionOrderModel := model.NewAdminPromotionOrderModel(db, cacheConf) // ============================== 其他模型 ============================== exampleModel := model.NewExampleModel(db, cacheConf) globalNotificationsModel := model.NewGlobalNotificationsModel(db, cacheConf) authorizationDocumentModel := model.NewAuthorizationDocumentModel(db, cacheConf) - // ============================== 第三方服务初始化 ============================== tianyuanapi, err := tianyuanapi.NewClient(tianyuanapi.Config{ AccessID: c.Tianyuanapi.AccessID, @@ -185,11 +180,9 @@ func NewServiceContext(c config.Config) *ServiceContext { agentService := service.NewAgentService(c, orderModel, agentModel, agentWalletModel, agentRelationModel, agentLinkModel, agentOrderModel, agentCommissionModel, agentRebateModel, agentUpgradeModel, agentWithdrawalModel, agentConfigModel, agentProductConfigModel, - agentRealNameModel, agentWithdrawalTaxModel) + agentRealNameModel, agentWithdrawalTaxModel, agentFreezeTaskModel) userService := service.NewUserService(&c, userModel, userAuthModel, userTempModel, agentModel) dictService := service.NewDictService(adminDictTypeModel, adminDictDataModel) - adminPromotionLinkStatsService := service.NewAdminPromotionLinkStatsService(adminPromotionLinkModel, - adminPromotionLinkStatsTotalModel, adminPromotionLinkStatsHistoryModel) imageService := service.NewImageService() authorizationService := service.NewAuthorizationService(c, authorizationDocumentModel) @@ -209,9 +202,9 @@ func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, Redis: redisClient, - AuthInterceptor: middleware.NewAuthInterceptorMiddleware(c).Handle, - UserAuthInterceptor: middleware.NewUserAuthInterceptorMiddleware().Handle, - AdminAuthInterceptor: middleware.NewAdminAuthInterceptorMiddleware(c, + AuthInterceptor: middleware.NewAuthInterceptorMiddleware(c).Handle, + UserAuthInterceptor: middleware.NewUserAuthInterceptorMiddleware().Handle, + AdminAuthInterceptor: middleware.NewAdminAuthInterceptorMiddleware(c, adminUserModel, adminUserRoleModel, adminRoleModel, adminApiModel, adminRoleApiModel).Handle, // 用户相关模型 @@ -233,56 +226,53 @@ func NewServiceContext(c config.Config) *ServiceContext { QueryCleanupConfigModel: queryCleanupConfigModel, // 代理相关模型(新系统) - AgentModel: agentModel, - AgentWalletModel: agentWalletModel, - AgentRelationModel: agentRelationModel, - AgentLinkModel: agentLinkModel, - AgentOrderModel: agentOrderModel, - AgentCommissionModel: agentCommissionModel, - AgentRebateModel: agentRebateModel, - AgentUpgradeModel: agentUpgradeModel, - AgentWithdrawalModel: agentWithdrawalModel, - AgentConfigModel: agentConfigModel, - AgentProductConfigModel: agentProductConfigModel, - AgentRealNameModel: agentRealNameModel, - AgentWithdrawalTaxModel: agentWithdrawalTaxModel, - AgentInviteCodeModel: agentInviteCodeModel, + AgentModel: agentModel, + AgentWalletModel: agentWalletModel, + AgentRelationModel: agentRelationModel, + AgentLinkModel: agentLinkModel, + AgentOrderModel: agentOrderModel, + AgentCommissionModel: agentCommissionModel, + AgentRebateModel: agentRebateModel, + AgentUpgradeModel: agentUpgradeModel, + AgentWithdrawalModel: agentWithdrawalModel, + AgentConfigModel: agentConfigModel, + AgentProductConfigModel: agentProductConfigModel, + AgentRealNameModel: agentRealNameModel, + AgentWithdrawalTaxModel: agentWithdrawalTaxModel, + AgentInviteCodeModel: agentInviteCodeModel, + AgentInviteCodeUsageModel: agentInviteCodeUsageModel, + AgentFreezeTaskModel: agentFreezeTaskModel, + AgentShortLinkModel: agentShortLinkModel, // 管理后台相关模型 - AdminApiModel: adminApiModel, - AdminMenuModel: adminMenuModel, - AdminRoleModel: adminRoleModel, - AdminRoleApiModel: adminRoleApiModel, - AdminRoleMenuModel: adminRoleMenuModel, - AdminUserModel: adminUserModel, - AdminUserRoleModel: adminUserRoleModel, - AdminDictDataModel: adminDictDataModel, - AdminDictTypeModel: adminDictTypeModel, - AdminPromotionLinkModel: adminPromotionLinkModel, - AdminPromotionLinkStatsTotalModel: adminPromotionLinkStatsTotalModel, - AdminPromotionLinkStatsHistoryModel: adminPromotionLinkStatsHistoryModel, - AdminPromotionOrderModel: adminPromotionOrderModel, + AdminApiModel: adminApiModel, + AdminMenuModel: adminMenuModel, + AdminRoleModel: adminRoleModel, + AdminRoleApiModel: adminRoleApiModel, + AdminRoleMenuModel: adminRoleMenuModel, + AdminUserModel: adminUserModel, + AdminUserRoleModel: adminUserRoleModel, + AdminDictDataModel: adminDictDataModel, + AdminDictTypeModel: adminDictTypeModel, // 其他模型 - ExampleModel: exampleModel, - GlobalNotificationsModel: globalNotificationsModel, + ExampleModel: exampleModel, + GlobalNotificationsModel: globalNotificationsModel, AuthorizationDocumentModel: authorizationDocumentModel, // 服务 - AlipayService: alipayService, - WechatPayService: wechatPayService, - ApplePayService: applePayService, - ApiRequestService: apiRequestService, - AsynqServer: asynqServer, - AsynqService: asynqService, - VerificationService: verificationService, - AgentService: agentService, - UserService: userService, - DictService: dictService, - AdminPromotionLinkStatsService: adminPromotionLinkStatsService, - ImageService: imageService, - AuthorizationService: authorizationService, - + AlipayService: alipayService, + WechatPayService: wechatPayService, + ApplePayService: applePayService, + ApiRequestService: apiRequestService, + AsynqServer: asynqServer, + AsynqService: asynqService, + VerificationService: verificationService, + AgentService: agentService, + UserService: userService, + DictService: dictService, + ImageService: imageService, + AuthorizationService: authorizationService, } } diff --git a/app/main/api/internal/types/adminagent.go b/app/main/api/internal/types/adminagent.go new file mode 100644 index 0000000..f6c5a92 --- /dev/null +++ b/app/main/api/internal/types/adminagent.go @@ -0,0 +1,200 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminAuditAgentReq struct { + AuditId int64 `json:"audit_id"` // 审核记录ID + Status int64 `json:"status"` // 审核状态:1=通过,2=拒绝 + AuditReason string `json:"audit_reason"` // 审核原因(拒绝时必填) +} + +type AdminAuditAgentResp struct { + Success bool `json:"success"` +} + +type AdminAuditWithdrawalReq struct { + WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID + Status int64 `json:"status"` // 审核状态:2=通过,3=拒绝 + Remark string `json:"remark"` // 备注 +} + +type AdminAuditWithdrawalResp struct { + Success bool `json:"success"` +} + +type AdminGenerateDiamondInviteCodeReq struct { + Count int64 `json:"count"` // 生成数量 + ExpireDays int64 `json:"expire_days,optional"` // 过期天数(可选,0表示不过期) + Remark string `json:"remark,optional"` // 备注(可选) +} + +type AdminGenerateDiamondInviteCodeResp struct { + Codes []string `json:"codes"` // 生成的邀请码列表 +} + +type AdminGetAgentCommissionListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + OrderId *int64 `form:"order_id,optional"` // 订单ID(可选) + Status *int64 `form:"status,optional"` // 状态(可选) +} + +type AdminGetAgentCommissionListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentCommissionListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentConfigResp struct { + LevelBonus LevelBonusConfig `json:"level_bonus"` // 等级加成配置 + UpgradeFee UpgradeFeeConfig `json:"upgrade_fee"` // 升级费用配置 + UpgradeRebate UpgradeRebateConfig `json:"upgrade_rebate"` // 升级返佣配置 + DirectParentRebate DirectParentRebateConfig `json:"direct_parent_rebate"` // 直接上级返佣配置 + MaxGoldRebateAmount float64 `json:"max_gold_rebate_amount"` // 黄金代理最大返佣金额 + CommissionFreeze CommissionFreezeConfig `json:"commission_freeze"` // 佣金冻结配置 + TaxRate float64 `json:"tax_rate"` // 税率 + TaxExemptionAmount float64 `json:"tax_exemption_amount"` // 免税额度 +} + +type AdminGetAgentLinkListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductId *int64 `form:"product_id,optional"` // 产品ID(可选) + LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) +} + +type AdminGetAgentLinkListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentLinkListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Mobile *string `form:"mobile,optional"` // 手机号(可选) + Region *string `form:"region,optional"` // 区域(可选) + Level *int64 `form:"level,optional"` // 等级(可选) + TeamLeaderId *int64 `form:"team_leader_id,optional"` // 团队首领ID(可选) +} + +type AdminGetAgentListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentOrderListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + OrderId *int64 `form:"order_id,optional"` // 订单ID(可选) + ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选) +} + +type AdminGetAgentOrderListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentOrderListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentProductConfigListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductId *int64 `form:"product_id,optional"` // 产品ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名称(可选,用于搜索) +} + +type AdminGetAgentProductConfigListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentProductConfigItem `json:"items"` // 列表数据 +} + +type AdminGetAgentRealNameListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + Status *int64 `form:"status,optional"` // 状态(可选):1=未验证,2=已通过 +} + +type AdminGetAgentRealNameListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentRealNameListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentRebateListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + SourceAgentId *int64 `form:"source_agent_id,optional"` // 来源代理ID(可选) + RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选) +} + +type AdminGetAgentRebateListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentRebateListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentUpgradeListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + UpgradeType *int64 `form:"upgrade_type,optional"` // 升级类型(可选) + Status *int64 `form:"status,optional"` // 状态(可选) +} + +type AdminGetAgentUpgradeListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentUpgradeListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentWithdrawalListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) +} + +type AdminGetAgentWithdrawalListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentWithdrawalListItem `json:"items"` // 列表数据 +} + +type AdminGetInviteCodeListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Code *string `form:"code,optional"` // 邀请码(可选) + AgentId *int64 `form:"agent_id,optional"` // 发放代理ID(可选,NULL表示平台发放) + TargetLevel *int64 `form:"target_level,optional"` // 目标等级(可选) + Status *int64 `form:"status,optional"` // 状态(可选) +} + +type AdminGetInviteCodeListResp struct { + Total int64 `json:"total"` // 总数 + Items []InviteCodeListItem `json:"items"` // 列表数据 +} + +type AdminUpdateAgentConfigReq struct { + LevelBonus *LevelBonusConfig `json:"level_bonus,optional"` // 等级加成配置 + UpgradeFee *UpgradeFeeConfig `json:"upgrade_fee,optional"` // 升级费用配置 + UpgradeRebate *UpgradeRebateConfig `json:"upgrade_rebate,optional"` // 升级返佣配置 + DirectParentRebate *DirectParentRebateConfig `json:"direct_parent_rebate,optional"` // 直接上级返佣配置 + MaxGoldRebateAmount *float64 `json:"max_gold_rebate_amount,optional"` // 黄金代理最大返佣金额 + CommissionFreeze *CommissionFreezeConfig `json:"commission_freeze,optional"` // 佣金冻结配置 + TaxRate *float64 `json:"tax_rate,optional"` // 税率 + TaxExemptionAmount *float64 `json:"tax_exemption_amount,optional"` // 免税额度 +} + +type AdminUpdateAgentConfigResp struct { + Success bool `json:"success"` +} + +type AdminUpdateAgentProductConfigReq struct { + Id int64 `json:"id"` // 主键 + BasePrice float64 `json:"base_price"` // 基础底价 + PriceRangeMax float64 `json:"price_range_max"` // 最高定价(对应数据库 system_max_price) + PriceThreshold *float64 `json:"price_threshold,optional"` // 提价标准阈值(可选) + PriceFeeRate *float64 `json:"price_fee_rate,optional"` // 提价手续费比例(可选) +} + +type AdminUpdateAgentProductConfigResp struct { + Success bool `json:"success"` +} diff --git a/app/main/api/internal/types/adminapi.go b/app/main/api/internal/types/adminapi.go new file mode 100644 index 0000000..3a7cd8e --- /dev/null +++ b/app/main/api/internal/types/adminapi.go @@ -0,0 +1,67 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminBatchUpdateApiStatusReq struct { + Ids []int64 `json:"ids"` + Status int64 `json:"status"` +} + +type AdminBatchUpdateApiStatusResp struct { + Success bool `json:"success"` +} + +type AdminCreateApiReq struct { + ApiName string `json:"api_name"` + ApiCode string `json:"api_code"` + Method string `json:"method"` + Url string `json:"url"` + Status int64 `json:"status,default=1"` + Description string `json:"description,optional"` +} + +type AdminCreateApiResp struct { + Id int64 `json:"id"` +} + +type AdminDeleteApiReq struct { + Id int64 `path:"id"` +} + +type AdminDeleteApiResp struct { + Success bool `json:"success"` +} + +type AdminGetApiDetailReq struct { + Id int64 `path:"id"` +} + +type AdminGetApiDetailResp struct { + AdminApiInfo +} + +type AdminGetApiListReq struct { + Page int64 `form:"page,default=1"` + PageSize int64 `form:"page_size,default=20"` + ApiName string `form:"api_name,optional"` + Method string `form:"method,optional"` + Status int64 `form:"status,optional"` +} + +type AdminGetApiListResp struct { + Items []AdminApiInfo `json:"items"` + Total int64 `json:"total"` +} + +type AdminUpdateApiReq struct { + Id int64 `path:"id"` + ApiName string `json:"api_name"` + ApiCode string `json:"api_code"` + Method string `json:"method"` + Url string `json:"url"` + Status int64 `json:"status"` + Description string `json:"description,optional"` +} + +type AdminUpdateApiResp struct { + Success bool `json:"success"` +} diff --git a/app/main/api/internal/types/adminauth.go b/app/main/api/internal/types/adminauth.go new file mode 100644 index 0000000..2b8548f --- /dev/null +++ b/app/main/api/internal/types/adminauth.go @@ -0,0 +1,15 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminLoginReq struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Captcha bool `json:"captcha" validate:"required"` +} + +type AdminLoginResp struct { + AccessToken string `json:"access_token"` + AccessExpire int64 `json:"access_expire"` + RefreshAfter int64 `json:"refresh_after"` + Roles []string `json:"roles"` +} diff --git a/app/main/api/internal/types/adminfeature.go b/app/main/api/internal/types/adminfeature.go new file mode 100644 index 0000000..76a0bb8 --- /dev/null +++ b/app/main/api/internal/types/adminfeature.go @@ -0,0 +1,75 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminConfigFeatureExampleReq struct { + FeatureId int64 `json:"feature_id"` // 功能ID + Data string `json:"data"` // 示例数据JSON +} + +type AdminConfigFeatureExampleResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminCreateFeatureReq struct { + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 +} + +type AdminCreateFeatureResp struct { + Id int64 `json:"id"` // 功能ID +} + +type AdminDeleteFeatureReq struct { + Id int64 `path:"id"` // 功能ID +} + +type AdminDeleteFeatureResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetFeatureDetailReq struct { + Id int64 `path:"id"` // 功能ID +} + +type AdminGetFeatureDetailResp struct { + Id int64 `json:"id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetFeatureExampleReq struct { + FeatureId int64 `path:"feature_id"` // 功能ID +} + +type AdminGetFeatureExampleResp struct { + Id int64 `json:"id"` // 示例数据ID + FeatureId int64 `json:"feature_id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Data string `json:"data"` // 示例数据JSON + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetFeatureListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ApiId *string `form:"api_id,optional"` // API标识 + Name *string `form:"name,optional"` // 描述 +} + +type AdminGetFeatureListResp struct { + Total int64 `json:"total"` // 总数 + Items []FeatureListItem `json:"items"` // 列表数据 +} + +type AdminUpdateFeatureReq struct { + Id int64 `path:"id"` // 功能ID + ApiId *string `json:"api_id,optional"` // API标识 + Name *string `json:"name,optional"` // 描述 +} + +type AdminUpdateFeatureResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminmenu.go b/app/main/api/internal/types/adminmenu.go new file mode 100644 index 0000000..a0e8e70 --- /dev/null +++ b/app/main/api/internal/types/adminmenu.go @@ -0,0 +1,97 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type CreateMenuReq struct { + Pid int64 `json:"pid,optional"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path,optional"` // 路由路径 + Component string `json:"component,optional"` // 组件路径 + Redirect string `json:"redirect,optional"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status,optional,default=1"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort,optional"` // 排序 +} + +type CreateMenuResp struct { + Id int64 `json:"id"` // 菜单ID +} + +type DeleteMenuReq struct { + Id int64 `path:"id"` // 菜单ID +} + +type DeleteMenuResp struct { + Success bool `json:"success"` // 是否成功 +} + +type GetMenuAllReq struct { +} + +type GetMenuAllResp struct { + Name string `json:"name"` + Path string `json:"path"` + Redirect string `json:"redirect,omitempty"` + Component string `json:"component,omitempty"` + Sort int64 `json:"sort"` + Meta map[string]interface{} `json:"meta"` + Children []GetMenuAllResp `json:"children"` +} + +type GetMenuDetailReq struct { + Id int64 `path:"id"` // 菜单ID +} + +type GetMenuDetailResp struct { + Id int64 `json:"id"` // 菜单ID + Pid int64 `json:"pid"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 组件路径 + Redirect string `json:"redirect"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"createTime"` // 创建时间 + UpdateTime string `json:"updateTime"` // 更新时间 +} + +type GetMenuListReq struct { + Name string `form:"name,optional"` // 菜单名称 + Path string `form:"path,optional"` // 路由路径 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 + Type string `form:"type,optional"` // 类型 +} + +type MenuListItem struct { + Id int64 `json:"id"` // 菜单ID + Pid int64 `json:"pid"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 组件路径 + Redirect string `json:"redirect"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"createTime"` // 创建时间 + Children []MenuListItem `json:"children"` // 子菜单 +} + +type UpdateMenuReq struct { + Id int64 `path:"id"` // 菜单ID + Pid int64 `json:"pid,optional"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path,optional"` // 路由路径 + Component string `json:"component,optional"` // 组件路径 + Redirect string `json:"redirect,optional"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort,optional"` // 排序 +} + +type UpdateMenuResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminnotification.go b/app/main/api/internal/types/adminnotification.go new file mode 100644 index 0000000..4a72c91 --- /dev/null +++ b/app/main/api/internal/types/adminnotification.go @@ -0,0 +1,74 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminCreateNotificationReq struct { + Title string `json:"title"` // 通知标题 + NotificationPage string `json:"notification_page"` // 通知页面 + Content string `json:"content"` // 通知内容 + StartDate string `json:"start_date"` // 生效开始日期(yyyy-MM-dd) + StartTime string `json:"start_time"` // 生效开始时间(HH:mm:ss) + EndDate string `json:"end_date"` // 生效结束日期(yyyy-MM-dd) + EndTime string `json:"end_time"` // 生效结束时间(HH:mm:ss) + Status int64 `json:"status"` // 状态:1-启用,0-禁用 +} + +type AdminCreateNotificationResp struct { + Id int64 `json:"id"` // 通知ID +} + +type AdminDeleteNotificationReq struct { + Id int64 `path:"id"` // 通知ID +} + +type AdminDeleteNotificationResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetNotificationDetailReq struct { + Id int64 `path:"id"` // 通知ID +} + +type AdminGetNotificationDetailResp struct { + Id int64 `json:"id"` // 通知ID + Title string `json:"title"` // 通知标题 + Content string `json:"content"` // 通知内容 + NotificationPage string `json:"notification_page"` // 通知页面 + StartDate string `json:"start_date"` // 生效开始日期 + StartTime string `json:"start_time"` // 生效开始时间 + EndDate string `json:"end_date"` // 生效结束日期 + EndTime string `json:"end_time"` // 生效结束时间 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetNotificationListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Title *string `form:"title,optional"` // 通知标题(可选) + NotificationPage *string `form:"notification_page,optional"` // 通知页面(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + StartDate *string `form:"start_date,optional"` // 开始日期范围(可选) + EndDate *string `form:"end_date,optional"` // 结束日期范围(可选) +} + +type AdminGetNotificationListResp struct { + Total int64 `json:"total"` // 总数 + Items []NotificationListItem `json:"items"` // 列表数据 +} + +type AdminUpdateNotificationReq struct { + Id int64 `path:"id"` // 通知ID + Title *string `json:"title,optional"` // 通知标题 + Content *string `json:"content,optional"` // 通知内容 + NotificationPage *string `json:"notification_page,optional"` // 通知页面 + StartDate *string `json:"start_date,optional"` // 生效开始日期 + StartTime *string `json:"start_time,optional"` // 生效开始时间 + EndDate *string `json:"end_date,optional"` // 生效结束日期 + EndTime *string `json:"end_time,optional"` // 生效结束时间 + Status *int64 `json:"status,optional"` // 状态 +} + +type AdminUpdateNotificationResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminorder.go b/app/main/api/internal/types/adminorder.go new file mode 100644 index 0000000..5eea448 --- /dev/null +++ b/app/main/api/internal/types/adminorder.go @@ -0,0 +1,108 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminCreateOrderReq struct { + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status,default=pending"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 +} + +type AdminCreateOrderResp struct { + Id int64 `json:"id"` // 订单ID +} + +type AdminDeleteOrderReq struct { + Id int64 `path:"id"` // 订单ID +} + +type AdminDeleteOrderResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetOrderDetailReq struct { + Id int64 `path:"id"` // 订单ID +} + +type AdminGetOrderDetailResp struct { + Id int64 `json:"id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + QueryState string `json:"query_state"` // 查询状态:pending-待查询,success-查询成功,failed-查询失败 processing-查询中 + CreateTime string `json:"create_time"` // 创建时间 + PayTime string `json:"pay_time"` // 支付时间 + RefundTime string `json:"refund_time"` // 退款时间 + UpdateTime string `json:"update_time"` // 更新时间 + IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 + AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 +} + +type AdminGetOrderListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + OrderNo string `form:"order_no,optional"` // 商户订单号 + PlatformOrderId string `form:"platform_order_id,optional"` // 支付订单号 + ProductName string `form:"product_name,optional"` // 产品名称 + PaymentPlatform string `form:"payment_platform,optional"` // 支付方式 + PaymentScene string `form:"payment_scene,optional"` // 支付平台 + Amount float64 `form:"amount,optional"` // 金额 + Status string `form:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 + CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 + PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 + PayTimeEnd string `form:"pay_time_end,optional"` // 支付时间结束 + RefundTimeStart string `form:"refund_time_start,optional"` // 退款时间开始 + RefundTimeEnd string `form:"refund_time_end,optional"` // 退款时间结束 +} + +type AdminGetOrderListResp struct { + Total int64 `json:"total"` // 总数 + Items []OrderListItem `json:"items"` // 列表 +} + +type AdminRefundOrderReq struct { + Id int64 `path:"id"` // 订单ID + RefundAmount float64 `json:"refund_amount"` // 退款金额 + RefundReason string `json:"refund_reason"` // 退款原因 +} + +type AdminRefundOrderResp struct { + Status string `json:"status"` // 退款状态 + RefundNo string `json:"refund_no"` // 退款单号 + Amount float64 `json:"amount"` // 退款金额 +} + +type AdminRetryAgentProcessReq struct { + Id int64 `path:"id"` // 订单ID +} + +type AdminRetryAgentProcessResp struct { + Status string `json:"status"` // 执行状态:success-成功,already_processed-已处理,failed-失败 + Message string `json:"message"` // 执行结果消息 + ProcessedAt string `json:"processed_at"` // 处理时间 +} + +type AdminUpdateOrderReq struct { + Id int64 `path:"id"` // 订单ID + OrderNo *string `json:"order_no,optional"` // 商户订单号 + PlatformOrderId *string `json:"platform_order_id,optional"` // 支付订单号 + ProductName *string `json:"product_name,optional"` // 产品名称 + PaymentPlatform *string `json:"payment_platform,optional"` // 支付方式 + PaymentScene *string `json:"payment_scene,optional"` // 支付平台 + Amount *float64 `json:"amount,optional"` // 金额 + Status *string `json:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + PayTime *string `json:"pay_time,optional"` // 支付时间 + RefundTime *string `json:"refund_time,optional"` // 退款时间 +} + +type AdminUpdateOrderResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminplatformuser.go b/app/main/api/internal/types/adminplatformuser.go new file mode 100644 index 0000000..4781e36 --- /dev/null +++ b/app/main/api/internal/types/adminplatformuser.go @@ -0,0 +1,66 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminCreatePlatformUserReq struct { + Mobile string `json:"mobile"` // 手机号 + Password string `json:"password"` // 密码 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 +} + +type AdminCreatePlatformUserResp struct { + Id int64 `json:"id"` // 用户ID +} + +type AdminDeletePlatformUserReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminDeletePlatformUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetPlatformUserDetailReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminGetPlatformUserDetailResp struct { + Id int64 `json:"id"` // 用户ID + Mobile string `json:"mobile"` // 手机号 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetPlatformUserListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Mobile string `form:"mobile,optional"` // 手机号 + Nickname string `form:"nickname,optional"` // 昵称 + Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 + CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 + CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 + OrderBy string `form:"order_by,optional"` // 排序字段 + OrderType string `form:"order_type,optional"` // 排序类型 +} + +type AdminGetPlatformUserListResp struct { + Total int64 `json:"total"` // 总数 + Items []PlatformUserListItem `json:"items"` // 列表 +} + +type AdminUpdatePlatformUserReq struct { + Id int64 `path:"id"` // 用户ID + Mobile *string `json:"mobile,optional"` // 手机号 + Password *string `json:"password,optional"` // 密码 + Nickname *string `json:"nickname,optional"` // 昵称 + Info *string `json:"info,optional"` // 备注信息 + Inside *int64 `json:"inside,optional"` // 是否内部用户 1-是 0-否 +} + +type AdminUpdatePlatformUserResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminproduct.go b/app/main/api/internal/types/adminproduct.go new file mode 100644 index 0000000..5bda1a0 --- /dev/null +++ b/app/main/api/internal/types/adminproduct.go @@ -0,0 +1,91 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminCreateProductReq struct { + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes,optional"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 +} + +type AdminCreateProductResp struct { + Id int64 `json:"id"` // 产品ID +} + +type AdminDeleteProductReq struct { + Id int64 `path:"id"` // 产品ID +} + +type AdminDeleteProductResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetProductDetailReq struct { + Id int64 `path:"id"` // 产品ID +} + +type AdminGetProductDetailResp struct { + Id int64 `json:"id"` // 产品ID + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetProductFeatureListReq struct { + ProductId int64 `path:"product_id"` // 产品ID +} + +type AdminGetProductFeatureListResp struct { + Id int64 `json:"id"` // 关联ID + ProductId int64 `json:"product_id"` // 产品ID + FeatureId int64 `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"` // 更新时间 +} + +type AdminGetProductListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductName *string `form:"product_name,optional"` // 服务名 + ProductEn *string `form:"product_en,optional"` // 英文名 +} + +type AdminGetProductListResp struct { + Total int64 `json:"total"` // 总数 + Items []ProductListItem `json:"items"` // 列表数据 +} + +type AdminUpdateProductFeaturesReq struct { + ProductId int64 `path:"product_id"` // 产品ID + Features []ProductFeatureItem `json:"features"` // 功能列表 +} + +type AdminUpdateProductFeaturesResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateProductReq struct { + Id int64 `path:"id"` // 产品ID + ProductName *string `json:"product_name,optional"` // 服务名 + 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"` // 售价 +} + +type AdminUpdateProductResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminquery.go b/app/main/api/internal/types/adminquery.go new file mode 100644 index 0000000..001ab69 --- /dev/null +++ b/app/main/api/internal/types/adminquery.go @@ -0,0 +1,60 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminGetQueryCleanupConfigListReq struct { + Status int64 `form:"status,optional"` // 状态:1-启用,0-禁用 +} + +type AdminGetQueryCleanupConfigListResp struct { + Items []QueryCleanupConfigItem `json:"items"` // 配置列表 +} + +type AdminGetQueryCleanupDetailListReq struct { + LogId int64 `path:"log_id"` // 清理日志ID + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"page_size,default=20"` // 每页数量 +} + +type AdminGetQueryCleanupDetailListResp struct { + Total int64 `json:"total"` // 总数 + Items []QueryCleanupDetailItem `json:"items"` // 列表 +} + +type AdminGetQueryCleanupLogListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"page_size,default=20"` // 每页数量 + Status int64 `form:"status,optional"` // 状态:1-成功,2-失败 + StartTime string `form:"start_time,optional"` // 开始时间 + EndTime string `form:"end_time,optional"` // 结束时间 +} + +type AdminGetQueryCleanupLogListResp struct { + Total int64 `json:"total"` // 总数 + Items []QueryCleanupLogItem `json:"items"` // 列表 +} + +type AdminGetQueryDetailByOrderIdReq struct { + OrderId int64 `path:"order_id"` +} + +type AdminGetQueryDetailByOrderIdResp struct { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品ID + QueryParams map[string]interface{} `json:"query_params"` + QueryData []AdminQueryItem `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + +type AdminUpdateQueryCleanupConfigReq struct { + Id int64 `json:"id"` // 主键ID + ConfigValue string `json:"config_value"` // 配置值 + Status int64 `json:"status"` // 状态:1-启用,0-禁用 +} + +type AdminUpdateQueryCleanupConfigResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminrole.go b/app/main/api/internal/types/adminrole.go new file mode 100644 index 0000000..083c59c --- /dev/null +++ b/app/main/api/internal/types/adminrole.go @@ -0,0 +1,66 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type CreateRoleReq struct { + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort,default=0"` // 排序 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 +} + +type CreateRoleResp struct { + Id int64 `json:"id"` // 角色ID +} + +type DeleteRoleReq struct { + Id int64 `path:"id"` // 角色ID +} + +type DeleteRoleResp struct { + Success bool `json:"success"` // 是否成功 +} + +type GetRoleDetailReq struct { + Id int64 `path:"id"` // 角色ID +} + +type GetRoleDetailResp struct { + Id int64 `json:"id"` // 角色ID + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 +} + +type GetRoleListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Name string `form:"name,optional"` // 角色名称 + Code string `form:"code,optional"` // 角色编码 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 +} + +type GetRoleListResp struct { + Total int64 `json:"total"` // 总数 + Items []RoleListItem `json:"items"` // 列表 +} + +type UpdateRoleReq struct { + Id int64 `path:"id"` // 角色ID + RoleName *string `json:"role_name,optional"` // 角色名称 + RoleCode *string `json:"role_code,optional"` // 角色编码 + Description *string `json:"description,optional"` // 角色描述 + Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + Sort *int64 `json:"sort,optional"` // 排序 + MenuIds []int64 `json:"menu_ids,optional"` // 关联的菜单ID列表 +} + +type UpdateRoleResp struct { + Success bool `json:"success"` // 是否成功 +} diff --git a/app/main/api/internal/types/adminroleapi.go b/app/main/api/internal/types/adminroleapi.go new file mode 100644 index 0000000..ed17fa4 --- /dev/null +++ b/app/main/api/internal/types/adminroleapi.go @@ -0,0 +1,45 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminAssignRoleApiReq struct { + RoleId int64 `json:"role_id"` + ApiIds []int64 `json:"api_ids"` +} + +type AdminAssignRoleApiResp struct { + Success bool `json:"success"` +} + +type AdminGetAllApiListReq struct { + Status int64 `form:"status,optional,default=1"` +} + +type AdminGetAllApiListResp struct { + Items []AdminRoleApiInfo `json:"items"` +} + +type AdminGetRoleApiListReq struct { + RoleId int64 `path:"role_id"` +} + +type AdminGetRoleApiListResp struct { + Items []AdminRoleApiInfo `json:"items"` +} + +type AdminRemoveRoleApiReq struct { + RoleId int64 `json:"role_id"` + ApiIds []int64 `json:"api_ids"` +} + +type AdminRemoveRoleApiResp struct { + Success bool `json:"success"` +} + +type AdminUpdateRoleApiReq struct { + RoleId int64 `json:"role_id"` + ApiIds []int64 `json:"api_ids"` +} + +type AdminUpdateRoleApiResp struct { + Success bool `json:"success"` +} diff --git a/app/main/api/internal/types/adminuser.go b/app/main/api/internal/types/adminuser.go new file mode 100644 index 0000000..a908698 --- /dev/null +++ b/app/main/api/internal/types/adminuser.go @@ -0,0 +1,78 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AdminCreateUserReq struct { + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 +} + +type AdminCreateUserResp struct { + Id int64 `json:"id"` // 用户ID +} + +type AdminDeleteUserReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminDeleteUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetUserDetailReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminGetUserDetailResp struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 +} + +type AdminGetUserListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Username string `form:"username,optional"` // 用户名 + RealName string `form:"real_name,optional"` // 真实姓名 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 +} + +type AdminGetUserListResp struct { + Total int64 `json:"total"` // 总数 + Items []AdminUserListItem `json:"items"` // 列表 +} + +type AdminResetPasswordReq struct { + Id int64 `path:"id"` // 用户ID + Password string `json:"password"` // 新密码 +} + +type AdminResetPasswordResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateUserReq struct { + Id int64 `path:"id"` // 用户ID + Username *string `json:"username,optional"` // 用户名 + RealName *string `json:"real_name,optional"` // 真实姓名 + Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + RoleIds []int64 `json:"role_ids,optional"` // 关联的角色ID列表 +} + +type AdminUpdateUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUserInfoReq struct { +} + +type AdminUserInfoResp struct { + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Roles []string `json:"roles"` // 角色编码列表 +} diff --git a/app/main/api/internal/types/agent.go b/app/main/api/internal/types/agent.go new file mode 100644 index 0000000..aea1d59 --- /dev/null +++ b/app/main/api/internal/types/agent.go @@ -0,0 +1,279 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type AgentApplyReq struct { + Region string `json:"region,optional"` // 区域(可选) + Mobile string `json:"mobile"` // 手机号 + Code string `json:"code"` // 验证码 + InviteCode string `json:"invite_code"` // 邀请码(必填,只能通过邀请码成为代理) +} + +type AgentApplyResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type AgentGeneratingLinkReq struct { + ProductId int64 `json:"product_id"` // 产品ID + SetPrice float64 `json:"set_price"` // 设定价格 + TargetPath string `json:"target_path,optional"` // 目标地址(可选,默认为推广报告页面) +} + +type AgentGeneratingLinkResp struct { + LinkIdentifier string `json:"link_identifier"` // 推广链接标识 + FullLink string `json:"full_link"` // 完整短链URL +} + +type AgentInfoResp struct { + AgentId int64 `json:"agent_id"` // 代理ID + Level int64 `json:"level"` // 代理等级:1=普通,2=黄金,3=钻石 + LevelName string `json:"level_name"` // 等级名称 + Region string `json:"region"` // 区域(可选) + Mobile string `json:"mobile"` // 手机号 + WechatId string `json:"wechat_id"` // 微信号(可选) + TeamLeaderId int64 `json:"team_leader_id"` // 团队首领ID + IsRealName bool `json:"is_real_name"` // 是否已实名 +} + +type AgentProductConfigResp struct { + List []ProductConfigItem `json:"list"` +} + +type ApplyUpgradeReq struct { + ToLevel int64 `json:"to_level"` // 目标等级:2=黄金,3=钻石 +} + +type ApplyUpgradeResp struct { + UpgradeId int64 `json:"upgrade_id"` // 升级记录ID + OrderNo string `json:"order_no"` // 支付订单号 +} + +type ApplyWithdrawalReq struct { + Amount float64 `json:"amount"` // 提现金额 + PayeeAccount string `json:"payee_account"` // 收款账户 + PayeeName string `json:"payee_name"` // 收款人姓名 +} + +type ApplyWithdrawalResp struct { + WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID + WithdrawalNo string `json:"withdrawal_no"` // 提现单号 +} + +type ConversionRateResp struct { + MyConversionRate ConversionRateData `json:"my_conversion_rate"` // 我的转化率 + SubordinateConversionRate ConversionRateData `json:"subordinate_conversion_rate"` // 我的下级转化率 +} + +type DeleteInviteCodeReq struct { + Id int64 `json:"id"` // 邀请码ID +} + +type DeleteInviteCodeResp struct { +} + +type GenerateInviteCodeReq struct { + Count int64 `json:"count"` // 生成数量 + ExpireDays int64 `json:"expire_days,optional"` // 过期天数(可选,0表示不过期) + Remark string `json:"remark,optional"` // 备注(可选) +} + +type GenerateInviteCodeResp struct { + Codes []string `json:"codes"` // 生成的邀请码列表 +} + +type GetCommissionListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 +} + +type GetCommissionListResp struct { + Total int64 `json:"total"` // 总数 + List []CommissionItem `json:"list"` // 列表 +} + +type GetInviteCodeListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + Status int64 `form:"status,optional"` // 状态(可选) +} + +type GetInviteCodeListResp struct { + Total int64 `json:"total"` // 总数 + List []InviteCodeItem `json:"list"` // 列表 +} + +type GetInviteLinkReq struct { + InviteCode string `form:"invite_code"` // 邀请码 + TargetPath string `form:"target_path,optional"` // 目标地址(可选,默认为注册页面) +} + +type GetInviteLinkResp struct { + InviteLink string `json:"invite_link"` // 邀请链接 +} + +type GetLevelPrivilegeResp struct { + Levels []LevelPrivilegeItem `json:"levels"` // 各等级特权信息列表 +} + +type GetLinkDataReq struct { + LinkIdentifier string `form:"link_identifier"` // 推广链接标识 +} + +type GetLinkDataResp struct { + AgentId int64 `json:"agent_id"` // 代理ID + ProductId int64 `json:"product_id"` // 产品ID + SetPrice float64 `json:"set_price"` // 代理设定价格 + ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 + ProductName string `json:"product_name"` // 产品名称 + ProductEn string `json:"product_en"` // 产品英文标识 + SellPrice float64 `json:"sell_price"` // 销售价格(使用代理设定价格) + Description string `json:"description"` // 产品描述 + Features []Feature `json:"features"` // 产品功能列表 +} + +type GetRebateListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选):1=直接上级返佣,2=钻石上级返佣,3=黄金上级返佣 +} + +type GetRebateListResp struct { + Total int64 `json:"total"` // 总数 + List []RebateItem `json:"list"` // 列表 +} + +type GetRevenueInfoResp struct { + Balance float64 `json:"balance"` // 可用余额 + FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 + TotalEarnings float64 `json:"total_earnings"` // 累计收益(钱包总收益) + WithdrawnAmount float64 `json:"withdrawn_amount"` // 累计提现 + CommissionTotal float64 `json:"commission_total"` // 佣金累计总收益(推广订单获得的佣金) + CommissionToday float64 `json:"commission_today"` // 佣金今日收益 + CommissionMonth float64 `json:"commission_month"` // 佣金本月收益 + RebateTotal float64 `json:"rebate_total"` // 返佣累计总收益(包括推广返佣和升级返佣) + RebateToday float64 `json:"rebate_today"` // 返佣今日收益 + RebateMonth float64 `json:"rebate_month"` // 返佣本月收益 +} + +type GetSubordinateContributionDetailReq struct { + SubordinateId int64 `form:"subordinate_id"` // 下级代理ID + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + TabType string `form:"tab_type,optional"` // 标签页类型:order=订单列表,invite=邀请列表 +} + +type GetSubordinateContributionDetailResp struct { + Mobile string `json:"mobile"` // 手机号 + LevelName string `json:"level_name"` // 等级名称 + CreateTime string `json:"create_time"` // 创建时间 + OrderStats OrderStatistics `json:"order_stats"` // 订单统计(仅统计有返佣的订单) + RebateStats RebateStatistics `json:"rebate_stats"` // 返佣统计 + InviteStats InviteStatistics `json:"invite_stats"` // 邀请统计 + OrderList []OrderItem `json:"order_list"` // 订单列表(仅有贡献的订单) + InviteList []InviteItem `json:"invite_list"` // 邀请列表 + OrderListTotal int64 `json:"order_list_total"` // 订单列表总数 + InviteListTotal int64 `json:"invite_list_total"` // 邀请列表总数 +} + +type GetSubordinateListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 +} + +type GetSubordinateListResp struct { + Total int64 `json:"total"` // 总数 + List []SubordinateItem `json:"list"` // 列表 +} + +type GetTeamListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 + Mobile string `form:"mobile,optional"` // 手机号(可选,用于搜索) +} + +type GetTeamListResp struct { + Statistics TeamStatistics `json:"statistics"` // 统计数据 + Total int64 `json:"total"` // 总数 + List []TeamMemberItem `json:"list"` // 列表 +} + +type GetUpgradeListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 +} + +type GetUpgradeListResp struct { + Total int64 `json:"total"` // 总数 + List []UpgradeItem `json:"list"` // 列表 +} + +type GetUpgradeRebateListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 +} + +type GetUpgradeRebateListResp struct { + Total int64 `json:"total"` // 总数 + List []UpgradeRebateItem `json:"list"` // 列表 +} + +type GetWithdrawalListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数量 +} + +type GetWithdrawalListResp struct { + Total int64 `json:"total"` // 总数 + List []WithdrawalItem `json:"list"` // 列表 +} + +type RealNameAuthReq struct { + Name string `json:"name"` // 姓名 + IdCard string `json:"id_card"` // 身份证号 + Mobile string `json:"mobile"` // 手机号 + Code string `json:"code"` // 验证码 +} + +type RealNameAuthResp struct { + Status string `json:"status"` // 状态:pending=待审核,approved=已通过,rejected=已拒绝 +} + +type RegisterByInviteCodeReq struct { + InviteCode string `json:"invite_code"` // 邀请码 + Mobile string `json:"mobile"` // 手机号 + Code string `json:"code"` // 验证码 + Region string `json:"region,optional"` // 区域(可选) + WechatId string `json:"wechat_id,optional"` // 微信号(可选) +} + +type RegisterByInviteCodeResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + AgentId int64 `json:"agent_id"` // 代理ID + Level int64 `json:"level"` // 代理等级 + LevelName string `json:"level_name"` // 等级名称 +} + +type ShortLinkRedirectResp struct { +} + +type TeamStatisticsResp struct { + TotalCount int64 `json:"total_count"` // 团队总人数(不包括自己) + DirectCount int64 `json:"direct_count"` // 直接下级数量 + IndirectCount int64 `json:"indirect_count"` // 间接下级数量 + GoldCount int64 `json:"gold_count"` // 黄金代理数量 + NormalCount int64 `json:"normal_count"` // 普通代理数量 + TodayNewMembers int64 `json:"today_new_members"` // 今日新增成员 + MonthNewMembers int64 `json:"month_new_members"` // 本月新增成员 +} + +type UpgradeSubordinateReq struct { + SubordinateId int64 `json:"subordinate_id"` // 下级代理ID + ToLevel int64 `json:"to_level"` // 目标等级(只能是2=黄金) +} + +type UpgradeSubordinateResp struct { + Success bool `json:"success"` +} diff --git a/app/main/api/internal/types/app.go b/app/main/api/internal/types/app.go new file mode 100644 index 0000000..afb7f20 --- /dev/null +++ b/app/main/api/internal/types/app.go @@ -0,0 +1,12 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type HealthCheckResp struct { + Status string `json:"status"` // 服务状态 + Message string `json:"message"` // 状态信息 +} + +type GetAppVersionResp struct { + Version string `json:"version"` + WgtUrl string `json:"wgtUrl"` +} diff --git a/app/main/api/internal/types/auth.go b/app/main/api/internal/types/auth.go new file mode 100644 index 0000000..b9d80d2 --- /dev/null +++ b/app/main/api/internal/types/auth.go @@ -0,0 +1,7 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type SendSmsReq struct { + Mobile string `json:"mobile" validate:"required,mobile"` + ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"` +} diff --git a/app/main/api/internal/types/authorization.go b/app/main/api/internal/types/authorization.go new file mode 100644 index 0000000..7030697 --- /dev/null +++ b/app/main/api/internal/types/authorization.go @@ -0,0 +1,36 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type DownloadAuthorizationDocumentReq struct { + DocumentId int64 `json:"documentId" validate:"required"` // 授权书ID +} + +type DownloadAuthorizationDocumentResp struct { + FileName string `json:"fileName"` // 文件名 + FileUrl string `json:"fileUrl"` // 文件访问URL +} + +type GetAuthorizationDocumentByOrderReq struct { + OrderId int64 `json:"orderId" validate:"required"` // 订单ID +} + +type GetAuthorizationDocumentByOrderResp struct { + Documents []AuthorizationDocumentInfo `json:"documents"` // 授权书列表 +} + +type GetAuthorizationDocumentReq struct { + DocumentId int64 `json:"documentId" validate:"required"` // 授权书ID +} + +type GetAuthorizationDocumentResp struct { + DocumentId int64 `json:"documentId"` // 授权书ID + UserId int64 `json:"userId"` // 用户ID + OrderId int64 `json:"orderId"` // 订单ID + QueryId int64 `json:"queryId"` // 查询ID + FileName string `json:"fileName"` // 文件名 + FileUrl string `json:"fileUrl"` // 文件访问URL + FileSize int64 `json:"fileSize"` // 文件大小 + FileType string `json:"fileType"` // 文件类型 + Status string `json:"status"` // 状态 + CreateTime string `json:"createTime"` // 创建时间 +} diff --git a/app/main/api/internal/types/notification.go b/app/main/api/internal/types/notification.go new file mode 100644 index 0000000..ce76db9 --- /dev/null +++ b/app/main/api/internal/types/notification.go @@ -0,0 +1,7 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type GetNotificationsResp struct { + Notifications []Notification `json:"notifications"` // 通知列表 + Total int64 `json:"total"` // 总记录数 +} diff --git a/app/main/api/internal/types/pay.go b/app/main/api/internal/types/pay.go new file mode 100644 index 0000000..0811a6d --- /dev/null +++ b/app/main/api/internal/types/pay.go @@ -0,0 +1,28 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type IapCallbackReq struct { + OrderID int64 `json:"order_id" validate:"required"` + TransactionReceipt string `json:"transaction_receipt" validate:"required"` +} + +type PaymentCheckReq struct { + OrderNo string `json:"order_no" validate:"required"` +} + +type PaymentCheckResp struct { + Type string `json:"type"` + Status string `json:"status"` +} + +type PaymentReq struct { + Id string `json:"id"` + PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式) + PayType string `json:"pay_type" validate:"required,oneof=query agent_vip agent_upgrade"` +} + +type PaymentResp struct { + PrepayData interface{} `json:"prepay_data"` + PrepayId string `json:"prepay_id"` + OrderNo string `json:"order_no"` +} diff --git a/app/main/api/internal/types/payload.go b/app/main/api/internal/types/payload.go index 672918f..f638db8 100644 --- a/app/main/api/internal/types/payload.go +++ b/app/main/api/internal/types/payload.go @@ -3,3 +3,11 @@ package types type MsgPaySuccessQueryPayload struct { OrderID int64 `json:"order_id"` } + +type MsgAgentProcessPayload struct { + OrderID int64 `json:"order_id"` +} + +type MsgUnfreezeCommissionPayload struct { + FreezeTaskId int64 `json:"freeze_task_id"` // 冻结任务ID +} \ No newline at end of file diff --git a/app/main/api/internal/types/product.go b/app/main/api/internal/types/product.go new file mode 100644 index 0000000..0787aa2 --- /dev/null +++ b/app/main/api/internal/types/product.go @@ -0,0 +1,14 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type GetProductByEnRequest struct { + ProductEn string `path:"product_en"` +} + +type GetProductByIDRequest struct { + Id int64 `path:"id"` +} + +type ProductResponse struct { + Product +} diff --git a/app/main/api/internal/types/query.go b/app/main/api/internal/types/query.go index 518cb9f..525fe3d 100644 --- a/app/main/api/internal/types/query.go +++ b/app/main/api/internal/types/query.go @@ -1,125 +1,90 @@ +// Code generated by goctl. DO NOT EDIT. package types -type MarriageReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` -} -type HomeServiceReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` +type QueryDetailByOrderIdReq struct { + OrderId int64 `path:"order_id"` } -// RiskAssessment 查询请求结构 -type RiskAssessmentReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` +type QueryDetailByOrderNoReq struct { + OrderNo string `path:"order_no"` } -// CompanyInfo 查询请求结构 -type CompanyInfoReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` +type QueryExampleReq struct { + Feature string `form:"feature"` } -// RentalInfo 查询请求结构 -type RentalInfoReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` +type QueryGenerateShareLinkReq struct { + OrderId *int64 `json:"order_id,optional"` + OrderNo *string `json:"order_no,optional"` } -// PreLoanBackgroundCheck 查询请求结构 -type PreLoanBackgroundCheckReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` +type QueryGenerateShareLinkResp struct { + ShareLink string `json:"share_link"` } -// BackgroundCheck 查询请求结构 -type BackgroundCheckReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` -} -type PersonalDataReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` -} -type ConsumerFinanceReportReq struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` -} -type EntLawsuitReq struct { - EntName string `json:"ent_name" validate:"required,name"` - EntCode string `json:"ent_code" validate:"required,USCI"` - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` -} -type TocPhoneThreeElements struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` -} -type TocPhoneTwoElements struct { - Name string `json:"name" validate:"required,name"` - Mobile string `json:"mobile" validate:"required,mobile"` -} -type TocIDCardTwoElements struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` -} -type TocDualMarriage struct { - NameMan string `json:"name_man" validate:"required,name"` - IDCardMan string `json:"id_card_man" validate:"required,idCard"` - NameWoman string `json:"name_woman" validate:"required,name"` - IDCardWoman string `json:"id_card_woman" validate:"required,idCard"` -} -type TocPersonVehicleVerification struct { - Name string `json:"name" validate:"required,name"` - CarType string `json:"car_type" validate:"required"` - CarLicense string `json:"car_license" validate:"required"` +type QueryListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 } -// 银行卡黑名单 -type TocBankCardBlacklist struct { - Name string `json:"name" validate:"required,name"` - IDCard string `json:"id_card" validate:"required,idCard"` - Mobile string `json:"mobile" validate:"required,mobile"` - BankCard string `json:"bank_card" validate:"required"` +type QueryListResp struct { + Total int64 `json:"total"` // 总记录数 + List []Query `json:"list"` // 查询列表 } -// 手机号码风险 -type TocPhoneNumberRisk struct { - Mobile string `json:"mobile" validate:"required,mobile"` +type QueryProvisionalOrderReq struct { + Id string `path:"id"` } -// 手机二次卡 -type TocPhoneSecondaryCard struct { - Mobile string `json:"mobile" validate:"required,mobile"` - StartDate string `json:"start_date" validate:"required"` +type QueryProvisionalOrderResp struct { + Name string `json:"name"` + IdCard string `json:"id_card"` + Mobile string `json:"mobile"` + Product Product `json:"product"` } -type AgentQueryData struct { - Mobile string `json:"mobile"` - Code string `json:"code"` +type QueryRetryReq struct { + Id int64 `path:"id"` } -type AgentIdentifier struct { - ProductID int64 `json:"product_id"` - AgentID int64 `json:"agent_id"` - SetPrice float64 `json:"set_price"` + +type QueryRetryResp struct { + Query +} + +type QueryServiceReq struct { + Product string `path:"product"` + Data string `json:"data" validate:"required"` + AgentIdentifier string `json:"agent_identifier,optional"` + App bool `json:"app,optional"` +} + +type QueryServiceResp struct { + Id string `json:"id"` + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type QueryShareDetailReq struct { + Id string `path:"id"` +} + +type QuerySingleTestReq struct { + Params map[string]interface{} `json:"params"` + Api string `json:"api"` +} + +type QuerySingleTestResp struct { + Data interface{} `json:"data"` + Api string `json:"api"` +} + +type UpdateQueryDataReq struct { + Id int64 `json:"id"` // 查询ID + QueryData string `json:"query_data"` // 查询数据(未加密的JSON) +} + +type UpdateQueryDataResp struct { + Id int64 `json:"id"` + UpdatedAt string `json:"updated_at"` // 更新时间 } diff --git a/app/main/api/internal/types/queryMap.go b/app/main/api/internal/types/queryMap.go index 13256a6..15c3365 100644 --- a/app/main/api/internal/types/queryMap.go +++ b/app/main/api/internal/types/queryMap.go @@ -1,5 +1,130 @@ package types +type MarriageReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type HomeServiceReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// RiskAssessment 查询请求结构 +type RiskAssessmentReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// CompanyInfo 查询请求结构 +type CompanyInfoReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// RentalInfo 查询请求结构 +type RentalInfoReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// PreLoanBackgroundCheck 查询请求结构 +type PreLoanBackgroundCheckReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// BackgroundCheck 查询请求结构 +type BackgroundCheckReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type PersonalDataReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +type ConsumerFinanceReportReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type EntLawsuitReq struct { + EntName string `json:"ent_name" validate:"required,name"` + EntCode string `json:"ent_code" validate:"required,USCI"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type TocPhoneThreeElements struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` +} +type TocPhoneTwoElements struct { + Name string `json:"name" validate:"required,name"` + Mobile string `json:"mobile" validate:"required,mobile"` +} +type TocIDCardTwoElements struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` +} +type TocDualMarriage struct { + NameMan string `json:"name_man" validate:"required,name"` + IDCardMan string `json:"id_card_man" validate:"required,idCard"` + NameWoman string `json:"name_woman" validate:"required,name"` + IDCardWoman string `json:"id_card_woman" validate:"required,idCard"` +} +type TocPersonVehicleVerification struct { + Name string `json:"name" validate:"required,name"` + CarType string `json:"car_type" validate:"required"` + CarLicense string `json:"car_license" validate:"required"` +} + +// 银行卡黑名单 +type TocBankCardBlacklist struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + BankCard string `json:"bank_card" validate:"required"` +} + +// 手机号码风险 +type TocPhoneNumberRisk struct { + Mobile string `json:"mobile" validate:"required,mobile"` +} + +// 手机二次卡 +type TocPhoneSecondaryCard struct { + Mobile string `json:"mobile" validate:"required,mobile"` + StartDate string `json:"start_date" validate:"required"` +} + +type AgentQueryData struct { + Mobile string `json:"mobile"` + Code string `json:"code"` +} +type AgentIdentifier struct { + AgentID int64 `json:"agent_id"` // 代理ID + ProductID int64 `json:"product_id"` // 产品ID + SetPrice float64 `json:"set_price"` // 代理设定价格 +} + // 特殊名单 G26BJ05 var G26BJ05FieldMapping = map[string]string{ "IDCard": "id", diff --git a/app/main/api/internal/types/taskname.go b/app/main/api/internal/types/taskname.go index 33329ee..4a7b9e6 100644 --- a/app/main/api/internal/types/taskname.go +++ b/app/main/api/internal/types/taskname.go @@ -2,3 +2,6 @@ package types const MsgPaySuccessQuery = "msg:pay_success:query" const MsgCleanQueryData = "msg:clean_query_data" +const MsgAgentProcess = "msg:agent:process" +const MsgUnfreezeCommission = "msg:unfreeze:commission" +const MsgUnfreezeCommissionScan = "msg:unfreeze:commission:scan" diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index eca86cd..cf2c014 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -13,25 +13,6 @@ type AdminApiInfo struct { UpdateTime string `json:"update_time"` } -type AdminAssignRoleApiReq struct { - RoleId int64 `json:"role_id"` - ApiIds []int64 `json:"api_ids"` -} - -type AdminAssignRoleApiResp struct { - Success bool `json:"success"` -} - -type AdminAuditAgentReq struct { - AuditId int64 `json:"audit_id"` // 审核记录ID - Status int64 `json:"status"` // 审核状态:1=通过,2=拒绝 - AuditReason string `json:"audit_reason"` // 审核原因(拒绝时必填) -} - -type AdminAuditAgentResp struct { - Success bool `json:"success"` -} - type AdminAuditRealNameReq struct { RealNameId int64 `json:"real_name_id"` // 实名认证记录ID Status int64 `json:"status"` // 审核状态:2=通过,3=拒绝 @@ -42,692 +23,11 @@ type AdminAuditRealNameResp struct { Success bool `json:"success"` } -type AdminAuditWithdrawalReq struct { - WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID - Status int64 `json:"status"` // 审核状态:2=通过,3=拒绝 - Remark string `json:"remark"` // 备注 -} - -type AdminAuditWithdrawalResp struct { - Success bool `json:"success"` -} - -type AdminBatchUpdateApiStatusReq struct { - Ids []int64 `json:"ids"` - Status int64 `json:"status"` -} - -type AdminBatchUpdateApiStatusResp struct { - Success bool `json:"success"` -} - -type AdminConfigFeatureExampleReq struct { - FeatureId int64 `json:"feature_id"` // 功能ID - Data string `json:"data"` // 示例数据JSON -} - -type AdminConfigFeatureExampleResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminCreateApiReq struct { - ApiName string `json:"api_name"` - ApiCode string `json:"api_code"` - Method string `json:"method"` - Url string `json:"url"` - Status int64 `json:"status,default=1"` - Description string `json:"description,optional"` -} - -type AdminCreateApiResp struct { - Id int64 `json:"id"` -} - -type AdminCreateFeatureReq struct { - ApiId string `json:"api_id"` // API标识 - Name string `json:"name"` // 描述 -} - -type AdminCreateFeatureResp struct { - Id int64 `json:"id"` // 功能ID -} - -type AdminCreateNotificationReq struct { - Title string `json:"title"` // 通知标题 - NotificationPage string `json:"notification_page"` // 通知页面 - Content string `json:"content"` // 通知内容 - StartDate string `json:"start_date"` // 生效开始日期(yyyy-MM-dd) - StartTime string `json:"start_time"` // 生效开始时间(HH:mm:ss) - EndDate string `json:"end_date"` // 生效结束日期(yyyy-MM-dd) - EndTime string `json:"end_time"` // 生效结束时间(HH:mm:ss) - Status int64 `json:"status"` // 状态:1-启用,0-禁用 -} - -type AdminCreateNotificationResp struct { - Id int64 `json:"id"` // 通知ID -} - -type AdminCreateOrderReq struct { - OrderNo string `json:"order_no"` // 商户订单号 - PlatformOrderId string `json:"platform_order_id"` // 支付订单号 - ProductName string `json:"product_name"` // 产品名称 - PaymentPlatform string `json:"payment_platform"` // 支付方式 - PaymentScene string `json:"payment_scene"` // 支付平台 - Amount float64 `json:"amount"` // 金额 - Status string `json:"status,default=pending"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 - IsPromotion int64 `json:"is_promotion,default=0"` // 是否推广订单:0-否,1-是 -} - -type AdminCreateOrderResp struct { - Id int64 `json:"id"` // 订单ID -} - -type AdminCreatePlatformUserReq struct { - Mobile string `json:"mobile"` // 手机号 - Password string `json:"password"` // 密码 - Nickname string `json:"nickname"` // 昵称 - Info string `json:"info"` // 备注信息 - Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 -} - -type AdminCreatePlatformUserResp struct { - Id int64 `json:"id"` // 用户ID -} - -type AdminCreateProductReq struct { - ProductName string `json:"product_name"` // 服务名 - ProductEn string `json:"product_en"` // 英文名 - Description string `json:"description"` // 描述 - Notes string `json:"notes,optional"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 - SellPrice float64 `json:"sell_price"` // 售价 -} - -type AdminCreateProductResp struct { - Id int64 `json:"id"` // 产品ID -} - -type AdminCreateUserReq struct { - Username string `json:"username"` // 用户名 - RealName string `json:"real_name"` // 真实姓名 - Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 - RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 -} - -type AdminCreateUserResp struct { - Id int64 `json:"id"` // 用户ID -} - -type AdminDeleteApiReq struct { - Id int64 `path:"id"` -} - -type AdminDeleteApiResp struct { - Success bool `json:"success"` -} - -type AdminDeleteFeatureReq struct { - Id int64 `path:"id"` // 功能ID -} - -type AdminDeleteFeatureResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminDeleteNotificationReq struct { - Id int64 `path:"id"` // 通知ID -} - -type AdminDeleteNotificationResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminDeleteOrderReq struct { - Id int64 `path:"id"` // 订单ID -} - -type AdminDeleteOrderResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminDeletePlatformUserReq struct { - Id int64 `path:"id"` // 用户ID -} - -type AdminDeletePlatformUserResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminDeleteProductReq struct { - Id int64 `path:"id"` // 产品ID -} - -type AdminDeleteProductResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminDeleteUserReq struct { - Id int64 `path:"id"` // 用户ID -} - -type AdminDeleteUserResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminGenerateDiamondInviteCodeReq struct { - Count int64 `json:"count"` // 生成数量 - ExpireDays int64 `json:"expire_days,optional"` // 过期天数(可选,0表示不过期) - Remark string `json:"remark,optional"` // 备注(可选) -} - -type AdminGenerateDiamondInviteCodeResp struct { - Codes []string `json:"codes"` // 生成的邀请码列表 -} - -type AdminGetAgentCommissionListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - OrderId *int64 `form:"order_id,optional"` // 订单ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选) -} - -type AdminGetAgentCommissionListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentCommissionListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentConfigResp struct { - BasePrice float64 `json:"base_price"` // 基础底价 - SystemMaxPrice float64 `json:"system_max_price"` // 系统价格上限 - PriceThreshold float64 `json:"price_threshold"` // 提价标准阈值 - PriceFeeRate float64 `json:"price_fee_rate"` // 提价手续费比例 - LevelBonus LevelBonusConfig `json:"level_bonus"` // 等级加成配置 - UpgradeFee UpgradeFeeConfig `json:"upgrade_fee"` // 升级费用配置 - UpgradeRebate UpgradeRebateConfig `json:"upgrade_rebate"` // 升级返佣配置 - TaxRate float64 `json:"tax_rate"` // 税率 - TaxExemptionAmount float64 `json:"tax_exemption_amount"` // 免税额度 -} - -type AdminGetAgentLinkListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - ProductId *int64 `form:"product_id,optional"` // 产品ID(可选) - LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) -} - -type AdminGetAgentLinkListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentLinkListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - Mobile *string `form:"mobile,optional"` // 手机号(可选) - Region *string `form:"region,optional"` // 区域(可选) - Level *int64 `form:"level,optional"` // 等级(可选) - TeamLeaderId *int64 `form:"team_leader_id,optional"` // 团队首领ID(可选) -} - -type AdminGetAgentListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentOrderListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - OrderId *int64 `form:"order_id,optional"` // 订单ID(可选) - ProcessStatus *int64 `form:"process_status,optional"` // 处理状态(可选) -} - -type AdminGetAgentOrderListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentOrderListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentProductConfigListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - ProductId *int64 `form:"product_id,optional"` // 产品ID(可选) -} - -type AdminGetAgentProductConfigListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentProductConfigItem `json:"items"` // 列表数据 -} - -type AdminGetAgentRealNameListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选):1=未验证,2=已通过 -} - -type AdminGetAgentRealNameListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentRealNameListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentRebateListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - SourceAgentId *int64 `form:"source_agent_id,optional"` // 来源代理ID(可选) - RebateType *int64 `form:"rebate_type,optional"` // 返佣类型(可选) -} - -type AdminGetAgentRebateListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentRebateListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentUpgradeListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - UpgradeType *int64 `form:"upgrade_type,optional"` // 升级类型(可选) - Status *int64 `form:"status,optional"` // 状态(可选) -} - -type AdminGetAgentUpgradeListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentUpgradeListItem `json:"items"` // 列表数据 -} - -type AdminGetAgentWithdrawalListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) - Status *int64 `form:"status,optional"` // 状态(可选) - WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) -} - -type AdminGetAgentWithdrawalListResp struct { - Total int64 `json:"total"` // 总数 - Items []AgentWithdrawalListItem `json:"items"` // 列表数据 -} - -type AdminGetAllApiListReq struct { - Status int64 `form:"status,optional,default=1"` -} - -type AdminGetAllApiListResp struct { - Items []AdminRoleApiInfo `json:"items"` -} - -type AdminGetApiDetailReq struct { - Id int64 `path:"id"` -} - -type AdminGetApiDetailResp struct { - AdminApiInfo -} - -type AdminGetApiListReq struct { - Page int64 `form:"page,default=1"` - PageSize int64 `form:"page_size,default=20"` - ApiName string `form:"api_name,optional"` - Method string `form:"method,optional"` - Status int64 `form:"status,optional"` -} - -type AdminGetApiListResp struct { - Items []AdminApiInfo `json:"items"` - Total int64 `json:"total"` -} - -type AdminGetFeatureDetailReq struct { - Id int64 `path:"id"` // 功能ID -} - -type AdminGetFeatureDetailResp struct { - Id int64 `json:"id"` // 功能ID - ApiId string `json:"api_id"` // API标识 - Name string `json:"name"` // 描述 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 -} - -type AdminGetFeatureExampleReq struct { - FeatureId int64 `path:"feature_id"` // 功能ID -} - -type AdminGetFeatureExampleResp struct { - Id int64 `json:"id"` // 示例数据ID - FeatureId int64 `json:"feature_id"` // 功能ID - ApiId string `json:"api_id"` // API标识 - Data string `json:"data"` // 示例数据JSON - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 -} - -type AdminGetFeatureListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - ApiId *string `form:"api_id,optional"` // API标识 - Name *string `form:"name,optional"` // 描述 -} - -type AdminGetFeatureListResp struct { - Total int64 `json:"total"` // 总数 - Items []FeatureListItem `json:"items"` // 列表数据 -} - -type AdminGetInviteCodeListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - Code *string `form:"code,optional"` // 邀请码(可选) - AgentId *int64 `form:"agent_id,optional"` // 发放代理ID(可选,NULL表示平台发放) - TargetLevel *int64 `form:"target_level,optional"` // 目标等级(可选) - Status *int64 `form:"status,optional"` // 状态(可选) -} - -type AdminGetInviteCodeListResp struct { - Total int64 `json:"total"` // 总数 - Items []InviteCodeListItem `json:"items"` // 列表数据 -} - -type AdminGetNotificationDetailReq struct { - Id int64 `path:"id"` // 通知ID -} - -type AdminGetNotificationDetailResp struct { - Id int64 `json:"id"` // 通知ID - Title string `json:"title"` // 通知标题 - Content string `json:"content"` // 通知内容 - NotificationPage string `json:"notification_page"` // 通知页面 - StartDate string `json:"start_date"` // 生效开始日期 - StartTime string `json:"start_time"` // 生效开始时间 - EndDate string `json:"end_date"` // 生效结束日期 - EndTime string `json:"end_time"` // 生效结束时间 - Status int64 `json:"status"` // 状态 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 -} - -type AdminGetNotificationListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - Title *string `form:"title,optional"` // 通知标题(可选) - NotificationPage *string `form:"notification_page,optional"` // 通知页面(可选) - Status *int64 `form:"status,optional"` // 状态(可选) - StartDate *string `form:"start_date,optional"` // 开始日期范围(可选) - EndDate *string `form:"end_date,optional"` // 结束日期范围(可选) -} - -type AdminGetNotificationListResp struct { - Total int64 `json:"total"` // 总数 - Items []NotificationListItem `json:"items"` // 列表数据 -} - -type AdminGetOrderDetailReq struct { - Id int64 `path:"id"` // 订单ID -} - -type AdminGetOrderDetailResp struct { - Id int64 `json:"id"` // 订单ID - OrderNo string `json:"order_no"` // 商户订单号 - PlatformOrderId string `json:"platform_order_id"` // 支付订单号 - ProductName string `json:"product_name"` // 产品名称 - PaymentPlatform string `json:"payment_platform"` // 支付方式 - PaymentScene string `json:"payment_scene"` // 支付平台 - Amount float64 `json:"amount"` // 金额 - Status string `json:"status"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 - QueryState string `json:"query_state"` // 查询状态:pending-待查询,success-查询成功,failed-查询失败 processing-查询中 - CreateTime string `json:"create_time"` // 创建时间 - PayTime string `json:"pay_time"` // 支付时间 - RefundTime string `json:"refund_time"` // 退款时间 - IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 - UpdateTime string `json:"update_time"` // 更新时间 - IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 - AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 -} - -type AdminGetOrderListReq struct { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"pageSize,default=20"` // 每页数量 - OrderNo string `form:"order_no,optional"` // 商户订单号 - PlatformOrderId string `form:"platform_order_id,optional"` // 支付订单号 - ProductName string `form:"product_name,optional"` // 产品名称 - PaymentPlatform string `form:"payment_platform,optional"` // 支付方式 - PaymentScene string `form:"payment_scene,optional"` // 支付平台 - Amount float64 `form:"amount,optional"` // 金额 - Status string `form:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 - IsPromotion int64 `form:"is_promotion,optional,default=-1"` // 是否推广订单:0-否,1-是 - CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 - CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 - PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 - PayTimeEnd string `form:"pay_time_end,optional"` // 支付时间结束 - RefundTimeStart string `form:"refund_time_start,optional"` // 退款时间开始 - RefundTimeEnd string `form:"refund_time_end,optional"` // 退款时间结束 -} - -type AdminGetOrderListResp struct { - Total int64 `json:"total"` // 总数 - Items []OrderListItem `json:"items"` // 列表 -} - -type AdminGetPlatformUserDetailReq struct { - Id int64 `path:"id"` // 用户ID -} - -type AdminGetPlatformUserDetailResp struct { - Id int64 `json:"id"` // 用户ID - Mobile string `json:"mobile"` // 手机号 - Nickname string `json:"nickname"` // 昵称 - Info string `json:"info"` // 备注信息 - Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 -} - -type AdminGetPlatformUserListReq struct { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"pageSize,default=20"` // 每页数量 - Mobile string `form:"mobile,optional"` // 手机号 - Nickname string `form:"nickname,optional"` // 昵称 - Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 - CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 - CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 - OrderBy string `form:"order_by,optional"` // 排序字段 - OrderType string `form:"order_type,optional"` // 排序类型 -} - -type AdminGetPlatformUserListResp struct { - Total int64 `json:"total"` // 总数 - Items []PlatformUserListItem `json:"items"` // 列表 -} - -type AdminGetProductDetailReq struct { - Id int64 `path:"id"` // 产品ID -} - -type AdminGetProductDetailResp struct { - Id int64 `json:"id"` // 产品ID - ProductName string `json:"product_name"` // 服务名 - ProductEn string `json:"product_en"` // 英文名 - Description string `json:"description"` // 描述 - Notes string `json:"notes"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本 - SellPrice float64 `json:"sell_price"` // 售价 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 -} - -type AdminGetProductFeatureListReq struct { - ProductId int64 `path:"product_id"` // 产品ID -} - -type AdminGetProductFeatureListResp struct { - Id int64 `json:"id"` // 关联ID - ProductId int64 `json:"product_id"` // 产品ID - FeatureId int64 `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"` // 更新时间 -} - -type AdminGetProductListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"pageSize"` // 每页数量 - ProductName *string `form:"product_name,optional"` // 服务名 - ProductEn *string `form:"product_en,optional"` // 英文名 -} - -type AdminGetProductListResp struct { - Total int64 `json:"total"` // 总数 - Items []ProductListItem `json:"items"` // 列表数据 -} - -type AdminGetQueryCleanupConfigListReq struct { - Status int64 `form:"status,optional"` // 状态:1-启用,0-禁用 -} - -type AdminGetQueryCleanupConfigListResp struct { - Items []QueryCleanupConfigItem `json:"items"` // 配置列表 -} - -type AdminGetQueryCleanupDetailListReq struct { - LogId int64 `path:"log_id"` // 清理日志ID - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"page_size,default=20"` // 每页数量 -} - -type AdminGetQueryCleanupDetailListResp struct { - Total int64 `json:"total"` // 总数 - Items []QueryCleanupDetailItem `json:"items"` // 列表 -} - -type AdminGetQueryCleanupLogListReq struct { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"page_size,default=20"` // 每页数量 - Status int64 `form:"status,optional"` // 状态:1-成功,2-失败 - StartTime string `form:"start_time,optional"` // 开始时间 - EndTime string `form:"end_time,optional"` // 结束时间 -} - -type AdminGetQueryCleanupLogListResp struct { - Total int64 `json:"total"` // 总数 - Items []QueryCleanupLogItem `json:"items"` // 列表 -} - -type AdminGetQueryDetailByOrderIdReq struct { - OrderId int64 `path:"order_id"` -} - -type AdminGetQueryDetailByOrderIdResp struct { - Id int64 `json:"id"` // 主键ID - OrderId int64 `json:"order_id"` // 订单ID - UserId int64 `json:"user_id"` // 用户ID - ProductName string `json:"product_name"` // 产品ID - QueryParams map[string]interface{} `json:"query_params"` - QueryData []AdminQueryItem `json:"query_data"` - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 - QueryState string `json:"query_state"` // 查询状态 -} - -type AdminGetRoleApiListReq struct { - RoleId int64 `path:"role_id"` -} - -type AdminGetRoleApiListResp struct { - Items []AdminRoleApiInfo `json:"items"` -} - -type AdminGetUserDetailReq struct { - Id int64 `path:"id"` // 用户ID -} - -type AdminGetUserDetailResp struct { - Id int64 `json:"id"` // 用户ID - Username string `json:"username"` // 用户名 - RealName string `json:"real_name"` // 真实姓名 - Status int64 `json:"status"` // 状态:0-禁用,1-启用 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 - RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 -} - -type AdminGetUserListReq struct { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"pageSize,default=20"` // 每页数量 - Username string `form:"username,optional"` // 用户名 - RealName string `form:"real_name,optional"` // 真实姓名 - Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 -} - -type AdminGetUserListResp struct { - Total int64 `json:"total"` // 总数 - Items []AdminUserListItem `json:"items"` // 列表 -} - -type AdminLoginReq struct { - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` - Captcha bool `json:"captcha" validate:"required"` -} - -type AdminLoginResp struct { - AccessToken string `json:"access_token"` - AccessExpire int64 `json:"access_expire"` - RefreshAfter int64 `json:"refresh_after"` - Roles []string `json:"roles"` -} - type AdminQueryItem struct { Feature interface{} `json:"feature"` Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct } -type AdminRefundOrderReq struct { - Id int64 `path:"id"` // 订单ID - RefundAmount float64 `json:"refund_amount"` // 退款金额 - RefundReason string `json:"refund_reason"` // 退款原因 -} - -type AdminRefundOrderResp struct { - Status string `json:"status"` // 退款状态 - RefundNo string `json:"refund_no"` // 退款单号 - Amount float64 `json:"amount"` // 退款金额 -} - -type AdminRemoveRoleApiReq struct { - RoleId int64 `json:"role_id"` - ApiIds []int64 `json:"api_ids"` -} - -type AdminRemoveRoleApiResp struct { - Success bool `json:"success"` -} - -type AdminResetPasswordReq struct { - Id int64 `path:"id"` // 用户ID - Password string `json:"password"` // 新密码 -} - -type AdminResetPasswordResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminRetryAgentProcessReq struct { - Id int64 `path:"id"` // 订单ID -} - -type AdminRetryAgentProcessResp struct { - Status string `json:"status"` // 执行状态:success-成功,already_processed-已处理,failed-失败 - Message string `json:"message"` // 执行结果消息 - ProcessedAt string `json:"processed_at"` // 处理时间 -} - type AdminRoleApiInfo struct { Id int64 `json:"id"` RoleId int64 `json:"role_id"` @@ -740,166 +40,6 @@ type AdminRoleApiInfo struct { Description string `json:"description"` } -type AdminUpdateAgentConfigReq struct { - BasePrice *float64 `json:"base_price,optional"` // 基础底价 - SystemMaxPrice *float64 `json:"system_max_price,optional"` // 系统价格上限 - PriceThreshold *float64 `json:"price_threshold,optional"` // 提价标准阈值 - PriceFeeRate *float64 `json:"price_fee_rate,optional"` // 提价手续费比例 - TaxRate *float64 `json:"tax_rate,optional"` // 税率 - TaxExemptionAmount *float64 `json:"tax_exemption_amount,optional"` // 免税额度 -} - -type AdminUpdateAgentConfigResp struct { - Success bool `json:"success"` -} - -type AdminUpdateAgentProductConfigReq struct { - Id int64 `json:"id"` // 主键 - BasePrice float64 `json:"base_price"` // 基础底价 - PriceRangeMin float64 `json:"price_range_min"` // 最低定价 - PriceRangeMax float64 `json:"price_range_max"` // 最高定价 - PriceThreshold float64 `json:"price_threshold"` // 提价标准阈值 - PriceFeeRate float64 `json:"price_fee_rate"` // 提价手续费比例 -} - -type AdminUpdateAgentProductConfigResp struct { - Success bool `json:"success"` -} - -type AdminUpdateApiReq struct { - Id int64 `path:"id"` - ApiName string `json:"api_name"` - ApiCode string `json:"api_code"` - Method string `json:"method"` - Url string `json:"url"` - Status int64 `json:"status"` - Description string `json:"description,optional"` -} - -type AdminUpdateApiResp struct { - Success bool `json:"success"` -} - -type AdminUpdateFeatureReq struct { - Id int64 `path:"id"` // 功能ID - ApiId *string `json:"api_id,optional"` // API标识 - Name *string `json:"name,optional"` // 描述 -} - -type AdminUpdateFeatureResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdateNotificationReq struct { - Id int64 `path:"id"` // 通知ID - Title *string `json:"title,optional"` // 通知标题 - Content *string `json:"content,optional"` // 通知内容 - NotificationPage *string `json:"notification_page,optional"` // 通知页面 - StartDate *string `json:"start_date,optional"` // 生效开始日期 - StartTime *string `json:"start_time,optional"` // 生效开始时间 - EndDate *string `json:"end_date,optional"` // 生效结束日期 - EndTime *string `json:"end_time,optional"` // 生效结束时间 - Status *int64 `json:"status,optional"` // 状态 -} - -type AdminUpdateNotificationResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdateOrderReq struct { - Id int64 `path:"id"` // 订单ID - OrderNo *string `json:"order_no,optional"` // 商户订单号 - PlatformOrderId *string `json:"platform_order_id,optional"` // 支付订单号 - ProductName *string `json:"product_name,optional"` // 产品名称 - PaymentPlatform *string `json:"payment_platform,optional"` // 支付方式 - PaymentScene *string `json:"payment_scene,optional"` // 支付平台 - Amount *float64 `json:"amount,optional"` // 金额 - Status *string `json:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 - PayTime *string `json:"pay_time,optional"` // 支付时间 - RefundTime *string `json:"refund_time,optional"` // 退款时间 - IsPromotion *int64 `json:"is_promotion,optional"` // 是否推广订单:0-否,1-是 -} - -type AdminUpdateOrderResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdatePlatformUserReq struct { - Id int64 `path:"id"` // 用户ID - Mobile *string `json:"mobile,optional"` // 手机号 - Password *string `json:"password,optional"` // 密码 - Nickname *string `json:"nickname,optional"` // 昵称 - Info *string `json:"info,optional"` // 备注信息 - Inside *int64 `json:"inside,optional"` // 是否内部用户 1-是 0-否 -} - -type AdminUpdatePlatformUserResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdateProductFeaturesReq struct { - ProductId int64 `path:"product_id"` // 产品ID - Features []ProductFeatureItem `json:"features"` // 功能列表 -} - -type AdminUpdateProductFeaturesResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdateProductReq struct { - Id int64 `path:"id"` // 产品ID - ProductName *string `json:"product_name,optional"` // 服务名 - 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"` // 售价 -} - -type AdminUpdateProductResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdateQueryCleanupConfigReq struct { - Id int64 `json:"id"` // 主键ID - ConfigValue string `json:"config_value"` // 配置值 - Status int64 `json:"status"` // 状态:1-启用,0-禁用 -} - -type AdminUpdateQueryCleanupConfigResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUpdateRoleApiReq struct { - RoleId int64 `json:"role_id"` - ApiIds []int64 `json:"api_ids"` -} - -type AdminUpdateRoleApiResp struct { - Success bool `json:"success"` -} - -type AdminUpdateUserReq struct { - Id int64 `path:"id"` // 用户ID - Username *string `json:"username,optional"` // 用户名 - RealName *string `json:"real_name,optional"` // 真实姓名 - Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 - RoleIds []int64 `json:"role_ids,optional"` // 关联的角色ID列表 -} - -type AdminUpdateUserResp struct { - Success bool `json:"success"` // 是否成功 -} - -type AdminUserInfoReq struct { -} - -type AdminUserInfoResp struct { - Username string `json:"username"` // 用户名 - RealName string `json:"real_name"` // 真实姓名 - Roles []string `json:"roles"` // 角色编码列表 -} - type AdminUserListItem struct { Id int64 `json:"id"` // 用户ID Username string `json:"username"` // 用户名 @@ -909,19 +49,6 @@ type AdminUserListItem struct { RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 } -type AgentApplyReq struct { - Region string `json:"region,optional"` // 区域(可选) - Mobile string `json:"mobile"` // 手机号 - Code string `json:"code"` // 验证码 - InviteCode string `json:"invite_code"` // 邀请码(必填,只能通过邀请码成为代理) -} - -type AgentApplyResp struct { - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` -} - type AgentCommissionListItem struct { Id int64 `json:"id"` // 主键 AgentId int64 `json:"agent_id"` // 代理ID @@ -932,26 +59,6 @@ type AgentCommissionListItem struct { CreateTime string `json:"create_time"` // 创建时间 } -type AgentGeneratingLinkReq struct { - ProductId int64 `json:"product_id"` // 产品ID - SetPrice float64 `json:"set_price"` // 设定价格 -} - -type AgentGeneratingLinkResp struct { - LinkIdentifier string `json:"link_identifier"` // 推广链接标识 -} - -type AgentInfoResp struct { - AgentId int64 `json:"agent_id"` // 代理ID - Level int64 `json:"level"` // 代理等级:1=普通,2=黄金,3=钻石 - LevelName string `json:"level_name"` // 等级名称 - Region string `json:"region"` // 区域(可选) - Mobile string `json:"mobile"` // 手机号 - WechatId string `json:"wechat_id"` // 微信号(可选) - TeamLeaderId int64 `json:"team_leader_id"` // 团队首领ID - IsRealName bool `json:"is_real_name"` // 是否已实名 -} - type AgentLinkListItem struct { Id int64 `json:"id"` // 主键 AgentId int64 `json:"agent_id"` // 代理ID @@ -1007,10 +114,6 @@ type AgentProductConfigItem struct { CreateTime string `json:"create_time"` // 创建时间 } -type AgentProductConfigResp struct { - List []ProductConfigItem `json:"list"` -} - type AgentRealNameListItem struct { Id int64 `json:"id"` // 主键 AgentId int64 `json:"agent_id"` // 代理ID @@ -1058,26 +161,6 @@ type AgentWithdrawalListItem struct { CreateTime string `json:"create_time"` // 创建时间 } -type ApplyUpgradeReq struct { - ToLevel int64 `json:"to_level"` // 目标等级:2=黄金,3=钻石 -} - -type ApplyUpgradeResp struct { - UpgradeId int64 `json:"upgrade_id"` // 升级记录ID - OrderNo string `json:"order_no"` // 支付订单号 -} - -type ApplyWithdrawalReq struct { - Amount float64 `json:"amount"` // 提现金额 - PayeeAccount string `json:"payee_account"` // 收款账户 - PayeeName string `json:"payee_name"` // 收款人姓名 -} - -type ApplyWithdrawalResp struct { - WithdrawalId int64 `json:"withdrawal_id"` // 提现记录ID - WithdrawalNo string `json:"withdrawal_no"` // 提现单号 -} - type AuthorizationDocumentInfo struct { DocumentId int64 `json:"documentId"` // 授权书ID UserId int64 `json:"userId"` // 用户ID @@ -1091,95 +174,32 @@ type AuthorizationDocumentInfo struct { CreateTime string `json:"createTime"` // 创建时间 } -type BindMobileReq struct { - Mobile string `json:"mobile" validate:"required,mobile"` - Code string `json:"code" validate:"required"` -} - -type BindMobileResp struct { - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` +type CommissionFreezeConfig struct { + Ratio float64 `json:"ratio"` // 佣金冻结比例(例如:0.1表示10%) + Threshold float64 `json:"threshold"` // 佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元) + Days int64 `json:"days"` // 佣金冻结解冻天数(单位:天,例如:30表示30天后解冻) } type CommissionItem struct { Id int64 `json:"id"` // 记录ID OrderId int64 `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 订单号 ProductName string `json:"product_name"` // 产品名称 Amount float64 `json:"amount"` // 佣金金额 Status int64 `json:"status"` // 状态:1=已发放,2=已冻结,3=已取消 CreateTime string `json:"create_time"` // 创建时间 } -type CreateMenuReq struct { - Pid int64 `json:"pid,optional"` // 父菜单ID - Name string `json:"name"` // 路由名称 - Path string `json:"path,optional"` // 路由路径 - Component string `json:"component,optional"` // 组件路径 - Redirect string `json:"redirect,optional"` // 重定向路径 - Meta map[string]interface{} `json:"meta"` // 路由元数据 - Status int64 `json:"status,optional,default=1"` // 状态:0-禁用,1-启用 - Type string `json:"type"` // 类型 - Sort int64 `json:"sort,optional"` // 排序 +type ConversionRateData struct { + Daily []PeriodConversionData `json:"daily"` // 日统计(今日、昨日、前日) + Weekly []PeriodConversionData `json:"weekly"` // 周统计(这周、前一周、前两周) + Monthly []PeriodConversionData `json:"monthly"` // 月统计(本月、上月、前两月) } -type CreateMenuResp struct { - Id int64 `json:"id"` // 菜单ID -} - -type CreatePromotionLinkReq struct { - Name string `json:"name"` // 链接名称 -} - -type CreatePromotionLinkResp struct { - Id int64 `json:"id"` // 链接ID - Url string `json:"url"` // 生成的推广链接URL -} - -type CreateRoleReq struct { - RoleName string `json:"role_name"` // 角色名称 - RoleCode string `json:"role_code"` // 角色编码 - Description string `json:"description"` // 角色描述 - Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 - Sort int64 `json:"sort,default=0"` // 排序 - MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 -} - -type CreateRoleResp struct { - Id int64 `json:"id"` // 角色ID -} - -type DeleteMenuReq struct { - Id int64 `path:"id"` // 菜单ID -} - -type DeleteMenuResp struct { - Success bool `json:"success"` // 是否成功 -} - -type DeletePromotionLinkReq struct { - Id int64 `path:"id"` // 链接ID -} - -type DeletePromotionLinkResp struct { - Success bool `json:"success"` // 是否成功 -} - -type DeleteRoleReq struct { - Id int64 `path:"id"` // 角色ID -} - -type DeleteRoleResp struct { - Success bool `json:"success"` // 是否成功 -} - -type DownloadAuthorizationDocumentReq struct { - DocumentId int64 `json:"documentId" validate:"required"` // 授权书ID -} - -type DownloadAuthorizationDocumentResp struct { - FileName string `json:"fileName"` // 文件名 - FileUrl string `json:"fileUrl"` // 文件访问URL +type DirectParentRebateConfig struct { + Diamond float64 `json:"diamond"` // 直接上级是钻石的返佣金额(6元) + Gold float64 `json:"gold"` // 直接上级是黄金的返佣金额(3元) + Normal float64 `json:"normal"` // 直接上级是普通的返佣金额(2元) } type Feature struct { @@ -1196,273 +216,6 @@ type FeatureListItem struct { UpdateTime string `json:"update_time"` // 更新时间 } -type GenerateInviteCodeReq struct { - Count int64 `json:"count"` // 生成数量 - ExpireDays int64 `json:"expire_days,optional"` // 过期天数(可选,0表示不过期) - Remark string `json:"remark,optional"` // 备注(可选) -} - -type GenerateInviteCodeResp struct { - Codes []string `json:"codes"` // 生成的邀请码列表 -} - -type GetAuthorizationDocumentByOrderReq struct { - OrderId int64 `json:"orderId" validate:"required"` // 订单ID -} - -type GetAuthorizationDocumentByOrderResp struct { - Documents []AuthorizationDocumentInfo `json:"documents"` // 授权书列表 -} - -type GetAuthorizationDocumentReq struct { - DocumentId int64 `json:"documentId" validate:"required"` // 授权书ID -} - -type GetAuthorizationDocumentResp struct { - DocumentId int64 `json:"documentId"` // 授权书ID - UserId int64 `json:"userId"` // 用户ID - OrderId int64 `json:"orderId"` // 订单ID - QueryId int64 `json:"queryId"` // 查询ID - FileName string `json:"fileName"` // 文件名 - FileUrl string `json:"fileUrl"` // 文件访问URL - FileSize int64 `json:"fileSize"` // 文件大小 - FileType string `json:"fileType"` // 文件类型 - Status string `json:"status"` // 状态 - CreateTime string `json:"createTime"` // 创建时间 -} - -type GetCommissionListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 -} - -type GetCommissionListResp struct { - Total int64 `json:"total"` // 总数 - List []CommissionItem `json:"list"` // 列表 -} - -type GetInviteCodeListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 - Status int64 `form:"status,optional"` // 状态(可选) -} - -type GetInviteCodeListResp struct { - Total int64 `json:"total"` // 总数 - List []InviteCodeItem `json:"list"` // 列表 -} - -type GetInviteLinkResp struct { - InviteLink string `json:"invite_link"` // 邀请链接 - QrCodeUrl string `json:"qr_code_url"` // 二维码URL -} - -type GetLinkDataReq struct { - LinkIdentifier string `form:"link_identifier"` // 推广链接标识 -} - -type GetLinkDataResp struct { - AgentId int64 `json:"agent_id"` // 代理ID - ProductId int64 `json:"product_id"` // 产品ID - SetPrice float64 `json:"set_price"` // 代理设定价格 - ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 - ProductName string `json:"product_name"` // 产品名称 -} - -type GetMenuAllReq struct { -} - -type GetMenuAllResp struct { - Name string `json:"name"` - Path string `json:"path"` - Redirect string `json:"redirect,omitempty"` - Component string `json:"component,omitempty"` - Sort int64 `json:"sort"` - Meta map[string]interface{} `json:"meta"` - Children []GetMenuAllResp `json:"children"` -} - -type GetMenuDetailReq struct { - Id int64 `path:"id"` // 菜单ID -} - -type GetMenuDetailResp struct { - Id int64 `json:"id"` // 菜单ID - Pid int64 `json:"pid"` // 父菜单ID - Name string `json:"name"` // 路由名称 - Path string `json:"path"` // 路由路径 - Component string `json:"component"` // 组件路径 - Redirect string `json:"redirect"` // 重定向路径 - Meta map[string]interface{} `json:"meta"` // 路由元数据 - Status int64 `json:"status"` // 状态:0-禁用,1-启用 - Type string `json:"type"` // 类型 - Sort int64 `json:"sort"` // 排序 - CreateTime string `json:"createTime"` // 创建时间 - UpdateTime string `json:"updateTime"` // 更新时间 -} - -type GetMenuListReq struct { - Name string `form:"name,optional"` // 菜单名称 - Path string `form:"path,optional"` // 路由路径 - Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 - Type string `form:"type,optional"` // 类型 -} - -type GetNotificationsResp struct { - Notifications []Notification `json:"notifications"` // 通知列表 - Total int64 `json:"total"` // 总记录数 -} - -type GetProductByEnRequest struct { - ProductEn string `path:"product_en"` -} - -type GetProductByIDRequest struct { - Id int64 `path:"id"` -} - -type GetPromotionLinkDetailReq struct { - Id int64 `path:"id"` // 链接ID -} - -type GetPromotionLinkDetailResp struct { - Name string `json:"name"` // 链接名称 - Url string `json:"url"` // 推广链接URL - ClickCount int64 `json:"click_count"` // 点击数 - PayCount int64 `json:"pay_count"` // 付费次数 - PayAmount string `json:"pay_amount"` // 付费金额 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 - LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 - LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 -} - -type GetPromotionLinkListReq struct { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"pageSize,default=20"` // 每页数量 - Name string `form:"name,optional"` // 链接名称 - Url string `form:"url,optional"` // 推广链接URL -} - -type GetPromotionLinkListResp struct { - Total int64 `json:"total"` // 总数 - Items []PromotionLinkItem `json:"items"` // 列表 -} - -type GetPromotionStatsHistoryReq struct { - StartDate string `form:"start_date"` // 开始日期,格式:YYYY-MM-DD - EndDate string `form:"end_date"` // 结束日期,格式:YYYY-MM-DD -} - -type GetPromotionStatsTotalReq struct { -} - -type GetPromotionStatsTotalResp struct { - TodayPayAmount float64 `json:"today_pay_amount"` // 今日金额 - TodayClickCount int64 `json:"today_click_count"` // 今日点击数 - TodayPayCount int64 `json:"today_pay_count"` // 今日付费次数 - TotalPayAmount float64 `json:"total_pay_amount"` // 总金额 - TotalClickCount int64 `json:"total_click_count"` // 总点击数 - TotalPayCount int64 `json:"total_pay_count"` // 总付费次数 -} - -type GetRebateListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 -} - -type GetRebateListResp struct { - Total int64 `json:"total"` // 总数 - List []RebateItem `json:"list"` // 列表 -} - -type GetRevenueInfoResp struct { - Balance float64 `json:"balance"` // 可用余额 - FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 - TotalEarnings float64 `json:"total_earnings"` // 累计收益 - WithdrawnAmount float64 `json:"withdrawn_amount"` // 累计提现 -} - -type GetRoleDetailReq struct { - Id int64 `path:"id"` // 角色ID -} - -type GetRoleDetailResp struct { - Id int64 `json:"id"` // 角色ID - RoleName string `json:"role_name"` // 角色名称 - RoleCode string `json:"role_code"` // 角色编码 - Description string `json:"description"` // 角色描述 - Status int64 `json:"status"` // 状态:0-禁用,1-启用 - Sort int64 `json:"sort"` // 排序 - CreateTime string `json:"create_time"` // 创建时间 - UpdateTime string `json:"update_time"` // 更新时间 - MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 -} - -type GetRoleListReq struct { - Page int64 `form:"page,default=1"` // 页码 - PageSize int64 `form:"pageSize,default=20"` // 每页数量 - Name string `form:"name,optional"` // 角色名称 - Code string `form:"code,optional"` // 角色编码 - Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 -} - -type GetRoleListResp struct { - Total int64 `json:"total"` // 总数 - Items []RoleListItem `json:"items"` // 列表 -} - -type GetSignatureReq struct { - Url string `json:"url"` -} - -type GetSignatureResp struct { - AppId string `json:"appId"` - Timestamp int64 `json:"timestamp"` - NonceStr string `json:"nonceStr"` - Signature string `json:"signature"` -} - -type GetSubordinateListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 -} - -type GetSubordinateListResp struct { - Total int64 `json:"total"` // 总数 - List []SubordinateItem `json:"list"` // 列表 -} - -type GetUpgradeListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 -} - -type GetUpgradeListResp struct { - Total int64 `json:"total"` // 总数 - List []UpgradeItem `json:"list"` // 列表 -} - -type GetWithdrawalListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数量 -} - -type GetWithdrawalListResp struct { - Total int64 `json:"total"` // 总数 - List []WithdrawalItem `json:"list"` // 列表 -} - -type HealthCheckResp struct { - Status string `json:"status"` // 服务状态 - Message string `json:"message"` // 状态信息 -} - -type IapCallbackReq struct { - OrderID int64 `json:"order_id" validate:"required"` - TransactionReceipt string `json:"transaction_receipt" validate:"required"` -} - type InviteCodeItem struct { Id int64 `json:"id"` // 记录ID Code string `json:"code"` // 邀请码 @@ -1489,36 +242,36 @@ type InviteCodeListItem struct { CreateTime string `json:"create_time"` // 创建时间 } +type InviteItem struct { + AgentId int64 `json:"agent_id"` // 代理ID + Level int64 `json:"level"` // 等级 + LevelName string `json:"level_name"` // 等级名称 + Mobile string `json:"mobile"` // 手机号 + CreateTime string `json:"create_time"` // 邀请时间 +} + +type InviteStatistics struct { + TotalInvites int64 `json:"total_invites"` // 总的邀请 + TodayInvites int64 `json:"today_invites"` // 今日邀请 + MonthInvites int64 `json:"month_invites"` // 月邀请 +} + type LevelBonusConfig struct { Diamond int64 `json:"diamond"` // 钻石加成:0 Gold int64 `json:"gold"` // 黄金加成:3 Normal int64 `json:"normal"` // 普通加成:6 } -type MenuListItem struct { - Id int64 `json:"id"` // 菜单ID - Pid int64 `json:"pid"` // 父菜单ID - Name string `json:"name"` // 路由名称 - Path string `json:"path"` // 路由路径 - Component string `json:"component"` // 组件路径 - Redirect string `json:"redirect"` // 重定向路径 - Meta map[string]interface{} `json:"meta"` // 路由元数据 - Status int64 `json:"status"` // 状态:0-禁用,1-启用 - Type string `json:"type"` // 类型 - Sort int64 `json:"sort"` // 排序 - CreateTime string `json:"createTime"` // 创建时间 - Children []MenuListItem `json:"children"` // 子菜单 -} - -type MobileCodeLoginReq struct { - Mobile string `json:"mobile"` - Code string `json:"code" validate:"required"` -} - -type MobileCodeLoginResp struct { - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` +type LevelPrivilegeItem struct { + Level int64 `json:"level"` // 等级:1=普通,2=黄金,3=钻石 + LevelName string `json:"level_name"` // 等级名称 + LevelBonus float64 `json:"level_bonus"` // 等级加成(元) + PriceReduction float64 `json:"price_reduction"` // 底价降低(元,相对于当前等级) + PromoteRebate string `json:"promote_rebate"` // 推广返佣说明 + MaxPromoteRebate float64 `json:"max_promote_rebate"` // 最高推广返佣金额(元) + UpgradeRebate string `json:"upgrade_rebate"` // 下级升级返佣说明 + Privileges []string `json:"privileges"` // 特权列表 + CanUpgradeSubordinate bool `json:"can_upgrade_subordinate"` // 是否可以升级下级 } type Notification struct { @@ -1545,6 +298,15 @@ type NotificationListItem struct { UpdateTime string `json:"update_time"` // 更新时间 } +type OrderItem struct { + OrderNo string `json:"order_no"` // 订单号 + ProductId int64 `json:"product_id"` // 产品ID + ProductName string `json:"product_name"` // 产品名称 + OrderAmount float64 `json:"order_amount"` // 订单金额 + RebateAmount float64 `json:"rebate_amount"` // 返佣金额 + CreateTime string `json:"create_time"` // 创建时间 +} + type OrderListItem struct { Id int64 `json:"id"` // 订单ID OrderNo string `json:"order_no"` // 商户订单号 @@ -1558,30 +320,23 @@ type OrderListItem struct { CreateTime string `json:"create_time"` // 创建时间 PayTime string `json:"pay_time"` // 支付时间 RefundTime string `json:"refund_time"` // 退款时间 - IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 IsAgentOrder bool `json:"is_agent_order"` // 是否是代理订单 AgentProcessStatus string `json:"agent_process_status"` // 代理事务处理状态:not_agent-非代理订单,success-处理成功,failed-处理失败,pending-待处理 } -type PaymentCheckReq struct { - OrderNo string `json:"order_no" validate:"required"` +type OrderStatistics struct { + TotalOrders int64 `json:"total_orders"` // 总订单量(仅统计有返佣的订单) + MonthOrders int64 `json:"month_orders"` // 月订单 + TodayOrders int64 `json:"today_orders"` // 今日订单 } -type PaymentCheckResp struct { - Type string `json:"type"` - Status string `json:"status"` -} - -type PaymentReq struct { - Id string `json:"id"` - PayMethod string `json:"pay_method"` - PayType string `json:"pay_type" validate:"required,oneof=query agent_vip"` -} - -type PaymentResp struct { - PrepayData interface{} `json:"prepay_data"` - PrepayId string `json:"prepay_id"` - OrderNo string `json:"order_no"` +type PeriodConversionData struct { + PeriodLabel string `json:"period_label"` // 时间段标签(如:今日、昨日、前日) + QueryUsers int64 `json:"query_users"` // 查询订单数(订单数量,不是用户数) + PaidUsers int64 `json:"paid_users"` // 付费订单数(订单数量,不是用户数) + TotalAmount float64 `json:"total_amount"` // 总金额(已支付订单的总金额) + QueryUserCount int64 `json:"query_user_count"` // 查询用户数(去重user_id后的用户数量) + PaidUserCount int64 `json:"paid_user_count"` // 付费用户数(去重user_id后的用户数量) } type PlatformUserListItem struct { @@ -1606,8 +361,7 @@ type Product struct { type ProductConfigItem struct { ProductId int64 `json:"product_id"` // 产品ID ProductName string `json:"product_name"` // 产品名称 - BasePrice float64 `json:"base_price"` // 基础底价 - LevelBonus float64 `json:"level_bonus"` // 等级加成 + ProductEn string `json:"product_en"` // 产品英文标识 ActualBasePrice float64 `json:"actual_base_price"` // 实际底价 PriceRangeMin float64 `json:"price_range_min"` // 最低价格 PriceRangeMax float64 `json:"price_range_max"` // 最高价格 @@ -1634,31 +388,6 @@ type ProductListItem struct { UpdateTime string `json:"update_time"` // 更新时间 } -type ProductResponse struct { - Product -} - -type PromotionLinkItem struct { - Id int64 `json:"id"` // 链接ID - Name string `json:"name"` // 链接名称 - Url string `json:"url"` // 推广链接URL - ClickCount int64 `json:"click_count"` // 点击数 - PayCount int64 `json:"pay_count"` // 付费次数 - PayAmount string `json:"pay_amount"` // 付费金额 - CreateTime string `json:"create_time"` // 创建时间 - LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 - LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 -} - -type PromotionStatsHistoryItem struct { - Id int64 `json:"id"` // 记录ID - LinkId int64 `json:"link_id"` // 链接ID - PayAmount float64 `json:"pay_amount"` // 金额 - ClickCount int64 `json:"click_count"` // 点击数 - PayCount int64 `json:"pay_count"` // 付费次数 - StatsDate string `json:"stats_date"` // 统计日期 -} - type Query struct { Id int64 `json:"id"` // 主键ID OrderId int64 `json:"order_id"` // 订单ID @@ -1705,53 +434,11 @@ type QueryCleanupLogItem struct { CreateTime string `json:"create_time"` // 创建时间 } -type QueryDetailByOrderIdReq struct { - OrderId int64 `path:"order_id"` -} - -type QueryDetailByOrderNoReq struct { - OrderNo string `path:"order_no"` -} - -type QueryExampleReq struct { - Feature string `form:"feature"` -} - -type QueryGenerateShareLinkReq struct { - OrderId *int64 `json:"order_id,optional"` - OrderNo *string `json:"order_no,optional"` -} - -type QueryGenerateShareLinkResp struct { - ShareLink string `json:"share_link"` -} - type QueryItem struct { Feature interface{} `json:"feature"` Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct } -type QueryListReq struct { - Page int64 `form:"page"` // 页码 - PageSize int64 `form:"page_size"` // 每页数据量 -} - -type QueryListResp struct { - Total int64 `json:"total"` // 总记录数 - List []Query `json:"list"` // 查询列表 -} - -type QueryProvisionalOrderReq struct { - Id string `path:"id"` -} - -type QueryProvisionalOrderResp struct { - Name string `json:"name"` - IdCard string `json:"id_card"` - Mobile string `json:"mobile"` - Product Product `json:"product"` -} - type QueryReq struct { Data string `json:"data" validate:"required"` } @@ -1760,85 +447,22 @@ type QueryResp struct { Id string `json:"id"` } -type QueryRetryReq struct { - Id int64 `path:"id"` -} - -type QueryRetryResp struct { - Query -} - -type QueryServiceReq struct { - Product string `path:"product"` - Data string `json:"data" validate:"required"` - AgentIdentifier string `json:"agent_identifier,optional"` - App bool `json:"app,optional"` -} - -type QueryServiceResp struct { - Id string `json:"id"` - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` -} - -type QueryShareDetailReq struct { - Id string `path:"id"` -} - -type QuerySingleTestReq struct { - Params map[string]interface{} `json:"params"` - Api string `json:"api"` -} - -type QuerySingleTestResp struct { - Data interface{} `json:"data"` - Api string `json:"api"` -} - -type RealNameAuthReq struct { - Name string `json:"name"` // 姓名 - IdCard string `json:"id_card"` // 身份证号 - Mobile string `json:"mobile"` // 手机号 - Code string `json:"code"` // 验证码 -} - -type RealNameAuthResp struct { - Status string `json:"status"` // 状态:pending=待审核,approved=已通过,rejected=已拒绝 -} - type RebateItem struct { - Id int64 `json:"id"` // 记录ID - SourceAgentId int64 `json:"source_agent_id"` // 来源代理ID - OrderId int64 `json:"order_id"` // 订单ID - RebateType int64 `json:"rebate_type"` // 返佣类型:1=直接上级,2=钻石上级,3=黄金上级 - Amount float64 `json:"amount"` // 返佣金额 - CreateTime string `json:"create_time"` // 创建时间 + Id int64 `json:"id"` // 记录ID + SourceAgentId int64 `json:"source_agent_id"` // 来源代理ID + SourceAgentMobile string `json:"source_agent_mobile"` // 来源代理手机号 + SourceAgentLevel int64 `json:"source_agent_level"` // 来源代理等级:1=普通,2=黄金,3=钻石 + OrderId int64 `json:"order_id"` // 订单ID + OrderNo string `json:"order_no"` // 订单号 + RebateType int64 `json:"rebate_type"` // 返佣类型:1=直接上级,2=钻石上级,3=黄金上级 + Amount float64 `json:"amount"` // 返佣金额 + CreateTime string `json:"create_time"` // 创建时间 } -type RecordLinkClickReq struct { - Path string `path:"path"` // 链接路径 -} - -type RecordLinkClickResp struct { - Success bool `json:"success"` // 是否成功 -} - -type RegisterByInviteCodeReq struct { - InviteCode string `json:"invite_code"` // 邀请码 - Mobile string `json:"mobile"` // 手机号 - Code string `json:"code"` // 验证码 - Region string `json:"region,optional"` // 区域(可选) - WechatId string `json:"wechat_id,optional"` // 微信号(可选) -} - -type RegisterByInviteCodeResp struct { - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` - AgentId int64 `json:"agent_id"` // 代理ID - Level int64 `json:"level"` // 代理等级 - LevelName string `json:"level_name"` // 等级名称 +type RebateStatistics struct { + TotalRebateAmount float64 `json:"total_rebate_amount"` // 总贡献返佣金额 + TodayRebateAmount float64 `json:"today_rebate_amount"` // 今日贡献返佣金额 + MonthRebateAmount float64 `json:"month_rebate_amount"` // 月贡献返佣金额 } type RoleListItem struct { @@ -1862,67 +486,31 @@ type SubordinateItem struct { TotalAmount float64 `json:"total_amount"` // 总金额 } -type TeamLevelStats struct { - Diamond int64 `json:"diamond"` // 钻石数量 - Gold int64 `json:"gold"` // 黄金数量 - Normal int64 `json:"normal"` // 普通数量 +type TeamMemberItem struct { + AgentId int64 `json:"agent_id"` // 代理ID + Level int64 `json:"level"` // 等级 + LevelName string `json:"level_name"` // 等级名称 + Mobile string `json:"mobile"` // 手机号 + CreateTime string `json:"create_time"` // 加入团队时间 + TodayQueries int64 `json:"today_queries"` // 当日查询量 + TotalQueries int64 `json:"total_queries"` // 总查询量 + TotalRebateAmount float64 `json:"total_rebate_amount"` // 返佣给我的总金额 + TodayRebateAmount float64 `json:"today_rebate_amount"` // 返佣给我的今日金额 + TotalInvites int64 `json:"total_invites"` // 邀请加入团队的总人数 + TodayInvites int64 `json:"today_invites"` // 邀请加入团队的今日人数 + IsDirect bool `json:"is_direct"` // 是否直接下级 } -type TeamStatisticsResp struct { - TotalCount int64 `json:"total_count"` // 团队总人数(包括自己) - DirectCount int64 `json:"direct_count"` // 直接下级数量 - IndirectCount int64 `json:"indirect_count"` // 间接下级数量 - ByLevel TeamLevelStats `json:"by_level"` // 按等级统计 -} - -type UpdateMenuReq struct { - Id int64 `path:"id"` // 菜单ID - Pid int64 `json:"pid,optional"` // 父菜单ID - Name string `json:"name"` // 路由名称 - Path string `json:"path,optional"` // 路由路径 - Component string `json:"component,optional"` // 组件路径 - Redirect string `json:"redirect,optional"` // 重定向路径 - Meta map[string]interface{} `json:"meta"` // 路由元数据 - Status int64 `json:"status,optional"` // 状态:0-禁用,1-启用 - Type string `json:"type"` // 类型 - Sort int64 `json:"sort,optional"` // 排序 -} - -type UpdateMenuResp struct { - Success bool `json:"success"` // 是否成功 -} - -type UpdatePromotionLinkReq struct { - Id int64 `path:"id"` // 链接ID - Name *string `json:"name,optional"` // 链接名称 -} - -type UpdatePromotionLinkResp struct { - Success bool `json:"success"` // 是否成功 -} - -type UpdateQueryDataReq struct { - Id int64 `json:"id"` // 查询ID - QueryData string `json:"query_data"` // 查询数据(未加密的JSON) -} - -type UpdateQueryDataResp struct { - Id int64 `json:"id"` - UpdatedAt string `json:"updated_at"` // 更新时间 -} - -type UpdateRoleReq struct { - Id int64 `path:"id"` // 角色ID - RoleName *string `json:"role_name,optional"` // 角色名称 - RoleCode *string `json:"role_code,optional"` // 角色编码 - Description *string `json:"description,optional"` // 角色描述 - Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 - Sort *int64 `json:"sort,optional"` // 排序 - MenuIds []int64 `json:"menu_ids,optional"` // 关联的菜单ID列表 -} - -type UpdateRoleResp struct { - Success bool `json:"success"` // 是否成功 +type TeamStatistics struct { + TotalMembers int64 `json:"total_members"` // 团队成员总数 + TodayNewMembers int64 `json:"today_new_members"` // 团队人员今日新增 + MonthNewMembers int64 `json:"month_new_members"` // 团队人员月新增 + TotalQueries int64 `json:"total_queries"` // 团队总查询总量 + TodayPromotions int64 `json:"today_promotions"` // 团队今日推广量 + MonthPromotions int64 `json:"month_promotions"` // 团队月推广量 + TotalEarnings float64 `json:"total_earnings"` // 依靠团队得到的总收益 + TodayEarnings float64 `json:"today_earnings"` // 今日收益 + MonthEarnings float64 `json:"month_earnings"` // 月收益 } type UpgradeFeeConfig struct { @@ -1948,13 +536,15 @@ type UpgradeRebateConfig struct { ToDiamondRebate float64 `json:"to_diamond_rebate"` // 升级为钻石返佣:680 } -type UpgradeSubordinateReq struct { - SubordinateId int64 `json:"subordinate_id"` // 下级代理ID - ToLevel int64 `json:"to_level"` // 目标等级(只能是2=黄金) -} - -type UpgradeSubordinateResp struct { - Success bool `json:"success"` +type UpgradeRebateItem struct { + Id int64 `json:"id"` // 记录ID + SourceAgentId int64 `json:"source_agent_id"` // 来源代理ID(升级的代理) + SourceAgentMobile string `json:"source_agent_mobile"` // 来源代理手机号 + OrderNo string `json:"order_no"` // 订单号 + FromLevel int64 `json:"from_level"` // 原等级 + ToLevel int64 `json:"to_level"` // 目标等级 + Amount float64 `json:"amount"` // 返佣金额 + CreateTime string `json:"create_time"` // 创建时间 } type User struct { @@ -1964,30 +554,6 @@ type User struct { UserType int64 `json:"userType"` } -type UserInfoResp struct { - UserInfo User `json:"userInfo"` -} - -type WXH5AuthReq struct { - Code string `json:"code"` -} - -type WXH5AuthResp struct { - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` -} - -type WXMiniAuthReq struct { - Code string `json:"code"` -} - -type WXMiniAuthResp struct { - AccessToken string `json:"accessToken"` - AccessExpire int64 `json:"accessExpire"` - RefreshAfter int64 `json:"refreshAfter"` -} - type WithdrawalItem struct { Id int64 `json:"id"` // 记录ID WithdrawalNo string `json:"withdrawal_no"` // 提现单号 @@ -2000,13 +566,3 @@ type WithdrawalItem struct { Remark string `json:"remark"` // 备注 CreateTime string `json:"create_time"` // 创建时间 } - -type GetAppVersionResp struct { - Version string `json:"version"` - WgtUrl string `json:"wgtUrl"` -} - -type SendSmsReq struct { - Mobile string `json:"mobile" validate:"required,mobile"` - ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"` -} diff --git a/app/main/api/internal/types/user.go b/app/main/api/internal/types/user.go new file mode 100644 index 0000000..dd5b290 --- /dev/null +++ b/app/main/api/internal/types/user.go @@ -0,0 +1,59 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type BindMobileReq struct { + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +type BindMobileResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type GetSignatureReq struct { + Url string `json:"url"` +} + +type GetSignatureResp struct { + AppId string `json:"appId"` + Timestamp int64 `json:"timestamp"` + NonceStr string `json:"nonceStr"` + Signature string `json:"signature"` +} + +type MobileCodeLoginReq struct { + Mobile string `json:"mobile"` + Code string `json:"code" validate:"required"` +} + +type MobileCodeLoginResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type UserInfoResp struct { + UserInfo User `json:"userInfo"` +} + +type WXH5AuthReq struct { + Code string `json:"code"` +} + +type WXH5AuthResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type WXMiniAuthReq struct { + Code string `json:"code"` +} + +type WXMiniAuthResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} diff --git a/app/main/model/adminPromotionLinkModel.go b/app/main/model/adminPromotionLinkModel.go deleted file mode 100644 index d82c42e..0000000 --- a/app/main/model/adminPromotionLinkModel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ AdminPromotionLinkModel = (*customAdminPromotionLinkModel)(nil) - -type ( - // AdminPromotionLinkModel is an interface to be customized, add more methods here, - // and implement the added methods in customAdminPromotionLinkModel. - AdminPromotionLinkModel interface { - adminPromotionLinkModel - } - - customAdminPromotionLinkModel struct { - *defaultAdminPromotionLinkModel - } -) - -// NewAdminPromotionLinkModel returns a model for the database table. -func NewAdminPromotionLinkModel(conn sqlx.SqlConn, c cache.CacheConf) AdminPromotionLinkModel { - return &customAdminPromotionLinkModel{ - defaultAdminPromotionLinkModel: newAdminPromotionLinkModel(conn, c), - } -} diff --git a/app/main/model/adminPromotionLinkModel_gen.go b/app/main/model/adminPromotionLinkModel_gen.go deleted file mode 100644 index 5d05ab7..0000000 --- a/app/main/model/adminPromotionLinkModel_gen.go +++ /dev/null @@ -1,409 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "ycc-server/common/globalkey" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - adminPromotionLinkFieldNames = builder.RawFieldNames(&AdminPromotionLink{}) - adminPromotionLinkRows = strings.Join(adminPromotionLinkFieldNames, ",") - adminPromotionLinkRowsExpectAutoSet = strings.Join(stringx.Remove(adminPromotionLinkFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - adminPromotionLinkRowsWithPlaceHolder = strings.Join(stringx.Remove(adminPromotionLinkFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheHmAdminPromotionLinkIdPrefix = "cache:ycc:adminPromotionLink:id:" - cacheHmAdminPromotionLinkUrlPrefix = "cache:ycc:adminPromotionLink:url:" -) - -type ( - adminPromotionLinkModel interface { - Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionLink) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*AdminPromotionLink, error) - FindOneByUrl(ctx context.Context, url string) (*AdminPromotionLink, error) - Update(ctx context.Context, session sqlx.Session, data *AdminPromotionLink) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AdminPromotionLink) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionLink) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionLink, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLink, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLink, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionLink, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionLink, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultAdminPromotionLinkModel struct { - sqlc.CachedConn - table string - } - - AdminPromotionLink struct { - Id int64 `db:"id"` - CreateTime time.Time `db:"create_time"` - UpdateTime time.Time `db:"update_time"` - DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 - DelState int64 `db:"del_state"` - Version int64 `db:"version"` // 版本号 - Url string `db:"url"` // 推广链接URL - Name string `db:"name"` // 推广链接名称 - AdminUserId int64 `db:"admin_user_id"` // 推广者账号ID - } -) - -func newAdminPromotionLinkModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAdminPromotionLinkModel { - return &defaultAdminPromotionLinkModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`admin_promotion_link`", - } -} - -func (m *defaultAdminPromotionLinkModel) Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionLink) (sql.Result, error) { - data.DelState = globalkey.DelStateNo - hmAdminPromotionLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkIdPrefix, data.Id) - hmAdminPromotionLinkUrlKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkUrlPrefix, data.Url) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, adminPromotionLinkRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Url, data.Name, data.AdminUserId) - } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Url, data.Name, data.AdminUserId) - }, hmAdminPromotionLinkIdKey, hmAdminPromotionLinkUrlKey) -} - -func (m *defaultAdminPromotionLinkModel) FindOne(ctx context.Context, id int64) (*AdminPromotionLink, error) { - hmAdminPromotionLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkIdPrefix, id) - var resp AdminPromotionLink - err := m.QueryRowCtx(ctx, &resp, hmAdminPromotionLinkIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionLinkRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindOneByUrl(ctx context.Context, url string) (*AdminPromotionLink, error) { - hmAdminPromotionLinkUrlKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkUrlPrefix, url) - var resp AdminPromotionLink - err := m.QueryRowIndexCtx(ctx, &resp, hmAdminPromotionLinkUrlKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `url` = ? and del_state = ? limit 1", adminPromotionLinkRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, url, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkModel) Update(ctx context.Context, session sqlx.Session, newData *AdminPromotionLink) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - hmAdminPromotionLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkIdPrefix, data.Id) - hmAdminPromotionLinkUrlKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkUrlPrefix, data.Url) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, adminPromotionLinkRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Url, newData.Name, newData.AdminUserId, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Url, newData.Name, newData.AdminUserId, newData.Id) - }, hmAdminPromotionLinkIdKey, hmAdminPromotionLinkUrlKey) -} - -func (m *defaultAdminPromotionLinkModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AdminPromotionLink) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - hmAdminPromotionLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkIdPrefix, data.Id) - hmAdminPromotionLinkUrlKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkUrlPrefix, data.Url) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, adminPromotionLinkRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Url, newData.Name, newData.AdminUserId, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Url, newData.Name, newData.AdminUserId, newData.Id, oldVersion) - }, hmAdminPromotionLinkIdKey, hmAdminPromotionLinkUrlKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultAdminPromotionLinkModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionLink) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "AdminPromotionLinkModel delete err : %+v", err) - } - return nil -} - -func (m *defaultAdminPromotionLinkModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionLink, error) { - - builder = builder.Columns(adminPromotionLinkRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLink - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLink, error) { - - builder = builder.Columns(adminPromotionLinkRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLink - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLink, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(adminPromotionLinkRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*AdminPromotionLink - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionLink, error) { - - builder = builder.Columns(adminPromotionLinkRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLink - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionLink, error) { - - builder = builder.Columns(adminPromotionLinkRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLink - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultAdminPromotionLinkModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultAdminPromotionLinkModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - hmAdminPromotionLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkIdPrefix, id) - hmAdminPromotionLinkUrlKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkUrlPrefix, data.Url) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, hmAdminPromotionLinkIdKey, hmAdminPromotionLinkUrlKey) - return err -} -func (m *defaultAdminPromotionLinkModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkIdPrefix, primary) -} -func (m *defaultAdminPromotionLinkModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionLinkRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultAdminPromotionLinkModel) tableName() string { - return m.table -} diff --git a/app/main/model/adminPromotionLinkStatsHistoryModel.go b/app/main/model/adminPromotionLinkStatsHistoryModel.go deleted file mode 100644 index 685db69..0000000 --- a/app/main/model/adminPromotionLinkStatsHistoryModel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ AdminPromotionLinkStatsHistoryModel = (*customAdminPromotionLinkStatsHistoryModel)(nil) - -type ( - // AdminPromotionLinkStatsHistoryModel is an interface to be customized, add more methods here, - // and implement the added methods in customAdminPromotionLinkStatsHistoryModel. - AdminPromotionLinkStatsHistoryModel interface { - adminPromotionLinkStatsHistoryModel - } - - customAdminPromotionLinkStatsHistoryModel struct { - *defaultAdminPromotionLinkStatsHistoryModel - } -) - -// NewAdminPromotionLinkStatsHistoryModel returns a model for the database table. -func NewAdminPromotionLinkStatsHistoryModel(conn sqlx.SqlConn, c cache.CacheConf) AdminPromotionLinkStatsHistoryModel { - return &customAdminPromotionLinkStatsHistoryModel{ - defaultAdminPromotionLinkStatsHistoryModel: newAdminPromotionLinkStatsHistoryModel(conn, c), - } -} diff --git a/app/main/model/adminPromotionLinkStatsHistoryModel_gen.go b/app/main/model/adminPromotionLinkStatsHistoryModel_gen.go deleted file mode 100644 index b55e5fe..0000000 --- a/app/main/model/adminPromotionLinkStatsHistoryModel_gen.go +++ /dev/null @@ -1,413 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "ycc-server/common/globalkey" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - adminPromotionLinkStatsHistoryFieldNames = builder.RawFieldNames(&AdminPromotionLinkStatsHistory{}) - adminPromotionLinkStatsHistoryRows = strings.Join(adminPromotionLinkStatsHistoryFieldNames, ",") - adminPromotionLinkStatsHistoryRowsExpectAutoSet = strings.Join(stringx.Remove(adminPromotionLinkStatsHistoryFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - adminPromotionLinkStatsHistoryRowsWithPlaceHolder = strings.Join(stringx.Remove(adminPromotionLinkStatsHistoryFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheHmAdminPromotionLinkStatsHistoryIdPrefix = "cache:ycc:adminPromotionLinkStatsHistory:id:" - cacheHmAdminPromotionLinkStatsHistoryLinkIdStatsDatePrefix = "cache:ycc:adminPromotionLinkStatsHistory:linkId:statsDate:" -) - -type ( - adminPromotionLinkStatsHistoryModel interface { - Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsHistory) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*AdminPromotionLinkStatsHistory, error) - FindOneByLinkIdStatsDate(ctx context.Context, linkId int64, statsDate time.Time) (*AdminPromotionLinkStatsHistory, error) - Update(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsHistory) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsHistory) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsHistory) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionLinkStatsHistory, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsHistory, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsHistory, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionLinkStatsHistory, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionLinkStatsHistory, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultAdminPromotionLinkStatsHistoryModel struct { - sqlc.CachedConn - table string - } - - AdminPromotionLinkStatsHistory struct { - Id int64 `db:"id"` - CreateTime time.Time `db:"create_time"` - UpdateTime time.Time `db:"update_time"` - DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 - DelState int64 `db:"del_state"` - Version int64 `db:"version"` // 版本号 - LinkId int64 `db:"link_id"` // 推广链接ID - StatsDate time.Time `db:"stats_date"` // 统计日期 - ClickCount int64 `db:"click_count"` // 点击数 - PayCount int64 `db:"pay_count"` // 付费次数 - PayAmount float64 `db:"pay_amount"` // 付费金额 - LastClickTime sql.NullTime `db:"last_click_time"` // 最后点击时间 - LastPayTime sql.NullTime `db:"last_pay_time"` // 最后付费时间 - } -) - -func newAdminPromotionLinkStatsHistoryModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAdminPromotionLinkStatsHistoryModel { - return &defaultAdminPromotionLinkStatsHistoryModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`admin_promotion_link_stats_history`", - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsHistory) (sql.Result, error) { - data.DelState = globalkey.DelStateNo - hmAdminPromotionLinkStatsHistoryIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsHistoryIdPrefix, data.Id) - hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey := fmt.Sprintf("%s%v:%v", cacheHmAdminPromotionLinkStatsHistoryLinkIdStatsDatePrefix, data.LinkId, data.StatsDate) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, adminPromotionLinkStatsHistoryRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.LinkId, data.StatsDate, data.ClickCount, data.PayCount, data.PayAmount, data.LastClickTime, data.LastPayTime) - } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.LinkId, data.StatsDate, data.ClickCount, data.PayCount, data.PayAmount, data.LastClickTime, data.LastPayTime) - }, hmAdminPromotionLinkStatsHistoryIdKey, hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey) -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindOne(ctx context.Context, id int64) (*AdminPromotionLinkStatsHistory, error) { - hmAdminPromotionLinkStatsHistoryIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsHistoryIdPrefix, id) - var resp AdminPromotionLinkStatsHistory - err := m.QueryRowCtx(ctx, &resp, hmAdminPromotionLinkStatsHistoryIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionLinkStatsHistoryRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindOneByLinkIdStatsDate(ctx context.Context, linkId int64, statsDate time.Time) (*AdminPromotionLinkStatsHistory, error) { - hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey := fmt.Sprintf("%s%v:%v", cacheHmAdminPromotionLinkStatsHistoryLinkIdStatsDatePrefix, linkId, statsDate) - var resp AdminPromotionLinkStatsHistory - err := m.QueryRowIndexCtx(ctx, &resp, hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `link_id` = ? and `stats_date` = ? and del_state = ? limit 1", adminPromotionLinkStatsHistoryRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, linkId, statsDate, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) Update(ctx context.Context, session sqlx.Session, newData *AdminPromotionLinkStatsHistory) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - hmAdminPromotionLinkStatsHistoryIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsHistoryIdPrefix, data.Id) - hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey := fmt.Sprintf("%s%v:%v", cacheHmAdminPromotionLinkStatsHistoryLinkIdStatsDatePrefix, data.LinkId, data.StatsDate) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, adminPromotionLinkStatsHistoryRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.StatsDate, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.StatsDate, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id) - }, hmAdminPromotionLinkStatsHistoryIdKey, hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey) -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AdminPromotionLinkStatsHistory) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - hmAdminPromotionLinkStatsHistoryIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsHistoryIdPrefix, data.Id) - hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey := fmt.Sprintf("%s%v:%v", cacheHmAdminPromotionLinkStatsHistoryLinkIdStatsDatePrefix, data.LinkId, data.StatsDate) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, adminPromotionLinkStatsHistoryRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.StatsDate, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.StatsDate, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id, oldVersion) - }, hmAdminPromotionLinkStatsHistoryIdKey, hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsHistory) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "AdminPromotionLinkStatsHistoryModel delete err : %+v", err) - } - return nil -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionLinkStatsHistory, error) { - - builder = builder.Columns(adminPromotionLinkStatsHistoryRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsHistory - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsHistory, error) { - - builder = builder.Columns(adminPromotionLinkStatsHistoryRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsHistory - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsHistory, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(adminPromotionLinkStatsHistoryRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*AdminPromotionLinkStatsHistory - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionLinkStatsHistory, error) { - - builder = builder.Columns(adminPromotionLinkStatsHistoryRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsHistory - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionLinkStatsHistory, error) { - - builder = builder.Columns(adminPromotionLinkStatsHistoryRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsHistory - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultAdminPromotionLinkStatsHistoryModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - hmAdminPromotionLinkStatsHistoryIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsHistoryIdPrefix, id) - hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey := fmt.Sprintf("%s%v:%v", cacheHmAdminPromotionLinkStatsHistoryLinkIdStatsDatePrefix, data.LinkId, data.StatsDate) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, hmAdminPromotionLinkStatsHistoryIdKey, hmAdminPromotionLinkStatsHistoryLinkIdStatsDateKey) - return err -} -func (m *defaultAdminPromotionLinkStatsHistoryModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsHistoryIdPrefix, primary) -} -func (m *defaultAdminPromotionLinkStatsHistoryModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionLinkStatsHistoryRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultAdminPromotionLinkStatsHistoryModel) tableName() string { - return m.table -} diff --git a/app/main/model/adminPromotionLinkStatsTotalModel.go b/app/main/model/adminPromotionLinkStatsTotalModel.go deleted file mode 100644 index e5dfacc..0000000 --- a/app/main/model/adminPromotionLinkStatsTotalModel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ AdminPromotionLinkStatsTotalModel = (*customAdminPromotionLinkStatsTotalModel)(nil) - -type ( - // AdminPromotionLinkStatsTotalModel is an interface to be customized, add more methods here, - // and implement the added methods in customAdminPromotionLinkStatsTotalModel. - AdminPromotionLinkStatsTotalModel interface { - adminPromotionLinkStatsTotalModel - } - - customAdminPromotionLinkStatsTotalModel struct { - *defaultAdminPromotionLinkStatsTotalModel - } -) - -// NewAdminPromotionLinkStatsTotalModel returns a model for the database table. -func NewAdminPromotionLinkStatsTotalModel(conn sqlx.SqlConn, c cache.CacheConf) AdminPromotionLinkStatsTotalModel { - return &customAdminPromotionLinkStatsTotalModel{ - defaultAdminPromotionLinkStatsTotalModel: newAdminPromotionLinkStatsTotalModel(conn, c), - } -} diff --git a/app/main/model/adminPromotionLinkStatsTotalModel_gen.go b/app/main/model/adminPromotionLinkStatsTotalModel_gen.go deleted file mode 100644 index 8b2eeb6..0000000 --- a/app/main/model/adminPromotionLinkStatsTotalModel_gen.go +++ /dev/null @@ -1,412 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "ycc-server/common/globalkey" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - adminPromotionLinkStatsTotalFieldNames = builder.RawFieldNames(&AdminPromotionLinkStatsTotal{}) - adminPromotionLinkStatsTotalRows = strings.Join(adminPromotionLinkStatsTotalFieldNames, ",") - adminPromotionLinkStatsTotalRowsExpectAutoSet = strings.Join(stringx.Remove(adminPromotionLinkStatsTotalFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - adminPromotionLinkStatsTotalRowsWithPlaceHolder = strings.Join(stringx.Remove(adminPromotionLinkStatsTotalFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheHmAdminPromotionLinkStatsTotalIdPrefix = "cache:ycc:adminPromotionLinkStatsTotal:id:" - cacheHmAdminPromotionLinkStatsTotalLinkIdPrefix = "cache:ycc:adminPromotionLinkStatsTotal:linkId:" -) - -type ( - adminPromotionLinkStatsTotalModel interface { - Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsTotal) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*AdminPromotionLinkStatsTotal, error) - FindOneByLinkId(ctx context.Context, linkId int64) (*AdminPromotionLinkStatsTotal, error) - Update(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsTotal) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsTotal) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsTotal) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionLinkStatsTotal, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsTotal, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsTotal, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionLinkStatsTotal, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionLinkStatsTotal, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultAdminPromotionLinkStatsTotalModel struct { - sqlc.CachedConn - table string - } - - AdminPromotionLinkStatsTotal struct { - Id int64 `db:"id"` - CreateTime time.Time `db:"create_time"` - UpdateTime time.Time `db:"update_time"` - DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 - DelState int64 `db:"del_state"` - Version int64 `db:"version"` // 版本号 - LinkId int64 `db:"link_id"` // 推广链接ID - ClickCount int64 `db:"click_count"` // 总点击数 - PayCount int64 `db:"pay_count"` // 总付费次数 - PayAmount float64 `db:"pay_amount"` // 总付费金额 - LastClickTime sql.NullTime `db:"last_click_time"` // 最后点击时间 - LastPayTime sql.NullTime `db:"last_pay_time"` // 最后付费时间 - } -) - -func newAdminPromotionLinkStatsTotalModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAdminPromotionLinkStatsTotalModel { - return &defaultAdminPromotionLinkStatsTotalModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`admin_promotion_link_stats_total`", - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsTotal) (sql.Result, error) { - data.DelState = globalkey.DelStateNo - hmAdminPromotionLinkStatsTotalIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalIdPrefix, data.Id) - hmAdminPromotionLinkStatsTotalLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalLinkIdPrefix, data.LinkId) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, adminPromotionLinkStatsTotalRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.LinkId, data.ClickCount, data.PayCount, data.PayAmount, data.LastClickTime, data.LastPayTime) - } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.LinkId, data.ClickCount, data.PayCount, data.PayAmount, data.LastClickTime, data.LastPayTime) - }, hmAdminPromotionLinkStatsTotalIdKey, hmAdminPromotionLinkStatsTotalLinkIdKey) -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindOne(ctx context.Context, id int64) (*AdminPromotionLinkStatsTotal, error) { - hmAdminPromotionLinkStatsTotalIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalIdPrefix, id) - var resp AdminPromotionLinkStatsTotal - err := m.QueryRowCtx(ctx, &resp, hmAdminPromotionLinkStatsTotalIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionLinkStatsTotalRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindOneByLinkId(ctx context.Context, linkId int64) (*AdminPromotionLinkStatsTotal, error) { - hmAdminPromotionLinkStatsTotalLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalLinkIdPrefix, linkId) - var resp AdminPromotionLinkStatsTotal - err := m.QueryRowIndexCtx(ctx, &resp, hmAdminPromotionLinkStatsTotalLinkIdKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `link_id` = ? and del_state = ? limit 1", adminPromotionLinkStatsTotalRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, linkId, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) Update(ctx context.Context, session sqlx.Session, newData *AdminPromotionLinkStatsTotal) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - hmAdminPromotionLinkStatsTotalIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalIdPrefix, data.Id) - hmAdminPromotionLinkStatsTotalLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalLinkIdPrefix, data.LinkId) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, adminPromotionLinkStatsTotalRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id) - }, hmAdminPromotionLinkStatsTotalIdKey, hmAdminPromotionLinkStatsTotalLinkIdKey) -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AdminPromotionLinkStatsTotal) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - hmAdminPromotionLinkStatsTotalIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalIdPrefix, data.Id) - hmAdminPromotionLinkStatsTotalLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalLinkIdPrefix, data.LinkId) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, adminPromotionLinkStatsTotalRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.ClickCount, newData.PayCount, newData.PayAmount, newData.LastClickTime, newData.LastPayTime, newData.Id, oldVersion) - }, hmAdminPromotionLinkStatsTotalIdKey, hmAdminPromotionLinkStatsTotalLinkIdKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionLinkStatsTotal) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "AdminPromotionLinkStatsTotalModel delete err : %+v", err) - } - return nil -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionLinkStatsTotal, error) { - - builder = builder.Columns(adminPromotionLinkStatsTotalRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsTotal - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsTotal, error) { - - builder = builder.Columns(adminPromotionLinkStatsTotalRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsTotal - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionLinkStatsTotal, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(adminPromotionLinkStatsTotalRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*AdminPromotionLinkStatsTotal - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionLinkStatsTotal, error) { - - builder = builder.Columns(adminPromotionLinkStatsTotalRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsTotal - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionLinkStatsTotal, error) { - - builder = builder.Columns(adminPromotionLinkStatsTotalRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionLinkStatsTotal - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultAdminPromotionLinkStatsTotalModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - hmAdminPromotionLinkStatsTotalIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalIdPrefix, id) - hmAdminPromotionLinkStatsTotalLinkIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalLinkIdPrefix, data.LinkId) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, hmAdminPromotionLinkStatsTotalIdKey, hmAdminPromotionLinkStatsTotalLinkIdKey) - return err -} -func (m *defaultAdminPromotionLinkStatsTotalModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheHmAdminPromotionLinkStatsTotalIdPrefix, primary) -} -func (m *defaultAdminPromotionLinkStatsTotalModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionLinkStatsTotalRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultAdminPromotionLinkStatsTotalModel) tableName() string { - return m.table -} diff --git a/app/main/model/adminPromotionOrderModel.go b/app/main/model/adminPromotionOrderModel.go deleted file mode 100644 index 9d8366f..0000000 --- a/app/main/model/adminPromotionOrderModel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ AdminPromotionOrderModel = (*customAdminPromotionOrderModel)(nil) - -type ( - // AdminPromotionOrderModel is an interface to be customized, add more methods here, - // and implement the added methods in customAdminPromotionOrderModel. - AdminPromotionOrderModel interface { - adminPromotionOrderModel - } - - customAdminPromotionOrderModel struct { - *defaultAdminPromotionOrderModel - } -) - -// NewAdminPromotionOrderModel returns a model for the database table. -func NewAdminPromotionOrderModel(conn sqlx.SqlConn, c cache.CacheConf) AdminPromotionOrderModel { - return &customAdminPromotionOrderModel{ - defaultAdminPromotionOrderModel: newAdminPromotionOrderModel(conn, c), - } -} diff --git a/app/main/model/adminPromotionOrderModel_gen.go b/app/main/model/adminPromotionOrderModel_gen.go deleted file mode 100644 index 566ccc4..0000000 --- a/app/main/model/adminPromotionOrderModel_gen.go +++ /dev/null @@ -1,410 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "ycc-server/common/globalkey" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" -) - -var ( - adminPromotionOrderFieldNames = builder.RawFieldNames(&AdminPromotionOrder{}) - adminPromotionOrderRows = strings.Join(adminPromotionOrderFieldNames, ",") - adminPromotionOrderRowsExpectAutoSet = strings.Join(stringx.Remove(adminPromotionOrderFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - adminPromotionOrderRowsWithPlaceHolder = strings.Join(stringx.Remove(adminPromotionOrderFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheHmAdminPromotionOrderIdPrefix = "cache:ycc:adminPromotionOrder:id:" - cacheHmAdminPromotionOrderOrderIdPrefix = "cache:ycc:adminPromotionOrder:orderId:" -) - -type ( - adminPromotionOrderModel interface { - Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionOrder) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*AdminPromotionOrder, error) - FindOneByOrderId(ctx context.Context, orderId int64) (*AdminPromotionOrder, error) - Update(ctx context.Context, session sqlx.Session, data *AdminPromotionOrder) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AdminPromotionOrder) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionOrder) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionOrder, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionOrder, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionOrder, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionOrder, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionOrder, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultAdminPromotionOrderModel struct { - sqlc.CachedConn - table string - } - - AdminPromotionOrder struct { - Id int64 `db:"id"` - CreateTime time.Time `db:"create_time"` - UpdateTime time.Time `db:"update_time"` - DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 - DelState int64 `db:"del_state"` - Version int64 `db:"version"` // 版本号 - LinkId int64 `db:"link_id"` // 推广链接ID - OrderId int64 `db:"order_id"` // 订单ID - UserId int64 `db:"user_id"` // 下单用户ID - AdminUserId int64 `db:"admin_user_id"` // 推广者账号ID - } -) - -func newAdminPromotionOrderModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAdminPromotionOrderModel { - return &defaultAdminPromotionOrderModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`admin_promotion_order`", - } -} - -func (m *defaultAdminPromotionOrderModel) Insert(ctx context.Context, session sqlx.Session, data *AdminPromotionOrder) (sql.Result, error) { - data.DelState = globalkey.DelStateNo - hmAdminPromotionOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderIdPrefix, data.Id) - hmAdminPromotionOrderOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderOrderIdPrefix, data.OrderId) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?)", m.table, adminPromotionOrderRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.LinkId, data.OrderId, data.UserId, data.AdminUserId) - } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.LinkId, data.OrderId, data.UserId, data.AdminUserId) - }, hmAdminPromotionOrderIdKey, hmAdminPromotionOrderOrderIdKey) -} - -func (m *defaultAdminPromotionOrderModel) FindOne(ctx context.Context, id int64) (*AdminPromotionOrder, error) { - hmAdminPromotionOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderIdPrefix, id) - var resp AdminPromotionOrder - err := m.QueryRowCtx(ctx, &resp, hmAdminPromotionOrderIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionOrderRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindOneByOrderId(ctx context.Context, orderId int64) (*AdminPromotionOrder, error) { - hmAdminPromotionOrderOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderOrderIdPrefix, orderId) - var resp AdminPromotionOrder - err := m.QueryRowIndexCtx(ctx, &resp, hmAdminPromotionOrderOrderIdKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `order_id` = ? and del_state = ? limit 1", adminPromotionOrderRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, orderId, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAdminPromotionOrderModel) Update(ctx context.Context, session sqlx.Session, newData *AdminPromotionOrder) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - hmAdminPromotionOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderIdPrefix, data.Id) - hmAdminPromotionOrderOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderOrderIdPrefix, data.OrderId) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, adminPromotionOrderRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.OrderId, newData.UserId, newData.AdminUserId, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.OrderId, newData.UserId, newData.AdminUserId, newData.Id) - }, hmAdminPromotionOrderIdKey, hmAdminPromotionOrderOrderIdKey) -} - -func (m *defaultAdminPromotionOrderModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AdminPromotionOrder) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - hmAdminPromotionOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderIdPrefix, data.Id) - hmAdminPromotionOrderOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderOrderIdPrefix, data.OrderId) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, adminPromotionOrderRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.OrderId, newData.UserId, newData.AdminUserId, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.LinkId, newData.OrderId, newData.UserId, newData.AdminUserId, newData.Id, oldVersion) - }, hmAdminPromotionOrderIdKey, hmAdminPromotionOrderOrderIdKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultAdminPromotionOrderModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AdminPromotionOrder) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "AdminPromotionOrderModel delete err : %+v", err) - } - return nil -} - -func (m *defaultAdminPromotionOrderModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AdminPromotionOrder, error) { - - builder = builder.Columns(adminPromotionOrderRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionOrder - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionOrder, error) { - - builder = builder.Columns(adminPromotionOrderRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionOrder - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AdminPromotionOrder, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(adminPromotionOrderRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*AdminPromotionOrder - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AdminPromotionOrder, error) { - - builder = builder.Columns(adminPromotionOrderRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionOrder - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionOrderModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AdminPromotionOrder, error) { - - builder = builder.Columns(adminPromotionOrderRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AdminPromotionOrder - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAdminPromotionOrderModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultAdminPromotionOrderModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultAdminPromotionOrderModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - hmAdminPromotionOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderIdPrefix, id) - hmAdminPromotionOrderOrderIdKey := fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderOrderIdPrefix, data.OrderId) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, hmAdminPromotionOrderIdKey, hmAdminPromotionOrderOrderIdKey) - return err -} -func (m *defaultAdminPromotionOrderModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheHmAdminPromotionOrderIdPrefix, primary) -} -func (m *defaultAdminPromotionOrderModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", adminPromotionOrderRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultAdminPromotionOrderModel) tableName() string { - return m.table -} diff --git a/app/main/model/agentFreezeTaskModel.go b/app/main/model/agentFreezeTaskModel.go new file mode 100644 index 0000000..ac3121d --- /dev/null +++ b/app/main/model/agentFreezeTaskModel.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ AgentFreezeTaskModel = (*customAgentFreezeTaskModel)(nil) + +type ( + // AgentFreezeTaskModel is an interface to be customized, add more methods here, + // and implement the added methods in customAgentFreezeTaskModel. + AgentFreezeTaskModel interface { + agentFreezeTaskModel + } + + customAgentFreezeTaskModel struct { + *defaultAgentFreezeTaskModel + } +) + +// NewAgentFreezeTaskModel returns a model for the database table. +func NewAgentFreezeTaskModel(conn sqlx.SqlConn, c cache.CacheConf) AgentFreezeTaskModel { + return &customAgentFreezeTaskModel{ + defaultAgentFreezeTaskModel: newAgentFreezeTaskModel(conn, c), + } +} diff --git a/app/main/model/agentFreezeTaskModel_gen.go b/app/main/model/agentFreezeTaskModel_gen.go new file mode 100644 index 0000000..0b53fc7 --- /dev/null +++ b/app/main/model/agentFreezeTaskModel_gen.go @@ -0,0 +1,377 @@ +// Code generated by goctl. DO NOT EDIT! + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "time" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" + "ycc-server/common/globalkey" +) + +var ( + agentFreezeTaskFieldNames = builder.RawFieldNames(&AgentFreezeTask{}) + agentFreezeTaskRows = strings.Join(agentFreezeTaskFieldNames, ",") + agentFreezeTaskRowsExpectAutoSet = strings.Join(stringx.Remove(agentFreezeTaskFieldNames, "`id`", "`create_time`", "`update_time`"), ",") + agentFreezeTaskRowsWithPlaceHolder = strings.Join(stringx.Remove(agentFreezeTaskFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" + + cacheYccAgentFreezeTaskIdPrefix = "cache:ycc:agentFreezeTask:id:" +) + +type ( + agentFreezeTaskModel interface { + Insert(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*AgentFreezeTask, error) + Update(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) (sql.Result, error) + UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) error + Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error + SelectBuilder() squirrel.SelectBuilder + DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) error + FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) + FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) + FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AgentFreezeTask, error) + FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentFreezeTask, error) + FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentFreezeTask, int64, error) + FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentFreezeTask, error) + FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentFreezeTask, error) + Delete(ctx context.Context, session sqlx.Session, id int64) error + } + + defaultAgentFreezeTaskModel struct { + sqlc.CachedConn + table string + } + + AgentFreezeTask struct { + Id int64 `db:"id"` // 主键ID + AgentId int64 `db:"agent_id"` // 代理ID + OrderId int64 `db:"order_id"` // 订单ID + CommissionId int64 `db:"commission_id"` // 佣金记录ID + FreezeAmount float64 `db:"freeze_amount"` // 冻结金额 + OrderPrice float64 `db:"order_price"` // 订单单价 + FreezeRatio float64 `db:"freeze_ratio"` // 冻结比例(例如:0.1000表示10%) + Status int64 `db:"status"` // 状态:1=待解冻,2=已解冻,3=已取消 + FreezeTime time.Time `db:"freeze_time"` // 冻结时间 + UnfreezeTime time.Time `db:"unfreeze_time"` // 解冻时间(冻结时间+1个月) + ActualUnfreezeTime sql.NullTime `db:"actual_unfreeze_time"` // 实际解冻时间 + Remark sql.NullString `db:"remark"` // 备注 + CreateTime time.Time `db:"create_time"` // 创建时间 + UpdateTime time.Time `db:"update_time"` // 更新时间 + DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 + DelState int64 `db:"del_state"` // 删除状态:0=未删除,1=已删除 + Version int64 `db:"version"` // 版本号(乐观锁) + } +) + +func newAgentFreezeTaskModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAgentFreezeTaskModel { + return &defaultAgentFreezeTaskModel{ + CachedConn: sqlc.NewConn(conn, c), + table: "`agent_freeze_task`", + } +} + +func (m *defaultAgentFreezeTaskModel) Insert(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) (sql.Result, error) { + data.DelState = globalkey.DelStateNo + yccAgentFreezeTaskIdKey := fmt.Sprintf("%s%v", cacheYccAgentFreezeTaskIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentFreezeTaskRowsExpectAutoSet) + if session != nil { + return session.ExecCtx(ctx, query, data.AgentId, data.OrderId, data.CommissionId, data.FreezeAmount, data.OrderPrice, data.FreezeRatio, data.Status, data.FreezeTime, data.UnfreezeTime, data.ActualUnfreezeTime, data.Remark, data.DeleteTime, data.DelState, data.Version) + } + return conn.ExecCtx(ctx, query, data.AgentId, data.OrderId, data.CommissionId, data.FreezeAmount, data.OrderPrice, data.FreezeRatio, data.Status, data.FreezeTime, data.UnfreezeTime, data.ActualUnfreezeTime, data.Remark, data.DeleteTime, data.DelState, data.Version) + }, yccAgentFreezeTaskIdKey) +} + +func (m *defaultAgentFreezeTaskModel) FindOne(ctx context.Context, id int64) (*AgentFreezeTask, error) { + yccAgentFreezeTaskIdKey := fmt.Sprintf("%s%v", cacheYccAgentFreezeTaskIdPrefix, id) + var resp AgentFreezeTask + err := m.QueryRowCtx(ctx, &resp, yccAgentFreezeTaskIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentFreezeTaskRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAgentFreezeTaskModel) Update(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) (sql.Result, error) { + yccAgentFreezeTaskIdKey := fmt.Sprintf("%s%v", cacheYccAgentFreezeTaskIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentFreezeTaskRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.AgentId, data.OrderId, data.CommissionId, data.FreezeAmount, data.OrderPrice, data.FreezeRatio, data.Status, data.FreezeTime, data.UnfreezeTime, data.ActualUnfreezeTime, data.Remark, data.DeleteTime, data.DelState, data.Version, data.Id) + } + return conn.ExecCtx(ctx, query, data.AgentId, data.OrderId, data.CommissionId, data.FreezeAmount, data.OrderPrice, data.FreezeRatio, data.Status, data.FreezeTime, data.UnfreezeTime, data.ActualUnfreezeTime, data.Remark, data.DeleteTime, data.DelState, data.Version, data.Id) + }, yccAgentFreezeTaskIdKey) +} + +func (m *defaultAgentFreezeTaskModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) error { + + oldVersion := data.Version + data.Version += 1 + + var sqlResult sql.Result + var err error + + yccAgentFreezeTaskIdKey := fmt.Sprintf("%s%v", cacheYccAgentFreezeTaskIdPrefix, data.Id) + sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentFreezeTaskRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.AgentId, data.OrderId, data.CommissionId, data.FreezeAmount, data.OrderPrice, data.FreezeRatio, data.Status, data.FreezeTime, data.UnfreezeTime, data.ActualUnfreezeTime, data.Remark, data.DeleteTime, data.DelState, data.Version, data.Id, oldVersion) + } + return conn.ExecCtx(ctx, query, data.AgentId, data.OrderId, data.CommissionId, data.FreezeAmount, data.OrderPrice, data.FreezeRatio, data.Status, data.FreezeTime, data.UnfreezeTime, data.ActualUnfreezeTime, data.Remark, data.DeleteTime, data.DelState, data.Version, data.Id, oldVersion) + }, yccAgentFreezeTaskIdKey) + if err != nil { + return err + } + updateCount, err := sqlResult.RowsAffected() + if err != nil { + return err + } + if updateCount == 0 { + return ErrNoRowsUpdate + } + + return nil +} + +func (m *defaultAgentFreezeTaskModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentFreezeTask) error { + data.DelState = globalkey.DelStateYes + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := m.UpdateWithVersion(ctx, session, data); err != nil { + return errors.Wrapf(errors.New("delete soft failed "), "AgentFreezeTaskModel delete err : %+v", err) + } + return nil +} + +func (m *defaultAgentFreezeTaskModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") + } + + builder = builder.Columns("IFNULL(SUM(" + field + "),0)") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp float64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultAgentFreezeTaskModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") + } + + builder = builder.Columns("COUNT(" + field + ")") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp int64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultAgentFreezeTaskModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AgentFreezeTask, error) { + + builder = builder.Columns(agentFreezeTaskRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentFreezeTask + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentFreezeTaskModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentFreezeTask, error) { + + builder = builder.Columns(agentFreezeTaskRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentFreezeTask + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentFreezeTaskModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentFreezeTask, int64, error) { + + total, err := m.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + builder = builder.Columns(agentFreezeTaskRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, total, err + } + + var resp []*AgentFreezeTask + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, total, nil + default: + return nil, total, err + } +} + +func (m *defaultAgentFreezeTaskModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentFreezeTask, error) { + + builder = builder.Columns(agentFreezeTaskRows) + + if preMinId > 0 { + builder = builder.Where(" id < ? ", preMinId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentFreezeTask + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentFreezeTaskModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentFreezeTask, error) { + + builder = builder.Columns(agentFreezeTaskRows) + + if preMaxId > 0 { + builder = builder.Where(" id > ? ", preMaxId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentFreezeTask + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentFreezeTaskModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { + + return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + return fn(ctx, session) + }) + +} + +func (m *defaultAgentFreezeTaskModel) SelectBuilder() squirrel.SelectBuilder { + return squirrel.Select().From(m.table) +} +func (m *defaultAgentFreezeTaskModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { + yccAgentFreezeTaskIdKey := fmt.Sprintf("%s%v", cacheYccAgentFreezeTaskIdPrefix, id) + _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + if session != nil { + return session.ExecCtx(ctx, query, id) + } + return conn.ExecCtx(ctx, query, id) + }, yccAgentFreezeTaskIdKey) + return err +} +func (m *defaultAgentFreezeTaskModel) formatPrimary(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheYccAgentFreezeTaskIdPrefix, primary) +} +func (m *defaultAgentFreezeTaskModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentFreezeTaskRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) +} + +func (m *defaultAgentFreezeTaskModel) tableName() string { + return m.table +} diff --git a/app/main/model/agentInviteCodeUsageModel.go b/app/main/model/agentInviteCodeUsageModel.go new file mode 100644 index 0000000..8af056e --- /dev/null +++ b/app/main/model/agentInviteCodeUsageModel.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ AgentInviteCodeUsageModel = (*customAgentInviteCodeUsageModel)(nil) + +type ( + // AgentInviteCodeUsageModel is an interface to be customized, add more methods here, + // and implement the added methods in customAgentInviteCodeUsageModel. + AgentInviteCodeUsageModel interface { + agentInviteCodeUsageModel + } + + customAgentInviteCodeUsageModel struct { + *defaultAgentInviteCodeUsageModel + } +) + +// NewAgentInviteCodeUsageModel returns a model for the database table. +func NewAgentInviteCodeUsageModel(conn sqlx.SqlConn, c cache.CacheConf) AgentInviteCodeUsageModel { + return &customAgentInviteCodeUsageModel{ + defaultAgentInviteCodeUsageModel: newAgentInviteCodeUsageModel(conn, c), + } +} diff --git a/app/main/model/agentInviteCodeUsageModel_gen.go b/app/main/model/agentInviteCodeUsageModel_gen.go new file mode 100644 index 0000000..ebbe8c7 --- /dev/null +++ b/app/main/model/agentInviteCodeUsageModel_gen.go @@ -0,0 +1,372 @@ +// Code generated by goctl. DO NOT EDIT! + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "time" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" + "ycc-server/common/globalkey" +) + +var ( + agentInviteCodeUsageFieldNames = builder.RawFieldNames(&AgentInviteCodeUsage{}) + agentInviteCodeUsageRows = strings.Join(agentInviteCodeUsageFieldNames, ",") + agentInviteCodeUsageRowsExpectAutoSet = strings.Join(stringx.Remove(agentInviteCodeUsageFieldNames, "`id`", "`create_time`", "`update_time`"), ",") + agentInviteCodeUsageRowsWithPlaceHolder = strings.Join(stringx.Remove(agentInviteCodeUsageFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" + + cacheYccAgentInviteCodeUsageIdPrefix = "cache:ycc:agentInviteCodeUsage:id:" +) + +type ( + agentInviteCodeUsageModel interface { + Insert(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*AgentInviteCodeUsage, error) + Update(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) (sql.Result, error) + UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) error + Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error + SelectBuilder() squirrel.SelectBuilder + DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) error + FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) + FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) + FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AgentInviteCodeUsage, error) + FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCodeUsage, error) + FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCodeUsage, int64, error) + FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentInviteCodeUsage, error) + FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentInviteCodeUsage, error) + Delete(ctx context.Context, session sqlx.Session, id int64) error + } + + defaultAgentInviteCodeUsageModel struct { + sqlc.CachedConn + table string + } + + AgentInviteCodeUsage struct { + Id int64 `db:"id"` // 主键ID + InviteCodeId int64 `db:"invite_code_id"` // 邀请码ID(关联agent_invite_code表) + Code string `db:"code"` // 邀请码(冗余字段,便于查询) + UserId int64 `db:"user_id"` // 使用用户ID + AgentId int64 `db:"agent_id"` // 成为的代理ID + AgentLevel int64 `db:"agent_level"` // 代理等级:1=普通,2=黄金,3=钻石 + UsedTime time.Time `db:"used_time"` // 使用时间 + CreateTime time.Time `db:"create_time"` // 创建时间 + UpdateTime time.Time `db:"update_time"` // 更新时间 + DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 + DelState int64 `db:"del_state"` // 删除状态:0=未删除,1=已删除 + Version int64 `db:"version"` // 版本号(乐观锁) + } +) + +func newAgentInviteCodeUsageModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAgentInviteCodeUsageModel { + return &defaultAgentInviteCodeUsageModel{ + CachedConn: sqlc.NewConn(conn, c), + table: "`agent_invite_code_usage`", + } +} + +func (m *defaultAgentInviteCodeUsageModel) Insert(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) (sql.Result, error) { + data.DelState = globalkey.DelStateNo + yccAgentInviteCodeUsageIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeUsageIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentInviteCodeUsageRowsExpectAutoSet) + if session != nil { + return session.ExecCtx(ctx, query, data.InviteCodeId, data.Code, data.UserId, data.AgentId, data.AgentLevel, data.UsedTime, data.DeleteTime, data.DelState, data.Version) + } + return conn.ExecCtx(ctx, query, data.InviteCodeId, data.Code, data.UserId, data.AgentId, data.AgentLevel, data.UsedTime, data.DeleteTime, data.DelState, data.Version) + }, yccAgentInviteCodeUsageIdKey) +} + +func (m *defaultAgentInviteCodeUsageModel) FindOne(ctx context.Context, id int64) (*AgentInviteCodeUsage, error) { + yccAgentInviteCodeUsageIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeUsageIdPrefix, id) + var resp AgentInviteCodeUsage + err := m.QueryRowCtx(ctx, &resp, yccAgentInviteCodeUsageIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentInviteCodeUsageRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) Update(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) (sql.Result, error) { + yccAgentInviteCodeUsageIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeUsageIdPrefix, data.Id) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentInviteCodeUsageRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.InviteCodeId, data.Code, data.UserId, data.AgentId, data.AgentLevel, data.UsedTime, data.DeleteTime, data.DelState, data.Version, data.Id) + } + return conn.ExecCtx(ctx, query, data.InviteCodeId, data.Code, data.UserId, data.AgentId, data.AgentLevel, data.UsedTime, data.DeleteTime, data.DelState, data.Version, data.Id) + }, yccAgentInviteCodeUsageIdKey) +} + +func (m *defaultAgentInviteCodeUsageModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) error { + + oldVersion := data.Version + data.Version += 1 + + var sqlResult sql.Result + var err error + + yccAgentInviteCodeUsageIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeUsageIdPrefix, data.Id) + sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentInviteCodeUsageRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, data.InviteCodeId, data.Code, data.UserId, data.AgentId, data.AgentLevel, data.UsedTime, data.DeleteTime, data.DelState, data.Version, data.Id, oldVersion) + } + return conn.ExecCtx(ctx, query, data.InviteCodeId, data.Code, data.UserId, data.AgentId, data.AgentLevel, data.UsedTime, data.DeleteTime, data.DelState, data.Version, data.Id, oldVersion) + }, yccAgentInviteCodeUsageIdKey) + if err != nil { + return err + } + updateCount, err := sqlResult.RowsAffected() + if err != nil { + return err + } + if updateCount == 0 { + return ErrNoRowsUpdate + } + + return nil +} + +func (m *defaultAgentInviteCodeUsageModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentInviteCodeUsage) error { + data.DelState = globalkey.DelStateYes + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := m.UpdateWithVersion(ctx, session, data); err != nil { + return errors.Wrapf(errors.New("delete soft failed "), "AgentInviteCodeUsageModel delete err : %+v", err) + } + return nil +} + +func (m *defaultAgentInviteCodeUsageModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") + } + + builder = builder.Columns("IFNULL(SUM(" + field + "),0)") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp float64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") + } + + builder = builder.Columns("COUNT(" + field + ")") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp int64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AgentInviteCodeUsage, error) { + + builder = builder.Columns(agentInviteCodeUsageRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentInviteCodeUsage + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCodeUsage, error) { + + builder = builder.Columns(agentInviteCodeUsageRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentInviteCodeUsage + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCodeUsage, int64, error) { + + total, err := m.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + builder = builder.Columns(agentInviteCodeUsageRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, total, err + } + + var resp []*AgentInviteCodeUsage + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, total, nil + default: + return nil, total, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentInviteCodeUsage, error) { + + builder = builder.Columns(agentInviteCodeUsageRows) + + if preMinId > 0 { + builder = builder.Where(" id < ? ", preMinId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentInviteCodeUsage + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentInviteCodeUsage, error) { + + builder = builder.Columns(agentInviteCodeUsageRows) + + if preMaxId > 0 { + builder = builder.Where(" id > ? ", preMaxId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentInviteCodeUsage + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentInviteCodeUsageModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { + + return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + return fn(ctx, session) + }) + +} + +func (m *defaultAgentInviteCodeUsageModel) SelectBuilder() squirrel.SelectBuilder { + return squirrel.Select().From(m.table) +} +func (m *defaultAgentInviteCodeUsageModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { + yccAgentInviteCodeUsageIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeUsageIdPrefix, id) + _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + if session != nil { + return session.ExecCtx(ctx, query, id) + } + return conn.ExecCtx(ctx, query, id) + }, yccAgentInviteCodeUsageIdKey) + return err +} +func (m *defaultAgentInviteCodeUsageModel) formatPrimary(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheYccAgentInviteCodeUsageIdPrefix, primary) +} +func (m *defaultAgentInviteCodeUsageModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentInviteCodeUsageRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) +} + +func (m *defaultAgentInviteCodeUsageModel) tableName() string { + return m.table +} diff --git a/app/main/model/agentModel.go b/app/main/model/agentModel.go index 2aef816..eea1285 100644 --- a/app/main/model/agentModel.go +++ b/app/main/model/agentModel.go @@ -1,6 +1,10 @@ package model import ( + "context" + "fmt" + + "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/sqlx" ) @@ -12,6 +16,8 @@ type ( // and implement the added methods in customAgentModel. AgentModel interface { agentModel + // UpdateInTransaction 在事务中更新刚插入的记录,避免 UpdateWithVersion 中的 FindOne 缓存问题 + UpdateInTransaction(ctx context.Context, session sqlx.Session, data *Agent) error } customAgentModel struct { @@ -25,3 +31,35 @@ func NewAgentModel(conn sqlx.SqlConn, c cache.CacheConf) AgentModel { defaultAgentModel: newAgentModel(conn, c), } } + +// UpdateInTransaction 在事务中更新刚插入的记录,避免 UpdateWithVersion 中的 FindOne 缓存问题 +// 注意:此方法假设记录刚插入,version 为 0,更新后 version 为 1 +func (m *customAgentModel) UpdateInTransaction(ctx context.Context, session sqlx.Session, data *Agent) error { + if session == nil { + return errors.New("session is required for UpdateInTransaction") + } + + // 直接执行 SQL UPDATE,避免 FindOne 的缓存问题 + // 字段顺序:user_id, level, region, mobile, wechat_id, team_leader_id, delete_time, del_state, version + query := fmt.Sprintf("update %s set `user_id`=?, `level`=?, `region`=?, `mobile`=?, `wechat_id`=?, `team_leader_id`=?, `delete_time`=?, `del_state`=?, `version`=? where `id`=? and `version`=?", m.table) + + // 确保 version 从 0 增加到 1 + oldVersion := data.Version + data.Version = oldVersion + 1 + + result, err := session.ExecCtx(ctx, query, data.UserId, data.Level, data.Region, data.Mobile, data.WechatId, data.TeamLeaderId, data.DeleteTime, data.DelState, data.Version, data.Id, oldVersion) + if err != nil { + return errors.Wrapf(err, "UpdateInTransaction failed") + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return errors.Wrapf(err, "UpdateInTransaction failed to get rows affected") + } + + if rowsAffected == 0 { + return errors.Wrapf(ErrNoRowsUpdate, "UpdateInTransaction: no rows updated, version may not match") + } + + return nil +} diff --git a/app/main/model/agentModel_gen.go b/app/main/model/agentModel_gen.go index dbc27a6..d209c2f 100644 --- a/app/main/model/agentModel_gen.go +++ b/app/main/model/agentModel_gen.go @@ -10,6 +10,8 @@ import ( "time" + "ycc-server/common/globalkey" + "github.com/Masterminds/squirrel" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/builder" @@ -17,7 +19,6 @@ import ( "github.com/zeromicro/go-zero/core/stores/sqlc" "github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stringx" - "ycc-server/common/globalkey" ) var ( diff --git a/app/main/model/agentProductConfigModel_gen.go b/app/main/model/agentProductConfigModel_gen.go index b9c1eb2..6c81aec 100644 --- a/app/main/model/agentProductConfigModel_gen.go +++ b/app/main/model/agentProductConfigModel_gen.go @@ -58,7 +58,6 @@ type ( AgentProductConfig struct { Id int64 `db:"id"` // 主键ID ProductId int64 `db:"product_id"` // 产品ID - ProductName string `db:"product_name"` // 产品名称 BasePrice float64 `db:"base_price"` // 基础底价(BasePrice) SystemMaxPrice float64 `db:"system_max_price"` // 系统价格上限(SystemMaxPrice) PriceThreshold sql.NullFloat64 `db:"price_threshold"` // 提价标准阈值(PriceThreshold) @@ -83,11 +82,11 @@ func (m *defaultAgentProductConfigModel) Insert(ctx context.Context, session sql yccAgentProductConfigIdKey := fmt.Sprintf("%s%v", cacheYccAgentProductConfigIdPrefix, data.Id) yccAgentProductConfigProductIdKey := fmt.Sprintf("%s%v", cacheYccAgentProductConfigProductIdPrefix, data.ProductId) return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentProductConfigRowsExpectAutoSet) + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentProductConfigRowsExpectAutoSet) if session != nil { - return session.ExecCtx(ctx, query, data.ProductId, data.ProductName, data.BasePrice, data.SystemMaxPrice, data.PriceThreshold, data.PriceFeeRate, data.DeleteTime, data.DelState, data.Version) + return session.ExecCtx(ctx, query, data.ProductId, data.BasePrice, data.SystemMaxPrice, data.PriceThreshold, data.PriceFeeRate, data.DeleteTime, data.DelState, data.Version) } - return conn.ExecCtx(ctx, query, data.ProductId, data.ProductName, data.BasePrice, data.SystemMaxPrice, data.PriceThreshold, data.PriceFeeRate, data.DeleteTime, data.DelState, data.Version) + return conn.ExecCtx(ctx, query, data.ProductId, data.BasePrice, data.SystemMaxPrice, data.PriceThreshold, data.PriceFeeRate, data.DeleteTime, data.DelState, data.Version) }, yccAgentProductConfigIdKey, yccAgentProductConfigProductIdKey) } @@ -138,9 +137,9 @@ func (m *defaultAgentProductConfigModel) Update(ctx context.Context, session sql return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentProductConfigRowsWithPlaceHolder) if session != nil { - return session.ExecCtx(ctx, query, newData.ProductId, newData.ProductName, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) + return session.ExecCtx(ctx, query, newData.ProductId, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) } - return conn.ExecCtx(ctx, query, newData.ProductId, newData.ProductName, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) + return conn.ExecCtx(ctx, query, newData.ProductId, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) }, yccAgentProductConfigIdKey, yccAgentProductConfigProductIdKey) } @@ -161,9 +160,9 @@ func (m *defaultAgentProductConfigModel) UpdateWithVersion(ctx context.Context, sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentProductConfigRowsWithPlaceHolder) if session != nil { - return session.ExecCtx(ctx, query, newData.ProductId, newData.ProductName, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) + return session.ExecCtx(ctx, query, newData.ProductId, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) } - return conn.ExecCtx(ctx, query, newData.ProductId, newData.ProductName, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) + return conn.ExecCtx(ctx, query, newData.ProductId, newData.BasePrice, newData.SystemMaxPrice, newData.PriceThreshold, newData.PriceFeeRate, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) }, yccAgentProductConfigIdKey, yccAgentProductConfigProductIdKey) if err != nil { return err diff --git a/app/main/model/agentShortLinkModel.go b/app/main/model/agentShortLinkModel.go new file mode 100644 index 0000000..8ce3e38 --- /dev/null +++ b/app/main/model/agentShortLinkModel.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +var _ AgentShortLinkModel = (*customAgentShortLinkModel)(nil) + +type ( + // AgentShortLinkModel is an interface to be customized, add more methods here, + // and implement the added methods in customAgentShortLinkModel. + AgentShortLinkModel interface { + agentShortLinkModel + } + + customAgentShortLinkModel struct { + *defaultAgentShortLinkModel + } +) + +// NewAgentShortLinkModel returns a model for the database table. +func NewAgentShortLinkModel(conn sqlx.SqlConn, c cache.CacheConf) AgentShortLinkModel { + return &customAgentShortLinkModel{ + defaultAgentShortLinkModel: newAgentShortLinkModel(conn, c), + } +} diff --git a/app/main/model/agentShortLinkModel_gen.go b/app/main/model/agentShortLinkModel_gen.go new file mode 100644 index 0000000..6dd4e0f --- /dev/null +++ b/app/main/model/agentShortLinkModel_gen.go @@ -0,0 +1,465 @@ +// Code generated by goctl. DO NOT EDIT! + +package model + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "time" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" + "ycc-server/common/globalkey" +) + +var ( + agentShortLinkFieldNames = builder.RawFieldNames(&AgentShortLink{}) + agentShortLinkRows = strings.Join(agentShortLinkFieldNames, ",") + agentShortLinkRowsExpectAutoSet = strings.Join(stringx.Remove(agentShortLinkFieldNames, "`id`", "`create_time`", "`update_time`"), ",") + agentShortLinkRowsWithPlaceHolder = strings.Join(stringx.Remove(agentShortLinkFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" + + cacheYccAgentShortLinkIdPrefix = "cache:ycc:agentShortLink:id:" + cacheYccAgentShortLinkInviteCodeIdTypeDelStatePrefix = "cache:ycc:agentShortLink:inviteCodeId:type:delState:" + cacheYccAgentShortLinkLinkIdTypeDelStatePrefix = "cache:ycc:agentShortLink:linkId:type:delState:" + cacheYccAgentShortLinkShortCodeDelStatePrefix = "cache:ycc:agentShortLink:shortCode:delState:" +) + +type ( + agentShortLinkModel interface { + Insert(ctx context.Context, session sqlx.Session, data *AgentShortLink) (sql.Result, error) + FindOne(ctx context.Context, id int64) (*AgentShortLink, error) + FindOneByInviteCodeIdTypeDelState(ctx context.Context, inviteCodeId sql.NullInt64, tp int64, delState int64) (*AgentShortLink, error) + FindOneByLinkIdTypeDelState(ctx context.Context, linkId sql.NullInt64, tp int64, delState int64) (*AgentShortLink, error) + FindOneByShortCodeDelState(ctx context.Context, shortCode string, delState int64) (*AgentShortLink, error) + Update(ctx context.Context, session sqlx.Session, data *AgentShortLink) (sql.Result, error) + UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AgentShortLink) error + Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error + SelectBuilder() squirrel.SelectBuilder + DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentShortLink) error + FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) + FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) + FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AgentShortLink, error) + FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentShortLink, error) + FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentShortLink, int64, error) + FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentShortLink, error) + FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentShortLink, error) + Delete(ctx context.Context, session sqlx.Session, id int64) error + } + + defaultAgentShortLinkModel struct { + sqlc.CachedConn + table string + } + + AgentShortLink struct { + Id int64 `db:"id"` // 主键ID + Type int64 `db:"type"` // 类型:1=推广报告(promotion),2=邀请好友(invite) + LinkId sql.NullInt64 `db:"link_id"` // 推广链接ID(关联agent_link表,仅推广报告类型使用) + InviteCodeId sql.NullInt64 `db:"invite_code_id"` // 邀请码ID(关联agent_invite_code表,仅邀请好友类型使用) + LinkIdentifier sql.NullString `db:"link_identifier"` // 推广链接标识(加密,仅推广报告类型使用) + InviteCode sql.NullString `db:"invite_code"` // 邀请码(仅邀请好友类型使用) + ShortCode string `db:"short_code"` // 短链标识(6位随机字符串) + TargetPath string `db:"target_path"` // 目标地址(前端传入,如:/agent/promotionInquire/xxx 或 /register?invite_code=xxx) + PromotionDomain string `db:"promotion_domain"` // 推广域名(生成短链时使用的域名) + CreateTime time.Time `db:"create_time"` // 创建时间 + UpdateTime time.Time `db:"update_time"` // 更新时间 + DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 + DelState int64 `db:"del_state"` // 删除状态:0=未删除,1=已删除 + Version int64 `db:"version"` // 版本号(乐观锁) + } +) + +func newAgentShortLinkModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAgentShortLinkModel { + return &defaultAgentShortLinkModel{ + CachedConn: sqlc.NewConn(conn, c), + table: "`agent_short_link`", + } +} + +func (m *defaultAgentShortLinkModel) Insert(ctx context.Context, session sqlx.Session, data *AgentShortLink) (sql.Result, error) { + data.DelState = globalkey.DelStateNo + yccAgentShortLinkIdKey := fmt.Sprintf("%s%v", cacheYccAgentShortLinkIdPrefix, data.Id) + yccAgentShortLinkInviteCodeIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkInviteCodeIdTypeDelStatePrefix, data.InviteCodeId, data.Type, data.DelState) + yccAgentShortLinkLinkIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkLinkIdTypeDelStatePrefix, data.LinkId, data.Type, data.DelState) + yccAgentShortLinkShortCodeDelStateKey := fmt.Sprintf("%s%v:%v", cacheYccAgentShortLinkShortCodeDelStatePrefix, data.ShortCode, data.DelState) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentShortLinkRowsExpectAutoSet) + if session != nil { + return session.ExecCtx(ctx, query, data.Type, data.LinkId, data.InviteCodeId, data.LinkIdentifier, data.InviteCode, data.ShortCode, data.TargetPath, data.PromotionDomain, data.DeleteTime, data.DelState, data.Version) + } + return conn.ExecCtx(ctx, query, data.Type, data.LinkId, data.InviteCodeId, data.LinkIdentifier, data.InviteCode, data.ShortCode, data.TargetPath, data.PromotionDomain, data.DeleteTime, data.DelState, data.Version) + }, yccAgentShortLinkIdKey, yccAgentShortLinkInviteCodeIdTypeDelStateKey, yccAgentShortLinkLinkIdTypeDelStateKey, yccAgentShortLinkShortCodeDelStateKey) +} + +func (m *defaultAgentShortLinkModel) FindOne(ctx context.Context, id int64) (*AgentShortLink, error) { + yccAgentShortLinkIdKey := fmt.Sprintf("%s%v", cacheYccAgentShortLinkIdPrefix, id) + var resp AgentShortLink + err := m.QueryRowCtx(ctx, &resp, yccAgentShortLinkIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentShortLinkRows, m.table) + return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) FindOneByInviteCodeIdTypeDelState(ctx context.Context, inviteCodeId sql.NullInt64, tp int64, delState int64) (*AgentShortLink, error) { + yccAgentShortLinkInviteCodeIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkInviteCodeIdTypeDelStatePrefix, inviteCodeId, tp, delState) + var resp AgentShortLink + err := m.QueryRowIndexCtx(ctx, &resp, yccAgentShortLinkInviteCodeIdTypeDelStateKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { + query := fmt.Sprintf("select %s from %s where `invite_code_id` = ? and `type` = ? and `del_state` = ? and del_state = ? limit 1", agentShortLinkRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, inviteCodeId, tp, delState, globalkey.DelStateNo); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) FindOneByLinkIdTypeDelState(ctx context.Context, linkId sql.NullInt64, tp int64, delState int64) (*AgentShortLink, error) { + yccAgentShortLinkLinkIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkLinkIdTypeDelStatePrefix, linkId, tp, delState) + var resp AgentShortLink + err := m.QueryRowIndexCtx(ctx, &resp, yccAgentShortLinkLinkIdTypeDelStateKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { + query := fmt.Sprintf("select %s from %s where `link_id` = ? and `type` = ? and `del_state` = ? and del_state = ? limit 1", agentShortLinkRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, linkId, tp, delState, globalkey.DelStateNo); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) FindOneByShortCodeDelState(ctx context.Context, shortCode string, delState int64) (*AgentShortLink, error) { + yccAgentShortLinkShortCodeDelStateKey := fmt.Sprintf("%s%v:%v", cacheYccAgentShortLinkShortCodeDelStatePrefix, shortCode, delState) + var resp AgentShortLink + err := m.QueryRowIndexCtx(ctx, &resp, yccAgentShortLinkShortCodeDelStateKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { + query := fmt.Sprintf("select %s from %s where `short_code` = ? and `del_state` = ? and del_state = ? limit 1", agentShortLinkRows, m.table) + if err := conn.QueryRowCtx(ctx, &resp, query, shortCode, delState, globalkey.DelStateNo); err != nil { + return nil, err + } + return resp.Id, nil + }, m.queryPrimary) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) Update(ctx context.Context, session sqlx.Session, newData *AgentShortLink) (sql.Result, error) { + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return nil, err + } + yccAgentShortLinkIdKey := fmt.Sprintf("%s%v", cacheYccAgentShortLinkIdPrefix, data.Id) + yccAgentShortLinkInviteCodeIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkInviteCodeIdTypeDelStatePrefix, data.InviteCodeId, data.Type, data.DelState) + yccAgentShortLinkLinkIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkLinkIdTypeDelStatePrefix, data.LinkId, data.Type, data.DelState) + yccAgentShortLinkShortCodeDelStateKey := fmt.Sprintf("%s%v:%v", cacheYccAgentShortLinkShortCodeDelStatePrefix, data.ShortCode, data.DelState) + return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentShortLinkRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, newData.Type, newData.LinkId, newData.InviteCodeId, newData.LinkIdentifier, newData.InviteCode, newData.ShortCode, newData.TargetPath, newData.PromotionDomain, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) + } + return conn.ExecCtx(ctx, query, newData.Type, newData.LinkId, newData.InviteCodeId, newData.LinkIdentifier, newData.InviteCode, newData.ShortCode, newData.TargetPath, newData.PromotionDomain, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) + }, yccAgentShortLinkIdKey, yccAgentShortLinkInviteCodeIdTypeDelStateKey, yccAgentShortLinkLinkIdTypeDelStateKey, yccAgentShortLinkShortCodeDelStateKey) +} + +func (m *defaultAgentShortLinkModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AgentShortLink) error { + + oldVersion := newData.Version + newData.Version += 1 + + var sqlResult sql.Result + var err error + + data, err := m.FindOne(ctx, newData.Id) + if err != nil { + return err + } + yccAgentShortLinkIdKey := fmt.Sprintf("%s%v", cacheYccAgentShortLinkIdPrefix, data.Id) + yccAgentShortLinkInviteCodeIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkInviteCodeIdTypeDelStatePrefix, data.InviteCodeId, data.Type, data.DelState) + yccAgentShortLinkLinkIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkLinkIdTypeDelStatePrefix, data.LinkId, data.Type, data.DelState) + yccAgentShortLinkShortCodeDelStateKey := fmt.Sprintf("%s%v:%v", cacheYccAgentShortLinkShortCodeDelStatePrefix, data.ShortCode, data.DelState) + sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentShortLinkRowsWithPlaceHolder) + if session != nil { + return session.ExecCtx(ctx, query, newData.Type, newData.LinkId, newData.InviteCodeId, newData.LinkIdentifier, newData.InviteCode, newData.ShortCode, newData.TargetPath, newData.PromotionDomain, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) + } + return conn.ExecCtx(ctx, query, newData.Type, newData.LinkId, newData.InviteCodeId, newData.LinkIdentifier, newData.InviteCode, newData.ShortCode, newData.TargetPath, newData.PromotionDomain, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) + }, yccAgentShortLinkIdKey, yccAgentShortLinkInviteCodeIdTypeDelStateKey, yccAgentShortLinkLinkIdTypeDelStateKey, yccAgentShortLinkShortCodeDelStateKey) + if err != nil { + return err + } + updateCount, err := sqlResult.RowsAffected() + if err != nil { + return err + } + if updateCount == 0 { + return ErrNoRowsUpdate + } + + return nil +} + +func (m *defaultAgentShortLinkModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentShortLink) error { + data.DelState = globalkey.DelStateYes + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := m.UpdateWithVersion(ctx, session, data); err != nil { + return errors.Wrapf(errors.New("delete soft failed "), "AgentShortLinkModel delete err : %+v", err) + } + return nil +} + +func (m *defaultAgentShortLinkModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") + } + + builder = builder.Columns("IFNULL(SUM(" + field + "),0)") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp float64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultAgentShortLinkModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { + + if len(field) == 0 { + return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") + } + + builder = builder.Columns("COUNT(" + field + ")") + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return 0, err + } + + var resp int64 + err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return 0, err + } +} + +func (m *defaultAgentShortLinkModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AgentShortLink, error) { + + builder = builder.Columns(agentShortLinkRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentShortLink + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentShortLink, error) { + + builder = builder.Columns(agentShortLinkRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentShortLink + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentShortLink, int64, error) { + + total, err := m.FindCount(ctx, builder, "id") + if err != nil { + return nil, 0, err + } + + builder = builder.Columns(agentShortLinkRows) + + if orderBy == "" { + builder = builder.OrderBy("id DESC") + } else { + builder = builder.OrderBy(orderBy) + } + + if page < 1 { + page = 1 + } + offset := (page - 1) * pageSize + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, total, err + } + + var resp []*AgentShortLink + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, total, nil + default: + return nil, total, err + } +} + +func (m *defaultAgentShortLinkModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentShortLink, error) { + + builder = builder.Columns(agentShortLinkRows) + + if preMinId > 0 { + builder = builder.Where(" id < ? ", preMinId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentShortLink + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentShortLink, error) { + + builder = builder.Columns(agentShortLinkRows) + + if preMaxId > 0 { + builder = builder.Where(" id > ? ", preMaxId) + } + + query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() + if err != nil { + return nil, err + } + + var resp []*AgentShortLink + err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) + switch err { + case nil: + return resp, nil + default: + return nil, err + } +} + +func (m *defaultAgentShortLinkModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { + + return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + return fn(ctx, session) + }) + +} + +func (m *defaultAgentShortLinkModel) SelectBuilder() squirrel.SelectBuilder { + return squirrel.Select().From(m.table) +} +func (m *defaultAgentShortLinkModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + + yccAgentShortLinkIdKey := fmt.Sprintf("%s%v", cacheYccAgentShortLinkIdPrefix, id) + yccAgentShortLinkInviteCodeIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkInviteCodeIdTypeDelStatePrefix, data.InviteCodeId, data.Type, data.DelState) + yccAgentShortLinkLinkIdTypeDelStateKey := fmt.Sprintf("%s%v:%v:%v", cacheYccAgentShortLinkLinkIdTypeDelStatePrefix, data.LinkId, data.Type, data.DelState) + yccAgentShortLinkShortCodeDelStateKey := fmt.Sprintf("%s%v:%v", cacheYccAgentShortLinkShortCodeDelStatePrefix, data.ShortCode, data.DelState) + _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + if session != nil { + return session.ExecCtx(ctx, query, id) + } + return conn.ExecCtx(ctx, query, id) + }, yccAgentShortLinkIdKey, yccAgentShortLinkInviteCodeIdTypeDelStateKey, yccAgentShortLinkLinkIdTypeDelStateKey, yccAgentShortLinkShortCodeDelStateKey) + return err +} +func (m *defaultAgentShortLinkModel) formatPrimary(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheYccAgentShortLinkIdPrefix, primary) +} +func (m *defaultAgentShortLinkModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { + query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentShortLinkRows, m.table) + return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) +} + +func (m *defaultAgentShortLinkModel) tableName() string { + return m.table +} diff --git a/deploy/script/gen_models.ps1 b/deploy/script/gen_models.ps1 index ff52ea2..d674bc0 100644 --- a/deploy/script/gen_models.ps1 +++ b/deploy/script/gen_models.ps1 @@ -1,16 +1,15 @@ # 设置输出编码为UTF-8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -# 数据库连接信息 - 修改了URL格式 -$DB_URL = "ycc:5vg67b3UNHu8@(127.0.0.1:21001)/ycc" +# 目录配置 $OUTPUT_DIR = "./model" -$TEMPLATE_DIR = "../template" +$TARGET_DIR = "../../app/main/model" # 表名列表 $tables = @( # ============================================ # 新代理系统表 # ============================================ - "agent", + # "agent", # "agent_audit", # "agent_wallet", # "agent_relation", @@ -20,9 +19,12 @@ $tables = @( # "agent_rebate", # "agent_upgrade", # "agent_withdrawal", - # "agent_config", - # "agent_product_config", - "agent_real_name" + # "agent_config" + # "agent_product_config" + # "agent_invite_code_usage" + # "agent_freeze_task" + "agent_short_link" + # "agent_real_name" # "agent_withdrawal_tax" # "agent_invite_code" # ============================================ @@ -60,5 +62,68 @@ $tables = @( # 为每个表生成模型 foreach ($table in $tables) { + Write-Host "正在生成表: $table" -ForegroundColor Green goctl model mysql datasource -url="ycc:5vg67b3UNHu8@tcp(127.0.0.1:21001)/ycc" -table="$table" -dir="./model" --home="../template" -cache=true --style=goZero + + # 移动生成的文件到目标目录 + if (Test-Path $OUTPUT_DIR) { + $sourceFiles = Get-ChildItem -Path $OUTPUT_DIR -File + + foreach ($file in $sourceFiles) { + $fileName = $file.Name + $targetPath = Join-Path $TARGET_DIR $fileName + $sourcePath = $file.FullName + + # 检查文件类型并决定是否移动 + $shouldMove = $false + $shouldOverwrite = $false + + if ($fileName -eq "vars.go") { + # vars.go: 如果目标目录不存在才移动 + if (-not (Test-Path $targetPath)) { + $shouldMove = $true + Write-Host " 移动 $fileName (vars.go 不存在于目标目录)" -ForegroundColor Yellow + } + else { + Write-Host " 跳过 $fileName (vars.go 已存在于目标目录,防止覆盖)" -ForegroundColor Cyan + } + } + elseif ($fileName -match "_gen\.go$") { + # 带 _gen 后缀的文件: 直接覆盖 + $shouldMove = $true + $shouldOverwrite = $true + Write-Host " 移动 $fileName (覆盖 _gen 文件)" -ForegroundColor Yellow + } + else { + # 不带 _gen 后缀的文件: 如果目标目录不存在才移动 + if (-not (Test-Path $targetPath)) { + $shouldMove = $true + Write-Host " 移动 $fileName (非 _gen 文件不存在于目标目录)" -ForegroundColor Yellow + } + else { + Write-Host " 跳过 $fileName (非 _gen 文件已存在于目标目录,防止覆盖)" -ForegroundColor Cyan + } + } + + # 执行移动操作 + if ($shouldMove) { + # 确保目标目录存在 + if (-not (Test-Path $TARGET_DIR)) { + New-Item -ItemType Directory -Path $TARGET_DIR -Force | Out-Null + } + + # 如果目标文件存在且需要覆盖,先删除 + if ($shouldOverwrite -and (Test-Path $targetPath)) { + Remove-Item -Path $targetPath -Force + } + + # 移动文件 + Move-Item -Path $sourcePath -Destination $targetPath -Force + Write-Host " ✓ 已移动到: $targetPath" -ForegroundColor Green + } + } + } } + +Write-Host "" +Write-Host '所有模型文件生成并移动完成!' -ForegroundColor Green diff --git a/deploy/script/model/agentInviteCodeModel.go b/deploy/script/model/agentInviteCodeModel.go deleted file mode 100644 index 9b02e4a..0000000 --- a/deploy/script/model/agentInviteCodeModel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ AgentInviteCodeModel = (*customAgentInviteCodeModel)(nil) - -type ( - // AgentInviteCodeModel is an interface to be customized, add more methods here, - // and implement the added methods in customAgentInviteCodeModel. - AgentInviteCodeModel interface { - agentInviteCodeModel - } - - customAgentInviteCodeModel struct { - *defaultAgentInviteCodeModel - } -) - -// NewAgentInviteCodeModel returns a model for the database table. -func NewAgentInviteCodeModel(conn sqlx.SqlConn, c cache.CacheConf) AgentInviteCodeModel { - return &customAgentInviteCodeModel{ - defaultAgentInviteCodeModel: newAgentInviteCodeModel(conn, c), - } -} diff --git a/deploy/script/model/agentInviteCodeModel_gen.go b/deploy/script/model/agentInviteCodeModel_gen.go deleted file mode 100644 index d515a54..0000000 --- a/deploy/script/model/agentInviteCodeModel_gen.go +++ /dev/null @@ -1,414 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" - "ycc-server/common/globalkey" -) - -var ( - agentInviteCodeFieldNames = builder.RawFieldNames(&AgentInviteCode{}) - agentInviteCodeRows = strings.Join(agentInviteCodeFieldNames, ",") - agentInviteCodeRowsExpectAutoSet = strings.Join(stringx.Remove(agentInviteCodeFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - agentInviteCodeRowsWithPlaceHolder = strings.Join(stringx.Remove(agentInviteCodeFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheYccAgentInviteCodeIdPrefix = "cache:ycc:agentInviteCode:id:" - cacheYccAgentInviteCodeCodePrefix = "cache:ycc:agentInviteCode:code:" -) - -type ( - agentInviteCodeModel interface { - Insert(ctx context.Context, session sqlx.Session, data *AgentInviteCode) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*AgentInviteCode, error) - FindOneByCode(ctx context.Context, code string) (*AgentInviteCode, error) - Update(ctx context.Context, session sqlx.Session, data *AgentInviteCode) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *AgentInviteCode) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentInviteCode) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*AgentInviteCode, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCode, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCode, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentInviteCode, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentInviteCode, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultAgentInviteCodeModel struct { - sqlc.CachedConn - table string - } - - AgentInviteCode struct { - Id int64 `db:"id"` // 主键ID - Code string `db:"code"` // 邀请码(唯一) - AgentId sql.NullInt64 `db:"agent_id"` // 发放代理ID(NULL表示平台发放的钻石邀请码) - TargetLevel int64 `db:"target_level"` // 目标等级:1=普通,2=黄金,3=钻石 - Status int64 `db:"status"` // 状态:0=未使用,1=已使用,2=已失效(所有邀请码只能使用一次,使用后立即失效) - UsedUserId sql.NullInt64 `db:"used_user_id"` // 使用用户ID - UsedAgentId sql.NullInt64 `db:"used_agent_id"` // 使用代理ID - UsedTime sql.NullTime `db:"used_time"` // 使用时间 - ExpireTime sql.NullTime `db:"expire_time"` // 过期时间(可选) - Remark sql.NullString `db:"remark"` // 备注 - CreateTime time.Time `db:"create_time"` // 创建时间 - UpdateTime time.Time `db:"update_time"` // 更新时间 - DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 - DelState int64 `db:"del_state"` // 删除状态:0=未删除,1=已删除 - Version int64 `db:"version"` // 版本号(乐观锁) - } -) - -func newAgentInviteCodeModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultAgentInviteCodeModel { - return &defaultAgentInviteCodeModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`agent_invite_code`", - } -} - -func (m *defaultAgentInviteCodeModel) Insert(ctx context.Context, session sqlx.Session, data *AgentInviteCode) (sql.Result, error) { - data.DelState = globalkey.DelStateNo - yccAgentInviteCodeCodeKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeCodePrefix, data.Code) - yccAgentInviteCodeIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeIdPrefix, data.Id) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, agentInviteCodeRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.Code, data.AgentId, data.TargetLevel, data.Status, data.UsedUserId, data.UsedAgentId, data.UsedTime, data.ExpireTime, data.Remark, data.DeleteTime, data.DelState, data.Version) - } - return conn.ExecCtx(ctx, query, data.Code, data.AgentId, data.TargetLevel, data.Status, data.UsedUserId, data.UsedAgentId, data.UsedTime, data.ExpireTime, data.Remark, data.DeleteTime, data.DelState, data.Version) - }, yccAgentInviteCodeCodeKey, yccAgentInviteCodeIdKey) -} - -func (m *defaultAgentInviteCodeModel) FindOne(ctx context.Context, id int64) (*AgentInviteCode, error) { - yccAgentInviteCodeIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeIdPrefix, id) - var resp AgentInviteCode - err := m.QueryRowCtx(ctx, &resp, yccAgentInviteCodeIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentInviteCodeRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAgentInviteCodeModel) FindOneByCode(ctx context.Context, code string) (*AgentInviteCode, error) { - yccAgentInviteCodeCodeKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeCodePrefix, code) - var resp AgentInviteCode - err := m.QueryRowIndexCtx(ctx, &resp, yccAgentInviteCodeCodeKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `code` = ? and del_state = ? limit 1", agentInviteCodeRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, code, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultAgentInviteCodeModel) Update(ctx context.Context, session sqlx.Session, newData *AgentInviteCode) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - yccAgentInviteCodeCodeKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeCodePrefix, data.Code) - yccAgentInviteCodeIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeIdPrefix, data.Id) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, agentInviteCodeRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.Code, newData.AgentId, newData.TargetLevel, newData.Status, newData.UsedUserId, newData.UsedAgentId, newData.UsedTime, newData.ExpireTime, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.Code, newData.AgentId, newData.TargetLevel, newData.Status, newData.UsedUserId, newData.UsedAgentId, newData.UsedTime, newData.ExpireTime, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id) - }, yccAgentInviteCodeCodeKey, yccAgentInviteCodeIdKey) -} - -func (m *defaultAgentInviteCodeModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *AgentInviteCode) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - yccAgentInviteCodeCodeKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeCodePrefix, data.Code) - yccAgentInviteCodeIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeIdPrefix, data.Id) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, agentInviteCodeRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.Code, newData.AgentId, newData.TargetLevel, newData.Status, newData.UsedUserId, newData.UsedAgentId, newData.UsedTime, newData.ExpireTime, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.Code, newData.AgentId, newData.TargetLevel, newData.Status, newData.UsedUserId, newData.UsedAgentId, newData.UsedTime, newData.ExpireTime, newData.Remark, newData.DeleteTime, newData.DelState, newData.Version, newData.Id, oldVersion) - }, yccAgentInviteCodeCodeKey, yccAgentInviteCodeIdKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultAgentInviteCodeModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *AgentInviteCode) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "AgentInviteCodeModel delete err : %+v", err) - } - return nil -} - -func (m *defaultAgentInviteCodeModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAgentInviteCodeModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultAgentInviteCodeModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*AgentInviteCode, error) { - - builder = builder.Columns(agentInviteCodeRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*AgentInviteCode - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAgentInviteCodeModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCode, error) { - - builder = builder.Columns(agentInviteCodeRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AgentInviteCode - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAgentInviteCodeModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*AgentInviteCode, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(agentInviteCodeRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*AgentInviteCode - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultAgentInviteCodeModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*AgentInviteCode, error) { - - builder = builder.Columns(agentInviteCodeRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AgentInviteCode - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAgentInviteCodeModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*AgentInviteCode, error) { - - builder = builder.Columns(agentInviteCodeRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*AgentInviteCode - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultAgentInviteCodeModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultAgentInviteCodeModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultAgentInviteCodeModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - yccAgentInviteCodeCodeKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeCodePrefix, data.Code) - yccAgentInviteCodeIdKey := fmt.Sprintf("%s%v", cacheYccAgentInviteCodeIdPrefix, id) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, yccAgentInviteCodeCodeKey, yccAgentInviteCodeIdKey) - return err -} -func (m *defaultAgentInviteCodeModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheYccAgentInviteCodeIdPrefix, primary) -} -func (m *defaultAgentInviteCodeModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", agentInviteCodeRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultAgentInviteCodeModel) tableName() string { - return m.table -} diff --git a/deploy/sql/add_agent_short_link_table.sql b/deploy/sql/add_agent_short_link_table.sql new file mode 100644 index 0000000..6cd0149 --- /dev/null +++ b/deploy/sql/add_agent_short_link_table.sql @@ -0,0 +1,47 @@ +-- ============================================ +-- 代理短链表 +-- 说明:存储推广链接和邀请链接的短链映射关系 +-- +-- 功能说明: +-- 1. 支持两种类型:promotion(推广报告)和 invite(邀请好友) +-- 2. 为每个链接生成一个短链标识(6位随机字符串) +-- 3. 短链格式:https://推广域名/s/{short_code} +-- 4. 短链重定向到前端传入的target_path +-- +-- 执行步骤: +-- 1. 执行此 SQL 创建表 +-- 2. 使用 goctl 生成 Model:agent_short_link -> AgentShortLinkModel +-- 3. 在 servicecontext.go 中添加 AgentShortLinkModel +-- ============================================ +CREATE TABLE `agent_short_link` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `type` tinyint NOT NULL COMMENT '类型:1=推广报告(promotion),2=邀请好友(invite)', + `link_id` bigint DEFAULT NULL COMMENT '推广链接ID(关联agent_link表,仅推广报告类型使用)', + `invite_code_id` bigint DEFAULT NULL COMMENT '邀请码ID(关联agent_invite_code表,仅邀请好友类型使用)', + `link_identifier` varchar(200) DEFAULT NULL COMMENT '推广链接标识(加密,仅推广报告类型使用)', + `invite_code` varchar(50) DEFAULT NULL COMMENT '邀请码(仅邀请好友类型使用)', + `short_code` varchar(20) NOT NULL COMMENT '短链标识(6位随机字符串)', + `target_path` varchar(500) NOT NULL COMMENT '目标地址(前端传入,如:/agent/promotionInquire/xxx 或 /register?invite_code=xxx)', + `promotion_domain` varchar(200) NOT NULL COMMENT '推广域名(生成短链时使用的域名)', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT 0 COMMENT '删除状态:0=未删除,1=已删除', + `version` bigint NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_short_code` (`short_code`, `del_state`) COMMENT '短链标识唯一索引', + UNIQUE KEY `uk_link_id_type` ( + `link_id`, + `type`, + `del_state` + ) COMMENT '同一推广链接同一类型只能有一个有效短链', + UNIQUE KEY `uk_invite_code_id_type` ( + `invite_code_id`, + `type`, + `del_state` + ) COMMENT '同一邀请码同一类型只能有一个有效短链', + KEY `idx_link_identifier` (`link_identifier`) COMMENT '推广链接标识索引', + KEY `idx_invite_code` (`invite_code`) COMMENT '邀请码索引', + KEY `idx_type` (`type`) COMMENT '类型索引', + KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '代理短链表'; \ No newline at end of file diff --git a/deploy/sql/add_invite_code_usage_table.sql b/deploy/sql/add_invite_code_usage_table.sql new file mode 100644 index 0000000..59942b8 --- /dev/null +++ b/deploy/sql/add_invite_code_usage_table.sql @@ -0,0 +1,46 @@ +-- ============================================ +-- 邀请码使用历史表 +-- 说明:记录每次邀请码的使用情况,支持统计和查询 +-- +-- 功能说明: +-- 1. 记录每个邀请码每次使用的详细信息 +-- 2. 支持统计每个邀请码邀请了多少代理 +-- 3. 支持查询某个代理是通过哪个邀请码成为代理的 +-- 4. 保留完整的使用历史记录 +-- +-- 执行步骤: +-- 1. 执行此 SQL 创建表和添加字段 +-- 2. 使用 goctl 生成 Model:agent_invite_code_usage -> AgentInviteCodeUsageModel +-- 3. 在 servicecontext.go 中添加 AgentInviteCodeUsageModel +-- ============================================ +CREATE TABLE `agent_invite_code_usage` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `invite_code_id` bigint NOT NULL COMMENT '邀请码ID(关联agent_invite_code表)', + `code` varchar(50) NOT NULL COMMENT '邀请码(冗余字段,便于查询)', + `user_id` bigint NOT NULL COMMENT '使用用户ID', + `agent_id` bigint NOT NULL COMMENT '成为的代理ID', + `agent_level` tinyint NOT NULL COMMENT '代理等级:1=普通,2=黄金,3=钻石', + `used_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '使用时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT 0 COMMENT '删除状态:0=未删除,1=已删除', + `version` bigint NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)', + PRIMARY KEY (`id`), + KEY `idx_invite_code_id` (`invite_code_id`) COMMENT '关联邀请码ID', + KEY `idx_code` (`code`) COMMENT '邀请码索引', + KEY `idx_user_id` (`user_id`) COMMENT '用户ID索引', + KEY `idx_agent_id` (`agent_id`) COMMENT '代理ID索引', + KEY `idx_used_time` (`used_time`) COMMENT '使用时间索引', + KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引', + KEY `idx_code_time` (`code`, `used_time`) COMMENT '邀请码和使用时间复合索引' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '邀请码使用历史表'; + +-- ============================================ +-- 在agent表中添加invite_code_id字段(可选,便于直接查询代理是通过哪个邀请码成为的) +-- ============================================ +ALTER TABLE `agent` +ADD COLUMN `invite_code_id` bigint DEFAULT NULL COMMENT '通过哪个邀请码成为代理(关联agent_invite_code表)' AFTER `team_leader_id`; + +ALTER TABLE `agent` +ADD KEY `idx_invite_code_id` (`invite_code_id`) COMMENT '邀请码ID索引'; \ No newline at end of file diff --git a/deploy/sql/agent_config_migration.sql b/deploy/sql/agent_config_migration.sql new file mode 100644 index 0000000..673063c --- /dev/null +++ b/deploy/sql/agent_config_migration.sql @@ -0,0 +1,56 @@ +-- ============================================ +-- 代理配置表重构 SQL 脚本 +-- 执行顺序:按照下面的顺序依次执行 +-- ============================================ + +-- ============================================ +-- 步骤1:删除价格相关配置项 +-- 说明:价格配置已改为在产品配置表(agent_product_config)中按产品配置 +-- ============================================ +DELETE FROM `agent_config` WHERE `config_key` IN ( + 'base_price', + 'system_max_price', + 'price_threshold', + 'price_fee_rate' +); + +-- ============================================ +-- 步骤2:统一配置键命名 +-- 说明:修改配置键名称,使其与代码逻辑一致 +-- ============================================ + +-- 修改等级加成配置键 +UPDATE `agent_config` SET `config_key` = 'level_1_bonus' WHERE `config_key` = 'level_bonus_normal'; +UPDATE `agent_config` SET `config_key` = 'level_2_bonus' WHERE `config_key` = 'level_bonus_gold'; +UPDATE `agent_config` SET `config_key` = 'level_3_bonus' WHERE `config_key` = 'level_bonus_diamond'; + +-- 修改升级费用配置键 +UPDATE `agent_config` SET `config_key` = 'upgrade_to_gold_fee' WHERE `config_key` = 'upgrade_fee_normal_to_gold'; +UPDATE `agent_config` SET `config_key` = 'upgrade_to_diamond_fee' WHERE `config_key` = 'upgrade_fee_to_diamond'; + +-- 修改升级返佣配置键 +UPDATE `agent_config` SET `config_key` = 'upgrade_to_gold_rebate' WHERE `config_key` = 'upgrade_rebate_normal_to_gold'; +UPDATE `agent_config` SET `config_key` = 'upgrade_to_diamond_rebate' WHERE `config_key` = 'upgrade_rebate_to_diamond'; + +-- ============================================ +-- 步骤3:添加缺失的配置项 +-- 说明:添加免税额度配置项(如果不存在) +-- ============================================ +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +SELECT 'tax_exemption_amount', '0.00', 'tax', '提现免税额度(元,默认0)' +WHERE NOT EXISTS ( + SELECT 1 FROM `agent_config` WHERE `config_key` = 'tax_exemption_amount' +); + +-- ============================================ +-- 验证查询:检查配置项是否正确 +-- ============================================ +SELECT `config_key`, `config_value`, `config_type`, `description` +FROM `agent_config` +WHERE `del_state` = 0 +ORDER BY `config_type`, `config_key`; + +-- ============================================ +-- 执行完成后,请重新生成 agent_config 相关的 Model 代码 +-- ============================================ + diff --git a/deploy/sql/agent_freeze_task_migration.sql b/deploy/sql/agent_freeze_task_migration.sql new file mode 100644 index 0000000..7ef9ca0 --- /dev/null +++ b/deploy/sql/agent_freeze_task_migration.sql @@ -0,0 +1,32 @@ +-- ============================================ +-- 代理佣金冻结任务表 +-- 说明:用于记录需要解冻的佣金冻结任务,保证异步任务的一致性和持久化 +-- ============================================ + +CREATE TABLE `agent_freeze_task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `agent_id` bigint NOT NULL COMMENT '代理ID', + `order_id` bigint NOT NULL COMMENT '订单ID', + `commission_id` bigint NOT NULL COMMENT '佣金记录ID', + `freeze_amount` decimal(10, 2) NOT NULL COMMENT '冻结金额', + `order_price` decimal(10, 2) NOT NULL COMMENT '订单单价', + `freeze_ratio` decimal(5, 4) NOT NULL DEFAULT 0.1000 COMMENT '冻结比例(例如:0.1000表示10%)', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1=待解冻,2=已解冻,3=已取消', + `freeze_time` datetime NOT NULL COMMENT '冻结时间', + `unfreeze_time` datetime NOT NULL COMMENT '解冻时间(冻结时间+1个月)', + `actual_unfreeze_time` datetime DEFAULT NULL COMMENT '实际解冻时间', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT 0 COMMENT '删除状态:0=未删除,1=已删除', + `version` bigint NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)', + PRIMARY KEY (`id`), + KEY `idx_agent_id` (`agent_id`), + KEY `idx_order_id` (`order_id`), + KEY `idx_commission_id` (`commission_id`), + KEY `idx_status` (`status`), + KEY `idx_unfreeze_time` (`unfreeze_time`), + KEY `idx_agent_status` (`agent_id`, `status`), + KEY `idx_create_time` (`create_time`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '代理佣金冻结任务表'; \ No newline at end of file diff --git a/deploy/sql/remove_product_name_from_agent_product_config.sql b/deploy/sql/remove_product_name_from_agent_product_config.sql new file mode 100644 index 0000000..30f2950 --- /dev/null +++ b/deploy/sql/remove_product_name_from_agent_product_config.sql @@ -0,0 +1,25 @@ +-- ============================================ +-- 移除代理产品配置表中的 product_name 字段 +-- 说明:改为通过 product_id 关联查询 product 表获取产品名称 +-- 执行时间:2025-01-XX +-- ============================================ + +-- 删除 product_name 字段 +ALTER TABLE `agent_product_config` DROP COLUMN `product_name`; + +-- ============================================ +-- 验证查询:检查字段是否已删除 +-- ============================================ +DESC `agent_product_config`; + +-- ============================================ +-- 验证查询:通过关联查询获取产品名称 +-- ============================================ +SELECT apc.id, apc.product_id, p.product_name, apc.base_price, apc.system_max_price, apc.price_threshold, apc.price_fee_rate, apc.del_state, apc.create_time +FROM + `agent_product_config` apc + LEFT JOIN `product` p ON apc.product_id = p.id + AND p.del_state = 0 +WHERE + apc.del_state = 0 +ORDER BY apc.product_id; \ No newline at end of file diff --git a/deploy/sql/sync_agent_product_config.sql b/deploy/sql/sync_agent_product_config.sql new file mode 100644 index 0000000..d5e0c4d --- /dev/null +++ b/deploy/sql/sync_agent_product_config.sql @@ -0,0 +1,112 @@ +-- ============================================ +-- 同步产品表数据到代理产品配置表 +-- 说明:为现有产品创建对应的代理产品配置记录 +-- 执行时间:2025-01-XX +-- ============================================ + +-- 方式1:使用 INSERT IGNORE(如果记录已存在则忽略) +-- 注意:product_name 字段已移除,改为通过 product_id 关联查询 product 表获取 +INSERT IGNORE INTO + `agent_product_config` ( + `product_id`, + `base_price`, + `system_max_price`, + `price_threshold`, + `price_fee_rate`, + `del_state`, + `version` + ) +VALUES ( + 1, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ), + ( + 2, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ), + ( + 3, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ), + ( + 4, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ), + ( + 5, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ), + ( + 6, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ), + ( + 7, + 0.00, + 9999.99, + NULL, + NULL, + 0, + 0 + ); + +-- ============================================ +-- 方式2:使用 INSERT ... ON DUPLICATE KEY UPDATE(如果记录已存在则忽略) +-- 注意:product_name 字段已移除,不再需要更新 +-- ============================================ +/* +INSERT INTO `agent_product_config` +(`product_id`, `base_price`, `system_max_price`, `price_threshold`, `price_fee_rate`, `del_state`, `version`) +VALUES +(1, 0.00, 9999.99, NULL, NULL, 0, 0), +(2, 0.00, 9999.99, NULL, NULL, 0, 0), +(3, 0.00, 9999.99, NULL, NULL, 0, 0), +(4, 0.00, 9999.99, NULL, NULL, 0, 0), +(5, 0.00, 9999.99, NULL, NULL, 0, 0), +(6, 0.00, 9999.99, NULL, NULL, 0, 0), +(7, 0.00, 9999.99, NULL, NULL, 0, 0) +ON DUPLICATE KEY UPDATE +`product_id` = VALUES(`product_id`); +*/ + +-- ============================================ +-- 验证查询:检查同步结果 +-- ============================================ +SELECT apc.id, apc.product_id, p.product_name, apc.base_price, apc.system_max_price, apc.price_threshold, apc.price_fee_rate, apc.del_state, apc.create_time +FROM + `agent_product_config` apc + LEFT JOIN `product` p ON apc.product_id = p.id + AND p.del_state = 0 +WHERE + apc.del_state = 0 +ORDER BY apc.product_id; \ No newline at end of file diff --git a/distributeNormalAgentBonus配置项说明.md b/distributeNormalAgentBonus配置项说明.md new file mode 100644 index 0000000..c7beaa0 --- /dev/null +++ b/distributeNormalAgentBonus配置项说明.md @@ -0,0 +1,101 @@ +# distributeNormalAgentBonus 函数使用的配置项说明 + +## 函数位置 +`ycc-proxy-server/app/main/api/internal/service/agentService.go:254-478` + +## 使用的配置项列表 + +### 1. `direct_parent_amount_normal` +- **配置键**:`direct_parent_amount_normal` +- **类型**:浮点数(float64) +- **默认值**:2.0元 +- **使用位置**:第376行 +- **用途**:普通代理给直接上级普通代理的返佣金额 +- **使用场景**: + - 当直接上级是普通代理时使用 + - 无论后续层级有多少普通代理,这部分金额只给推广人的直接上级 +- **示例**:等级加成6元,给直接上级普通代理2元 + +### 2. `direct_parent_amount_gold` +- **配置键**:`direct_parent_amount_gold` +- **类型**:浮点数(float64) +- **默认值**:3.0元 +- **使用位置**:第326行 +- **用途**:普通代理给直接上级黄金代理的返佣金额 +- **使用场景**: + - 当直接上级是黄金代理时使用 + - 这部分金额给直接上级黄金代理,剩余部分继续向上分配给钻石上级 +- **示例**:等级加成6元,给直接上级黄金代理3元,剩余3元给钻石上级 + +### 3. `max_gold_rebate_amount` +- **配置键**:`max_gold_rebate_amount` +- **类型**:浮点数(float64) +- **默认值**:3.0元 +- **使用位置**:第492行 +- **用途**:普通代理给黄金上级的最大返佣金额 +- **使用场景**: + - 当直接上级是普通代理,且只有黄金上级(没有钻石上级)时使用 + - 即使剩余金额超过这个值,也只能给黄金上级最多这个金额,超出部分归平台 +- **示例**:剩余4元,最大限额3元,则给黄金3元,剩余1元归平台 + +### 4. `level_2_bonus` +- **配置键**:`level_2_bonus` +- **类型**:整数(int64),从配置读取时转换为浮点数计算 +- **默认值**:3元 +- **使用位置**:第438行(通过 `getLevelBonus(ctx, 2)` 调用) +- **用途**:黄金代理的等级加成 +- **使用场景**: + - 用于计算等级加成的差(普通等级加成 - 黄金等级加成) + - 当直接上级是普通代理,且上级链中有黄金和钻石时,用于计算给黄金的返佣金额 +- **计算逻辑**: + - 等级加成差 = 普通等级加成(6元)- 黄金等级加成(3元)= 3元 + - 给黄金的金额 = 等级加成差(3元)- 已给普通的部分(2元)= 1元 + +### 5. `level_1_bonus` +- **配置键**:`level_1_bonus` +- **类型**:整数(int64) +- **默认值**:6元 +- **使用位置**:通过 `amount` 参数传入(在 `AgentProcess` 中通过 `getLevelBonus(ctx, 1)` 获取) +- **用途**:普通代理的等级加成 +- **说明**: + - 不在 `distributeNormalAgentBonus` 函数中直接读取 + - 作为 `amount` 参数传入函数 + - 这是需要分配的总额 + +## 配置项使用总结表 + +| 配置键 | 默认值 | 使用场景 | 代码行号 | +|--------|--------|---------|---------| +| `direct_parent_amount_normal` | 2.0元 | 直接上级是普通代理时,给直接上级的返佣金额 | 376 | +| `direct_parent_amount_gold` | 3.0元 | 直接上级是黄金代理时,给直接上级的返佣金额 | 326 | +| `max_gold_rebate_amount` | 3.0元 | 只有黄金上级(无钻石)时,给黄金的最大返佣金额 | 492 | +| `level_2_bonus` | 3元 | 黄金代理等级加成,用于计算等级加成差 | 438 | +| `level_1_bonus` | 6元 | 普通代理等级加成,通过参数传入 | - | + +## 配置项在代码中的调用关系 + +``` +AgentProcess (处理订单) + ↓ + 调用 getLevelBonus(ctx, agent.Level) 获取普通代理等级加成 + ↓ + 传入 distributeNormalAgentBonus(amount=6元, ...) + ↓ + 在函数内使用: + 1. getRebateConfigFloat("direct_parent_amount_normal", 2.0) # 第376行 + 2. getRebateConfigFloat("direct_parent_amount_gold", 3.0) # 第326行 + 3. getRebateConfigFloat("max_gold_rebate_amount", 3.0) # 第492行 + 4. getLevelBonus(ctx, 2) -> 读取 level_2_bonus # 第438行 +``` + +## 注意事项 + +1. **所有配置项都支持动态配置**:如果配置不存在,会使用默认值 +2. **配置读取失败会记录日志**:确保配置问题时可以追踪 +3. **配置值的类型转换**: + - 返佣配置直接使用浮点数(支持小数) + - 等级加成从配置读取时为整数,计算时转换为浮点数 +4. **配置项命名规范**: + - 返佣配置使用描述性名称:`direct_parent_amount_normal`、`direct_parent_amount_gold`、`max_gold_rebate_amount` + - 等级加成配置使用数字后缀:`level_1_bonus`、`level_2_bonus` + diff --git a/代理系统测试用例清单.md b/代理系统测试用例清单.md new file mode 100644 index 0000000..fb0890c --- /dev/null +++ b/代理系统测试用例清单.md @@ -0,0 +1,448 @@ +# 代理系统测试用例清单 + +## 一、邀请下级代理测试 + +### 1.1 钻石代理邀请下级 +- [ ] **钻石邀请普通代理** + - 预期:建立上下级关系,普通代理的 team_leader_id 指向钻石代理 + - 验证:关系类型为直接关系,团队首领正确 + +- [ ] **钻石邀请黄金代理** + - 预期:建立上下级关系,黄金代理的 team_leader_id 指向钻石代理 + - 验证:关系类型为直接关系,团队首领正确 + +- [ ] **钻石邀请钻石代理(不允许)** + - 预期:拒绝,提示"代理等级不能高于上级代理" + - 验证:邀请失败,无关系建立 + +### 1.2 黄金代理邀请下级 +- [ ] **黄金邀请普通代理** + - 预期:建立上下级关系,普通代理的 team_leader_id 指向上级钻石代理 + - 验证:关系类型为直接关系,团队首领正确(向上查找钻石) + +- [ ] **黄金邀请黄金代理(不允许)** + - 预期:拒绝,提示"代理等级不能高于上级代理" + - 验证:邀请失败,无关系建立 + +- [ ] **黄金邀请钻石代理(不允许)** + - 预期:拒绝,提示"代理等级不能高于上级代理" + - 验证:邀请失败,无关系建立 + +### 1.3 普通代理邀请下级 +- [ ] **普通邀请普通代理** + - 预期:建立上下级关系,普通代理的 team_leader_id 向上查找钻石/黄金 + - 验证:关系类型为直接关系,团队首领正确 + +- [ ] **普通邀请黄金代理(不允许)** + - 预期:拒绝,提示"代理等级不能高于上级代理" + - 验证:邀请失败,无关系建立 + +- [ ] **普通邀请钻石代理(不允许)** + - 预期:拒绝,提示"代理等级不能高于上级代理" + - 验证:邀请失败,无关系建立 + +### 1.4 多层级邀请测试 +- [ ] **钻石 → 黄金 → 普通(3层)** + - 预期:3层关系链,所有代理的 team_leader_id 指向钻石 + - 验证:关系链正确,团队首领一致 + +- [ ] **钻石 → 普通 → 普通 → 普通(4层)** + - 预期:4层关系链,所有代理的 team_leader_id 指向钻石 + - 验证:关系链正确,团队首领一致 + +- [ ] **钻石 → 普通 → 普通 → 普通 → 普通(5层)** + - 预期:5层关系链,所有代理的 team_leader_id 指向钻石 + - 验证:关系链正确,团队首领一致 + +--- + +## 二、推广报告收益测试(等级加成返佣) + +### 2.1 钻石代理推广报告 +- [ ] **钻石代理自己推广的报告** + - 等级加成:0元 + - 预期:无等级加成返佣,全部收益归自己 + - 验证:agent_rebate 表无记录,代理收益 = 设定价格 - 基础底价 - 提价成本 + +### 2.2 黄金代理推广报告 +- [ ] **黄金代理(上级是钻石)推广报告** + - 等级加成:3元 + - 预期:3元全部返佣给钻石上级 + - 验证:agent_rebate 表记录正确,钻石上级钱包增加3元 + +- [ ] **黄金代理(无上级/上级不是钻石)推广报告** + - 等级加成:3元 + - 预期:返佣归平台(异常情况) + - 验证:agent_rebate 表无记录 + +### 2.3 普通代理推广报告(等级加成6元) + +#### 2.3.1 直接上级是钻石 +- [ ] **普通代理(上级是钻石)推广报告** + - 等级加成:6元 + - 预期:6元全部返佣给钻石上级 + - 验证:agent_rebate 表记录正确,钻石上级钱包增加6元 + +#### 2.3.2 直接上级是黄金 +- [ ] **普通代理(上级是黄金,黄金上级是钻石)推广报告** + - 等级加成:6元 + - 预期: + - 3元给黄金上级(配置:normal_to_gold_rebate) + - 3元给钻石上级(上上级) + - 验证:agent_rebate 表有2条记录,金额分配正确 + +- [ ] **普通代理(上级是黄金,无钻石上级)推广报告** + - 等级加成:6元 + - 预期:3元给黄金上级,剩余3元归平台 + - 验证:agent_rebate 表只有1条记录(3元给黄金),剩余归平台 + +#### 2.3.3 直接上级是普通(多层普通代理) +- [ ] **普通 → 普通 → 钻石(3层)** + - 等级加成:6元 + - 预期: + - 2元给直接上级普通(配置:normal_to_normal_rebate) + - 4元给钻石上级(跳过中间普通,直接给钻石) + - 验证:agent_rebate 表有2条记录,金额分配正确 + +- [ ] **普通 → 普通 → 普通 → 钻石(4层)** + - 等级加成:6元 + - 预期: + - 2元给直接上级普通 + - 4元给钻石上级(跳过中间所有普通代理) + - 验证:agent_rebate 表有2条记录,金额分配正确 + +- [ ] **普通 → 普通 → 黄金(无钻石,3层)** + - 等级加成:6元 + - 预期: + - 2元给直接上级普通 + - 3元给黄金上级(配置:normal_to_gold_rebate_max) + - 1元归平台(超出部分) + - 验证:agent_rebate 表有2条记录,金额分配正确,剩余归平台 + +- [ ] **普通 → 普通 → 普通(全部是普通,无钻石/黄金)** + - 等级加成:6元 + - 预期: + - 2元给直接上级普通 + - 4元归平台(无钻石/黄金上级) + - 验证:agent_rebate 表只有1条记录(2元),剩余归平台 + +--- + +## 三、升级代理收益测试 + +### 3.1 自主付费升级 + +#### 3.1.1 普通 → 黄金(199元) +- [ ] **普通代理升级为黄金(上级是钻石)** + - 升级费用:199元 + - 返佣:139元给原直接上级 + - 预期: + - 原直接上级(钻石)钱包增加139元 + - 升级后不脱离关系(钻石 > 黄金) + - 仍属于原团队 + - 验证:升级成功,返佣记录在 agent_upgrade 表,钱包余额正确 + +- [ ] **普通代理升级为黄金(上级是黄金)** + - 升级费用:199元 + - 返佣:139元给原直接上级(黄金) + - 预期: + - 原直接上级(黄金)钱包增加139元 + - 升级后脱离关系(同级不能作为上下级) + - 保留团队关系(向上查找钻石) + - 验证:升级成功,关系脱离,团队首领正确 + +- [ ] **普通代理升级为黄金(上级是普通)** + - 升级费用:199元 + - 返佣:139元给原直接上级(普通) + - 预期: + - 原直接上级(普通)钱包增加139元 + - 升级后脱离关系(下级等级高于上级) + - 保留团队关系(向上查找钻石/黄金) + - 验证:升级成功,关系脱离,团队首领正确 + +#### 3.1.2 普通 → 钻石(980元) +- [ ] **普通代理升级为钻石(上级是钻石)** + - 升级费用:980元 + - 返佣:680元给原直接上级(钻石) + - 预期: + - 原直接上级(钻石)钱包增加680元 + - 升级后脱离关系(同级不能作为上下级) + - 独立成为新团队,team_leader_id = 自己 + - 所有下级跟随到新团队 + - 验证:升级成功,独立成团队,下级团队首领更新 + +- [ ] **普通代理升级为钻石(上级是黄金)** + - 升级费用:980元 + - 返佣:680元给原直接上级(黄金) + - 预期: + - 原直接上级(黄金)钱包增加680元 + - 升级后脱离关系(下级等级高于上级) + - 独立成为新团队,team_leader_id = 自己 + - 所有下级跟随到新团队 + - 验证:升级成功,独立成团队,下级团队首领更新 + +- [ ] **普通代理升级为钻石(上级是普通)** + - 升级费用:980元 + - 返佣:680元给原直接上级(普通) + - 预期: + - 原直接上级(普通)钱包增加680元 + - 升级后脱离关系(下级等级高于上级) + - 独立成为新团队,team_leader_id = 自己 + - 所有下级跟随到新团队 + - 验证:升级成功,独立成团队,下级团队首领更新 + +#### 3.1.3 黄金 → 钻石(980元) +- [ ] **黄金代理升级为钻石(上级是钻石)** + - 升级费用:980元 + - 返佣:680元给原直接上级(钻石) + - 预期: + - 原直接上级(钻石)钱包增加680元 + - 升级后脱离关系(同级不能作为上下级) + - 独立成为新团队,team_leader_id = 自己 + - 所有下级跟随到新团队 + - 验证:升级成功,独立成团队,下级团队首领更新 + +- [ ] **黄金代理升级为钻石(无上级)** + - 升级费用:980元 + - 返佣:无 + - 预期: + - 独立成为新团队,team_leader_id = 自己 + - 所有下级跟随到新团队 + - 验证:升级成功,独立成团队,下级团队首领更新 + +### 3.2 钻石升级下级(免费) +- [ ] **钻石升级下级(普通 → 黄金)** + - 升级费用:免费 + - 返佣:无 + - 预期: + - 被升级代理无需付费 + - 升级后根据原上级等级决定是否脱离关系 + - 保留团队关系 + - 验证:升级成功,费用为0,关系处理正确 + +--- + +## 四、升级后团队转移测试 + +### 4.1 普通 → 黄金升级(保留团队) + +#### 4.1.1 上级是钻石(不脱离) +- [ ] **普通 → 黄金(上级是钻石),有下级** + - 预期: + - 不脱离关系 + - 保留原团队(team_leader_id 不变) + - 所有下级(直接+间接)的 team_leader_id 不变 + - 验证:关系保留,所有下级团队首领不变 + +#### 4.1.2 上级是黄金(脱离关系) +- [ ] **普通 → 黄金(上级是黄金),有下级** + - 预期: + - 脱离直接上下级关系 + - 保留团队关系(向上查找钻石) + - 所有下级(直接+间接)的 team_leader_id 不变 + - 验证:关系脱离(RelationType=2),所有下级团队首领不变 + +#### 4.1.3 上级是普通(脱离关系) +- [ ] **普通 → 黄金(上级是普通),有下级** + - 预期: + - 脱离直接上下级关系 + - 保留团队关系(向上查找钻石/黄金) + - 所有下级(直接+间接)的 team_leader_id 不变 + - 验证:关系脱离,所有下级团队首领不变 + +### 4.2 升级为钻石(独立成新团队) + +#### 4.2.1 普通 → 钻石 +- [ ] **普通 → 钻石(上级是钻石),有下级** + - 预期: + - 脱离关系 + - 独立成新团队(team_leader_id = 自己) + - 所有直接下级的 team_leader_id 更新为自己 + - 所有间接下级的 team_leader_id 更新为自己(递归) + - 验证: + - 升级代理的 team_leader_id = 自己 + - 所有下级(直接+间接)的 team_leader_id = 升级代理ID + - 下级数量统计正确 + +- [ ] **普通 → 钻石(上级是黄金),有下级(2层)** + - 预期: + - 脱离关系 + - 独立成新团队 + - 直接下级跟随 + - 间接下级跟随 + - 验证:所有下级团队首领更新为新钻石 + +- [ ] **普通 → 钻石(上级是普通),有下级(3层以上)** + - 预期: + - 脱离关系 + - 独立成新团队 + - 所有层级的下级都跟随 + - 验证:所有下级团队首领更新为新钻石 + +#### 4.2.2 黄金 → 钻石 +- [ ] **黄金 → 钻石(上级是钻石),有下级** + - 预期: + - 脱离关系 + - 独立成新团队 + - 所有下级跟随 + - 验证:所有下级团队首领更新为新钻石 + +### 4.3 复杂团队转移场景 + +#### 4.3.1 多层级团队 +- [ ] **钻石A → 黄金B → 普通C → 普通D,B升级为钻石** + - 预期: + - B独立成新团队 + - C和D的 team_leader_id 更新为B + - A的团队:只剩自己 + - B的团队:B、C、D + - 验证:团队划分正确,关系链正确 + +#### 4.3.2 跨团队转移 +- [ ] **钻石A → 普通B → 普通C,C升级为钻石** + - 预期: + - C独立成新团队 + - C无下级,团队只有C自己 + - A的团队:A、B + - 验证:团队划分正确 + +#### 4.3.3 深度层级转移 +- [ ] **钻石A → 普通B → 普通C → 普通D → 普通E,C升级为钻石** + - 预期: + - C独立成新团队 + - D和E的 team_leader_id 更新为C + - A的团队:A、B + - C的团队:C、D、E + - 验证:团队划分正确,所有层级更新正确 + +--- + +## 五、综合测试场景 + +### 5.1 完整业务流程 +- [ ] **创建团队 → 邀请下级 → 推广报告 → 收益分配 → 升级 → 团队转移** + - 步骤: + 1. 创建钻石代理A + 2. A邀请黄金代理B + 3. B邀请普通代理C + 4. C邀请普通代理D + 5. D推广报告,验证收益分配 + 6. C升级为黄金,验证关系变化 + 7. B升级为钻石,验证团队转移 + - 验证:每个步骤的数据正确 + +### 5.2 收益统计测试 +- [ ] **查询代理收益统计(包含佣金和返佣)** + - 验证:agent_wallet 表的 Balance 和 TotalEarnings 正确 + +- [ ] **查询下级列表和统计** + - 验证:下级数量、团队规模统计正确 + +### 5.3 边界情况测试 +- [ ] **钻石代理无下级时升级(边界情况)** + - 验证:独立成团队,team_leader_id = 自己 + +- [ ] **普通代理无上级时升级** + - 验证:独立成团队,无返佣 + +- [ ] **多层普通代理链,无钻石/黄金上级** + - 验证:收益分配正确(部分归平台) + +--- + +## 六、数据验证检查点 + +### 6.1 关系表验证 +- [ ] agent_relation 表的关系类型正确(1=直接关系,2=已脱离) +- [ ] 脱离关系时,DetachReason 和 DetachTime 正确记录 + +### 6.2 钱包验证 +- [ ] agent_wallet 表的 Balance(可用余额)正确 +- [ ] agent_wallet 表的 FrozenBalance(冻结余额)正确(如有) +- [ ] agent_wallet 表的 TotalEarnings(累计收益)正确 + +### 6.3 返佣记录验证 +- [ ] agent_rebate 表的记录完整(推广报告返佣) +- [ ] agent_upgrade 表的返佣记录正确(升级返佣) +- [ ] 返佣金额计算正确 + +### 6.4 团队验证 +- [ ] agent 表的 team_leader_id 正确指向钻石代理 +- [ ] 升级后所有下级 team_leader_id 更新正确 + +### 6.5 订单和佣金验证 +- [ ] agent_order 表记录完整 +- [ ] agent_commission 表记录完整 +- [ ] 佣金金额计算正确 + +--- + +## 七、测试数据准备建议 + +### 7.1 创建测试代理账号 +建议准备以下测试账号(可用不同手机号): + +1. **钻石代理**: + - 钻石A(团队首领,无上级) + - 钻石B(团队首领,无上级) + +2. **黄金代理**: + - 黄金A(上级:钻石A) + - 黄金B(上级:钻石A) + - 黄金C(上级:钻石B) + +3. **普通代理**: + - 普通A(上级:钻石A) + - 普通B(上级:黄金A) + - 普通C(上级:普通B) + - 普通D(上级:普通C) + - 普通E(上级:普通D) + +### 7.2 测试产品配置 +- [ ] 确保有测试产品配置(agent_product_config 表) +- [ ] 配置基础底价、提价阈值、提价手续费比例 + +### 7.3 测试返佣配置 +- [ ] normal_to_normal_rebate(默认2元) +- [ ] normal_to_gold_rebate(默认3元) +- [ ] normal_to_gold_rebate_max(默认3元) + +--- + +## 八、测试执行顺序建议 + +1. **第一阶段**:基础功能测试 + - 邀请下级(各种组合) + - 验证关系建立 + - 验证团队首领 + +2. **第二阶段**:收益分配测试 + - 推广报告 + - 收益计算 + - 返佣分配 + +3. **第三阶段**:升级功能测试 + - 自主付费升级 + - 钻石升级下级 + - 升级返佣 + +4. **第四阶段**:团队转移测试 + - 普通→黄金升级(保留团队) + - 升级为钻石(独立成团队) + - 复杂场景测试 + +5. **第五阶段**:综合测试 + - 完整业务流程 + - 边界情况 + - 数据一致性验证 + +--- + +## 注意事项 + +1. **开发环境**:测试时确保使用开发环境(ENV=development),可跳过验证码校验 +2. **数据清理**:每次测试后建议清理测试数据,避免相互影响 +3. **事务验证**:注意验证事务的一致性,确保要么全部成功,要么全部回滚 +4. **并发测试**:如有需要,可进行并发场景测试(多代理同时升级等) +5. **日志记录**:测试过程中查看日志,确保业务流程正确 + diff --git a/代理配置表分析和优化建议.md b/代理配置表分析和优化建议.md new file mode 100644 index 0000000..168ba80 --- /dev/null +++ b/代理配置表分析和优化建议.md @@ -0,0 +1,774 @@ +# 代理配置表分析和优化建议 + +## 一、当前配置表结构分析 + +### 1.1 数据库表结构 + +**表名**: `agent_config` + +```sql +CREATE TABLE `agent_config` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `config_key` varchar(100) NOT NULL COMMENT '配置键(唯一)', + `config_value` varchar(500) NOT NULL COMMENT '配置值', + `config_type` varchar(50) NOT NULL COMMENT '配置类型:price=价格,bonus=等级加成,upgrade=升级费用,rebate=返佣,tax=税费', + `description` varchar(500) DEFAULT NULL COMMENT '配置描述', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `delete_time` datetime DEFAULT NULL, + `del_state` tinyint NOT NULL DEFAULT 0, + `version` bigint NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_config_key` (`config_key`), + KEY `idx_config_type` (`config_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 1.2 配置表设计评估 + +**优点**: +✅ **键值对存储灵活**:使用 `config_key` 和 `config_value` 的键值对模式,易于扩展新配置项 +✅ **类型分类清晰**:`config_type` 字段将配置分为 price、bonus、upgrade、rebate、tax 等类型,便于管理 +✅ **唯一索引合理**:`config_key` 的唯一索引确保配置键不重复 +✅ **版本控制**:包含 `version` 字段支持乐观锁,适合配置更新场景 +✅ **软删除支持**:支持软删除,保留历史配置记录 + +**缺点/问题**: +❌ **配置值类型限制**:`config_value` 为 `varchar(500)`,所有值都存储为字符串,需要在使用时转换 +❌ **缺少验证机制**:数据库层面无法验证配置值的格式和范围 +❌ **配置值长度限制**:对于复杂配置(如JSON对象),500字符可能不够 + +--- + +## 二、当前配置项清单 + +### 2.1 SQL初始化脚本中的配置项 + +根据 `agent_system_migration.sql` 文件,当前初始化的配置项: + +| 配置键 | 配置值 | 类型 | 说明 | +| ------------------------------- | ------- | ------- | ------------------------ | +| `base_price` | 0.00 | price | 系统基础底价 | +| `system_max_price` | 9999.99 | price | 系统价格上限 | +| `price_threshold` | 0.00 | price | 提价标准阈值 | +| `price_fee_rate` | 0.0000 | price | 提价手续费比例 | +| `level_bonus_normal` | 6.00 | bonus | 普通代理等级加成 | +| `level_bonus_gold` | 3.00 | bonus | 黄金代理等级加成 | +| `level_bonus_diamond` | 0.00 | bonus | 钻石代理等级加成 | +| `upgrade_fee_normal_to_gold` | 199.00 | upgrade | 普通→黄金升级费用 | +| `upgrade_fee_to_diamond` | 980.00 | upgrade | 升级为钻石费用 | +| `upgrade_rebate_normal_to_gold` | 139.00 | upgrade | 普通→黄金返佣金额 | +| `upgrade_rebate_to_diamond` | 680.00 | upgrade | 升级为钻石返佣金额 | +| `direct_parent_amount_diamond` | 6.00 | rebate | 直接上级是钻石的返佣金额 | +| `direct_parent_amount_gold` | 3.00 | rebate | 直接上级是黄金的返佣金额 | +| `direct_parent_amount_normal` | 2.00 | rebate | 直接上级是普通的返佣金额 | +| `max_gold_rebate_amount` | 3.00 | rebate | 黄金代理最大返佣金额 | +| `tax_rate` | 0.0600 | tax | 提现税率(6%) | + +**共15个配置项** + +### 2.2 代码中使用的配置键 + +#### 2.2.1 AgentService 中使用的配置键 + +- `base_price` - 基础底价 ✅ +- `price_threshold` - 提价标准阈值 ✅ +- `price_fee_rate` - 提价手续费比例 ✅ +- `level_bonus` - **硬编码,未从配置表读取** ❌ + +#### 2.2.2 AdminGetAgentConfigLogic 中使用的配置键 + +- `level_1_bonus` - 普通代理等级加成 ❌ **配置键不匹配** +- `level_2_bonus` - 黄金代理等级加成 ❌ **配置键不匹配** +- `level_3_bonus` - 钻石代理等级加成 ❌ **配置键不匹配** +- `upgrade_to_gold_fee` - 升级为黄金费用 ❌ **配置键不匹配** +- `upgrade_to_diamond_fee` - 升级为钻石费用 ❌ **配置键不匹配** +- `upgrade_to_gold_rebate` - 升级为黄金返佣 ❌ **配置键不匹配** +- `upgrade_to_diamond_rebate` - 升级为钻石返佣 ❌ **配置键不匹配** +- `tax_rate` - 税率 ✅ +- `tax_exemption_amount` - 免税额度 ❌ **配置项缺失** + +--- + +## 三、发现的问题 + +### 3.1 🔴 严重问题:价格配置不应在系统配置表中 + +**问题描述**: +价格相关配置(底价、上限、提价阈值、手续费比例)应该按产品配置,而不是全局系统配置。当前这些配置同时存在于 `agent_config` 和 `agent_product_config` 表中,但代码只从系统配置表读取,没有实现产品级别的配置覆盖。 + +**问题影响**: +- ❌ 不同产品无法设置不同的底价和价格策略 +- ❌ 代码中只读取系统配置,忽略了产品配置(`AgentService.AgentProcess`、`PaymentLogic`) +- ❌ 产品配置表虽然存在,但在订单处理逻辑中未被使用 + +**应该移除的系统配置项**: +- `base_price` - 应该只在产品配置表中 +- `system_max_price` - 应该只在产品配置表中 +- `price_threshold` - 应该只在产品配置表中(可选) +- `price_fee_rate` - 应该只在产品配置表中(可选) + +**正确的设计**: +- ✅ 所有价格相关配置都应该在 `agent_product_config` 表中按产品配置 +- ✅ 系统配置表只保留全局配置(等级加成、升级费用、税费等) +- ✅ 代码应该优先从产品配置读取,如果产品未配置,则使用默认值 + +### 3.2 🔴 严重问题:配置键命名不一致 + +**问题描述**: +SQL初始化脚本和代码逻辑中使用的配置键命名不一致,导致配置无法正确读取。 + +**具体情况**: + +| 配置项 | SQL初始化脚本 | 代码逻辑期望 | 状态 | +| ---------------- | ------------------------------- | --------------------------- | -------- | +| 普通代理等级加成 | `level_bonus_normal` | `level_1_bonus` | ❌ 不匹配 | +| 黄金代理等级加成 | `level_bonus_gold` | `level_2_bonus` | ❌ 不匹配 | +| 钻石代理等级加成 | `level_bonus_diamond` | `level_3_bonus` | ❌ 不匹配 | +| 升级为黄金费用 | `upgrade_fee_normal_to_gold` | `upgrade_to_gold_fee` | ❌ 不匹配 | +| 升级为钻石费用 | `upgrade_fee_to_diamond` | `upgrade_to_diamond_fee` | ❌ 不匹配 | +| 升级为黄金返佣 | `upgrade_rebate_normal_to_gold` | `upgrade_to_gold_rebate` | ❌ 不匹配 | +| 升级为钻石返佣 | `upgrade_rebate_to_diamond` | `upgrade_to_diamond_rebate` | ❌ 不匹配 | + +**影响**: +- 后台管理系统无法正确读取和显示配置值 +- 配置更新可能无法正常工作 + +### 3.3 🔴 严重问题:代码中硬编码等级加成 + +**问题位置**: +```go +// agentService.go:144-155 +func (s *AgentService) getLevelBonus(level int64) int64 { + switch level { + case 1: // 普通 + return 6 // ❌ 硬编码,应该从配置表读取 + case 2: // 黄金 + return 3 // ❌ 硬编码,应该从配置表读取 + case 3: // 钻石 + return 0 // ❌ 硬编码,应该从配置表读取 + default: + return 0 + } +} +``` + +**问题影响**: +- 等级加成值无法通过后台配置动态调整 +- 必须修改代码才能更改等级加成 +- 违反了配置化的设计原则 + +### 3.4 🟡 中等问题:缺少配置项 + +**缺失的配置项**: +1. `tax_exemption_amount` - 免税额度(前端接口需要,但SQL初始化脚本中未包含) +2. `upgrade_fee_gold_to_diamond` - 黄金→钻石升级费用(如果独立配置) + +### 3.5 🟡 中等问题:配置键命名规范不统一 + +**当前问题**: +- 部分使用下划线分隔:`level_bonus_normal` +- 部分使用数字后缀:`level_1_bonus` +- 部分使用驼峰式:`upgrade_fee_normal_to_gold` + +**建议**:统一命名规范 + +### 3.6 🟢 轻微问题:配置值存储方式 + +**当前方式**:所有配置值以字符串形式存储,需要在使用时转换 +- `strconv.ParseFloat()` 转换为浮点数 +- `strconv.ParseInt()` 转换为整数 + +**影响**:每次读取都需要类型转换,性能影响较小,但容易出错 + +--- + +## 四、配置使用情况分析 + +### 4.1 实际使用的配置项 + +**在业务逻辑中使用的配置**: +1. ✅ `base_price` - 订单处理时使用 +2. ✅ `price_threshold` - 计算提价成本时使用 +3. ✅ `price_fee_rate` - 计算提价成本时使用 +4. ❌ `level_bonus_*` - **未使用,代码中硬编码** + +**在后台管理中使用的配置**: +1. ❌ `level_1_bonus` - 读取配置(但键名不匹配) +2. ❌ `level_2_bonus` - 读取配置(但键名不匹配) +3. ❌ `level_3_bonus` - 读取配置(但键名不匹配) +4. ❌ `upgrade_to_gold_fee` - 读取配置(但键名不匹配) +5. ❌ `upgrade_to_diamond_fee` - 读取配置(但键名不匹配) +6. ❌ `upgrade_to_gold_rebate` - 读取配置(但键名不匹配) +7. ❌ `upgrade_to_diamond_rebate` - 读取配置(但键名不匹配) +8. ✅ `tax_rate` - 读取配置 +9. ❌ `tax_exemption_amount` - 读取配置(但配置项缺失) + +### 4.2 未使用的配置项 + +以下配置项在SQL中初始化了,但在代码中**似乎未使用**: +- `direct_parent_amount_diamond` - 直接上级是钻石的返佣金额 +- `direct_parent_amount_gold` - 直接上级是黄金的返佣金额 +- `direct_parent_amount_normal` - 直接上级是普通的返佣金额 +- `max_gold_rebate_amount` - 黄金代理最大返佣金额 + +**说明**:这些配置项可能被硬编码在 `distributeNormalAgentBonus` 等函数中,需要确认是否需要配置化。 + +--- + +## 五、优化建议 + +### 5.1 🔴 立即修复:移除系统配置表中的价格配置 + +**问题**: +价格相关配置(`base_price`、`system_max_price`、`price_threshold`、`price_fee_rate`)应该完全由产品配置表管理,系统配置表中不应该存在这些配置。 + +**修复步骤**: + +1. **从系统配置表中删除价格相关配置**: + +```sql +-- 删除系统配置表中的价格相关配置 +DELETE FROM `agent_config` WHERE `config_key` IN ( + 'base_price', + 'system_max_price', + 'price_threshold', + 'price_fee_rate' +); +``` + +2. **修改订单处理逻辑,从产品配置表读取**: + +需要修改的文件: +- `app/main/api/internal/service/agentService.go` - `AgentProcess` 方法 +- `app/main/api/internal/logic/pay/paymentlogic.go` - 创建订单时的价格计算 + +**示例代码修改**(`agentService.go`): + +```go +// AgentProcess 处理代理订单(新系统) +func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) error { + // ... 前面的代码不变 ... + + // 4. 获取产品配置(优先使用产品配置,如果不存在则使用默认值) + productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId) + } + + // 使用产品配置的底价,如果产品未配置则使用默认值0 + basePrice := 0.0 + if productConfig != nil { + basePrice = productConfig.BasePrice + } + + // ... 使用basePrice计算实际底价 ... + + // 6.2 计算提价成本(使用产品配置) + priceThreshold := 0.0 + priceFeeRate := 0.0 + if productConfig != nil { + if productConfig.PriceThreshold.Valid { + priceThreshold = productConfig.PriceThreshold.Float64 + } + if productConfig.PriceFeeRate.Valid { + priceFeeRate = productConfig.PriceFeeRate.Float64 + } + } + priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate) + + // ... 后续代码 ... +} +``` + +3. **更新后台管理系统**: + +- 移除系统配置页面中的价格相关配置项 +- 确保产品配置页面可以正确配置这些价格参数 + +### 5.2 🔴 立即修复:统一配置键命名 + +**方案一:修改SQL初始化脚本,使用代码期望的键名** + +```sql +-- 修改等级加成配置键 +UPDATE `agent_config` SET `config_key` = 'level_1_bonus' WHERE `config_key` = 'level_bonus_normal'; +UPDATE `agent_config` SET `config_key` = 'level_2_bonus' WHERE `config_key` = 'level_bonus_gold'; +UPDATE `agent_config` SET `config_key` = 'level_3_bonus' WHERE `config_key` = 'level_bonus_diamond'; + +-- 修改升级费用配置键 +UPDATE `agent_config` SET `config_key` = 'upgrade_to_gold_fee' WHERE `config_key` = 'upgrade_fee_normal_to_gold'; +UPDATE `agent_config` SET `config_key` = 'upgrade_to_diamond_fee' WHERE `config_key` = 'upgrade_fee_to_diamond'; + +-- 修改升级返佣配置键 +UPDATE `agent_config` SET `config_key` = 'upgrade_to_gold_rebate' WHERE `config_key` = 'upgrade_rebate_normal_to_gold'; +UPDATE `agent_config` SET `config_key` = 'upgrade_to_diamond_rebate' WHERE `config_key` = 'upgrade_rebate_to_diamond'; +``` + +**方案二:修改代码逻辑,使用SQL中的键名** + +需要修改的文件: +- `app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go` +- `app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go` + +**推荐方案一**,因为: +- 代码中的命名更清晰(`level_1_bonus` 比 `level_bonus_normal` 更直观) +- 数字后缀与等级数字(1/2/3)对应,易于理解 + +### 5.3 🔴 立即修复:从配置表读取等级加成 + +**修改 `agentService.go` 中的 `getLevelBonus` 函数**: + +```go +// getLevelBonus 获取等级加成(从配置表读取) +func (s *AgentService) getLevelBonus(ctx context.Context, level int64) (int64, error) { + var configKey string + switch level { + case 1: + configKey = "level_1_bonus" + case 2: + configKey = "level_2_bonus" + case 3: + configKey = "level_3_bonus" + default: + return 0, nil + } + + bonus, err := s.getConfigFloat(ctx, configKey) + if err != nil { + // 配置不存在时返回默认值 + switch level { + case 1: + return 6, nil + case 2: + return 3, nil + case 3: + return 0, nil + } + return 0, nil + } + return int64(bonus), nil +} +``` + +**修改调用处**: +```go +// agentService.go:108 +levelBonus, err := s.getLevelBonus(ctx, agent.Level) +if err != nil { + return errors.Wrapf(err, "获取等级加成配置失败") +} +actualBasePrice := basePrice + float64(levelBonus) +``` + +### 5.4 🟡 补充缺失的配置项 + +**添加 `tax_exemption_amount` 配置**: + +```sql +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('tax_exemption_amount', '0.00', 'tax', '提现免税额度(元,默认0)'); +``` + +### 5.5 🟡 规范化配置键命名 + +**建议统一使用以下命名规范**: + +``` +{类型}_{编号/级别}_{属性} +``` + +**示例**: +- ✅ `level_1_bonus` - 等级1的加成 +- ✅ `level_2_bonus` - 等级2的加成 +- ✅ `level_3_bonus` - 等级3的加成 +- ✅ `upgrade_to_gold_fee` - 升级到黄金的费用 +- ✅ `upgrade_to_diamond_fee` - 升级到钻石的费用 +- ✅ `upgrade_to_gold_rebate` - 升级到黄金的返佣 +- ✅ `upgrade_to_diamond_rebate` - 升级到钻石的返佣 + +### 5.6 🟢 优化建议:添加配置验证 + +**在更新配置时添加验证逻辑**: + +```go +// 验证配置值的合理性 +func validateConfigValue(key string, value float64) error { + switch key { + case "tax_rate": + if value < 0 || value > 1 { + return errors.New("税率必须在0-1之间") + } + case "price_fee_rate": + if value < 0 || value > 1 { + return errors.New("提价费率必须在0-1之间") + } + case "base_price", "system_max_price": + if value < 0 { + return errors.New("价格配置不能为负数") + } + } + return nil +} +``` + +--- + +## 六、配置项完整清单(修正后) + +### 6.1 价格相关配置(应移除,改为产品配置) + +**⚠️ 重要说明**:以下配置项应该从系统配置表(`agent_config`)中移除,改为在产品配置表(`agent_product_config`)中按产品配置。 + +| 配置键 | 说明 | 配置位置 | +| ---------------------- | ----------------------------- | ----------------------------- | +| ~~`base_price`~~ | ~~系统基础底价~~ | ✅ 应在 `agent_product_config` | +| ~~`system_max_price`~~ | ~~系统价格上限~~ | ✅ 应在 `agent_product_config` | +| ~~`price_threshold`~~ | ~~提价标准阈值~~ | ✅ 应在 `agent_product_config` | +| ~~`price_fee_rate`~~ | ~~提价手续费比例(0-1之间)~~ | ✅ 应在 `agent_product_config` | + +**产品配置表结构**(`agent_product_config`): +- `base_price` - 产品基础底价(必填) +- `system_max_price` - 产品价格上限(必填) +- `price_threshold` - 提价标准阈值(可选,NULL时表示不设阈值) +- `price_fee_rate` - 提价手续费比例(可选,NULL时表示不收费) + +### 6.2 等级加成配置(bonus类型) + +| 配置键 | 默认值 | 说明 | +| --------------- | ------ | ---------------------- | +| `level_1_bonus` | 6.00 | 普通代理等级加成(元) | +| `level_2_bonus` | 3.00 | 黄金代理等级加成(元) | +| `level_3_bonus` | 0.00 | 钻石代理等级加成(元) | + +### 6.3 升级费用配置(upgrade类型) + +| 配置键 | 默认值 | 说明 | +| --------------------------- | ------ | ------------------------ | +| `upgrade_to_gold_fee` | 199.00 | 普通→黄金升级费用(元) | +| `upgrade_to_diamond_fee` | 980.00 | 升级为钻石费用(元) | +| `upgrade_to_gold_rebate` | 139.00 | 普通→黄金返佣金额(元) | +| `upgrade_to_diamond_rebate` | 680.00 | 升级为钻石返佣金额(元) | + +**注意**:`gold_to_diamond` 的费用和返佣可以通过计算得出: +- 费用:`upgrade_to_diamond_fee - upgrade_to_gold_fee = 980 - 199 = 781元` +- 返佣:可以通过前端或后端计算,不单独存储 + +### 6.4 返佣规则配置(rebate类型) + +| 配置键 | 默认值 | 说明 | +| ------------------------------ | ------ | ------------------------------ | +| `direct_parent_amount_diamond` | 6.00 | 直接上级是钻石的返佣金额(元) | +| `direct_parent_amount_gold` | 3.00 | 直接上级是黄金的返佣金额(元) | +| `direct_parent_amount_normal` | 2.00 | 直接上级是普通的返佣金额(元) | +| `max_gold_rebate_amount` | 3.00 | 黄金代理最大返佣金额(元) | + +**建议**:这些配置项目前在代码中硬编码,建议配置化。 + +### 6.5 税费配置(tax类型) + +| 配置键 | 默认值 | 说明 | +| ---------------------- | ------ | ---------------------- | +| `tax_rate` | 0.0600 | 提现税率(6%,即0.06) | +| `tax_exemption_amount` | 0.00 | 免税额度(元,默认0) | + +--- + +## 七、配置表的合理性评估 + +### 7.1 表结构设计 + +| 评估项 | 评分 | 说明 | +| ------------ | ----- | ---------------------------- | +| **灵活性** | ⭐⭐⭐⭐⭐ | 键值对设计非常灵活,易于扩展 | +| **可维护性** | ⭐⭐⭐⭐ | 类型分类清晰,便于管理 | +| **性能** | ⭐⭐⭐⭐ | 唯一索引优化查询,缓存支持 | +| **类型安全** | ⭐⭐⭐ | 所有值都是字符串,需要转换 | +| **验证机制** | ⭐⭐ | 缺少数据库层面的验证 | + +### 7.2 总体评价 + +**设计优点**: +✅ 采用键值对存储,扩展性强 +✅ 类型分类清晰,便于管理 +✅ 支持版本控制和软删除 + +**存在的问题**: +❌ **价格配置应该在产品配置表,而不是系统配置表**(严重设计问题) +❌ 配置键命名不一致(严重) +❌ 部分配置硬编码(严重) +❌ 缺少部分配置项(中等) +❌ 缺少配置验证机制(中等) + +**总体评分**:⭐⭐⭐(3/5) + +**结论**:配置表的设计思路基本合理,但存在**价格配置位置设计错误**的严重问题。价格相关配置应该完全由产品配置表管理,系统配置表只应该包含全局配置(等级加成、升级费用、税费等)。修复这些问题后,配置表设计将更加完善和符合项目规范。 + +--- + +## 八、修复后的完整配置SQL + +```sql +-- ============================================ +-- 代理系统配置初始化(修正版) +-- ============================================ + +-- 删除价格相关配置(应该在产品配置表中) +DELETE FROM `agent_config` WHERE `config_key` IN ( + 'base_price', + 'system_max_price', + 'price_threshold', + 'price_fee_rate' +); + +-- 删除旧的不一致配置(如果存在) +DELETE FROM `agent_config` WHERE `config_key` IN ( + 'level_bonus_normal', + 'level_bonus_gold', + 'level_bonus_diamond', + 'upgrade_fee_normal_to_gold', + 'upgrade_fee_to_diamond', + 'upgrade_rebate_normal_to_gold', + 'upgrade_rebate_to_diamond' +); + +-- 插入修正后的配置项 +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) VALUES +-- 注意:价格相关配置(base_price, system_max_price, price_threshold, price_fee_rate) +-- 已从系统配置表中移除,改为在产品配置表(agent_product_config)中按产品配置 + +-- 等级加成配置(修正键名) +('level_1_bonus', '6.00', 'bonus', '普通代理等级加成(6元)'), +('level_2_bonus', '3.00', 'bonus', '黄金代理等级加成(3元)'), +('level_3_bonus', '0.00', 'bonus', '钻石代理等级加成(0元)'), + +-- 升级费用配置(修正键名) +('upgrade_to_gold_fee', '199.00', 'upgrade', '普通→黄金升级费用(199元)'), +('upgrade_to_diamond_fee', '980.00', 'upgrade', '升级为钻石费用(980元)'), + +-- 升级返佣配置(修正键名) +('upgrade_to_gold_rebate', '139.00', 'upgrade', '普通→黄金返佣金额(139元)'), +('upgrade_to_diamond_rebate', '680.00', 'upgrade', '升级为钻石返佣金额(680元)'), + +-- 返佣规则配置 +('direct_parent_amount_diamond', '6.00', 'rebate', '直接上级是钻石的返佣金额(6元)'), +('direct_parent_amount_gold', '3.00', 'rebate', '直接上级是黄金的返佣金额(3元)'), +('direct_parent_amount_normal', '2.00', 'rebate', '直接上级是普通的返佣金额(2元)'), +('max_gold_rebate_amount', '3.00', 'rebate', '黄金代理最大返佣金额(3元)'), + +-- 税费配置 +('tax_rate', '0.0600', 'tax', '提现税率(6%,即0.06)'), +('tax_exemption_amount', '0.00', 'tax', '提现免税额度(元,默认0)') +ON DUPLICATE KEY UPDATE + `config_value` = VALUES(`config_value`), + `update_time` = NOW(); +``` + +--- + +## 九、建议的改进方案 + +### 方案A:保持当前键值对设计(推荐) + +**优点**: +- 灵活性强,易于扩展 +- 不需要修改表结构 +- 符合项目当前架构 + +**需要做的修改**: +1. 统一配置键命名 +2. 修复代码硬编码问题 +3. 补充缺失的配置项 +4. 添加配置验证逻辑 + +### 方案B:改为结构化JSON配置(可选) + +**如果配置项继续增长,可以考虑**: +- 将相关配置组合成JSON对象存储在 `config_value` 中 +- 例如:`level_bonus_config` 存储 `{"1": 6, "2": 3, "3": 0}` + +**缺点**: +- 需要修改所有读取配置的代码 +- 不利于单个配置项的独立更新 + +**建议**:当前配置项数量不多(约15个),保持键值对设计更合适。 + +--- + +## 十、总结 + +### 当前状态 +- ✅ 配置表设计思路合理 +- ❌ **价格配置设计错误**(应该在产品配置表,不在系统配置表) +- ❌ 配置键命名不一致(需要立即修复) +- ❌ 部分配置硬编码(需要立即修复) +- ⚠️ 缺少部分配置项(需要补充) + +### 修复优先级 +1. **P0(紧急)**:移除系统配置表中的价格配置,改为完全由产品配置表管理 +2. **P0(紧急)**:修改订单处理逻辑,从产品配置表读取价格参数 +3. **P0(紧急)**:统一配置键命名,修复不一致问题 +4. **P0(紧急)**:修改代码,从配置表读取等级加成,移除硬编码 +5. **P1(重要)**:补充缺失的配置项(`tax_exemption_amount`) +6. **P2(建议)**:添加配置验证逻辑 +7. **P2(建议)**:将返佣规则配置化(当前硬编码) + +### 是否符合项目 + +**配置表设计**:✅ 基本符合,但价格配置的位置设计错误 + +**存在的问题**: +1. ❌ **价格配置应该在产品配置表中,而不是系统配置表**(严重设计问题) +2. ❌ 配置键命名不一致 +3. ❌ 部分配置硬编码 +4. ⚠️ 缺少部分配置项 + +**结论**:需要修复上述问题后才能完全符合项目的配置化设计理念。**最重要的是将价格配置从系统配置表移除,改为完全由产品配置表管理。** + +--- + +## 十一、产品配置表使用规范 + +### 11.1 产品配置表结构 + +**表名**: `agent_product_config` + +每个产品都应该有对应的配置记录,包含以下字段: + +| 字段 | 类型 | 必填 | 说明 | +| ------------------ | ------------- | ---- | ----------------------------------------- | +| `product_id` | bigint | ✅ 是 | 产品ID(唯一) | +| `product_name` | varchar(100) | ✅ 是 | 产品名称 | +| `base_price` | decimal(10,2) | ✅ 是 | 产品基础底价 | +| `system_max_price` | decimal(10,2) | ✅ 是 | 产品价格上限 | +| `price_threshold` | decimal(10,2) | ❌ 否 | 提价标准阈值(NULL表示不设阈值) | +| `price_fee_rate` | decimal(5,4) | ❌ 否 | 提价手续费比例(NULL表示不收费,0-1之间) | + +### 11.2 配置优先级和默认值 + +**规则**: +1. 所有价格参数必须从产品配置表读取 +2. 如果产品配置记录不存在,应该报错或使用合理的默认值(建议报错,强制每个产品必须配置) +3. 可选字段(`price_threshold`、`price_fee_rate`)如果为NULL,表示不启用该功能 + - `price_threshold = NULL` → 不设提价阈值 + - `price_fee_rate = NULL` → 不收取提价手续费 + +### 11.3 代码修改示例 + +#### 修改 AgentService.AgentProcess 方法 + +```go +// AgentProcess 处理代理订单(新系统) +func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) error { + // 1-3. 前面的代码不变... + + // 4. 获取产品配置(必须存在) + productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "产品配置不存在, productId: %d,请先在后台配置产品价格参数", order.ProductId) + } + return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId) + } + + // 使用产品配置的底价 + basePrice := productConfig.BasePrice + + // 6. 使用事务处理订单 + return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { + // 6.1 计算实际底价和代理收益 + levelBonus, err := s.getLevelBonus(ctx, agent.Level) + if err != nil { + return errors.Wrapf(err, "获取等级加成配置失败") + } + actualBasePrice := basePrice + float64(levelBonus) + + // 6.2 计算提价成本(使用产品配置) + priceThreshold := 0.0 + priceFeeRate := 0.0 + if productConfig.PriceThreshold.Valid { + priceThreshold = productConfig.PriceThreshold.Float64 + } + if productConfig.PriceFeeRate.Valid { + priceFeeRate = productConfig.PriceFeeRate.Float64 + } + priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate) + + // 6.3 计算代理收益 + agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost + + // ... 后续代码不变 ... + }) +} +``` + +#### 修改 PaymentLogic 创建订单时的价格计算 + +```go +// 如果是代理推广订单,创建完整的代理订单记录 +if data.AgentIdentifier != "" && agentLinkModel != nil { + // ... 获取代理信息 ... + + // 获取产品配置(必须存在) + productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, product.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "生成订单失败,产品配置不存在, productId: %d,请先在后台配置产品价格参数", product.Id) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "生成订单, 查询产品配置失败: %+v", err) + } + + // 使用产品配置的底价 + basePrice := productConfig.BasePrice + + // 计算实际底价(基础底价+等级加成) + levelBonus, _ := l.getLevelBonus(agent.Level) + actualBasePrice := basePrice + float64(levelBonus) + + // 计算提价成本(使用产品配置) + priceThreshold := 0.0 + priceFeeRate := 0.0 + if productConfig.PriceThreshold.Valid { + priceThreshold = productConfig.PriceThreshold.Float64 + } + if productConfig.PriceFeeRate.Valid { + priceFeeRate = productConfig.PriceFeeRate.Float64 + } + + priceCost := 0.0 + if agentLinkModel.SetPrice > priceThreshold { + priceCost = (agentLinkModel.SetPrice - priceThreshold) * priceFeeRate + } + + // 计算代理收益 + agentProfit := agentLinkModel.SetPrice - actualBasePrice - priceCost + + // ... 创建代理订单记录 ... +} +``` + +### 11.4 需要修改的文件清单 + +需要修改以支持产品配置的代码文件: + +1. **`app/main/api/internal/service/agentService.go`** + - `AgentProcess()` 方法:从产品配置表读取价格参数 + +2. **`app/main/api/internal/logic/pay/paymentlogic.go`** + - 创建订单时的价格计算:从产品配置表读取价格参数 + +3. **`app/main/api/internal/logic/agent/getagentproductconfiglogic.go`** + - 移除从系统配置表读取价格参数的逻辑(只保留从产品配置表读取) + +4. **`app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go`** + - 移除价格相关配置项的返回 + +5. **`app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go`** + - 移除价格相关配置项的更新逻辑 + +6. **后台管理系统前端** + - 系统配置页面:移除价格相关配置项 + - 产品配置页面:确保可以正确配置所有价格参数 + diff --git a/佣金冻结功能完成总结.md b/佣金冻结功能完成总结.md new file mode 100644 index 0000000..d184eaa --- /dev/null +++ b/佣金冻结功能完成总结.md @@ -0,0 +1,136 @@ +# 佣金冻结功能实现完成总结 + +## ✅ 已完成的工作 + +### 1. 数据库表 +- ✅ 创建了 `agent_freeze_task` 表(`deploy/sql/agent_freeze_task_migration.sql`) +- ✅ 表结构包含:代理ID、订单ID、佣金ID、冻结金额、订单单价、冻结比例、状态、冻结时间、解冻时间等 + +### 2. 核心业务逻辑 +- ✅ 修改了 `giveAgentCommission` 函数(`agentService.go`) + - 增加订单单价参数 + - 实现冻结逻辑:订单单价 >= 100元时,冻结订单单价的10%(可配置) + - 创建冻结任务记录 + - 更新钱包余额和冻结余额 + +### 3. 异步任务系统 +- ✅ 创建了解冻任务处理器(`queue/unfreezeCommission.go`) +- ✅ 添加了 `SendUnfreezeTask` 方法到 `AsynqService` +- ✅ 在 `agentProcess.go` 中,代理处理成功后自动发送解冻任务 +- ✅ 注册了解冻任务路由 + +### 4. 配置支持 +- ✅ 冻结比例从配置表 `agent_config` 读取,配置键为 `commission_freeze_ratio` + +### 5. ServiceContext 更新 +- ✅ 添加了 `AgentFreezeTaskModel` 字段 +- ✅ 初始化了 `agentFreezeTaskModel` +- ✅ 传递给 `AgentService` +- ✅ 添加到返回的 `ServiceContext` + +## 📋 功能说明 + +### 触发条件 +- 订单单价 >= 配置阈值(默认100元,可通过 `commission_freeze_threshold` 配置) + +### 冻结规则 +- 冻结阈值:从配置表读取(配置键:`commission_freeze_threshold`,默认100元) +- 冻结比例:从配置表读取(配置键:`commission_freeze_ratio`,默认10%) +- 冻结金额 = 订单单价 × 冻结比例 +- 冻结金额不能超过佣金金额 +- 例如:订单单价140元,佣金134元,冻结14元(140 × 10%),实际到账120元(134 - 14) + +### 冻结时长 +- 从配置表读取(配置键:`commission_freeze_days`,默认30天) +- 注意:配置只在创建任务时读取,已创建的任务不受后续配置修改影响 + +### 解冻机制 +- 通过异步任务延迟执行 +- 1个月后自动解冻 +- 解冻时:`FrozenBalance -= 冻结金额`,`Balance += 冻结金额` + +### 数据持久化 +- 冻结任务记录在 `agent_freeze_task` 表中 +- 保证任务的一致性和持久化 +- 支持任务重试和状态追踪 + +## 🔧 还需要完成的工作 + +### 1. 添加配置项 +在 `agent_config` 表中添加配置: +```sql +-- 冻结阈值(订单单价达到此金额才触发冻结,默认100元) +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('commission_freeze_threshold', '100', 'rebate', '佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元)'); + +-- 冻结比例(默认10%) +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('commission_freeze_ratio', '0.1', 'rebate', '佣金冻结比例(例如:0.1表示10%)'); + +-- 解冻天数(默认30天,即1个月) +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('commission_freeze_days', '30', 'rebate', '佣金冻结解冻天数(单位:天,例如:30表示30天后解冻)'); +``` + +### 2. 运行SQL创建表(如果还没运行) +```bash +mysql -u用户名 -p数据库名 < deploy/sql/agent_freeze_task_migration.sql +``` + +## 📝 代码文件清单 + +### 新增文件 +1. `deploy/sql/agent_freeze_task_migration.sql` - 冻结任务表SQL +2. `app/main/api/internal/queue/unfreezeCommission.go` - 解冻任务处理器 +3. `佣金冻结功能实现说明.md` - 实现说明文档 + +### 修改文件 +1. `app/main/api/internal/service/agentService.go` - 修改佣金发放逻辑 +2. `app/main/api/internal/service/asynqService.go` - 添加发送解冻任务方法 +3. `app/main/api/internal/queue/agentProcess.go` - 添加发送解冻任务逻辑 +4. `app/main/api/internal/queue/routes.go` - 注册解冻任务路由 +5. `app/main/api/internal/types/taskname.go` - 添加解冻任务类型 +6. `app/main/api/internal/types/payload.go` - 添加解冻任务负载类型 +7. `app/main/api/internal/svc/servicecontext.go` - 添加 AgentFreezeTaskModel + +## 🎯 功能流程 + +``` +订单支付成功 + ↓ +代理处理(AgentProcess) + ↓ +发放佣金(giveAgentCommission) + ↓ +判断:订单单价 >= 100元? + ├─ 是 → 计算冻结金额(订单单价 × 10%) + │ 创建冻结任务记录 + │ 更新钱包:Balance += (佣金-冻结金额), FrozenBalance += 冻结金额 + │ 发送解冻异步任务(延迟配置天数) + │ + └─ 否 → 正常发放佣金,不冻结 + ↓ +配置天数后 + ↓ +解冻任务执行(UnfreezeCommissionHandler) + ↓ +更新冻结任务状态为已解冻 + ↓ +更新钱包:FrozenBalance -= 冻结金额, Balance += 冻结金额 +``` + +## ✅ 代码检查 +- ✅ 所有文件已通过编译检查 +- ✅ 没有 lint 错误 +- ✅ ServiceContext 已正确更新 +- ✅ 异步任务路由已注册 + +## 🚀 下一步 +1. 运行SQL创建表(如果还没运行) +2. 添加配置项到 `agent_config` 表 +3. 测试功能: + - 测试订单单价 < 100元:不应冻结 + - 测试订单单价 >= 100元:应冻结订单单价的10% + - 测试冻结金额超过佣金:应冻结全部佣金 + - 测试解冻任务:在配置的天数后应自动解冻 + diff --git a/佣金冻结功能实现说明.md b/佣金冻结功能实现说明.md new file mode 100644 index 0000000..d9ec35c --- /dev/null +++ b/佣金冻结功能实现说明.md @@ -0,0 +1,106 @@ +# 佣金冻结功能实现说明 + +## 功能需求 +当订单单价 >= 配置阈值(默认100元)时,抽取订单单价的配置比例(默认10%)放到冻结余额,冻结配置天数(默认30天)后自动解冻。所有配置项都可在配置表中修改,但配置只在创建任务时读取,已创建的任务不受后续配置修改影响。 + +## 实现步骤 + +### 1. 运行SQL创建冻结任务表 +```bash +# 执行SQL文件 +mysql -u用户名 -p数据库名 < deploy/sql/agent_freeze_task_migration.sql +``` + +### 2. 生成Model +运行代码生成工具生成 `AgentFreezeTaskModel`: +```bash +# 在 ycc-proxy-server 目录下运行 +goctl model mysql datasource -url="数据库连接字符串" -table="agent_freeze_task" -dir="./app/main/model" -cache +``` + +### 3. 更新ServiceContext +在 `app/main/api/internal/svc/servicecontext.go` 中: + +#### 3.1 添加字段 +```go +AgentFreezeTaskModel model.AgentFreezeTaskModel +``` + +#### 3.2 初始化Model +在 `NewServiceContext` 函数中添加: +```go +agentFreezeTaskModel := model.NewAgentFreezeTaskModel(db, cacheConf) +``` + +#### 3.3 传递给AgentService +修改 `NewAgentService` 调用,添加参数: +```go +agentService := service.NewAgentService(c, orderModel, agentModel, agentWalletModel, + agentRelationModel, agentLinkModel, agentOrderModel, agentCommissionModel, agentRebateModel, + agentUpgradeModel, agentWithdrawalModel, agentConfigModel, agentProductConfigModel, + agentRealNameModel, agentWithdrawalTaxModel, agentFreezeTaskModel) // 添加这个参数 +``` + +#### 3.4 添加到ServiceContext返回 +在返回的 `ServiceContext` 结构体中添加: +```go +AgentFreezeTaskModel: agentFreezeTaskModel, +``` + +### 4. 添加配置项 +在 `agent_config` 表中添加配置: +```sql +-- 冻结阈值(订单单价达到此金额才触发冻结,默认100元) +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('commission_freeze_threshold', '100', 'rebate', '佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元)'); + +-- 冻结比例(默认10%) +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('commission_freeze_ratio', '0.1', 'rebate', '佣金冻结比例(例如:0.1表示10%)'); + +-- 解冻天数(默认30天,即1个月) +INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) +VALUES ('commission_freeze_days', '30', 'rebate', '佣金冻结解冻天数(单位:天,例如:30表示30天后解冻)'); +``` + +### 5. 代码已完成的修改 + +#### 5.1 已修改的文件 +- ✅ `deploy/sql/agent_freeze_task_migration.sql` - 创建冻结任务表 +- ✅ `app/main/api/internal/service/agentService.go` - 修改 `giveAgentCommission` 函数,增加冻结逻辑 +- ✅ `app/main/api/internal/service/asynqService.go` - 添加 `SendUnfreezeTask` 方法 +- ✅ `app/main/api/internal/queue/unfreezeCommission.go` - 创建解冻任务处理器 +- ✅ `app/main/api/internal/queue/routes.go` - 注册解冻任务路由 +- ✅ `app/main/api/internal/queue/agentProcess.go` - 在代理处理成功后发送解冻任务 +- ✅ `app/main/api/internal/types/taskname.go` - 添加解冻任务类型 +- ✅ `app/main/api/internal/types/payload.go` - 添加解冻任务负载类型 + +#### 5.2 核心逻辑说明 + +**冻结逻辑**(在 `giveAgentCommission` 中): +1. 判断订单单价是否 >= 100元 +2. 如果是,从配置表读取冻结比例(默认10%) +3. 计算冻结金额 = 订单单价 × 冻结比例(不超过佣金金额) +4. 创建冻结任务记录(状态=待解冻,解冻时间=从配置读取的天数后) +5. 更新钱包:`Balance += (佣金金额 - 冻结金额)`,`FrozenBalance += 冻结金额` + +**解冻逻辑**(在 `unfreezeCommission.go` 中): +1. 查询冻结任务 +2. 检查解冻时间是否已到 +3. 更新冻结任务状态为已解冻 +4. 更新钱包:`FrozenBalance -= 冻结金额`,`Balance += 冻结金额` + +### 6. 注意事项 + +1. **Model生成**:必须先运行SQL并生成Model,否则代码无法编译 +2. **配置项**:需要在 `agent_config` 表中添加 `commission_freeze_ratio` 配置项 +3. **异步任务**:解冻任务通过 asynq 延迟执行,确保在配置的天数后自动解冻 +4. **数据一致性**:所有操作都在事务中执行,保证数据一致性 + +### 7. 测试建议 + +1. 测试订单单价 < 100元:不应冻结 +2. 测试订单单价 >= 100元:应冻结订单单价的10% +3. 测试冻结金额超过佣金:应冻结全部佣金 +4. 测试解冻任务:在配置的天数后应自动解冻 + diff --git a/删除推广订单功能计划.md b/删除推广订单功能计划.md new file mode 100644 index 0000000..b9e05a2 --- /dev/null +++ b/删除推广订单功能计划.md @@ -0,0 +1,213 @@ +# 删除推广订单功能计划 + +## 目标 +删除后台以及后端中promotion(推广订单)相关功能,但**保留推广数据分析和推广链接管理**功能。 + +## 功能区分 + +### ✅ 需要保留的功能 +1. **推广链接管理** + - 创建推广链接 + - 更新推广链接 + - 删除推广链接 + - 获取推广链接列表 + - 获取推广链接详情 + - 记录链接点击 + +2. **推广数据分析** + - 获取推广总统计 + - 获取推广历史记录 + +### ❌ 需要删除的功能 +1. **推广订单功能** + - AdminPromotionOrder 模型及相关代码 + - 订单管理中的 IsPromotion 字段 + - 订单创建/更新/删除中的推广订单逻辑 + - 前端订单管理中的推广订单字段 + +--- + +## 详细删除计划 + +### 阶段一:后端代码清理 + +#### 1.1 删除推广订单模型文件 +- [ ] 删除 `ycc-proxy-server/app/main/model/adminPromotionOrderModel.go` +- [ ] 删除 `ycc-proxy-server/app/main/model/adminPromotionOrderModel_gen.go` +- [ ] 确认 `ycc-proxy-server/deploy/script/gen_models.ps1` 中 `admin_promotion_order` 已被注释(第57行,已确认已注释) + +#### 1.2 从 ServiceContext 中移除推广订单模型 +- [ ] 从 `ycc-proxy-server/app/main/api/internal/svc/servicecontext.go` 中删除: + - `AdminPromotionOrderModel` 字段声明 + - `AdminPromotionOrderModel` 初始化代码 + +#### 1.3 修改订单管理相关逻辑 + +**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admincreateorderlogic.go`** +- [ ] 删除 `IsPromotion` 字段处理逻辑(第75-87行) +- [ ] 删除 `AdminPromotionOrderModel` 的引用 + +**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go`** +- [ ] 删除推广订单状态处理逻辑(第79-101行) +- [ ] 删除 `AdminPromotionOrderModel` 的引用 + +**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go`** +- [ ] 删除删除推广订单记录的逻辑(第44-51行) +- [ ] 删除 `AdminPromotionOrderModel` 的引用 + +**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go`** +- [ ] 删除 `IsPromotion` 查询条件(第57-59行) +- [ ] 删除判断是否为推广订单的逻辑(第277-280行) +- [ ] 删除返回结果中的 `IsPromotion` 字段赋值 + +**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go`** +- [ ] 删除判断是否为推广订单的逻辑(第43-47行) +- [ ] 删除返回结果中的 `IsPromotion` 字段赋值(第121行) + +#### 1.4 修改 API 定义文件 + +**文件:`ycc-proxy-server/app/main/api/desc/admin/order.api`** +- [ ] 从 `AdminGetOrderListReq` 中删除 `IsPromotion` 字段(第56行) +- [ ] 从 `AdminGetOrderListResp` 的 `OrderItem` 中删除 `IsPromotion` 字段(第83行) +- [ ] 从 `AdminGetOrderDetailResp` 中删除 `IsPromotion` 字段(第105行) +- [ ] 从 `AdminCreateOrderReq` 中删除 `IsPromotion` 字段(第119行) +- [ ] 从 `AdminUpdateOrderReq` 中删除 `IsPromotion` 字段(第137行) + +#### 1.5 修改 Types 定义 + +**文件:`ycc-proxy-server/app/main/api/internal/types/types.go`** +- [ ] 从 `AdminGetOrderListReq` 中删除 `IsPromotion` 字段 +- [ ] 从 `AdminGetOrderListResp` 的 `OrderItem` 中删除 `IsPromotion` 字段 +- [ ] 从 `AdminGetOrderDetailResp` 中删除 `IsPromotion` 字段 +- [ ] 从 `AdminCreateOrderReq` 中删除 `IsPromotion` 字段 +- [ ] 从 `AdminUpdateOrderResp` 中删除 `IsPromotion` 字段 + +#### 1.6 重新生成代码 +- [ ] 运行 `goctl api go -api desc/admin/order.api -dir . -style gozero` +- [ ] 检查生成的代码是否正确 + +--- + +### 阶段二:前端代码清理 + +#### 2.1 修改订单管理页面 + +**文件:`ycc-proxy-admin/apps/web-antd/src/views/order/order/data.ts`** +- [ ] 删除推广订单列定义(第110-121行) +- [ ] 删除表单中的推广订单字段(第224-225行) + +**文件:`ycc-proxy-admin/apps/web-antd/src/api/order/order.ts`** +- [ ] 从接口类型定义中删除 `is_promotion` 字段(第19行) + +#### 2.2 检查其他前端文件 +- [ ] 搜索前端代码中是否还有其他地方引用了 `is_promotion` 或 `IsPromotion` +- [ ] 删除所有相关引用 + +--- + +### 阶段三:数据库清理(可选) + +#### 3.1 数据库表处理 +- [ ] 确认是否删除 `admin_promotion_order` 表 + - 如果保留历史数据,可以保留表但不使用 + - 如果需要完全清理,可以执行删除表的SQL + +#### 3.2 数据库迁移脚本(如需要) +- [ ] 创建迁移脚本,删除 `admin_promotion_order` 表(如果决定删除) + +--- + +### 阶段四:验证和测试 + +#### 4.1 功能验证 +- [ ] 验证推广链接管理功能正常 +- [ ] 验证推广数据分析功能正常 +- [ ] 验证订单创建功能正常(不再有推广订单选项) +- [ ] 验证订单更新功能正常(不再有推广订单选项) +- [ ] 验证订单列表查询功能正常(不再显示推广订单字段) +- [ ] 验证订单详情查看功能正常(不再显示推广订单字段) + +#### 4.2 代码检查 +- [ ] 运行 `go build` 确保后端代码编译通过 +- [ ] 运行前端构建确保前端代码正常 +- [ ] 检查是否有编译错误或警告 +- [ ] 检查是否有未使用的导入 + +--- + +## 注意事项 + +1. **保留的功能不受影响** + - 推广链接管理(`AdminPromotionLink`)完全保留 + - 推广数据分析(`AdminPromotionLinkStatsTotal`, `AdminPromotionLinkStatsHistory`)完全保留 + - 记录链接点击功能完全保留 + +2. **数据库表处理** + - `admin_promotion_order` 表可以保留(用于历史数据),但不再使用 + - 或者完全删除该表(需要确认是否有历史数据需要保留) + +3. **代码生成** + - 修改 `.api` 文件后需要重新生成代码 + - 确保生成的代码与手动修改的代码一致 + +4. **向后兼容** + - 如果前端已经部署,需要确保前端和后端同时更新 + - 或者先更新后端,保持向后兼容一段时间 + +--- + +## 执行顺序建议 + +1. **第一步**:修改后端 API 定义文件(`.api` 文件) +2. **第二步**:重新生成后端代码 +3. **第三步**:手动修改后端逻辑代码(删除推广订单相关逻辑) +4. **第四步**:修改前端代码 +5. **第五步**:测试验证 +6. **第六步**:清理数据库(如需要) + +--- + +## 文件清单 + +### 需要修改的后端文件 +1. `ycc-proxy-server/app/main/api/desc/admin/order.api` +2. `ycc-proxy-server/app/main/api/internal/types/types.go` +3. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admincreateorderlogic.go` +4. `ycc-proxy-server/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go` +5. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go` +6. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go` +7. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go` +8. `ycc-proxy-server/app/main/api/internal/svc/servicecontext.go` +9. `ycc-proxy-server/app/main/model/vars.go`(如需要) + +### 需要删除的后端文件 +1. `ycc-proxy-server/app/main/model/adminPromotionOrderModel.go` +2. `ycc-proxy-server/app/main/model/adminPromotionOrderModel_gen.go` + +### 需要修改的前端文件 +1. `ycc-proxy-admin/apps/web-antd/src/views/order/order/data.ts` +2. `ycc-proxy-admin/apps/web-antd/src/api/order/order.ts` + +### 需要保留的文件(推广链接和数据分析) +1. `ycc-proxy-server/app/main/api/desc/admin/promotion.api` ✅ +2. `ycc-proxy-server/app/main/api/internal/logic/admin_promotion/*` ✅ +3. `ycc-proxy-server/app/main/api/internal/handler/admin_promotion/*` ✅ +4. `ycc-proxy-server/app/main/model/adminPromotionLinkModel*` ✅ +5. `ycc-proxy-server/app/main/model/adminPromotionLinkStats*` ✅ +6. `ycc-proxy-server/app/main/api/internal/service/adminPromotionLinkStatsService.go` ✅ +7. `ycc-proxy-admin/apps/web-antd/src/views/promotion/*` ✅ +8. `ycc-proxy-admin/apps/web-antd/src/api/promotion/*` ✅ + +--- + +## 完成标志 + +- [ ] 所有后端代码修改完成 +- [ ] 所有前端代码修改完成 +- [ ] 代码编译通过 +- [ ] 功能测试通过 +- [ ] 推广链接管理功能正常 +- [ ] 推广数据分析功能正常 +- [ ] 订单管理功能正常(无推广订单相关功能) +- [ ] 无编译错误和警告 + diff --git a/定时清理报告系统问题分析.md b/定时清理报告系统问题分析.md new file mode 100644 index 0000000..0f2961d --- /dev/null +++ b/定时清理报告系统问题分析.md @@ -0,0 +1,111 @@ +# 定时清理报告系统问题分析 + +## 🔴 严重问题 + +### 1. **重复插入清理日志记录(第129-137行)** +**问题**:每批次都插入新的清理日志记录,导致同一个清理任务产生多条日志记录。 + +**影响**: +- 数据冗余,同一个清理任务会有多条日志 +- 无法准确统计单次清理的总影响行数 +- 日志记录混乱 + +**当前代码**: +```go +// 4. 保存清理日志(每批次都记录) +cleanupLogInsertResult, err := l.svcCtx.QueryCleanupLogModel.Insert(ctx, session, cleanupLog) +``` + +**应该**:先创建一条日志记录,然后只更新 `AffectedRows` 字段,或者只在最后插入一次。 + +### 2. **大事务问题(第96行)** +**问题**:整个清理过程在一个大事务中,如果数据量很大,会导致: +- 事务持续时间过长(可能几小时) +- 锁等待时间过长 +- 可能导致事务超时 +- 数据库连接占用时间过长 +- 如果中途失败,所有操作回滚,已处理的数据无法恢复 + +**影响**: +- 数据库性能下降 +- 可能导致其他操作阻塞 +- 数据量大时可能失败 + +**建议**:每个批次使用独立事务,或者使用小事务分批提交。 + +### 3. **缺少超时控制** +**问题**:没有为整个清理任务设置超时,如果数据量很大,可能会运行很长时间。 + +**影响**: +- 任务可能无限期运行 +- 无法及时响应系统关闭信号 + +**建议**:添加超时控制,例如最多运行1小时。 + +### 4. **查询条件缺少排序(第100-103行)** +**问题**:查询条件没有排序,可能导致每次查询的结果不一致。 + +**影响**: +- 可能导致某些数据被重复处理或遗漏 +- 无法保证处理顺序 + +**建议**:添加 `OrderBy("id ASC")` 或 `OrderBy("create_time ASC")`。 + +## 🟡 中等问题 + +### 5. **缺少进度监控** +**问题**:没有记录处理进度,如果任务中断,无法知道处理到哪里了。 + +**影响**: +- 任务中断后无法恢复 +- 无法监控处理进度 + +**建议**:记录已处理的批次数和最后处理的ID。 + +### 6. **错误处理不够完善** +**问题**:如果某个批次失败,整个事务回滚,但已经处理的数据无法恢复。 + +**影响**: +- 部分数据可能已经处理,但失败后全部回滚 +- 无法知道哪些数据已经处理过 + +**建议**:每个批次使用独立事务,失败时只回滚当前批次。 + +### 7. **缺少优雅关闭支持** +**问题**:如果服务关闭,正在处理的任务可能被中断。 + +**影响**: +- 数据可能处于不一致状态 +- 无法记录已处理的进度 + +**建议**:检查 context 是否被取消,优雅退出。 + +## 🟢 小问题 + +### 8. **日志信息不够详细** +**问题**:日志信息可以更详细,便于排查问题。 + +**建议**:记录批次处理情况、处理时间等。 + +### 9. **配置验证不足** +**问题**:没有验证配置值的合理性(如批次大小、保留天数)。 + +**建议**:添加配置验证,确保配置值在合理范围内。 + +## 修复建议优先级 + +### 🔴 高优先级(必须修复) +1. **修复重复插入日志问题** - 影响数据准确性 +2. **修复大事务问题** - 影响性能和可靠性 +3. **添加查询排序** - 影响数据一致性 + +### 🟡 中优先级(建议修复) +4. **添加超时控制** - 提高可靠性 +5. **改进错误处理** - 提高容错性 +6. **添加进度监控** - 便于排查问题 + +### 🟢 低优先级(可选) +7. **优雅关闭支持** - 如果服务很少重启可以不做 +8. **更详细的日志** - 便于排查问题 +9. **配置验证** - 提高健壮性 + diff --git a/报告查询链路和代理分配逻辑检查报告.md b/报告查询链路和代理分配逻辑检查报告.md new file mode 100644 index 0000000..051f13a --- /dev/null +++ b/报告查询链路和代理分配逻辑检查报告.md @@ -0,0 +1,221 @@ +# 报告查询链路和代理分配逻辑检查报告 + +## 一、报告查询链路检查 + +### 1.1 整体流程 + +``` +用户支付订单 + ↓ +支付回调(支付宝/微信) + ↓ +更新订单状态为 "paid" + ↓ +发送异步任务 SendQueryTask(order.Id) + ↓ +异步任务处理 PaySuccessNotifyUserHandler.ProcessTask + ├─ 创建报告记录(query表) + ├─ 生成授权书 + ├─ 调用API请求服务获取报告数据 + ├─ 更新报告状态为 "success" + ├─ 调用代理处理 AgentService.AgentProcess + └─ 删除Redis缓存 +``` + +### 1.2 链路检查结果 + +✅ **链路基本通顺**,但存在以下问题: + +#### ✅ 问题1:代理处理失败时的处理逻辑 - 已修复 + +**原问题**: +- 代理处理在报告生成成功后同步执行 +- 如果代理处理失败,会触发 `handleError`,但报告已经生成成功 + +**修复方案**: +1. ✅ 创建了独立的代理处理异步任务 Handler(`app/main/api/internal/queue/agentProcess.go`) +2. ✅ 修改 `paySuccessNotify.go` 改为发送异步任务,不再阻塞报告流程 +3. ✅ 代理处理失败时只记录日志,不影响报告使用 +4. ✅ 保留了手动重试接口供管理员使用 + +**修复后的代码**: +```go +// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程) +if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil { + // 代理处理任务发送失败,只记录日志,不影响报告流程 + logx.Errorf("发送代理处理任务失败,订单ID: %d, 错误: %v", order.Id, asyncErr) +} +``` + +### 1.3 支付回调链路 + +✅ **支付回调链路正常**: +- 支付宝回调:`AlipayCallbackLogic.handleQueryOrderPayment` +- 微信回调:`WechatPayCallbackLogic.handleQueryOrderPayment` +- 支付成功后正确发送异步任务 + +--- + +## 二、代理分配逻辑检查 + +### 2.1 代理收益计算 + +✅ **代理收益计算逻辑正确** + +**计算公式**: +```go +实际底价 = 基础底价 + 等级加成 +提价成本 = (设定价格 - 提价阈值) × 提价手续费比例(当设定价格 > 提价阈值时) +代理收益 = 设定价格 - 实际底价 - 提价成本 +``` + +**位置**:`app/main/api/internal/service/agentService.go:131-132` + +**验证**: +- ✅ 使用产品配置的底价(从 `agent_product_config` 表读取) +- ✅ 等级加成从配置表读取(支持动态配置) +- ✅ 提价成本计算逻辑正确 + +### 2.2 等级加成返佣分配 + +#### 2.2.1 黄金代理(等级加成3元) + +✅ **实现正确** +- 全部给钻石上级 +- 找不到钻石上级时,返佣归平台 + +**位置**:`app/main/api/internal/service/agentService.go:233-244` + +#### 2.2.2 普通代理(等级加成6元) + +✅ **已按照新规则修复** + +**新规则**(已确认并实现): +1. **直接上级是钻石**:等级加成全部给钻石上级 +2. **直接上级是黄金**:一部分给黄金上级(配置:`normal_to_gold_rebate`,默认3元),剩余给钻石上级 +3. **直接上级是普通**: + - 一部分给直接上级普通(配置:`normal_to_normal_rebate`,默认2元) + - 剩余金额: + - 有钻石上级:剩余全部给钻石上级 + - 只有黄金上级:最多给黄金上级(配置:`normal_to_gold_rebate_max`,默认3元),超出归平台 + - 都没有:全部归平台 + +**代码实现**(`app/main/api/internal/service/agentService.go:254-368`): +- ✅ 按照新规则完全重写 +- ✅ 支持从配置表读取返佣金额(如果配置不存在使用默认值) +- ✅ 跳过多层普通代理,直接查找钻石/黄金上级 + +**配置项**: +- `normal_to_normal_rebate`:普通代理给直接上级普通的金额(默认2元) +- `normal_to_gold_rebate`:普通代理给直接上级黄金的金额(默认3元) +- `normal_to_gold_rebate_max`:普通代理给黄金上级的最大金额(默认3元) + +### 2.3 代理收益分配流程 + +✅ **分配流程正确** + +**流程**: +1. 检查是否是代理订单 +2. 检查订单是否已处理(防重复) +3. 获取代理信息和产品配置 +4. 使用事务处理(保证原子性): + - 计算代理收益 + - 更新代理订单状态 + - 发放代理佣金到代理钱包 + - 分配等级加成返佣给上级链 + +**位置**:`app/main/api/internal/service/agentService.go:76-156` + +### 2.4 事务处理和错误处理 + +✅ **事务处理正确** + +**验证**: +- ✅ 使用 `AgentWalletModel.Trans` 事务处理 +- ✅ 所有数据库操作在同一事务中 +- ✅ 任何步骤失败都会回滚 + +**位置**:`app/main/api/internal/service/agentService.go:109` + +### 2.5 防重复处理 + +✅ **防重复处理正确** + +**验证**: +- ✅ 检查 `agent_order.ProcessStatus == 1` 判断是否已处理 +- ✅ 已处理的订单直接返回,不重复处理 + +**位置**:`app/main/api/internal/service/agentService.go:87-91` + +--- + +## 三、其他发现 + +### 3.1 重试机制 + +✅ **已有手动重试接口** + +**位置**: +- 接口:`POST /api/v1/admin/order/retry-agent-process/:id` +- Logic:`app/main/api/internal/logic/admin_order/adminretryagentprocesslogic.go` + +**功能**: +- 管理员可以手动触发代理处理重试 +- 可以处理代理处理失败的情况 + +### 3.2 代理处理失败时的错误处理 + +**当前行为**: +- 代理处理失败会触发 `handleError` +- `handleError` 会尝试退款(如果报告状态为 pending) +- 但报告已经生成成功,可能不会触发退款 + +**建议**: +- 代理处理失败不应该触发退款(因为报告已经成功生成) +- 应该记录错误日志,供管理员手动重试 + +--- + +## 四、总结和建议 + +### 4.1 需要确认的问题 + +1. **普通代理等级加成返佣分配规则** + - 文档和代码实现不一致 + - 需要确认正确的业务规则 + +### 4.2 建议改进 + +1. **代理处理失败的处理** + - 建议将代理处理改为独立的异步任务,或至少确保失败不影响报告使用 + - 失败时只记录日志,不触发退款 + +2. **文档更新** + - 根据最终确认的业务规则,更新文档或代码 + +3. **日志增强** + - 在代理处理失败时记录更详细的日志,方便排查问题 + +### 4.3 整体评价 + +- ✅ 报告查询链路基本通顺 +- ✅ 代理收益计算逻辑正确 +- ✅ 事务处理和防重复处理正确 +- ⚠️ 普通代理等级加成返佣分配规则需要确认 +- ⚠️ 代理处理失败时的处理逻辑可以优化 + +--- + +## 五、代码位置索引 + +### 报告查询链路 +- 支付回调:`app/main/api/internal/logic/pay/alipaycallbacklogic.go:56-106` +- 异步任务:`app/main/api/internal/queue/paySuccessNotify.go:35-178` +- 报告查询:`app/main/api/internal/logic/query/querylistlogic.go` + +### 代理分配逻辑 +- 代理处理入口:`app/main/api/internal/service/agentService.go:76-156` +- 收益计算:`app/main/api/internal/service/agentService.go:131-132` +- 等级加成返佣:`app/main/api/internal/service/agentService.go:226-328` +- 普通代理返佣分配:`app/main/api/internal/service/agentService.go:254-328` + diff --git a/新代理系统完整文档.md b/新代理系统完整文档.md index 572bad0..457468c 100644 --- a/新代理系统完整文档.md +++ b/新代理系统完整文档.md @@ -59,20 +59,20 @@ #### 2.1.1 agent(代理基本信息表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| user_id | bigint | 用户ID(唯一) | -| level | tinyint | 代理等级:1=普通,2=黄金,3=钻石 | -| region | varchar(50) | 区域(可选) | -| mobile | varchar(50) | 手机号(加密) | -| wechat_id | varchar(100) | 微信号(可选) | -| team_leader_id | bigint | 团队首领ID(钻石代理的ID) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态:0=未删除,1=已删除 | -| version | bigint | 版本号(乐观锁) | +| 字段 | 类型 | 说明 | +| -------------- | ------------ | -------------------------------- | +| id | bigint | 主键ID | +| user_id | bigint | 用户ID(唯一) | +| level | tinyint | 代理等级:1=普通,2=黄金,3=钻石 | +| region | varchar(50) | 区域(可选) | +| mobile | varchar(50) | 手机号(加密) | +| wechat_id | varchar(100) | 微信号(可选) | +| team_leader_id | bigint | 团队首领ID(钻石代理的ID) | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态:0=未删除,1=已删除 | +| version | bigint | 版本号(乐观锁) | **索引**: - PRIMARY KEY (`id`) @@ -83,35 +83,35 @@ #### 2.1.2 agent_wallet(代理钱包表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID(唯一) | -| balance | decimal(10,2) | 可用余额 | -| frozen_balance | decimal(10,2) | 冻结余额 | -| total_earnings | decimal(10,2) | 累计收益 | -| withdrawn_amount | decimal(10,2) | 已提现金额 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ---------------- | ------------- | -------------- | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID(唯一) | +| balance | decimal(10,2) | 可用余额 | +| frozen_balance | decimal(10,2) | 冻结余额 | +| total_earnings | decimal(10,2) | 累计收益 | +| withdrawn_amount | decimal(10,2) | 已提现金额 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | #### 2.1.4 agent_relation(代理关系表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| parent_id | bigint | 上级代理ID | -| child_id | bigint | 下级代理ID | -| relation_type | tinyint | 关系类型:1=直接关系,2=已脱离 | -| detach_reason | varchar(200) | 脱离原因 | -| detach_time | datetime | 脱离时间 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ------------- | ------------ | ------------------------------ | +| id | bigint | 主键ID | +| parent_id | bigint | 上级代理ID | +| child_id | bigint | 下级代理ID | +| relation_type | tinyint | 关系类型:1=直接关系,2=已脱离 | +| detach_reason | varchar(200) | 脱离原因 | +| detach_time | datetime | 脱离时间 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **关系类型说明**: - `relation_type = 1`: 直接关系(正常上下级) @@ -122,45 +122,45 @@ #### 2.1.5 agent_link(推广链接表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| user_id | bigint | 用户ID | -| product_id | bigint | 产品ID | -| link_identifier | varchar(200) | 链接标识(加密) | -| set_price | decimal(10,2) | 设定价格 | +| 字段 | 类型 | 说明 | +| ----------------- | ------------- | ----------------------------- | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID | +| user_id | bigint | 用户ID | +| product_id | bigint | 产品ID | +| link_identifier | varchar(200) | 链接标识(加密) | +| set_price | decimal(10,2) | 设定价格 | | actual_base_price | decimal(10,2) | 实际底价(基础底价+等级加成) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **唯一约束**: - UNIQUE KEY `uk_agent_product_price` (`agent_id`, `product_id`, `set_price`, `del_state`) #### 2.1.6 agent_order(代理订单表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| order_id | bigint | 订单ID | -| product_id | bigint | 产品ID | -| order_amount | decimal(10,2) | 订单金额(SetPrice) | -| set_price | decimal(10,2) | 设定价格 | -| actual_base_price | decimal(10,2) | 实际底价 | -| price_cost | decimal(10,2) | 提价成本 | -| agent_profit | decimal(10,2) | 代理收益 | -| process_status | tinyint | 处理状态:0=待处理,1=处理成功,2=处理失败 | -| process_time | datetime | 处理时间 | -| process_remark | varchar(500) | 处理备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ----------------- | ------------- | ------------------------------------------ | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID | +| order_id | bigint | 订单ID | +| product_id | bigint | 产品ID | +| order_amount | decimal(10,2) | 订单金额(SetPrice) | +| set_price | decimal(10,2) | 设定价格 | +| actual_base_price | decimal(10,2) | 实际底价 | +| price_cost | decimal(10,2) | 提价成本 | +| agent_profit | decimal(10,2) | 代理收益 | +| process_status | tinyint | 处理状态:0=待处理,1=处理成功,2=处理失败 | +| process_time | datetime | 处理时间 | +| process_remark | varchar(500) | 处理备注 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **处理状态说明**: - `process_status = 0`: 待处理(订单创建后) @@ -169,38 +169,38 @@ #### 2.1.7 agent_commission(代理佣金表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| order_id | bigint | 订单ID | -| product_id | bigint | 产品ID | -| amount | decimal(10,2) | 佣金金额(代理收益) | -| status | tinyint | 状态:1=已发放 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ----------- | ------------- | -------------------- | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID | +| order_id | bigint | 订单ID | +| product_id | bigint | 产品ID | +| amount | decimal(10,2) | 佣金金额(代理收益) | +| status | tinyint | 状态:1=已发放 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | #### 2.1.8 agent_rebate(代理返佣表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 获得返佣的代理ID | -| source_agent_id | bigint | 来源代理ID(产生订单的代理) | -| order_id | bigint | 订单ID | -| product_id | bigint | 产品ID | -| rebate_type | tinyint | 返佣类型:1=直接上级返佣,2=钻石上级返佣,3=黄金上级返佣 | -| level_bonus | decimal(10,2) | 等级加成金额 | -| rebate_amount | decimal(10,2) | 返佣金额 | -| status | tinyint | 状态:1=已发放 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| --------------- | ------------- | -------------------------------------------------------- | +| id | bigint | 主键ID | +| agent_id | bigint | 获得返佣的代理ID | +| source_agent_id | bigint | 来源代理ID(产生订单的代理) | +| order_id | bigint | 订单ID | +| product_id | bigint | 产品ID | +| rebate_type | tinyint | 返佣类型:1=直接上级返佣,2=钻石上级返佣,3=黄金上级返佣 | +| level_bonus | decimal(10,2) | 等级加成金额 | +| rebate_amount | decimal(10,2) | 返佣金额 | +| status | tinyint | 状态:1=已发放 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **返佣类型说明**: - `rebate_type = 1`: 直接上级返佣(普通代理的等级加成给直接上级) @@ -209,25 +209,25 @@ #### 2.1.9 agent_upgrade(代理升级表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| from_level | tinyint | 原等级 | -| to_level | tinyint | 目标等级 | -| upgrade_type | tinyint | 升级类型:1=自主付费,2=钻石升级下级 | -| upgrade_fee | decimal(10,2) | 升级费用 | -| rebate_amount | decimal(10,2) | 返佣金额(给原直接上级) | -| rebate_agent_id | bigint | 返佣代理ID(原直接上级) | -| operator_agent_id | bigint | 操作代理ID(钻石升级下级时使用) | -| order_no | varchar(100) | 支付订单号 | -| status | tinyint | 状态:1=待处理,2=已完成,3=已失败 | -| remark | varchar(500) | 备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ----------------- | ------------- | ------------------------------------ | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID | +| from_level | tinyint | 原等级 | +| to_level | tinyint | 目标等级 | +| upgrade_type | tinyint | 升级类型:1=自主付费,2=钻石升级下级 | +| upgrade_fee | decimal(10,2) | 升级费用 | +| rebate_amount | decimal(10,2) | 返佣金额(给原直接上级) | +| rebate_agent_id | bigint | 返佣代理ID(原直接上级) | +| operator_agent_id | bigint | 操作代理ID(钻石升级下级时使用) | +| order_no | varchar(100) | 支付订单号 | +| status | tinyint | 状态:1=待处理,2=已完成,3=已失败 | +| remark | varchar(500) | 备注 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **升级类型说明**: - `upgrade_type = 1`: 自主付费升级(代理自己付费) @@ -235,23 +235,23 @@ #### 2.1.10 agent_withdrawal(代理提现表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| withdraw_no | varchar(100) | 提现单号(唯一) | -| payee_account | varchar(100) | 收款账户(支付宝账号) | -| payee_name | varchar(100) | 收款人姓名 | -| amount | decimal(10,2) | 提现金额 | -| tax_amount | decimal(10,2) | 税费金额 | -| actual_amount | decimal(10,2) | 实际到账金额 | -| status | tinyint | 状态:1=待审核,2=审核通过,3=审核拒绝,4=提现中,5=提现成功,6=提现失败 | -| remark | varchar(500) | 备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ------------- | ------------- | ------------------------------------------------------------------------ | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID | +| withdraw_no | varchar(100) | 提现单号(唯一) | +| payee_account | varchar(100) | 收款账户(支付宝账号) | +| payee_name | varchar(100) | 收款人姓名 | +| amount | decimal(10,2) | 提现金额 | +| tax_amount | decimal(10,2) | 税费金额 | +| actual_amount | decimal(10,2) | 实际到账金额 | +| status | tinyint | 状态:1=待审核,2=审核通过,3=审核拒绝,4=提现中,5=提现成功,6=提现失败 | +| remark | varchar(500) | 备注 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **状态说明**: - `status = 1`: 待审核(代理申请后) @@ -263,25 +263,25 @@ #### 2.1.11 agent_withdrawal_tax(代理提现扣税表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| withdrawal_id | bigint | 提现记录ID | -| year_month | bigint | 年月(格式:YYYYMM,如202401) | -| withdrawal_amount | decimal(10,2) | 提现金额 | -| taxable_amount | decimal(10,2) | 应税金额 | -| tax_rate | decimal(5,4) | 税率 | -| tax_amount | decimal(10,2) | 税费金额 | -| actual_amount | decimal(10,2) | 实际到账金额 | -| tax_status | tinyint | 扣税状态:1=待扣税,2=已扣税 | -| tax_time | datetime | 扣税时间 | -| remark | varchar(500) | 备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ----------------- | ------------- | ------------------------------ | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID | +| withdrawal_id | bigint | 提现记录ID | +| year_month | bigint | 年月(格式:YYYYMM,如202401) | +| withdrawal_amount | decimal(10,2) | 提现金额 | +| taxable_amount | decimal(10,2) | 应税金额 | +| tax_rate | decimal(5,4) | 税率 | +| tax_amount | decimal(10,2) | 税费金额 | +| actual_amount | decimal(10,2) | 实际到账金额 | +| tax_status | tinyint | 扣税状态:1=待扣税,2=已扣税 | +| tax_time | datetime | 扣税时间 | +| remark | varchar(500) | 备注 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **扣税状态说明**: - `tax_status = 1`: 待扣税(提现申请后) @@ -289,18 +289,18 @@ #### 2.1.12 agent_config(代理系统配置表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| config_key | varchar(100) | 配置键(唯一) | -| config_value | varchar(500) | 配置值 | -| config_type | varchar(50) | 配置类型:price=价格,bonus=等级加成,upgrade=升级费用,rebate=返佣,tax=税费 | -| description | varchar(500) | 配置描述 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ------------ | ------------ | ----------------------------------------------------------------------------- | +| id | bigint | 主键ID | +| config_key | varchar(100) | 配置键(唯一) | +| config_value | varchar(500) | 配置值 | +| config_type | varchar(50) | 配置类型:price=价格,bonus=等级加成,upgrade=升级费用,rebate=返佣,tax=税费 | +| description | varchar(500) | 配置描述 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **配置键列表**: - `base_price`: 基础底价 @@ -319,36 +319,36 @@ #### 2.1.13 agent_product_config(代理产品配置表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| product_id | bigint | 产品ID(唯一) | -| product_name | varchar(100) | 产品名称 | -| base_price | decimal(10,2) | 基础底价(可覆盖系统配置) | -| system_max_price | decimal(10,2) | 系统价格上限(可覆盖系统配置) | -| price_threshold | decimal(10,2) | 提价标准阈值(可选,覆盖系统配置) | -| price_fee_rate | decimal(5,4) | 提价手续费比例(可选,覆盖系统配置) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ---------------- | ------------- | ------------------------------------ | +| id | bigint | 主键ID | +| product_id | bigint | 产品ID(唯一) | +| product_name | varchar(100) | 产品名称 | +| base_price | decimal(10,2) | 基础底价(可覆盖系统配置) | +| system_max_price | decimal(10,2) | 系统价格上限(可覆盖系统配置) | +| price_threshold | decimal(10,2) | 提价标准阈值(可选,覆盖系统配置) | +| price_fee_rate | decimal(5,4) | 提价手续费比例(可选,覆盖系统配置) | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | #### 2.1.14 agent_real_name(代理实名认证表) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID(唯一) | -| name | varchar(50) | 真实姓名 | -| id_card | varchar(50) | 身份证号(加密) | -| mobile | varchar(50) | 手机号(加密) | -| verify_time | datetime | 验证时间(三要素验证通过时间,NULL表示未验证) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | +| 字段 | 类型 | 说明 | +| ----------- | ----------- | ---------------------------------------------- | +| id | bigint | 主键ID | +| agent_id | bigint | 代理ID(唯一) | +| name | varchar(50) | 真实姓名 | +| id_card | varchar(50) | 身份证号(加密) | +| mobile | varchar(50) | 手机号(加密) | +| verify_time | datetime | 验证时间(三要素验证通过时间,NULL表示未验证) | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态 | +| version | bigint | 版本号 | **验证说明**: - `verify_time IS NULL`: 未验证(未通过三要素核验或未提交) @@ -382,19 +382,19 @@ agent (代理表) #### 3.1.1 等级定义 -| 等级 | 数值 | 名称 | 说明 | -|------|------|------|------| -| Level 1 | 1 | 普通代理 | 初始等级,所有新代理默认等级 | -| Level 2 | 2 | 黄金代理 | 中级等级,可通过付费或钻石升级获得 | -| Level 3 | 3 | 钻石代理 | 最高等级,团队首领,可通过付费获得 | +| 等级 | 数值 | 名称 | 说明 | +| ------- | ---- | -------- | ---------------------------------- | +| Level 1 | 1 | 普通代理 | 初始等级,所有新代理默认等级 | +| Level 2 | 2 | 黄金代理 | 中级等级,可通过付费或钻石升级获得 | +| Level 3 | 3 | 钻石代理 | 最高等级,团队首领,可通过付费获得 | #### 3.1.2 等级加成规则 -| 等级 | 等级加成 | 说明 | -|------|---------|------| -| 普通代理 | +6元 | 实际底价 = 基础底价 + 6元 | -| 黄金代理 | +3元 | 实际底价 = 基础底价 + 3元 | -| 钻石代理 | +0元 | 实际底价 = 基础底价 | +| 等级 | 等级加成 | 说明 | +| -------- | -------- | ------------------------- | +| 普通代理 | +6元 | 实际底价 = 基础底价 + 6元 | +| 黄金代理 | +3元 | 实际底价 = 基础底价 + 3元 | +| 钻石代理 | +0元 | 实际底价 = 基础底价 | **计算公式**: ``` @@ -414,13 +414,12 @@ agent (代理表) **核心原则**: 1. **下级不能比上级等级高**: 下级等级必须 ≤ 上级等级 2. **同级不能作为上下级**(除了普通代理): 黄金和钻石不能作为同级上下级 -3. **钻石 → 黄金禁止**: 钻石代理不能直接管理黄金代理(特殊规则) **允许的关系**: - 普通 → 普通 ✓(同级普通允许) - 黄金 → 普通 ✓(上级等级高于下级) - 钻石 → 普通 ✓(上级等级高于下级) -- 钻石 → 黄金 ✓(上级等级高于下级,但实际禁止,见规则3) +- 钻石 → 黄金 ✓(上级等级高于下级,允许建立关系) **禁止的关系**: - 普通 → 黄金 ✗(下级等级高于上级) @@ -428,7 +427,11 @@ agent (代理表) - 黄金 → 黄金 ✗(同级不能作为上下级) - 钻石 → 钻石 ✗(同级不能作为上下级) - 黄金 → 钻石 ✗(下级等级高于上级) -- 钻石 → 黄金 ✗(特殊规则禁止) + +**升级后脱离关系的规则**: +- 下级等级 > 上级等级 → 脱离关系 +- 同级(黄金或钻石)→ 脱离关系 +- 普通代理同级 → 不脱离关系(允许保持) #### 3.2.3 升级规则 @@ -437,7 +440,10 @@ agent (代理表) **升级场景**: 1. **普通 → 黄金**: - - 升级后必须脱离直接上级关系(因为黄金等级高于普通,或与黄金同级) + - 升级后根据上级等级决定是否脱离关系: + * 如果上级是普通代理:必须脱离(下级等级高于上级) + * 如果上级是黄金代理:必须脱离(同级不能作为上下级) + * 如果上级是钻石代理:不脱离(钻石等级高于黄金,允许保持关系) - 保留团队关系(通过团队首领钻石代理) - 仍属于原团队 - 所有下级(直接+间接)继续跟随该代理 @@ -468,12 +474,12 @@ agent (代理表) **升级费用和返佣规则**: -| 升级类型 | 升级费用 | 直接上级返佣 | 说明 | -|---------|---------|-------------|------| -| 普通→黄金 | 199元 | 139元 | 付费后立即返佣给直接上级 | -| 普通→钻石 | 980元 | 680元 | 付费后立即返佣给直接上级 | -| 黄金→钻石 | 980元 | 680元 | 付费后立即返佣给直接上级 | -| 钻石升级下级(普通→黄金) | 免费 | 无 | 钻石代理操作,被升级代理无需付费 | +| 升级类型 | 升级费用 | 直接上级返佣 | 说明 | +| ------------------------- | -------- | ------------ | -------------------------------- | +| 普通→黄金 | 199元 | 139元 | 付费后立即返佣给直接上级 | +| 普通→钻石 | 980元 | 680元 | 付费后立即返佣给直接上级 | +| 黄金→钻石 | 980元 | 680元 | 付费后立即返佣给直接上级 | +| 钻石升级下级(普通→黄金) | 免费 | 无 | 钻石代理操作,被升级代理无需付费 | **重要规则**: - ✅ **返佣给原直接上级**: 即使升级后脱离直接上下级关系,返佣仍然给原直接上级 @@ -515,25 +521,47 @@ agent (代理表) #### 3.4.2 等级加成返佣分配 -**普通代理(等级加成6元)**: -1. **优先级1**: 给直接上级(最多3元) - - 如果直接上级存在,分配3元给直接上级 - - 剩余金额 = 6 - 3 = 3元 -2. **优先级2**: 给钻石上级(剩余金额全部) - - 查找上级链中的钻石代理 - - 如果找到,剩余金额全部给钻石上级 -3. **优先级3**: 给黄金上级(最多3元,如果钻石上级不存在) - - 如果钻石上级不存在,查找黄金上级 - - 如果找到,最多分配3元给黄金上级 - - 如果剩余金额 > 3元,超出部分归平台 -4. **优先级4**: 都没有,剩余金额归平台 +**核心原则**: +- **基础底价(BasePrice)**:无论哪个等级,全部归平台(固定收入) +- **等级加成部分**:作为返佣分配给上级链 -**黄金代理(等级加成3元)**: -1. 全部给钻石上级(如有) -2. 如果找不到钻石上级,归平台 +**分配规则总结表**: -**钻石代理(等级加成0元)**: -- 无返佣分配 +| 代理等级 | 等级加成 | 直接上级 | 等级加成返佣分配规则 | +| -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------ | +| 钻石 | 0元 | 无 | 无返佣分配(加成为0) | +| 黄金 | 3元 | 钻石 | 3元全部给钻石上级 | +| 普通 | 6元 | 钻石 | 6元全部给钻石上级 | +| 普通 | 6元 | 黄金 | 3元给黄金上级,3元给钻石上级(上上级) | +| 普通 | 6元 | 普通 | 2元给直接上级普通,剩余4元:
- 有钻石:4元全部给钻石
- 只有黄金:3元给黄金,1元归平台
- 都没有:4元归平台 | + +**详细规则说明**: + +1. **普通代理(等级加成,例如6元)**: + - **直接上级是钻石**:等级加成全部给钻石上级 + - **直接上级是黄金**:一部分给黄金上级(配置:`normal_to_gold_rebate`,默认3元),剩余给钻石上级(上上级) + - **直接上级是普通**: + - 一部分给直接上级普通(配置:`normal_to_normal_rebate`,默认2元) + - 剩余金额: + - 有钻石上级:剩余全部给钻石上级 + - 只有黄金上级:最多给黄金上级(配置:`normal_to_gold_rebate_max`,默认3元),超出部分归平台 + - 都没有:全部归平台 + - **注意**:如果在团队上下级链路中这中间有很多层普通代理,给直接上级的金额只给推广人的直接上级,对于中间的会直接跳过,然后到黄金/钻石代理 + +2. **黄金代理(等级加成,例如3元)**: + - 全部给钻石上级(如有) + - 如果找不到钻石上级,归平台 + +3. **钻石代理(等级加成0元)**: + - 无返佣分配 + +**配置项说明**: +- 返佣金额的具体取值从配置表读取,支持动态配置 +- 配置键: + - `normal_to_normal_rebate`:普通代理给直接上级普通的金额(默认2元) + - `normal_to_gold_rebate`:普通代理给直接上级黄金的金额(默认3元) + - `normal_to_gold_rebate_max`:普通代理给黄金上级的最大金额(默认3元) +- 如果配置不存在,使用默认值 #### 3.4.3 返佣记录 @@ -923,8 +951,7 @@ agent (代理表) ┌─────────────────────────────────────────────────────────────┐ │ 12. 检查是否需要脱离直接上级关系 │ │ - 检查升级后等级是否高于上级 │ -│ - 检查是否同级(黄金/钻石) │ -│ - 检查是否钻石→黄金(禁止) │ +│ - 检查是否同级(黄金/钻石,普通代理同级除外) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌───────────────────────────────┐ @@ -1377,50 +1404,50 @@ func calculateTax(ctx context.Context, agentId int64, amount float64, yearMonth #### 6.1.1 公开接口(无需登录) -| 接口 | 方法 | 路径 | 说明 | -|------|------|------|------| -| 获取推广链接数据 | GET | `/api/v1/agent/link` | 根据LinkIdentifier获取推广链接信息 | +| 接口 | 方法 | 路径 | 说明 | +| ---------------------- | ---- | --------------------- | -------------------------------------------- | +| 获取推广链接数据 | GET | `/api/v1/agent/link` | 根据LinkIdentifier获取推广链接信息 | | 通过邀请码申请成为代理 | POST | `/api/v1/agent/apply` | 用户通过邀请码申请成为代理(必须提供邀请码) | #### 6.1.2 需要登录的接口 -| 接口 | 方法 | 路径 | 说明 | -|------|------|------|------| -| ~~查询代理申请状态~~ | ~~GET~~ | ~~`/api/v1/agent/audit/status`~~ | ~~查询当前用户的代理申请审核状态(已废弃:无需审核)~~ | -| 查看代理信息 | GET | `/api/v1/agent/info` | 获取当前代理的详细信息 | -| 生成推广链接 | POST | `/api/v1/agent/generating_link` | 生成产品推广链接 | -| 获取产品配置 | GET | `/api/v1/agent/product_config` | 获取代理可配置的产品价格信息 | -| 获取团队统计 | GET | `/api/v1/agent/team/statistics` | 获取团队统计数据 | -| 获取下级列表 | GET | `/api/v1/agent/subordinate/list` | 分页查询直接下级列表 | -| 获取收益信息 | GET | `/api/v1/agent/revenue` | 获取钱包余额和收益信息 | -| 获取佣金记录 | GET | `/api/v1/agent/commission/list` | 分页查询佣金记录 | -| 获取返佣记录 | GET | `/api/v1/agent/rebate/list` | 分页查询返佣记录 | -| 获取升级记录 | GET | `/api/v1/agent/upgrade/list` | 分页查询升级记录 | -| 申请升级 | POST | `/api/v1/agent/upgrade/apply` | 自主付费升级 | -| 钻石升级下级 | POST | `/api/v1/agent/upgrade/subordinate` | 钻石代理升级下级 | -| 获取提现列表 | GET | `/api/v1/agent/withdrawal/list` | 分页查询提现记录 | -| 申请提现 | POST | `/api/v1/agent/withdrawal/apply` | 申请提现 | -| 实名认证 | POST | `/api/v1/agent/real_name` | 提交实名认证信息(三要素核验,自动通过) | +| 接口 | 方法 | 路径 | 说明 | +| -------------------- | ------- | ----------------------------------- | ------------------------------------------------------ | +| ~~查询代理申请状态~~ | ~~GET~~ | ~~`/api/v1/agent/audit/status`~~ | ~~查询当前用户的代理申请审核状态(已废弃:无需审核)~~ | +| 查看代理信息 | GET | `/api/v1/agent/info` | 获取当前代理的详细信息 | +| 生成推广链接 | POST | `/api/v1/agent/generating_link` | 生成产品推广链接 | +| 获取产品配置 | GET | `/api/v1/agent/product_config` | 获取代理可配置的产品价格信息 | +| 获取团队统计 | GET | `/api/v1/agent/team/statistics` | 获取团队统计数据 | +| 获取下级列表 | GET | `/api/v1/agent/subordinate/list` | 分页查询直接下级列表 | +| 获取收益信息 | GET | `/api/v1/agent/revenue` | 获取钱包余额和收益信息 | +| 获取佣金记录 | GET | `/api/v1/agent/commission/list` | 分页查询佣金记录 | +| 获取返佣记录 | GET | `/api/v1/agent/rebate/list` | 分页查询返佣记录 | +| 获取升级记录 | GET | `/api/v1/agent/upgrade/list` | 分页查询升级记录 | +| 申请升级 | POST | `/api/v1/agent/upgrade/apply` | 自主付费升级 | +| 钻石升级下级 | POST | `/api/v1/agent/upgrade/subordinate` | 钻石代理升级下级 | +| 获取提现列表 | GET | `/api/v1/agent/withdrawal/list` | 分页查询提现记录 | +| 申请提现 | POST | `/api/v1/agent/withdrawal/apply` | 申请提现 | +| 实名认证 | POST | `/api/v1/agent/real_name` | 提交实名认证信息(三要素核验,自动通过) | ### 6.2 后台管理接口(admin_agent.api) -| 接口 | 方法 | 路径 | 说明 | -|------|------|------|------| -| 代理分页查询 | GET | `/api/v1/admin/agent/list` | 分页查询代理列表 | -| ~~代理审核~~ | ~~POST~~ | ~~`/api/v1/admin/agent/audit`~~ | ~~审核代理申请(已废弃:通过邀请码直接成为代理,无需审核)~~ | -| 推广链接分页查询 | GET | `/api/v1/admin/agent/link/list` | 分页查询推广链接 | -| 代理订单分页查询 | GET | `/api/v1/admin/agent/order/list` | 分页查询代理订单 | -| 代理佣金分页查询 | GET | `/api/v1/admin/agent/commission/list` | 分页查询佣金记录 | -| 代理返佣分页查询 | GET | `/api/v1/admin/agent/rebate/list` | 分页查询返佣记录 | -| 代理升级记录分页查询 | GET | `/api/v1/admin/agent/upgrade/list` | 分页查询升级记录 | -| 代理提现分页查询 | GET | `/api/v1/admin/agent/withdrawal/list` | 分页查询提现记录 | -| 代理提现审核 | POST | `/api/v1/admin/agent/withdrawal/audit` | 审核提现申请 | -| 代理实名认证分页查询 | GET | `/api/v1/admin/agent/real_name/list` | 分页查询实名认证记录 | -| ~~代理实名认证审核~~ | ~~POST~~ | ~~`/api/v1/admin/agent/real_name/audit`~~ | ~~审核实名认证(已废弃:改为三要素核验,无需审核)~~ | -| 系统配置查询 | GET | `/api/v1/admin/agent/config` | 查询系统配置 | -| 系统配置更新 | POST | `/api/v1/admin/agent/config/update` | 更新系统配置 | -| 产品配置分页查询 | GET | `/api/v1/admin/agent/product_config/list` | 分页查询产品配置 | -| 产品配置更新 | POST | `/api/v1/admin/agent/product_config/update` | 更新产品配置 | +| 接口 | 方法 | 路径 | 说明 | +| -------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------ | +| 代理分页查询 | GET | `/api/v1/admin/agent/list` | 分页查询代理列表 | +| ~~代理审核~~ | ~~POST~~ | ~~`/api/v1/admin/agent/audit`~~ | ~~审核代理申请(已废弃:通过邀请码直接成为代理,无需审核)~~ | +| 推广链接分页查询 | GET | `/api/v1/admin/agent/link/list` | 分页查询推广链接 | +| 代理订单分页查询 | GET | `/api/v1/admin/agent/order/list` | 分页查询代理订单 | +| 代理佣金分页查询 | GET | `/api/v1/admin/agent/commission/list` | 分页查询佣金记录 | +| 代理返佣分页查询 | GET | `/api/v1/admin/agent/rebate/list` | 分页查询返佣记录 | +| 代理升级记录分页查询 | GET | `/api/v1/admin/agent/upgrade/list` | 分页查询升级记录 | +| 代理提现分页查询 | GET | `/api/v1/admin/agent/withdrawal/list` | 分页查询提现记录 | +| 代理提现审核 | POST | `/api/v1/admin/agent/withdrawal/audit` | 审核提现申请 | +| 代理实名认证分页查询 | GET | `/api/v1/admin/agent/real_name/list` | 分页查询实名认证记录 | +| ~~代理实名认证审核~~ | ~~POST~~ | ~~`/api/v1/admin/agent/real_name/audit`~~ | ~~审核实名认证(已废弃:改为三要素核验,无需审核)~~ | +| 系统配置查询 | GET | `/api/v1/admin/agent/config` | 查询系统配置 | +| 系统配置更新 | POST | `/api/v1/admin/agent/config/update` | 更新系统配置 | +| 产品配置分页查询 | GET | `/api/v1/admin/agent/product_config/list` | 分页查询产品配置 | +| 产品配置更新 | POST | `/api/v1/admin/agent/product_config/update` | 更新产品配置 | --- @@ -1428,32 +1455,32 @@ func calculateTax(ctx context.Context, agentId int64, amount float64, yearMonth ### 7.1 系统配置项(agent_config表) -| 配置键 | 配置类型 | 默认值 | 说明 | -|--------|---------|--------|------| -| `base_price` | price | - | 基础底价(系统默认) | -| `system_max_price` | price | - | 系统价格上限 | -| `price_threshold` | price | - | 提价标准阈值 | -| `price_fee_rate` | price | - | 提价手续费比例(0-1之间的小数) | -| `level_1_bonus` | bonus | 6 | 普通代理等级加成(元) | -| `level_2_bonus` | bonus | 3 | 黄金代理等级加成(元) | -| `level_3_bonus` | bonus | 0 | 钻石代理等级加成(元) | -| `upgrade_to_gold_fee` | upgrade | 199 | 升级为黄金费用(元) | -| `upgrade_to_diamond_fee` | upgrade | 980 | 升级为钻石费用(元) | -| `upgrade_to_gold_rebate` | rebate | 139 | 升级为黄金返佣(元) | -| `upgrade_to_diamond_rebate` | rebate | 680 | 升级为钻石返佣(元) | -| `tax_rate` | tax | 0.06 | 税率(默认6%,即0.06) | -| `tax_exemption_amount` | tax | 0 | 免税额度(元,默认0) | +| 配置键 | 配置类型 | 默认值 | 说明 | +| --------------------------- | -------- | ------ | ------------------------------- | +| `base_price` | price | - | 基础底价(系统默认) | +| `system_max_price` | price | - | 系统价格上限 | +| `price_threshold` | price | - | 提价标准阈值 | +| `price_fee_rate` | price | - | 提价手续费比例(0-1之间的小数) | +| `level_1_bonus` | bonus | 6 | 普通代理等级加成(元) | +| `level_2_bonus` | bonus | 3 | 黄金代理等级加成(元) | +| `level_3_bonus` | bonus | 0 | 钻石代理等级加成(元) | +| `upgrade_to_gold_fee` | upgrade | 199 | 升级为黄金费用(元) | +| `upgrade_to_diamond_fee` | upgrade | 980 | 升级为钻石费用(元) | +| `upgrade_to_gold_rebate` | rebate | 139 | 升级为黄金返佣(元) | +| `upgrade_to_diamond_rebate` | rebate | 680 | 升级为钻石返佣(元) | +| `tax_rate` | tax | 0.06 | 税率(默认6%,即0.06) | +| `tax_exemption_amount` | tax | 0 | 免税额度(元,默认0) | ### 7.2 产品配置项(agent_product_config表) 每个产品可以单独配置以下参数,覆盖系统默认配置: -| 字段 | 说明 | -|------|------| -| `base_price` | 产品基础底价(覆盖系统配置) | -| `system_max_price` | 产品价格上限(覆盖系统配置) | -| `price_threshold` | 产品提价标准阈值(可选,覆盖系统配置) | -| `price_fee_rate` | 产品提价手续费比例(可选,覆盖系统配置) | +| 字段 | 说明 | +| ------------------ | ---------------------------------------- | +| `base_price` | 产品基础底价(覆盖系统配置) | +| `system_max_price` | 产品价格上限(覆盖系统配置) | +| `price_threshold` | 产品提价标准阈值(可选,覆盖系统配置) | +| `price_fee_rate` | 产品提价手续费比例(可选,覆盖系统配置) | ### 7.3 配置优先级 @@ -1712,13 +1739,12 @@ func calculateAgentProfit(setPrice, actualBasePrice, priceCost float64) float64 ### 11.1 升级后关系脱离规则 -**Q**: 为什么普通代理升级为黄金后要脱离直接上级关系? +**Q**: 普通代理升级为黄金后什么情况下会脱离直接上级关系? **A**: 根据关系约束规则: -- 下级不能比上级等级高 -- 同级不能作为上下级(除了普通代理) -- 如果直接上级是普通代理,升级后黄金等级高于普通,必须脱离 -- 如果直接上级是黄金/钻石,升级后同级,也必须脱离 +- **下级不能比上级等级高**:如果直接上级是普通代理,升级后黄金等级高于普通,必须脱离 +- **同级不能作为上下级(除了普通代理)**:如果直接上级是黄金代理,升级后同级,必须脱离 +- **钻石可以拥有黄金下级**:如果直接上级是钻石代理,升级后钻石等级高于黄金,不脱离关系,保持直接下级关系 ### 11.2 返佣分配规则 diff --git a/新代理系统检查清单.md b/新代理系统检查清单.md index 85b468d..449f137 100644 --- a/新代理系统检查清单.md +++ b/新代理系统检查清单.md @@ -386,8 +386,8 @@ ### 6.1 关系约束验证 - [ ] 下级等级不能高于上级 - [ ] 同级不能作为上下级(除了普通代理) -- [ ] 钻石→黄金禁止(特殊规则) -- [ ] 升级后关系脱离逻辑 +- [ ] 钻石可以拥有黄金下级(钻石等级高于黄金) +- [ ] 升级后关系脱离逻辑(根据上级等级判断) ### 6.2 团队首领逻辑 - [ ] 钻石代理独立成团队(`team_leader_id = 自己`) diff --git a/短链系统实现说明.md b/短链系统实现说明.md new file mode 100644 index 0000000..5101bd7 --- /dev/null +++ b/短链系统实现说明.md @@ -0,0 +1,135 @@ +# 短链系统实现说明 + +## 一、功能说明 + +短链系统用于生成推广链接和邀请链接的短链,格式为:`https://推广域名/s/{shortCode}` + +### 支持两种类型 +1. **推广报告(promotion)**:类型值为1 + - 用于推广产品查询服务 + - 前端传入目标地址,如:`/agent/promotionInquire/{linkIdentifier}` + +2. **邀请好友(invite)**:类型值为2 + - 用于邀请用户成为代理 + - 前端传入目标地址,如:`/register?invite_code=XXXXX` + +### 工作流程 +1. 前端调用接口时传入 `target_path`(目标地址) +2. 后端生成6位随机短链标识 +3. 短链存储在 `agent_short_link` 表中,包含类型和目标地址 +4. 返回短链URL:`https://推广域名/s/{shortCode}` +5. 用户访问短链时,根据 `target_path` 重定向到对应页面 + +## 二、执行步骤 + +### 1. 执行SQL创建表 + +```bash +# 执行SQL文件创建短链表 +mysql -u root -p your_database < deploy/sql/add_agent_short_link_table.sql +``` + +或者直接在数据库客户端执行 `deploy/sql/add_agent_short_link_table.sql` 文件。 + +### 2. 生成Model + +使用goctl工具生成Model代码: + +```bash +# Windows PowerShell +cd ycc-proxy-server +goctl model mysql datasource -url="ycc:5vg67b3UNHu8@tcp(127.0.0.1:21001)/ycc" -table="agent_short_link" -dir="./app/main/model" --home="./deploy/template" -cache=true --style=goZero + +# Linux/Mac +cd ycc-proxy-server +goctl model mysql datasource -url="ycc:5vg67b3UNHu8@tcp(127.0.0.1:21001)/ycc" -table="agent_short_link" -dir="./app/main/model" --home="./deploy/template" -cache=true --style=goZero +``` + +### 3. 代码已实现 + +代码已经实现完成,生成Model后即可使用。 + +### 4. 配置推广域名 + +在配置文件中设置推广域名: + +```yaml +# main.yaml 或 main.dev.yaml +Promotion: + PromotionDomain: "https://promo.example.com" # 推广域名 +``` + +## 三、接口说明 + +### 短链重定向接口 + +- **路径**: `/s/{shortCode}` +- **方法**: `GET` +- **说明**: 不需要 `/api/v1` 前缀,直接放在根路径 +- **功能**: 根据短链标识查询对应的推广链接,重定向到推广页面 + +### 生成推广链接接口 + +- **路径**: `/api/v1/agent/generating_link` +- **方法**: `POST` +- **请求参数**: + ```json + { + "product_id": 1, + "set_price": 10.00, + "target_path": "/agent/promotionInquire/{linkIdentifier}" // 前端传入目标地址 + } + ``` +- **返回**: + ```json + { + "link_identifier": "加密的链接标识", + "full_link": "https://推广域名/s/xxxxxx" + } + ``` + +### 生成邀请链接接口 + +- **路径**: `/api/v1/agent/invite_link` +- **方法**: `GET` +- **请求参数**: + - `invite_code`: 邀请码 + - `target_path`: 目标地址(可选,默认:`/register?invite_code=xxx`) +- **返回**: + ```json + { + "invite_link": "https://推广域名/s/xxxxxx" + } + ``` + +## 四、数据库表结构 + +### agent_short_link 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键ID | +| type | tinyint | 类型:1=推广报告,2=邀请好友 | +| link_id | bigint | 推广链接ID(关联agent_link表,仅推广报告使用) | +| invite_code_id | bigint | 邀请码ID(关联agent_invite_code表,仅邀请好友使用) | +| link_identifier | varchar(200) | 推广链接标识(加密,仅推广报告使用) | +| invite_code | varchar(50) | 邀请码(仅邀请好友使用) | +| short_code | varchar(20) | 短链标识(6位随机字符串) | +| target_path | varchar(500) | 目标地址(前端传入,如:/agent/promotionInquire/xxx) | +| promotion_domain | varchar(200) | 推广域名 | +| create_time | datetime | 创建时间 | +| update_time | datetime | 更新时间 | +| delete_time | datetime | 删除时间 | +| del_state | tinyint | 删除状态:0=未删除,1=已删除 | +| version | bigint | 版本号(乐观锁) | + +## 五、注意事项 + +1. 短链标识是6位随机字符串(大小写字母+数字) +2. 同一推广链接或邀请码同一类型只会生成一个短链(通过唯一索引保证) +3. 短链重定向使用 `target_path`(相对路径),域名切换由服务器配置处理 +4. 如果推广域名未配置,生成短链时会返回空字符串 +5. **前端必须传入 `target_path` 参数**,后端只负责确定推广域名并生成短链 +6. 推广报告类型:前端传入 `/agent/promotionInquire/{linkIdentifier}` +7. 邀请好友类型:前端传入 `/register?invite_code=XXXXX`(如果不传则使用默认值) + diff --git a/解冻任务优化建议.md b/解冻任务优化建议.md new file mode 100644 index 0000000..bad524c --- /dev/null +++ b/解冻任务优化建议.md @@ -0,0 +1,177 @@ +# 解冻任务优化建议 + +## 当前实现分析 + +### 优点 +✅ 使用信号量控制并发,避免数据库压力过大 +✅ 使用事务和乐观锁保证数据一致性 +✅ 双重检查防止重复处理 +✅ 错误处理不中断整体流程 + +### 可改进点 + +## 1. ⚠️ 超时控制(重要) + +**问题**:如果某个任务处理时间过长(比如数据库慢查询),会阻塞整个扫描流程。 + +**建议**:为每个任务添加超时控制 + +```go +// 为每个任务设置30秒超时 +taskCtx, cancel := context.WithTimeout(ctx, 30*time.Second) +defer cancel() + +err := l.svcCtx.AgentFreezeTaskModel.Trans(taskCtx, func(transCtx context.Context, session sqlx.Session) error { + // ... 处理逻辑 +}) +``` + +## 2. 📊 批次大小限制(可选) + +**问题**:如果任务量非常大(比如几千个),一次性查询所有会占用大量内存。 + +**建议**:如果任务量超过一定数量,可以分批处理 + +```go +const maxBatchSize = 1000 // 每次最多查询1000个 +if len(freezeTasks) > maxBatchSize { + logx.Warnf("任务数量过多(%d),本次只处理前%d个,剩余将在下次扫描处理", len(freezeTasks), maxBatchSize) + freezeTasks = freezeTasks[:maxBatchSize] +} +``` + +## 3. 🔄 错误分类处理 + +**问题**:所有错误都统一处理,没有区分临时错误和永久错误。 + +**建议**:区分错误类型,临时错误可以重试,永久错误跳过 + +```go +if err != nil { + // 判断错误类型 + if isTemporaryError(err) { + // 临时错误,记录但继续处理其他任务 + logx.Errorf("解冻任务临时失败,将在下次扫描重试: freezeTaskId=%d, err=%v", task.Id, err) + } else { + // 永久错误(如数据异常),记录详细日志 + logx.Errorf("解冻任务永久失败: freezeTaskId=%d, err=%v", task.Id, err) + } +} +``` + +## 4. ⏱️ 处理时间监控 + +**问题**:缺少处理时间统计,无法评估性能。 + +**建议**:记录处理时间,便于监控和优化 + +```go +startTime := time.Now() +// ... 处理逻辑 +duration := time.Since(startTime) +logx.Infof("解冻任务处理耗时: freezeTaskId=%d, duration=%v", task.Id, duration) +``` + +## 5. 🛡️ 优雅关闭支持 + +**问题**:如果服务关闭,正在处理的任务可能被中断。 + +**建议**:检查 context 是否被取消 + +```go +select { +case <-ctx.Done(): + logx.Infof("扫描任务被取消,已处理: 成功=%d, 失败=%d", successCount, failCount) + return ctx.Err() +default: + // 继续处理 +} +``` + +## 6. 📈 延迟统计 + +**问题**:无法知道任务延迟了多久才被处理。 + +**建议**:记录延迟时间,便于监控 + +```go +delay := time.Since(currentTask.UnfreezeTime) +if delay > 1*time.Hour { + logx.Warnf("解冻任务延迟处理: freezeTaskId=%d, 延迟=%v", task.Id, delay) +} +``` + +## 7. 🔍 数据库连接池监控 + +**问题**:并发处理时可能耗尽数据库连接池。 + +**建议**:监控连接池使用情况,如果连接数不足,降低并发数 + +```go +// 可以根据数据库连接池情况动态调整并发数 +maxConcurrency := 2 +if dbConnPoolAvailable < 5 { + maxConcurrency = 1 // 连接池紧张时降低并发 +} +``` + +## 8. 🎯 幂等性增强 + +**问题**:虽然有乐观锁,但可以进一步优化。 + +**建议**:添加更多的幂等性检查 + +```go +// 检查是否已经解冻过(通过 actual_unfreeze_time) +if currentTask.ActualUnfreezeTime.Valid { + logx.Infof("任务已解冻,跳过: freezeTaskId=%d", task.Id) + return nil +} +``` + +## 9. 📝 更详细的日志 + +**问题**:日志信息可以更详细,便于排查问题。 + +**建议**:添加更多上下文信息 + +```go +logx.Infof("解冻任务详情: freezeTaskId=%d, agentId=%d, amount=%.2f, orderPrice=%.2f, freezeTime=%v, unfreezeTime=%v, delay=%v", + task.Id, currentTask.AgentId, currentTask.FreezeAmount, currentTask.OrderPrice, + currentTask.FreezeTime, currentTask.UnfreezeTime, delay) +``` + +## 10. 🔧 配置化并发数 + +**问题**:并发数硬编码为2,不够灵活。 + +**建议**:从配置表读取并发数 + +```go +// 从配置表读取并发数,默认2 +maxConcurrency, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(ctx, "unfreeze_max_concurrency") +if err != nil || maxConcurrency == nil { + maxConcurrency = 2 // 默认值 +} +``` + +## 优先级建议 + +### 🔴 高优先级(建议立即实现) +1. **超时控制** - 防止任务卡死 +2. **错误分类处理** - 提高可靠性 + +### 🟡 中优先级(建议后续优化) +3. **处理时间监控** - 便于性能优化 +4. **延迟统计** - 便于监控 +5. **更详细的日志** - 便于排查问题 + +### 🟢 低优先级(可选) +6. **批次大小限制** - 如果任务量不大可以不做 +7. **优雅关闭** - 如果服务很少重启可以不做 +8. **配置化并发数** - 如果并发数不需要调整可以不做 + +## 实施建议 + +建议先实现**超时控制**和**错误分类处理**,这两个对可靠性影响最大。其他优化可以根据实际运行情况逐步添加。 + diff --git a/解冻任务实现方案说明.md b/解冻任务实现方案说明.md new file mode 100644 index 0000000..0beb0d3 --- /dev/null +++ b/解冻任务实现方案说明.md @@ -0,0 +1,132 @@ +# 解冻任务实现方案说明 + +## 方案对比 + +### 方案1:Asynq 延迟任务(已实现但未使用) +**优点:** +- ✅ 精确到秒级执行 +- ✅ 自动重试机制 +- ✅ 无需额外调度器 + +**缺点:** +- ❌ 依赖 Redis 持久化,Redis 数据丢失会导致任务丢失 +- ❌ 系统长时间停机可能导致延迟任务过期 +- ❌ 需要补偿机制 + +### 方案2:定时任务扫描(✅ 已采用) +**优点:** +- ✅ **数据持久化在数据库,更可靠**(核心优势) +- ✅ **系统停机后重启,定时任务会继续扫描并处理**(核心优势) +- ✅ 可以批量处理,效率高 +- ✅ 已有定时任务基础设施 +- ✅ 不依赖 Redis 持久化 + +**缺点:** +- ⚠️ 执行时间不够精确(取决于扫描频率,如每5分钟扫描一次) +- ⚠️ 需要处理并发扫描(已通过乐观锁解决) + +## 最终选择:定时任务扫描方案 + +### 选择理由 +1. **金融场景,可靠性优先**:涉及资金解冻,必须保证任务不丢失 +2. **解冻时间允许延迟**:解冻时间可以有一定的延迟(比如几分钟内都可以接受) +3. **已有基础设施**:项目中已有定时任务实现(`cleanQueryData.go`) +4. **数据库表已设计好**:`status` 和 `unfreeze_time` 字段支持扫描查询 + +## 实现细节 + +### 1. 定时任务配置 +- **执行频率**:每2小时执行一次(`0 */2 * * *`)- 节省性能 +- **任务类型**:`MsgUnfreezeCommissionScan` +- **处理器**:`UnfreezeCommissionScanHandler` +- **批次大小**:每次最多处理2个任务,避免并发太多 + +### 2. 扫描逻辑 +```go +// 查询条件: +// - status = 1(待解冻) +// - unfreeze_time <= 当前时间 +// - del_state = 0(未删除) +// - 按 unfreeze_time 升序排序 +``` + +### 3. 并发安全 +- 使用**乐观锁**(`version` 字段)确保并发安全 +- 每个任务在事务中处理,确保原子性 +- 双重检查:查询后再次检查状态,防止并发处理 + +### 4. 错误处理 +- 单个任务失败不影响其他任务 +- 记录详细的错误日志 +- 失败的任务会在下次扫描时重试 + +### 5. 性能优化 +- 使用数据库索引优化查询(`idx_status` 和 `idx_unfreeze_time`) +- **扫描频率**:每2小时扫描一次,减少数据库查询压力 +- **查询所有任务**:每次扫描找到所有需要解冻的任务(不限制数量) +- **并发控制**:使用信号量(Semaphore)限制最多同时处理2个任务 +- **批量处理**:所有任务都会处理,但通过并发控制避免同时处理太多,节省性能 + +## 代码文件 + +### 新增文件 +- `app/main/api/internal/queue/unfreezeCommissionScan.go` - 定时扫描处理器 + +### 修改文件 +- `app/main/api/internal/queue/routes.go` - 注册定时任务 +- `app/main/api/internal/queue/agentProcess.go` - 移除发送延迟任务的逻辑 +- `app/main/api/internal/types/taskname.go` - 添加任务类型常量 + +### 保留文件(备用) +- `app/main/api/internal/queue/unfreezeCommission.go` - 延迟任务处理器(保留作为备用) +- `app/main/api/internal/service/asynqService.go` - `SendUnfreezeTask` 方法(保留作为备用) + +## 执行流程 + +``` +定时任务启动(每2小时) + ↓ +扫描数据库:status=1 AND unfreeze_time <= 当前时间 + ↓ +查询所有需要解冻的任务(不限制数量) + ↓ +并发处理(使用信号量限制最多同时2个) + ├─ 任务1(goroutine 1) + ├─ 任务2(goroutine 2) + ├─ 任务3(等待,直到前2个完成) + ├─ 任务4(等待,直到前2个完成) + └─ ...(以此类推,两个两个处理) + ↓ +每个任务使用事务 + 乐观锁处理 + ↓ +更新任务状态:status = 2(已解冻) + ↓ +更新钱包:FrozenBalance -= 冻结金额, Balance += 冻结金额 + ↓ +等待所有任务完成 + ↓ +记录日志 +``` + +## 监控建议 + +1. **监控扫描任务执行情况** + - 检查定时任务是否正常执行 + - 监控每次扫描找到的任务数量 + - 监控成功/失败数量 + +2. **监控解冻延迟** + - 记录 `actual_unfreeze_time - unfreeze_time` 的差值 + - 如果延迟超过10分钟,需要检查定时任务是否正常 + +3. **监控异常情况** + - 冻结余额不足的情况(数据异常) + - 任务状态异常的情况 + +## 后续优化建议 + +1. **可配置扫描频率**:将扫描频率(当前5分钟)配置到配置表 +2. **批次大小限制**:如果任务量很大,可以限制每次处理的数量 +3. **告警机制**:如果连续多次扫描都失败,发送告警 +4. **补偿机制**:提供手动触发扫描的接口,用于紧急情况 + diff --git a/退款时代理处理逻辑分析.md b/退款时代理处理逻辑分析.md new file mode 100644 index 0000000..c1be031 --- /dev/null +++ b/退款时代理处理逻辑分析.md @@ -0,0 +1,232 @@ +# 退款时代理处理逻辑分析 + +## 当前流程分析 + +### 1. 订单支付成功后的流程 + +``` +支付成功 + ↓ +支付回调(支付宝/微信) + ↓ +更新订单状态为 "paid" + ↓ +发送异步任务 SendQueryTask(order.Id) + ↓ +PaySuccessNotifyUserHandler.ProcessTask + ├─ 创建查询记录(query表,状态为 "pending") + ├─ 生成授权书 + ├─ 调用API请求服务(第164行)← 可能失败 + ├─ 如果API成功: + │ ├─ 更新查询状态为 "success" + │ └─ 发送代理处理任务 SendAgentProcessTask(第192行) + └─ 如果API失败: + └─ handleError → 退款 → 更新订单状态为 "refunded" +``` + +### 2. 代理订单创建时机 + +**在支付时创建**(`paymentlogic.go:316-327`): +- 代理订单在用户支付时创建 +- `ProcessStatus = 0`(待处理) +- 此时还没有发放佣金和返佣 + +### 3. 代理处理任务执行时机 + +**只在查询成功后发送**(`paySuccessNotify.go:192`): +```go +// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程) +if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil { + // 代理处理任务发送失败,只记录日志,不影响报告流程 + logx.Errorf("发送代理处理任务失败,订单ID: %d, 错误: %v", order.Id, asyncErr) +} +``` + +### 4. API调用失败时的处理(第164-167行) + +```go +combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) +if err != nil { + return l.handleError(ctx, err, order, query) +} +``` + +**handleError 的处理**(第206-257行): +1. 删除Redis缓存 +2. 更新查询状态为 `failed` +3. **退款** +4. 更新订单状态为 `refunded` + +**关键点**:此时代理处理任务**还没有发送**(因为查询还没成功) + +--- + +## 问题分析 + +### ✅ 情况1:API调用失败,退款(正常情况) + +**流程**: +1. 支付成功,创建代理订单(`ProcessStatus = 0`) +2. 调用API失败(第164-167行) +3. 进入 `handleError`,退款 +4. 更新订单状态为 `refunded` +5. **代理处理任务还没发送**(查询未成功) + +**代理状态**: +- ✅ 代理订单 `ProcessStatus = 0`(未处理) +- ✅ 代理**没有收到**佣金 +- ✅ 代理**没有收到**返佣 +- ✅ **处理正确** + +### ⚠️ 情况2:查询成功但订单被退款(边界情况) + +**可能的场景**: +1. 支付成功,创建代理订单(`ProcessStatus = 0`) +2. 调用API成功,查询状态更新为 `success` +3. 发送代理处理任务(第192行) +4. **但在代理处理任务执行前**,订单被退款(比如管理员手动退款) + +**代理处理任务的保护机制**(`agentProcess.go:43-46`): +```go +// 检查订单状态 +if order.Status != "paid" { + logx.Infof("代理处理任务跳过,订单未支付: orderID=%d, status=%s", payload.OrderID, order.Status) + return nil // 订单未支付,不处理,不重试 +} +``` + +**代理状态**: +- ✅ 如果订单状态是 `refunded`,代理处理任务会跳过 +- ✅ 代理订单 `ProcessStatus` 仍然是 0 +- ✅ 代理**没有收到**佣金和返佣 +- ✅ **处理正确** + +### ⚠️ 情况3:代理已处理但订单被退款(需要处理) + +**可能的场景**: +1. 支付成功,创建代理订单(`ProcessStatus = 0`) +2. 调用API成功,查询状态更新为 `success` +3. 发送代理处理任务 +4. 代理处理任务执行,发放佣金和返佣(`ProcessStatus = 1`) +5. **之后**订单被退款(比如管理员手动退款) + +**当前问题**: +- ❌ **代理已经收到佣金和返佣** +- ❌ **没有撤销代理收益的逻辑** +- ❌ **退款回调中也没有处理代理订单的逻辑** + +--- + +## 发现的问题 + +### 问题1:退款回调中缺少代理订单处理 + +**当前退款回调逻辑**(`wechatpayrefundcallbacklogic.go` 和 `alipayrefundcallbacklogic.go`): +- ✅ 只更新订单状态和退款记录 +- ❌ **没有检查代理订单** +- ❌ **没有撤销代理收益** + +### 问题2:管理员手动退款时缺少代理订单处理 + +**当前管理员退款逻辑**(`adminrefundorderlogic.go`): +- ✅ 创建退款记录,更新订单状态 +- ❌ **没有检查代理订单** +- ❌ **没有撤销代理收益** + +--- + +## 建议的解决方案 + +### 方案1:在退款回调中处理代理订单(推荐) + +在退款成功回调中,检查代理订单并撤销收益: + +```go +// 在 handleQueryOrderRefund 中添加代理订单处理 +if status == refunddomestic.STATUS_SUCCESS { + // 更新订单状态 + order.Status = orderStatus + // ... + + // 检查并处理代理订单 + agentOrder, err := l.svcCtx.AgentOrderModel.FindOneByOrderId(ctx, order.Id) + if err == nil && agentOrder.ProcessStatus == 1 { + // 代理订单已处理,需要撤销收益 + err = l.svcCtx.AgentService.CancelAgentCommission(ctx, order.Id) + if err != nil { + logx.Errorf("撤销代理收益失败,订单ID: %d, 错误: %v", order.Id, err) + // 不阻断退款流程,只记录日志 + } + } +} +``` + +### 方案2:在管理员退款时处理代理订单 + +在 `AdminRefundOrderLogic` 中添加代理订单检查和处理。 + +### 方案3:在代理处理任务中增加订单状态检查(已有保护) + +当前已有保护机制(`agentProcess.go:43-46`),如果订单状态不是 `paid`,会跳过处理。 + +--- + +## 当前处理是否正确? + +### ✅ 对于 API 调用失败的情况 + +**完全正确**: +- 如果API调用失败(第164-167行),会进入退款流程 +- 此时代理处理任务还没发送(因为查询未成功) +- 代理订单 `ProcessStatus = 0`,代理没有收到收益 +- **处理正确,无需修改** + +### ⚠️ 对于已处理代理订单的退款情况 + +**存在问题**: +- 如果代理订单已经处理(`ProcessStatus = 1`),代理已收到佣金和返佣 +- 此时订单退款,**没有撤销代理收益的逻辑** +- 这会导致: + - 用户收到退款 + - 但代理仍然保留佣金和返佣 + - **资金不一致** + +--- + +## 建议修改 + +### 1. 在退款回调中添加代理订单检查 + +### 2. 在管理员退款中添加代理订单检查 + +### 3. 创建撤销代理收益的方法 + +```go +// CancelAgentCommission 撤销代理收益(订单退款时调用) +func (s *AgentService) CancelAgentCommission(ctx context.Context, orderId int64) error { + // 1. 查找代理订单 + // 2. 检查是否已处理 + // 3. 撤销佣金(从钱包扣除) + // 4. 撤销返佣(从上级钱包扣除) + // 5. 更新代理订单状态 + // 6. 创建撤销记录 +} +``` + +--- + +## 总结 + +### 当前情况(API调用失败退款) + +✅ **处理正确**: +- API调用失败时,代理处理任务还没发送 +- 代理订单未处理,代理没有收益 +- **无需修改** + +### 需要补充的场景 + +⚠️ **需要处理**: +- 代理订单已处理(`ProcessStatus = 1`)后订单退款的情况 +- 需要在退款回调和管理员退款中添加撤销代理收益的逻辑 + diff --git a/邀请码使用历史功能说明.md b/邀请码使用历史功能说明.md new file mode 100644 index 0000000..ce2c789 --- /dev/null +++ b/邀请码使用历史功能说明.md @@ -0,0 +1,107 @@ +# 邀请码使用历史功能说明 + +## 问题描述 + +当前系统中,`agent_invite_code` 表只记录了最后一次使用情况(`used_user_id`, `used_agent_id`, `used_time`)。对于普通邀请码(可以无限使用),每次使用都会覆盖之前的记录,导致: + +1. **无法统计**:无法统计每个邀请码总共邀请了多少代理 +2. **无法查询**:无法查询某个代理是通过哪个邀请码成为代理的 +3. **历史丢失**:无法保留完整的使用历史记录 + +## 解决方案 + +### 1. 创建使用历史表 + +创建 `agent_invite_code_usage` 表,记录每次邀请码使用的详细信息。 + +### 2. 在 agent 表中添加字段 + +在 `agent` 表中添加 `invite_code_id` 字段,便于直接查询代理是通过哪个邀请码成为的。 + +## 实施步骤 + +### 第一步:执行 SQL 迁移 + +```bash +# 执行 SQL 文件 +mysql -u root -p your_database < deploy/sql/add_invite_code_usage_table.sql +``` + +### 第二步:生成 Model + +```bash +# 生成 AgentInviteCodeUsage Model +goctl model mysql datasource -url="root:password@tcp(localhost:3306)/database" \ + -table="agent_invite_code_usage" \ + -dir="app/main/model" \ + -cache=true \ + --style=goZero \ + --home="./deploy/template" +``` + +### 第三步:更新 ServiceContext + +在 `app/main/api/internal/svc/servicecontext.go` 中添加: + +```go +AgentInviteCodeUsageModel model.AgentInviteCodeUsageModel +``` + +并在 `NewServiceContext` 中初始化: + +```go +AgentInviteCodeUsageModel: model.NewAgentInviteCodeUsageModel(db, cacheConf), +``` + +### 第四步:更新逻辑代码 + +更新以下文件,在使用邀请码时记录使用历史: + +1. `app/main/api/internal/logic/agent/registerbyinvitecodelogic.go` +2. `app/main/api/internal/logic/agent/applyforagentlogic.go` + +### 第五步:更新 Agent Model(可选) + +如果 agent 表添加了 `invite_code_id` 字段,需要重新生成 Agent Model 或手动更新。 + +## 功能说明 + +### 统计功能 + +- **统计邀请码邀请数量**:通过 `agent_invite_code_usage` 表,可以统计每个邀请码邀请了多少代理 +- **查询代理来源**:可以通过 `agent.invite_code_id` 或 `agent_invite_code_usage.agent_id` 查询代理是通过哪个邀请码成为的 + +### 数据完整性 + +- **保留完整历史**:每次使用邀请码都会记录一条使用历史,不会丢失 +- **支持多次使用**:普通邀请码可以多次使用,每次使用都会记录 + +## API 查询示例 + +### 查询某个邀请码邀请了多少代理 + +```sql +SELECT COUNT(*) FROM agent_invite_code_usage +WHERE invite_code_id = ? AND del_state = 0; +``` + +### 查询某个代理是通过哪个邀请码成为的 + +```sql +-- 方式1:通过 agent 表(如果添加了 invite_code_id 字段) +SELECT invite_code_id FROM agent WHERE id = ?; + +-- 方式2:通过使用历史表 +SELECT invite_code_id, code FROM agent_invite_code_usage +WHERE agent_id = ? AND del_state = 0 +ORDER BY used_time DESC LIMIT 1; +``` + +### 查询某个邀请码的所有使用记录 + +```sql +SELECT * FROM agent_invite_code_usage +WHERE invite_code_id = ? AND del_state = 0 +ORDER BY used_time DESC; +``` + diff --git a/邀请链接和二维码生成逻辑说明.md b/邀请链接和二维码生成逻辑说明.md new file mode 100644 index 0000000..1864d0f --- /dev/null +++ b/邀请链接和二维码生成逻辑说明.md @@ -0,0 +1,211 @@ +# 邀请链接和二维码生成逻辑说明 + +## 一、邀请链接生成逻辑 + +### 1. API 端点 +- **路径**: `GET /agent/invite_link` +- **处理器**: `GetInviteLinkHandler` +- **逻辑**: `GetInviteLinkLogic` + +### 2. 生成流程 + +#### 步骤 1: 验证代理身份 +```go +// 获取当前用户ID +userID := ctxdata.GetUidFromCtx(ctx) + +// 查询代理信息 +agent := AgentModel.FindOneByUserId(userID) +``` + +#### 步骤 2: 生成邀请码 +- 生成一个8位随机邀请码(使用 `tool.Krand(8, tool.KC_RAND_KIND_ALL)`) +- 检查邀请码是否已存在(最多重试10次) +- 创建邀请码记录: + - `agent_id`: 当前代理ID + - `target_level`: 1(普通代理) + - `status`: 0(未使用) + - `expire_time`: NULL(不过期) + - `remark`: "邀请链接生成" + +#### 步骤 3: 构建邀请链接 +```go +frontendDomain := "https://example.com" // TODO: 需要配置 +inviteLink := fmt.Sprintf("%s/register?invite_code=%s", frontendDomain, inviteCode) +``` + +#### 步骤 4: 生成二维码URL +```go +qrCodeUrl := fmt.Sprintf("%s/api/v1/image/qrcode?type=invitation&content=%s", frontendDomain, inviteLink) +``` + +### 3. 当前问题 +- ❌ `frontendDomain` 硬编码为 `"https://example.com"`,需要配置化 +- ❌ 二维码API (`/api/v1/image/qrcode`) 可能还未实现 + +## 二、二维码生成逻辑 + +### 1. 两种生成方式 + +#### 方式 A: 前端生成(当前使用) +- **位置**: `ycc-proxy-webview/src/components/QRcode.vue` +- **库**: `qrcode` (npm) +- **逻辑**: + ```javascript + // 如果提供了后端返回的 qrCodeUrl,优先使用 + if (mode === "invitation" && qrCodeUrl) { + // 加载后端生成的二维码图片 + qrImg.src = qrCodeUrl; + } else { + // 前端生成二维码 + QRCode.toDataURL(url, { width: 150, margin: 0 }); + } + ``` + +#### 方式 B: 后端生成(已实现服务但可能缺少API端点) +- **服务**: `ImageService.ProcessImageWithQRCode()` +- **功能**: + - 支持两种类型:`promote`(推广)和 `invitation`(邀请) + - 加载背景图片(`static/images/yq_qrcode_1.png` 或 `tg_qrcode_1.png`) + - 生成二维码并合成到背景图上 + - 返回 PNG 格式图片 + +### 2. 二维码海报生成流程 + +#### 推广海报(promote模式) +- **背景图**: `static/images/tg_qrcode_1.png` - `tg_qrcode_8.jpg`(8张轮播图) +- **二维码位置**: 左下角 + - 尺寸: 280px + - 位置: X=192px, Y=距离底部190px +- **用途**: 推广产品查询服务 + +#### 邀请海报(invitation模式) +- **背景图**: `static/images/yq_qrcode_1.png` +- **二维码位置**: 中间偏上 + - 尺寸: 360px + - 位置: 水平居中,垂直位置Y=555px +- **用途**: 邀请好友成为下级代理 + +### 3. 前端海报合成逻辑 + +```javascript +// 1. 加载海报背景图 +posterImg.src = posterImages[index]; + +// 2. 生成或加载二维码 +if (mode === "invitation" && qrCodeUrl) { + // 使用后端返回的二维码 + qrImg.src = qrCodeUrl; +} else { + // 前端生成二维码 + QRCode.toDataURL(url); +} + +// 3. 在Canvas上绘制 +ctx.drawImage(posterImg, 0, 0); // 背景 +ctx.drawImage(qrCodeImg, x, y, size, size); // 二维码 +``` + +## 三、数据流程 + +### 邀请链接流程 + +``` +用户点击"生成邀请链接" + ↓ +前端调用 GET /agent/invite_link + ↓ +后端逻辑 (GetInviteLinkLogic): + 1. 验证代理身份 + 2. 生成新的邀请码(8位随机,不过期) + 3. 保存到 agent_invite_code 表 + 4. 构建邀请链接: https://domain/register?invite_code=XXXXX + 5. 构建二维码URL: https://domain/api/v1/image/qrcode?type=invitation&content=... + ↓ +返回 { invite_link, qr_code_url } + ↓ +前端显示链接和二维码 +``` + +### 二维码使用流程 + +#### 邀请模式 (invitation) +``` +后端返回 qrCodeUrl + ↓ +前端 QRcode 组件加载二维码图片 + ↓ +如果加载成功: 直接使用后端生成的二维码海报 +如果加载失败: 降级到前端生成二维码 + ↓ +合成到邀请海报背景图 (yq_qrcode_1.png) + ↓ +用户可以保存或分享海报 +``` + +#### 推广模式 (promote) +``` +前端生成二维码 (使用 qrcode 库) + ↓ +合成到推广海报背景图 (tg_qrcode_1.png - 8.png) + ↓ +用户可以在多张海报中切换 + ↓ +用户可以保存或分享海报 +``` + +## 四、相关数据库表 + +### agent_invite_code +- 存储邀请码信息 +- 每个邀请链接对应一个邀请码记录 +- 备注:`"邀请链接生成"` 表示这是通过链接生成的 + +### agent_invite_code_usage +- 存储邀请码使用历史 +- 记录每个代理是通过哪个邀请码成为的 +- 支持统计和查询 + +## 五、待完善的问题 + +### 1. 前端域名配置 +- ❌ 当前硬编码为 `"https://example.com"` +- ✅ 应该从配置文件读取 + +### 2. 二维码API实现 +- ❓ `/api/v1/image/qrcode` 端点是否存在? +- ✅ `ImageService.ProcessImageWithQRCode()` 已实现 +- ❓ 是否需要创建对应的 Handler 和路由? + +### 3. 邀请码复用 +- 当前每次调用都生成新的邀请码 +- 是否应该复用已有的邀请码? + +## 六、链接格式对比 + +### 邀请链接(成为代理) +``` +格式: https://domain/register?invite_code=XXXXX +参数: invite_code (8位邀请码) +用途: 用户通过此链接注册成为代理 +``` + +### 推广链接(推广产品) +``` +格式: https://domain/agent/promotionInquire/{linkIdentifier} +参数: linkIdentifier (加密的JSON字符串,包含agent_id, product_id, set_price) +用途: 用户通过此链接查询产品 +``` + +## 七、文件位置 + +### 后端 +- **邀请链接逻辑**: `app/main/api/internal/logic/agent/getinvitelinklogic.go` +- **二维码服务**: `app/main/api/internal/service/imageService.go` +- **推广链接逻辑**: `app/main/api/internal/logic/agent/generatinglinklogic.go` + +### 前端 +- **二维码组件**: `ycc-proxy-webview/src/components/QRcode.vue` +- **邀请页面**: `ycc-proxy-webview/src/views/Invitation.vue` +- **推广查询页**: `ycc-proxy-webview/src/views/PromotionInquire.vue` +