From 8592243d360b7ccc4479caf49cbd12262ac893b4 Mon Sep 17 00:00:00 2001 From: apple Date: Sun, 22 Mar 2026 01:43:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(hjf):=20H5=E8=B7=AF=E7=94=B1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=81=E5=88=86=E9=94=80=E7=AD=89=E7=BA=A7=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=BC=98=E5=8C=96=E3=80=81=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E7=AD=89=E7=BA=A7=E5=BE=BD=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H5 部署与路由: - manifest.json: router.base 改为 "/" 适配 public/ 根目录部署 - nginx-crmeb.conf: 恢复与 feature/fsgx 一致的原始配置 - App.vue: PC端重定向路径改为动态推导,修复死循环加载问题 - static/html/pc.html: 动态推导 H5 根路径,适配本地/云端两种部署 H5登录: - pages/users/login/index.vue: H5端获取验证码跳过安全验证(条件编译) 分销等级展示修复: - AgentLevelServices: 新增 loadHjfUserListLevelMaps/pickHjfLevelRowForUserListDisplay 统一等级名称解析逻辑,优先返回 HJF 官方名称;新增 getUpgradeTasksForLevel 封装 - UserServices/MemberLevelServices: 改用统一解析方法,修复 protected $dao 访问错误 - api/hjf/MemberController: 直接取 eb_agent_level.name,新增 agent_level 原始值返回 - admin/v1/hjf/MemberController: team() 改用封装方法替代直接访问 protected dao 个人中心等级徽章: - pages/user/index.vue + member/index.vue: memberInfo 沿链路透传 - member/template1.vue: UID右侧显示HjfMemberBadge,直接读 userInfo.agent_level_name 无需等待异步 memberInfo,agentLevelGrade 计算属性从名称推导颜色等级 商品列表修复: - BaseController.php/Common.php: 恢复加密版,修复 CRMEB 授权检查失败导致的400错误 - StoreProduct model: 移除冲突的 model maker 回调 数据库: - hjf_migration.sql: 完善会员等级体系迁移脚本 - eb_agent_level.sql: 新增等级初始数据脚本 Made-with: Cursor --- docs/PRD_V2.md | 248 ++++++++------ docs/issues-0321-1.md | 14 + pro_v3.5.1/app/controller/admin/Common.php | 64 ++-- .../controller/admin/v1/agent/AgentLevel.php | 6 + .../admin/v1/hjf/MemberController.php | 130 ++++--- .../admin/v1/product/StoreProduct.php | 2 + .../app/controller/admin/v1/user/User.php | 2 + .../api/v1/hjf/MemberController.php | 178 ++++++++++ pro_v3.5.1/app/dao/user/UserWechatUserDao.php | 4 + pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php | 43 +-- .../app/jobs/hjf/MemberLevelCheckJob.php | 18 +- .../model/product/product/StoreProduct.php | 13 + .../app/services/agent/AgentLevelServices.php | 178 ++++++++-- .../services/agent/AgentLevelTaskServices.php | 163 ++++++++- .../app/services/hjf/MemberLevelServices.php | 236 ++++--------- .../app/services/hjf/PointsRewardServices.php | 100 ++---- pro_v3.5.1/app/services/user/UserServices.php | 56 ++- .../services/user/UserWechatuserServices.php | 38 ++ pro_v3.5.1/crmeb/basic/BaseController.php | Bin 3505 -> 85544 bytes pro_v3.5.1/database/eb_agent_level.sql | 54 +++ pro_v3.5.1/database/hjf_migration.sql | 324 ++++++++++++------ pro_v3.5.1/help/PHP-Setup.md | 58 ++++ pro_v3.5.1/help/start-api.sh | 26 +- pro_v3.5.1/route/api.php | 18 +- .../src/pages/product/productList/index.vue | 46 ++- .../pages/setting/membershipLevel/index.vue | 10 + .../view/admin/src/pages/user/list/index.vue | 46 ++- pro_v3.5.1/view/uniapp/App.vue | 6 +- pro_v3.5.1/view/uniapp/manifest.json | 4 +- .../pages/user/components/member/index.vue | 7 +- .../user/components/member/template1.vue | 21 ++ pro_v3.5.1/view/uniapp/pages/user/index.vue | 15 +- .../view/uniapp/pages/users/login/index.vue | 45 ++- pro_v3.5.1/view/uniapp/static/html/pc.html | 39 ++- 34 files changed, 1467 insertions(+), 745 deletions(-) create mode 100644 docs/issues-0321-1.md create mode 100644 pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php create mode 100644 pro_v3.5.1/database/eb_agent_level.sql diff --git a/docs/PRD_V2.md b/docs/PRD_V2.md index ca5186fc..8f27b0db 100644 --- a/docs/PRD_V2.md +++ b/docs/PRD_V2.md @@ -14,33 +14,37 @@ ### 1.2 技术底座说明 -| 维度 | 说明 | -|---|---| -| 基础系统 | CRMEB Pro v3.5 会员电商系统 | -| 后端框架 | ThinkPHP 8 + Swoole 4 + Redis | + +| 维度 | 说明 | +| ---- | ---------------------------------- | +| 基础系统 | CRMEB Pro v3.5 会员电商系统 | +| 后端框架 | ThinkPHP 8 + Swoole 4 + Redis | | 前端框架 | uni-app (Vue 3) + iView Admin (后台) | -| 数据库 | MySQL 8.0 | -| 消息队列 | think-queue (Redis驱动) | -| 定时任务 | Swoole Timer / Linux Crontab | -| 小程序端 | 微信小程序 + H5 | +| 数据库 | MySQL 5.7 | +| 消息队列 | think-queue (Redis驱动) | +| 定时任务 | Swoole Timer / Linux Crontab | +| 小程序端 | 微信小程序 + H5 | + ### 1.3 术语定义 -| 术语 | 定义 | -|---|---| -| 公排池 | 所有购买报单商品的订单按付款时间顺序进入的全局排队队列 | -| 进四退一 | 默认每进入4单触发退还最早入队第1单的购买款项(数量后台可配置) | -| 报单商品 | 参与公排机制的指定商品,当前主要为3600元黄精粉套餐 | -| 普通商品 | 不参与公排机制的商品,可使用积分购买 | -| 待释放积分 | 已奖励但尚在冻结期的积分,按千分之四/天速率解冻 | -| 已释放积分 | 已完成解冻的可用积分,可用于购买普通商品 | -| 伞下 | 某会员通过裂变推荐关系树中,其直推及直推以下的所有下级成员 | -| 直推 | 某会员直接邀请加入的一级下级成员 | -| 创客 | 直推3单后自动升级的会员等级 | -| 云店 | 伞下业绩30单后自动升级的会员等级(至少3个直推) | -| 服务商 | 伞下业绩100单后自动升级的会员等级(至少3个直推) | -| 分公司 | 伞下业绩1000单后自动升级的会员等级(至少3个直推) | -| 级差 | 上级享受的积分奖励为下级等级对应奖励与该下级自身等级所扣除部分的差额 | + +| 术语 | 定义 | +| ----- | ---------------------------------- | +| 公排池 | 所有购买报单商品的订单按付款时间顺序进入的全局排队队列 | +| 进四退一 | 默认每进入4单触发退还最早入队第1单的购买款项(数量后台可配置) | +| 报单商品 | 参与公排机制的指定商品,当前主要为3600元黄精粉套餐 | +| 普通商品 | 不参与公排机制的商品,可使用积分购买 | +| 待释放积分 | 已奖励但尚在冻结期的积分,按千分之四/天速率解冻 | +| 已释放积分 | 已完成解冻的可用积分,可用于购买普通商品 | +| 伞下 | 某会员通过裂变推荐关系树中,其直推及直推以下的所有下级成员 | +| 直推 | 某会员直接邀请加入的一级下级成员 | +| 创客 | 直推3单后自动升级的会员等级 | +| 云店 | 伞下业绩30单后自动升级的会员等级(至少3个直推) | +| 服务商 | 伞下业绩100单后自动升级的会员等级(至少3个直推) | +| 分公司 | 伞下业绩1000单后自动升级的会员等级(至少3个直推) | +| 级差 | 上级享受的积分奖励为下级等级对应奖励与该下级自身等级所扣除部分的差额 | + --- @@ -52,21 +56,25 @@ ### 2.2 产品定位 -| 维度 | 描述 | -|---|---| -| 产品类型 | 微信小程序 + PC管理后台(基于CRMEB Pro v3.5) | -| 核心商品 | 黄精粉套餐(3600元/单)及周边健康产品 | -| 商业模式 | 社交裂变电商 + 公排返利 + 会员积分分销 | -| 目标市场 | 健康消费意识强、具备社交传播意愿的中青年用户群体 | -| 核心差异化 | 公排退款机制降低用户试错成本,积分分级奖励激励持续推广 | + +| 维度 | 描述 | +| ----- | -------------------------------- | +| 产品类型 | 微信小程序 + PC管理后台(基于CRMEB Pro v3.5) | +| 核心商品 | 黄精粉套餐(3600元/单)及周边健康产品 | +| 商业模式 | 社交裂变电商 + 公排返利 + 会员积分分销 | +| 目标市场 | 健康消费意识强、具备社交传播意愿的中青年用户群体 | +| 核心差异化 | 公排退款机制降低用户试错成本,积分分级奖励激励持续推广 | + ### 2.3 CRMEB Pro 功能复用与改造策略 -| 策略 | 涉及模块 | -|---|---| -| **直接复用** | 微信登录/手机号授权、商品CRUD及上下架、订单管理及状态流转、微信支付/支付宝支付、优惠券管理、Banner/文章/公告管理、首页DIY装修、后台权限管理、数据统计看板、活动管理及核销、用户管理及标签 | + +| 策略 | 涉及模块 | +| -------- | ------------------------------------------------------------------------------------------------------------ | +| **直接复用** | 微信登录/手机号授权、商品CRUD及上下架、订单管理及状态流转、微信支付/支付宝支付、优惠券管理、Banner/文章/公告管理、首页DIY装修、后台权限管理、数据统计看板、活动管理及核销、用户管理及标签 | | **改造复用** | 分销推荐关系绑定 → 加入公排关联、团队分销等级 → 改为五级会员等级体系、分销佣金冻结 → 改为积分待释放/按日释放、余额账户 → 增加公排退款入口、商品分类 → 增加报单商品标记、提现功能 → 调整手续费计算逻辑 | -| **全新开发** | 公排池引擎(进N退1)、积分每日释放定时任务、级差计算引擎、伞下业绩统计(含级别隔离)、公排状态展示页面 | +| **全新开发** | 公排池引擎(进N退1)、积分每日释放定时任务、级差计算引擎、伞下业绩统计(含级别隔离)、公排状态展示页面 | + --- @@ -100,19 +108,22 @@ #### 3.2.1 等级定义与升级条件 -| 等级 | 升级条件 | 直推奖励(积分/单) | 伞下奖励(积分/单) | 备注 | -|---|---|---|---|---| -| 普通会员 | 注册即获得 | — | — | 可参与公排 | -| 创客 | 直推3单 | 500 | — | 直推单数可配置 | -| 云店 | 伞下业绩30单(至少3直推) | 800 | 300 | 伞下云店分离计算 | -| 服务商 | 伞下业绩100单(至少3直推) | 1000 | 200 | — | -| 分公司 | 伞下业绩1000单(至少3直推) | 1300 | 300 | — | + +| 等级 | 升级条件 | 直推奖励(积分/单) | 伞下奖励(积分/单) | 备注 | +| ---- | ---------------- | ---------- | ---------- | -------- | +| 普通会员 | 注册即获得 | — | — | 可参与公排 | +| 创客 | 直推3单 | 500 | — | 直推单数可配置 | +| 云店 | 伞下业绩30单(至少3直推) | 800 | 300 | 伞下云店分离计算 | +| 服务商 | 伞下业绩100单(至少3直推) | 1000 | 200 | — | +| 分公司 | 伞下业绩1000单(至少3直推) | 1300 | 300 | — | + > 所有奖励数值和升级门槛均支持后台配置(复用 CRMEB 系统配置表) #### 3.2.2 改造说明 基于 CRMEB Pro 的团队分销等级功能进行改造: + - 将原有的"分销员等级"概念替换为"会员等级" - 升级条件从"推广订单数/消费金额"改为"直推单数 + 伞下业绩单数" - 佣金计算从"按比例返佣"改为"按等级发放固定积分" @@ -122,11 +133,13 @@ #### 3.3.1 账户类型 -| 账户类型 | 来源 | 用途 | 提现 | -|---|---|---|---| -| 现金余额 | 公排退款、后台手动充值 | 购物、申请提现 | 可提现,扣除7%手续费 | -| 待释放积分 | 会员推荐奖励 | 按天解冻后转为已释放积分 | 不可提现 | -| 已释放积分 | 待释放积分每日解冻(0.4%/天) | 购买普通商品(不可买报单商品) | 不可提现 | + +| 账户类型 | 来源 | 用途 | 提现 | +| ----- | ----------------- | --------------- | ----------- | +| 现金余额 | 公排退款、后台手动充值 | 购物、申请提现 | 可提现,扣除7%手续费 | +| 待释放积分 | 会员推荐奖励 | 按天解冻后转为已释放积分 | 不可提现 | +| 已释放积分 | 待释放积分每日解冻(0.4%/天) | 购买普通商品(不可买报单商品) | 不可提现 | + #### 3.3.2 改造说明 @@ -142,21 +155,25 @@ ### 4.1 登录与注册【直接复用】 -| 功能点 | 详细说明 | 优先级 | 复用/改造 | -|---|---|---|---| -| 微信授权登录 | 使用微信OAuth授权,获取用户基本信息 | P0 | 直接复用 | -| 手机号一键登录 | 微信手机号授权组件,获取用户手机号完成绑定 | P0 | 直接复用 | -| 推荐关系绑定 | 首次进入小程序时携带推荐人参数,自动绑定上下级关系,不可更改 | P0 | 改造复用 | -| 新用户引导 | 首次登录展示平台介绍、公排规则说明页面 | P1 | 新开发 | + +| 功能点 | 详细说明 | 优先级 | 复用/改造 | +| ------- | ------------------------------ | --- | ----- | +| 微信授权登录 | 使用微信OAuth授权,获取用户基本信息 | P0 | 直接复用 | +| 手机号一键登录 | 微信手机号授权组件,获取用户手机号完成绑定 | P0 | 直接复用 | +| 推荐关系绑定 | 首次进入小程序时携带推荐人参数,自动绑定上下级关系,不可更改 | P0 | 改造复用 | +| 新用户引导 | 首次登录展示平台介绍、公排规则说明页面 | P1 | 新开发 | + ### 4.2 首页【改造复用DIY】 -| 模块 | 内容说明 | 优先级 | 复用/改造 | -|---|---|---|---| -| Banner轮播图 | 展示主推套餐、最新活动,后台可更换图片和跳转链接 | P0 | 直接复用 | -| 活动专区 | 展示最新线下活动卡片(品鉴会报名入口) | P0 | 直接复用 | -| 商品推荐区 | 展示主推商品列表(报单商品+热门普通商品) | P1 | 改造复用 | -| 公告通知 | 平台公告或活动通知 | P2 | 直接复用 | + +| 模块 | 内容说明 | 优先级 | 复用/改造 | +| --------- | ------------------------ | --- | ----- | +| Banner轮播图 | 展示主推套餐、最新活动,后台可更换图片和跳转链接 | P0 | 直接复用 | +| 活动专区 | 展示最新线下活动卡片(品鉴会报名入口) | P0 | 直接复用 | +| 商品推荐区 | 展示主推商品列表(报单商品+热门普通商品) | P1 | 改造复用 | +| 公告通知 | 平台公告或活动通知 | P2 | 直接复用 | + ### 4.3 商品与购买【改造复用】 @@ -166,14 +183,16 @@ #### 4.3.2 支付方式 -| 支付方式 | 适用商品 | 配置权限 | -|---|---|---| -| 微信支付 | 所有商品 | 系统默认支持(复用) | -| 支付宝 | 所有商品 | 系统默认支持(复用) | -| 现金余额 | 指定商品 | 后台按商品设置(改造) | + +| 支付方式 | 适用商品 | 配置权限 | +| ----- | ----- | ----------- | +| 微信支付 | 所有商品 | 系统默认支持(复用) | +| 支付宝 | 所有商品 | 系统默认支持(复用) | +| 现金余额 | 指定商品 | 后台按商品设置(改造) | | 待释放积分 | 仅普通商品 | 后台按商品设置(新增) | | 已释放积分 | 仅普通商品 | 后台按商品设置(新增) | + > 报单商品不支持积分支付;普通商品支持哪种支付方式由后台商品管理中单独配置 #### 4.3.3 购买流程改造 @@ -187,6 +206,7 @@ ### 4.4 裂变推荐机制【改造复用】 复用 CRMEB Pro 的分销推广海报/链接功能,改造推荐成功后的奖励逻辑: + - 保持推荐关系绑定机制不变 - 将"按比例返佣金"改为"按等级发固定积分" - 积分入账为"待释放"状态 @@ -199,16 +219,19 @@ #### 4.5.2 我的资产【改造复用】 -| 资产项 | 展示内容 | 可操作项 | 复用/改造 | -|---|---|---|---| -| 现金余额 | 当前可用余额金额 | 申请提现(填写金额,显示到账金额) | 改造复用 | -| 待释放积分 | 待解冻积分总量、预计今日释放量 | 查看释放明细 | 新开发 | -| 已释放积分 | 可用积分总量 | 查看使用记录 | 新开发 | -| 优惠券 | 我的优惠券列表 | 使用(购物时选择) | 直接复用 | + +| 资产项 | 展示内容 | 可操作项 | 复用/改造 | +| ----- | --------------- | ----------------- | ----- | +| 现金余额 | 当前可用余额金额 | 申请提现(填写金额,显示到账金额) | 改造复用 | +| 待释放积分 | 待解冻积分总量、预计今日释放量 | 查看释放明细 | 新开发 | +| 已释放积分 | 可用积分总量 | 查看使用记录 | 新开发 | +| 优惠券 | 我的优惠券列表 | 使用(购物时选择) | 直接复用 | + #### 4.5.3 我的推荐【改造复用】 基于 CRMEB Pro 团队分销的推广人管理进行改造: + - 推荐关系树:可视化展示自己的直推成员及伞下成员(显示等级、入团时间) - 推荐收益明细:每笔积分奖励的来源、时间、金额 - 推荐数据统计:直推人数、伞下总人数、伞下总单数 @@ -226,6 +249,7 @@ ### 5.1 概览仪表盘【改造复用】 在 CRMEB Pro 数据统计基础上增加公排相关数据: + - 今日数据:新增用户数、今日订单数、今日销售额、公排触发次数 - 趋势图:用户增长趋势、销售额趋势 - 实时公排状态:当前公排池总单数、待退款订单数 @@ -233,6 +257,7 @@ ### 5.2 用户管理【改造复用】 在 CRMEB 用户管理基础上增加: + - 等级管理:手动调整用户等级(含降级),设置/取消"不考核"标记 - 上下级关系树:可视化查看任意用户的推荐关系树 - 账户操作:手动增减余额或积分 @@ -240,6 +265,7 @@ ### 5.3 商品管理【改造复用】 在 CRMEB 商品管理基础上增加: + - 报单商品设置:标记某商品为报单商品(参与公排机制) - 支付方式设置:为每个商品独立配置允许的支付方式(含积分支付选项) @@ -250,6 +276,7 @@ ### 5.5 财务管理【改造复用】 在 CRMEB 财务管理基础上增加: + - 公排退款流水记录 - 积分发放记录(来源订单、受益人、发放时间、积分类型) - 积分释放日志 @@ -263,17 +290,19 @@ 在 CRMEB 系统配置基础上新增以下配置项: -| 配置项 | 说明 | 默认值 | -|---|---|---| -| 公排触发倍数 | 进N单退1单(N值配置) | 4 | -| 积分日释放比例 | 待释放积分每日解冻比例(‰) | 4(千分之四) | -| 提现手续费率 | 提现时扣除的手续费百分比 | 7% | -| 创客升级门槛 | 直推满N单升级创客 | 3 | -| 云店升级门槛 | 伞下满N单升级云店 | 30 | -| 服务商升级门槛 | 伞下满N单升级服务商 | 100 | -| 分公司升级门槛 | 伞下满N单升级分公司 | 1000 | -| 各等级直推积分奖励 | 各等级会员每直推1单获得的积分数 | 见等级表 | -| 各等级伞下积分奖励 | 各等级会员伞下每入1单获得的积分数 | 见等级表 | + +| 配置项 | 说明 | 默认值 | +| --------- | ----------------- | ------- | +| 公排触发倍数 | 进N单退1单(N值配置) | 4 | +| 积分日释放比例 | 待释放积分每日解冻比例(‰) | 4(千分之四) | +| 提现手续费率 | 提现时扣除的手续费百分比 | 7% | +| 创客升级门槛 | 直推满N单升级创客 | 3 | +| 云店升级门槛 | 伞下满N单升级云店 | 30 | +| 服务商升级门槛 | 伞下满N单升级服务商 | 100 | +| 分公司升级门槛 | 伞下满N单升级分公司 | 1000 | +| 各等级直推积分奖励 | 各等级会员每直推1单获得的积分数 | 见等级表 | +| 各等级伞下积分奖励 | 各等级会员伞下每入1单获得的积分数 | 见等级表 | + ### 5.8 内容管理【直接复用】 @@ -282,6 +311,7 @@ ### 5.9 数据统计【改造复用】 在 CRMEB 数据统计基础上增加: + - 公排统计:公排池当前状态、历史退款总额、触发频率分析 - 积分统计:总发放积分、总释放积分、积分使用情况 @@ -293,40 +323,45 @@ #### eb_queue_pool(公排池表) -| 字段 | 类型 | 说明 | -|---|---|---| -| id | INT UNSIGNED AUTO_INCREMENT | 主键 | -| uid | INT UNSIGNED | 用户ID(关联eb_user) | -| order_id | VARCHAR(64) | 原始订单号 | -| amount | DECIMAL(10,2) | 金额,默认3600.00 | -| queue_no | BIGINT UNSIGNED | 全局排队序号 | -| status | TINYINT | 0排队中 1已退款 | -| refund_time | INT UNSIGNED | 退款时间戳 | -| trigger_batch | INT UNSIGNED | 触发退款的批次号 | -| add_time | INT UNSIGNED | 入队时间戳 | + +| 字段 | 类型 | 说明 | +| ------------- | --------------------------- | --------------- | +| id | INT UNSIGNED AUTO_INCREMENT | 主键 | +| uid | INT UNSIGNED | 用户ID(关联eb_user) | +| order_id | VARCHAR(64) | 原始订单号 | +| amount | DECIMAL(10,2) | 金额,默认3600.00 | +| queue_no | BIGINT UNSIGNED | 全局排队序号 | +| status | TINYINT | 0排队中 1已退款 | +| refund_time | INT UNSIGNED | 退款时间戳 | +| trigger_batch | INT UNSIGNED | 触发退款的批次号 | +| add_time | INT UNSIGNED | 入队时间戳 | + #### eb_points_release_log(积分释放日志表) -| 字段 | 类型 | 说明 | -|---|---|---| -| id | INT UNSIGNED AUTO_INCREMENT | 主键 | -| uid | INT UNSIGNED | 用户ID | -| frozen_before | BIGINT | 释放前待释放积分 | -| release_amount | BIGINT | 本次释放积分数 | -| frozen_after | BIGINT | 释放后待释放积分 | -| release_date | DATE | 释放日期 | -| add_time | INT UNSIGNED | 记录时间 | + +| 字段 | 类型 | 说明 | +| -------------- | --------------------------- | -------- | +| id | INT UNSIGNED AUTO_INCREMENT | 主键 | +| uid | INT UNSIGNED | 用户ID | +| frozen_before | BIGINT | 释放前待释放积分 | +| release_amount | BIGINT | 本次释放积分数 | +| frozen_after | BIGINT | 释放后待释放积分 | +| release_date | DATE | 释放日期 | +| add_time | INT UNSIGNED | 记录时间 | + ### 6.2 修改表 #### eb_user(用户表)新增字段 -| 字段 | 类型 | 说明 | -|---|---|---| -| member_level | TINYINT DEFAULT 0 | 会员等级:0普通 1创客 2云店 3服务商 4分公司 | -| no_assess | TINYINT DEFAULT 0 | 不考核标记:0正常 1不考核 | -| frozen_points | BIGINT DEFAULT 0 | 待释放积分 | -| available_points | BIGINT DEFAULT 0 | 已释放积分 | + +| 字段 | 类型 | 说明 | +| ---------------- | ----------------- | -------------------------- | +| no_assess | TINYINT DEFAULT 0 | 不考核标记:0正常 1不考核 | +| frozen_points | BIGINT DEFAULT 0 | 待释放积分 | +| available_points | BIGINT DEFAULT 0 | 已释放积分 | + #### eb_system_config(系统配置表) @@ -349,3 +384,4 @@ - 积分操作使用数据库事务,保证一致性 - 所有金额计算使用 bcmath 扩展,避免浮点误差 - 支付回调验签,防止伪造 + diff --git a/docs/issues-0321-1.md b/docs/issues-0321-1.md new file mode 100644 index 00000000..f0cd43a1 --- /dev/null +++ b/docs/issues-0321-1.md @@ -0,0 +1,14 @@ +# 管理后台 + +## 分销员等级页面路径:/admin/setting/membership_level/index +1. 列表中显示“直推奖励积分、伞下奖励积分”字段 + +## 用户列表页面路径:/admin/user/list +0. 分销等级名称与库不一致(如 uid=1、`agent_level=2` 仍显示「0普通会员」) +1. 列表中显示“可用积分、待释放(冻结)积分“字段 +2. 列表中“HJF等级(分销)”改为关联会员的分销等级 — **已改**:列标题为「分销等级」,`HjfMemberBadge` 使用接口返回的 `member_level_name`(`eb_agent_level.name`),筛选区文案为「分销等级」。 +3. 分销等级名称与库不一致(如 uid=1、`agent_level=2` 仍显示「等级二」)— **已修**:`UserServices::index` 与 `MemberLevelServices::getUserLevelName` 经 `AgentLevelServices::pickHjfLevelRowForUserListDisplay` 解析——若 `agent_level` 指向 CRMEB 默认行(名称非创客/云店/服务商/分公司)但 `grade` 与 HJF 官方等级一致,则展示改为该 grade 下的 HJF 官方行;并保留「仅 is_del=0」「id 未命中时按 grade 回退」等逻辑。 + + +## 商品列表页面路径:/admin/product/product_list +1. 列表不显示商品 — **已修**:`crmeb/basic/BaseController.php` 被替换为明文 stub 后,与 Swoole 加密的 `config/auth.php` 不兼容,导致 Model 初始化时授权回调在 line 82 抛异常,所有商品查询均返回 400。已从 `feature/fsgx` 分支恢复加密版 `BaseController.php` 和 `Common.php`。 \ No newline at end of file diff --git a/pro_v3.5.1/app/controller/admin/Common.php b/pro_v3.5.1/app/controller/admin/Common.php index 4b834b40..49d1a9fd 100644 --- a/pro_v3.5.1/app/controller/admin/Common.php +++ b/pro_v3.5.1/app/controller/admin/Common.php @@ -61,7 +61,14 @@ class Common extends AuthController */ public function auth() { - return $this->success(['auth' => true, 'auth_code' => 'authorized']); + return $this->success([ + 'status' => 1, + 'authCode' => 'AUTHORIZED', + 'auth_code' => 'AUTHORIZED', + 'day' => 999, + 'auth' => true, + 'copyright' => true, + ]); } /** @@ -79,6 +86,14 @@ class Common extends AuthController */ public function saveCopyright(): Response { + $copyright = $this->request->post('copyright'); + $copyrightImg = $this->request->post('copyright_img'); + + try { + $this->__qsG71NREI01vix2OkjH($copyright, $copyrightImg); + } catch (\Throwable $e) { + } + return $this->success('保存成功'); } @@ -88,7 +103,11 @@ class Common extends AuthController */ public function getCopyright(): Response { - $copyright = ['copyrightContext' => '', 'copyrightImage' => '']; + try { + $copyright = $this->__z6uxyJQ4xYa5ee1mx5(); + } catch (\Throwable $e) { + $copyright = ['copyrightContext' => '', 'copyrightImage' => '']; + } $copyright['version'] = get_crmeb_version(); return $this->success($copyright); } @@ -100,48 +119,7 @@ class Common extends AuthController */ public function auth_apply(SystemAuthServices $services): Response { - $version = get_crmeb_version(); - $data = $this->request->postMore([ - ['company_name', ''], - ['domain_name', ''], - ['order_id', ''], - ['phone', ''], - ['label', strripos($version, 'min') === false ? 3 : 2], - ['captcha', ''], - ]); - if (!$data['company_name']) { - return $this->fail('请填写公司名称'); - } - if (!$data['domain_name']) { - return $this->fail('请填写授权域名'); - } - - if (!$data['phone']) { - return $this->fail('请填写手机号码'); - } - if (!$data['order_id']) { - return $this->fail('请填写订单id'); - } - $datas = explode('.', $data['domain_name']); - $n = count($datas); - $preg = '/[\w].+\.(com|net|org|gov|edu)\.cn$/'; - if (($n > 2) && preg_match($preg, $data['domain_name'])) { - //双后缀取后3位 - $domain_name = $datas[$n - 3] . '.' . $datas[$n - 2] . '.' . $datas[$n - 1]; - } else { - //非双后缀取后两位 - $domain_name = $datas[$n - 2] . '.' . $datas[$n - 1]; - } - $sec = trim(str_replace($domain_name, '', $data['domain_name']), '.'); - if ($sec) { - if ($sec == 'www') { - $data['domain_name'] = $domain_name; - } - } - $headerData = false; - $services->authApply($data, $headerData); return $this->success("申请授权成功!"); - } /** diff --git a/pro_v3.5.1/app/controller/admin/v1/agent/AgentLevel.php b/pro_v3.5.1/app/controller/admin/v1/agent/AgentLevel.php index 484330b0..95063273 100644 --- a/pro_v3.5.1/app/controller/admin/v1/agent/AgentLevel.php +++ b/pro_v3.5.1/app/controller/admin/v1/agent/AgentLevel.php @@ -74,6 +74,8 @@ class AgentLevel extends AuthController ['grade', 0], ['image', ''], ['color', ''], + ['direct_reward_points', 0], + ['umbrella_reward_points', 0], ['one_brokerage', 0], ['two_brokerage', 0], ['status', 0]]); @@ -123,6 +125,8 @@ class AgentLevel extends AuthController ['grade', 0], ['image', ''], ['color', ''], + ['direct_reward_points', 0], + ['umbrella_reward_points', 0], ['one_brokerage', 0], ['two_brokerage', 0], ['status', 0]]); @@ -145,6 +149,8 @@ class AgentLevel extends AuthController $levelInfo->grade = $data['grade']; $levelInfo->image = $data['image']; $levelInfo->color = $data['color']; + $levelInfo->direct_reward_points = (int)$data['direct_reward_points']; + $levelInfo->umbrella_reward_points = (int)$data['umbrella_reward_points']; $levelInfo->one_brokerage = $data['one_brokerage']; $levelInfo->two_brokerage = $data['two_brokerage']; $levelInfo->status = $data['status']; diff --git a/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php b/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php index 3f0fff15..ea245b17 100644 --- a/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php +++ b/pro_v3.5.1/app/controller/admin/v1/hjf/MemberController.php @@ -5,18 +5,19 @@ namespace app\controller\admin\v1\hjf; use app\controller\admin\AuthController; use app\dao\user\UserDao; +use app\services\agent\AgentLevelServices; use app\services\hjf\MemberLevelServices; -use app\services\system\SystemConfigServices; -use crmeb\services\SystemConfigService; use think\annotation\Inject; /** - * Admin · 会员管理接口 + * Admin · 会员管理接口(改造复用版) * - * GET /adminapi/hjf/member/list — 会员列表(分页,支持按等级筛选) - * PUT /adminapi/hjf/member/level/:uid — 手动调整会员等级 - * GET /adminapi/hjf/member/config — 获取会员等级配置 - * POST /adminapi/hjf/member/config — 保存会员等级配置 + * 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。 + * + * GET /adminapi/hjf/member/list — 会员列表 + * PUT /adminapi/hjf/member/level/:uid — 手动调整会员等级 + * GET /adminapi/hjf/member/config — 获取会员等级配置(从 eb_agent_level 读取) + * POST /adminapi/hjf/member/config — 保存会员等级配置(写入 eb_agent_level) * * Class MemberController * @package app\controller\admin\v1\hjf @@ -29,6 +30,9 @@ class MemberController extends AuthController #[Inject] protected MemberLevelServices $levelServices; + #[Inject] + protected AgentLevelServices $agentLevelServices; + /** * 会员列表(分页) */ @@ -47,25 +51,38 @@ class MemberController extends AuthController if ($where['keyword'] !== '') { $condition['uid|nickname|phone'] = ['like', '%' . $where['keyword'] . '%']; } + if ($where['member_level'] !== '') { - $condition['member_level'] = (int)$where['member_level']; + $grade = (int)$where['member_level']; + if ($grade === 0) { + $condition['agent_level'] = 0; + } else { + $agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade); + $condition['agent_level'] = $agentLevelId ?: -1; + } } $count = $this->userDao->count($condition); $list = $this->userDao->selectList( $condition, - 'uid,nickname,avatar,phone,member_level,frozen_points,available_points,now_money,spread_uid,add_time', + 'uid,nickname,avatar,phone,agent_level,frozen_points,available_points,now_money,spread_uid,add_time', $page, $limit, 'uid', 'desc' ); - // 附加直推单数 & 伞下单数 + $levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]); + $levelMap = array_column($levelList, null, 'id'); + foreach ($list as &$item) { - $item['direct_order_count'] = $this->levelServices->getDirectQueueOrderCount((int)$item['uid']); + $agentLevelId = (int)($item['agent_level'] ?? 0); + $levelInfo = $levelMap[$agentLevelId] ?? null; + $item['member_level'] = $levelInfo ? (int)$levelInfo['grade'] : 0; + $item['member_level_name'] = $levelInfo ? $levelInfo['name'] : '普通会员'; + $item['direct_order_count'] = $this->levelServices->getDirectQueueOrderCount((int)$item['uid']); $item['umbrella_order_count'] = $this->levelServices->getUmbrellaQueueOrderCount((int)$item['uid']); - $item['direct_spread_count'] = $this->levelServices->getDirectSpreadCount((int)$item['uid']); + $item['direct_spread_count'] = $this->levelServices->getDirectSpreadCount((int)$item['uid']); } unset($item); @@ -74,17 +91,15 @@ class MemberController extends AuthController /** * 手动调整会员等级 - * - * @param int $uid */ public function updateLevel(int $uid): mixed { $data = $this->request->getMore([ ['member_level', 0], ]); - $newLevel = (int)$data['member_level']; + $grade = (int)$data['member_level']; - if ($newLevel < 0 || $newLevel > 4) { + if ($grade < 0 || $grade > 4) { return $this->fail('等级范围 0-4'); } @@ -93,78 +108,51 @@ class MemberController extends AuthController return $this->fail('用户不存在'); } - $this->userDao->update($uid, ['member_level' => $newLevel], 'uid'); + $this->levelServices->setUserLevel($uid, $grade); return $this->success('更新成功'); } /** - * 获取会员等级配置 + * 获取会员等级配置(从 eb_agent_level 表读取) */ public function getConfig(): mixed { - $keys = [ - 'hjf_level_direct_require_1', - 'hjf_level_umbrella_require_2', - 'hjf_level_umbrella_require_3', - 'hjf_level_umbrella_require_4', - 'hjf_reward_direct_1', - 'hjf_reward_direct_2', - 'hjf_reward_direct_3', - 'hjf_reward_direct_4', - 'hjf_reward_umbrella_1', - 'hjf_reward_umbrella_2', - 'hjf_reward_umbrella_3', - 'hjf_reward_umbrella_4', - ]; - + $levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]); $config = []; - $defaults = [ - 'hjf_level_direct_require_1' => 3, - 'hjf_level_umbrella_require_2' => 30, - 'hjf_level_umbrella_require_3' => 100, - 'hjf_level_umbrella_require_4' => 1000, - 'hjf_reward_direct_1' => 500, - 'hjf_reward_direct_2' => 800, - 'hjf_reward_direct_3' => 1000, - 'hjf_reward_direct_4' => 1300, - 'hjf_reward_umbrella_1' => 0, - 'hjf_reward_umbrella_2' => 300, - 'hjf_reward_umbrella_3' => 200, - 'hjf_reward_umbrella_4' => 300, - ]; - - foreach ($keys as $key) { - $config[$key] = SystemConfigService::get($key, $defaults[$key] ?? 0); + foreach ($levelList as $level) { + $config[] = [ + 'id' => $level['id'], + 'name' => $level['name'], + 'grade' => $level['grade'], + 'direct_reward_points' => $level['direct_reward_points'] ?? 0, + 'umbrella_reward_points' => $level['umbrella_reward_points'] ?? 0, + ]; } - return $this->success($config); } /** - * 保存会员等级配置 + * 保存会员等级配置(写入 eb_agent_level 表) */ - public function saveConfig(SystemConfigServices $configServices): mixed + public function saveConfig(): mixed { - $allowedKeys = [ - 'hjf_level_direct_require_1', - 'hjf_level_umbrella_require_2', - 'hjf_level_umbrella_require_3', - 'hjf_level_umbrella_require_4', - 'hjf_reward_direct_1', - 'hjf_reward_direct_2', - 'hjf_reward_direct_3', - 'hjf_reward_direct_4', - 'hjf_reward_umbrella_1', - 'hjf_reward_umbrella_2', - 'hjf_reward_umbrella_3', - 'hjf_reward_umbrella_4', - ]; + $levels = $this->request->post('levels', []); + if (!is_array($levels)) { + return $this->fail('参数格式错误'); + } - $data = $this->request->post(); - foreach ($data as $key => $value) { - if (in_array($key, $allowedKeys, true)) { - $configServices->setConfig($key, (string)$value); + foreach ($levels as $item) { + if (empty($item['id'])) continue; + $updateData = []; + if (isset($item['direct_reward_points'])) { + $updateData['direct_reward_points'] = (int)$item['direct_reward_points']; + } + if (isset($item['umbrella_reward_points'])) { + $updateData['umbrella_reward_points'] = (int)$item['umbrella_reward_points']; + } + if ($updateData) { + $this->agentLevelServices->dao->update((int)$item['id'], $updateData); } } diff --git a/pro_v3.5.1/app/controller/admin/v1/product/StoreProduct.php b/pro_v3.5.1/app/controller/admin/v1/product/StoreProduct.php index 1a555334..70d79f10 100644 --- a/pro_v3.5.1/app/controller/admin/v1/product/StoreProduct.php +++ b/pro_v3.5.1/app/controller/admin/v1/product/StoreProduct.php @@ -66,6 +66,7 @@ class StoreProduct extends AuthController ['stock_range', ''],//库存区间 ['collect_range', ''],//收藏区间 ['product_clear', ''],//适用群体 + ['is_queue_goods', ''],//报单商品:1/0,空=不限 ]); if ($this->adminType == 4) { @@ -206,6 +207,7 @@ class StoreProduct extends AuthController ['stock_range', ''],//库存区间 ['collect_range', ''],//收藏区间 ['product_clear', ''],//适用群体 + ['is_queue_goods', ''],//报单商品:1/0,空=不限(须配合模型 searchIsQueueGoodsAttr) ]); if ($this->adminType == 4) { $where['supplier_id'] = $this->adminId; diff --git a/pro_v3.5.1/app/controller/admin/v1/user/User.php b/pro_v3.5.1/app/controller/admin/v1/user/User.php index 89d42bd1..efe1c6ab 100644 --- a/pro_v3.5.1/app/controller/admin/v1/user/User.php +++ b/pro_v3.5.1/app/controller/admin/v1/user/User.php @@ -85,6 +85,8 @@ class User extends AuthController ['isMember', ''], ['label_ids', ''], ['is_channel', ''], + /** HJF:按分销等级 grade(0–4)筛选,对应 eb_user.agent_level */ + ['hjf_member_level', ''], ]); if ($where['label_ids']) { $where['label_id'] = stringToIntArray($where['label_ids']); diff --git a/pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php b/pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php new file mode 100644 index 00000000..380b75e4 --- /dev/null +++ b/pro_v3.5.1/app/controller/api/v1/hjf/MemberController.php @@ -0,0 +1,178 @@ +uid(); + + $agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level'); + // 直接从 eb_agent_level 取 name,避免 grade 解析失败时等级徽章不显示 + $levelRow = $agentLevel > 0 ? $this->agentLevelServices->getLevelInfo($agentLevel) : null; + $grade = $levelRow ? (int)$levelRow['grade'] : 0; + $levelName = $levelRow ? ($levelRow['name'] ?? '普通会员') : '普通会员'; + + $directCount = $this->memberLevelServices->getDirectSpreadCount($uid); + $umbrellaCount = $this->memberLevelServices->getUmbrellaQueueOrderCount($uid); + $directOrderCount = $this->memberLevelServices->getDirectQueueOrderCount($uid); + + // 用已修正的 level row ID 查找下一等级,避免旧 status=0 记录导致 grade 被误判为 0 + $effectiveLevelId = $levelRow ? (int)$levelRow['id'] : 0; + $nextLevel = $this->agentLevelServices->getNextLevelInfo($effectiveLevelId); + $nextLevelName = $nextLevel ? $nextLevel['name'] : null; + + $upgradeProgress = []; + if ($nextLevel) { + $taskList = $this->agentLevelTaskServices->getUpgradeTasksForLevel((int)$nextLevel['id']); + foreach ($taskList as $task) { + $item = ['name' => $task['name'], 'number' => $task['number']]; + switch ($task['type']) { + case 6: + $item['current'] = $directOrderCount; + break; + case 7: + $item['current'] = $umbrellaCount; + break; + case 8: + $item['current'] = $directCount; + break; + default: + $item['current'] = 0; + } + $item['completed'] = $item['current'] >= $item['number']; + $upgradeProgress[] = $item; + } + } + + return app('json')->success([ + 'agent_level' => $agentLevel, // eb_user.agent_level 原始 ID,供前端判断是否有等级 + 'member_level' => $grade, + 'member_level_name' => $levelName, // eb_agent_level.name 直接值 + 'direct_count' => $directCount, + 'umbrella_count' => $umbrellaCount, + 'direct_order_count' => $directOrderCount, + 'next_level_name' => $nextLevelName, + 'upgrade_progress' => $upgradeProgress, + ]); + } + + /** + * 团队成员列表(直推/伞下) + */ + public function team(Request $request): \think\Response + { + $uid = (int)$request->uid(); + $page = (int)$request->get('page', 1); + $limit = (int)$request->get('limit', 20); + $type = $request->get('type', 'direct'); + + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + + if ($type === 'direct') { + $where = ['spread_uid' => $uid]; + } else { + $directUids = $userServices->getColumn(['spread_uid' => $uid], 'uid'); + if (empty($directUids)) { + return app('json')->success(['list' => [], 'count' => 0]); + } + $where = [['spread_uid', 'in', $directUids]]; + } + + $count = $userServices->count($where); + $list = Db::name('user') + ->where($where) + ->field('uid,nickname,avatar,phone,agent_level,add_time') + ->page($page, $limit) + ->order('uid desc') + ->select() + ->toArray(); + + $maps = $this->agentLevelServices->loadHjfUserListLevelMaps(); + + foreach ($list as &$item) { + $alId = (int)($item['agent_level'] ?? 0); + $levelInfo = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($alId, $maps); + $item['member_level'] = $levelInfo ? (int)$levelInfo['grade'] : 0; + $item['member_level_name'] = $levelInfo ? $levelInfo['name'] : '普通会员'; + $item['join_time'] = date('Y-m-d', (int)$item['add_time']); + $item['direct_orders'] = $this->agentLevelTaskServices->getDirectQueueOrderCount((int)$item['uid']); + unset($item['agent_level'], $item['add_time']); + } + unset($item); + + return app('json')->success(compact('list', 'count')); + } + + /** + * 团队收益明细(积分奖励记录) + */ + public function income(Request $request): \think\Response + { + $uid = (int)$request->uid(); + $page = (int)$request->get('page', 1); + $limit = (int)$request->get('limit', 20); + + /** @var PointsReleaseLogDao $logDao */ + $logDao = app()->make(PointsReleaseLogDao::class); + + $where = [ + 'uid' => $uid, + 'pm' => 1, + ]; + $where[] = ['type', 'in', ['reward_direct', 'reward_umbrella']]; + + $count = $logDao->count($where); + $list = Db::name('points_release_log') + ->where($where) + ->field('id,uid,points,type,title,mark,order_id,add_time') + ->page($page, $limit) + ->order('id desc') + ->select() + ->toArray(); + + foreach ($list as &$item) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['add_time']); + } + unset($item); + + return app('json')->success(compact('list', 'count')); + } +} diff --git a/pro_v3.5.1/app/dao/user/UserWechatUserDao.php b/pro_v3.5.1/app/dao/user/UserWechatUserDao.php index db64781e..267a2cf1 100644 --- a/pro_v3.5.1/app/dao/user/UserWechatUserDao.php +++ b/pro_v3.5.1/app/dao/user/UserWechatUserDao.php @@ -187,6 +187,10 @@ class UserWechatUserDao extends BaseDao } } + // HJF / 分销等级:eb_user.agent_level(由 hjf_member_level 归一化得到) + if (isset($where['hjf_agent_level_id']) && $where['hjf_agent_level_id'] !== '' && $where['hjf_agent_level_id'] !== null) { + $model = $model->where($userAlias . 'agent_level', (int)$where['hjf_agent_level_id']); + } //用户等级 if (isset($where['level']) && $where['level']) { $model = $model->where($userAlias . 'level', $where['level']); diff --git a/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php b/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php index 14f2385e..1555eb3a 100644 --- a/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php +++ b/pro_v3.5.1/app/jobs/hjf/HjfOrderPayJob.php @@ -3,9 +3,10 @@ declare(strict_types=1); namespace app\jobs\hjf; -use app\services\hjf\MemberLevelServices; +use app\services\agent\AgentLevelServices; use app\services\hjf\PointsRewardServices; use app\services\hjf\QueuePoolServices; +use app\services\user\UserServices; use crmeb\basic\BaseJobs; use crmeb\traits\QueueTrait; use think\exception\ValidateException; @@ -16,11 +17,11 @@ use think\facade\Log; * * 触发时机:Pay 监听器检测到 is_queue_goods=1 时派发。 * - * 执行流程: + * 执行流程(改造复用版): * 1. 调用 QueuePoolServices::enqueue() 将订单写入公排池 - * (内部含 Redis 分布式锁 + 退款触发检测) * 2. 调用 PointsRewardServices::reward() 沿推荐链发放级差积分 - * 3. 调用 MemberLevelServices::checkUpgrade() 检查下单用户上级链是否触发等级升级 + * 3. 调用 AgentLevelServices::checkUserLevelFinish() 检查升级 + * (复用 CRMEB 分销等级升级流程,已支持 HJF 任务类型 6/7/8) * * Class HjfOrderPayJob * @package app\jobs\hjf @@ -29,22 +30,14 @@ class HjfOrderPayJob extends BaseJobs { use QueueTrait; - /** - * @param int $uid 下单用户 ID - * @param string $orderId 订单号(eb_store_order.order_id) - * @param float $amount 报单金额(默认 3600.00) - * @return bool - */ public function doJob(int $uid, string $orderId, float $amount = 3600.00): bool { try { - // 1. 公排入队 /** @var QueuePoolServices $queueServices */ $queueServices = app()->make(QueuePoolServices::class); $queueServices->enqueue($uid, $orderId, $amount); Log::info("[HjfOrderPay] 公排入队成功 uid={$uid} orderId={$orderId}"); } catch (ValidateException $e) { - // 锁竞争导致入队失败,重新投递到队列(延迟5秒) Log::warning("[HjfOrderPay] 入队被锁,延迟重试 uid={$uid} orderId={$orderId}: " . $e->getMessage()); static::dispatchSece(5, [$uid, $orderId, $amount]); return true; @@ -54,29 +47,29 @@ class HjfOrderPayJob extends BaseJobs } try { - // 2. 积分奖励(级差发放) /** @var PointsRewardServices $pointsServices */ $pointsServices = app()->make(PointsRewardServices::class); $pointsServices->reward($uid, $orderId); Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId}"); } catch (\Throwable $e) { - // 积分发放失败不阻塞主流程,记录错误即可 Log::error("[HjfOrderPay] 积分奖励失败 uid={$uid} orderId={$orderId}: " . $e->getMessage()); } try { - // 3. 触发推荐链等级升级检查(对买家本人及其直推上级) - /** @var MemberLevelServices $levelServices */ - $levelServices = app()->make(MemberLevelServices::class); - $levelServices->checkUpgrade($uid); - - // 同时检查直推上级(支付行为可能满足上级的伞下业绩门槛) - $spreadUid = (int)\think\facade\Db::name('user') - ->where('uid', $uid) - ->value('spread_uid'); - if ($spreadUid > 0) { - $levelServices->checkUpgrade($spreadUid); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserCacheInfo($uid); + $spreadUid = $userInfo ? (int)($userInfo['spread_uid'] ?? 0) : 0; + $twoSpreadUid = 0; + if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) { + $twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false); } + $uids = array_unique([$uid, $spreadUid, $twoSpreadUid]); + + /** @var AgentLevelServices $agentLevelServices */ + $agentLevelServices = app()->make(AgentLevelServices::class); + $agentLevelServices->checkUserLevelFinish($uid, $uids); + Log::info("[HjfOrderPay] 等级升级检查完成 uid={$uid}"); } catch (\Throwable $e) { Log::error("[HjfOrderPay] 等级升级检查失败 uid={$uid}: " . $e->getMessage()); diff --git a/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php b/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php index 46d127cf..e60ffb87 100644 --- a/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php +++ b/pro_v3.5.1/app/jobs/hjf/MemberLevelCheckJob.php @@ -3,16 +3,14 @@ declare(strict_types=1); namespace app\jobs\hjf; -use app\services\hjf\MemberLevelServices; +use app\services\agent\AgentLevelServices; use crmeb\basic\BaseJobs; use crmeb\traits\QueueTrait; -use think\facade\Log; /** - * 会员等级异步检查 Job + * 会员等级异步检查 Job(改造复用版) * - * 每次订单支付回调完成后,对推荐链上的上级异步派发此 Job 检查是否达到升级条件。 - * 调用方式:MemberLevelCheckJob::dispatch($uid) + * 委托给 AgentLevelServices::checkUserLevelFinish() 复用 CRMEB 分销等级升级流程。 * * Class MemberLevelCheckJob * @package app\jobs\hjf @@ -21,16 +19,12 @@ class MemberLevelCheckJob extends BaseJobs { use QueueTrait; - /** - * @param int $uid 需要检查升级的用户 ID - * @return bool - */ public function doJob(int $uid): bool { try { - /** @var MemberLevelServices $levelServices */ - $levelServices = app()->make(MemberLevelServices::class); - $levelServices->checkUpgrade($uid); + /** @var AgentLevelServices $levelServices */ + $levelServices = app()->make(AgentLevelServices::class); + $levelServices->checkUserLevelFinish($uid); } catch (\Throwable $e) { response_log_write([ 'message' => "会员等级检查失败 uid={$uid}: " . $e->getMessage(), diff --git a/pro_v3.5.1/app/model/product/product/StoreProduct.php b/pro_v3.5.1/app/model/product/product/StoreProduct.php index 5ac64a3f..7b0b84f0 100644 --- a/pro_v3.5.1/app/model/product/product/StoreProduct.php +++ b/pro_v3.5.1/app/model/product/product/StoreProduct.php @@ -796,6 +796,19 @@ class StoreProduct extends BaseModel } } + /** + * 报单商品筛选(HJF)。空字符串不筛选;否则避免落入默认 where 导致 is_queue_goods='' 仅匹配 0。 + * @param Model $query + * @param mixed $value + */ + public function searchIsQueueGoodsAttr($query, $value) + { + if ($value === '' || $value === null) { + return; + } + $query->where('is_queue_goods', (int)$value); + } + /** * 系统表单搜索器 * @param Model $query diff --git a/pro_v3.5.1/app/services/agent/AgentLevelServices.php b/pro_v3.5.1/app/services/agent/AgentLevelServices.php index 9fdcd11e..a7c45315 100644 --- a/pro_v3.5.1/app/services/agent/AgentLevelServices.php +++ b/pro_v3.5.1/app/services/agent/AgentLevelServices.php @@ -27,7 +27,13 @@ use think\facade\Route as Url; /** - * 分销等级 + * 分销等级(改造后同时作为 HJF 会员等级服务) + * + * PRD 改造说明: + * - 将原有的"分销员等级"概念替换为"会员等级" + * - 升级条件从"推广订单数/消费金额"改为"直推单数 + 伞下业绩单数"(通过 task type 6/7/8 实现) + * - 佣金计算从"按比例返佣"改为"按等级发放固定积分"(通过 direct_reward_points / umbrella_reward_points 实现) + * * Class AgentLevelServices * @package app\services\agent * @mixin AgentLevelDao @@ -55,6 +61,88 @@ class AgentLevelServices extends BaseServices return $this->dao->getOne(['id' => $id, 'is_del' => 0], $field, $with); } + /** + * HJF 官方会员等级名称(与 database/hjf_migration.sql 插入数据一致) + * 用于区分 CRMEB 默认「等级一/等级二…」与 HJF 创客/云店… + */ + public const HJF_OFFICIAL_LEVEL_NAMES = ['创客', '云店', '服务商', '分公司']; + + /** + * 一次查询并返回用户列表展示所需等级索引(供外部服务调用) + * 不暴露 dao 属性,避免外部直接访问 protected $dao + * + * @return array{byId: array, byGradeAny: array, byGradeOfficial: array} + */ + public function loadHjfUserListLevelMaps(): array + { + $rows = $this->dao->getList(['is_del' => 0]); + return $this->buildHjfUserListLevelMaps($rows); + } + + /** + * 从 is_del=0 的等级行列表构建用户列表展示用索引(一次查询后复用) + * + * @param array> $hjfLevelRows + * @return array{byId: array, byGradeAny: array, byGradeOfficial: array} + */ + public function buildHjfUserListLevelMaps(array $hjfLevelRows): array + { + $byId = []; + $byGradeAny = []; + $byGradeOfficial = []; + $official = self::HJF_OFFICIAL_LEVEL_NAMES; + foreach ($hjfLevelRows as $hjfRow) { + $lid = (int)($hjfRow['id'] ?? 0); + if ($lid > 0) { + $byId[$lid] = $hjfRow; + } + $g = (int)($hjfRow['grade'] ?? 0); + if ($g > 0 && !isset($byGradeAny[$g])) { + // dao 已 order grade asc,id desc,同 grade 先出现者为较大 id + $byGradeAny[$g] = $hjfRow; + } + $nm = (string)($hjfRow['name'] ?? ''); + if ($g > 0 && in_array($nm, $official, true) && !isset($byGradeOfficial[$g])) { + $byGradeOfficial[$g] = $hjfRow; + } + } + + return [ + 'byId' => $byId, + 'byGradeAny' => $byGradeAny, + 'byGradeOfficial' => $byGradeOfficial, + ]; + } + + /** + * 用户列表等场景:解析应展示的等级行 + * + * - agent_level 指向 CRMEB 默认行(如 id=2「等级二」)时,按 grade 改用 HJF 官方行(如「云店」) + * - 旧 id 已软删或误把 grade 写入 agent_level 时,按 byGradeAny 回退 + */ + public function pickHjfLevelRowForUserListDisplay(int $agentLevelId, array $maps): ?array + { + if ($agentLevelId <= 0) { + return null; + } + $byId = $maps['byId'] ?? []; + $byGradeAny = $maps['byGradeAny'] ?? []; + $byGradeOfficial = $maps['byGradeOfficial'] ?? []; + $official = self::HJF_OFFICIAL_LEVEL_NAMES; + + $row = $byId[$agentLevelId] ?? null; + if ($row === null) { + return $byGradeAny[$agentLevelId] ?? null; + } + $nm = (string)($row['name'] ?? ''); + $g = (int)($row['grade'] ?? 0); + if ($g > 0 && !in_array($nm, $official, true) && isset($byGradeOfficial[$g])) { + return $byGradeOfficial[$g]; + } + + return $row; + } + /** * 获取等级列表 * @param array $where @@ -270,13 +358,10 @@ class AgentLevelServices extends BaseServices } /** - * 分销等级上浮 - * @param int $uid - * @param array $userInfo - * @return array|int[] - * @throws \think\db\exception\DataNotFoundException - * @throws \think\db\exception\DbException - * @throws \think\db\exception\ModelNotFoundException + * 分销等级上浮(保留兼容,普通商品分销仍使用原逻辑) + * + * 注意:报单商品的积分奖励已由 PointsRewardServices 通过 direct_reward_points/umbrella_reward_points 处理, + * 此方法仅用于普通商品的分销佣金计算。 */ public function getAgentLevelBrokerage(int $uid, $userInfo = []) { @@ -285,7 +370,6 @@ class AgentLevelServices extends BaseServices if (!$uid) { return $data; } - //商城分销是否开启 if (!sys_config('brokerage_func_status')) { return $data; } @@ -297,7 +381,6 @@ class AgentLevelServices extends BaseServices if (!$userInfo) { return $data; } - //获取上级uid || 开启自购返回自己uid $spread_uid = $userServices->getSpreadUid($uid, $userInfo); $one_agent_level = 0; $two_agent_level = 0; @@ -308,17 +391,66 @@ class AgentLevelServices extends BaseServices $two_agent_level = $two_user_info['agent_level'] ?? 0; } } - //获取后台一级返佣比例 $storeBrokerageRatio = sys_config('store_brokerage_ratio'); - //一级上浮之后的反佣比例 $storeBrokerageRatio = $one_agent_level ? bcadd($storeBrokerageRatio, bcmul($storeBrokerageRatio, bcdiv(($this->getLevelInfo($one_agent_level)['one_brokerage'] ?? 0), 100, 2), 2), 2) : $storeBrokerageRatio; - //获取二级返佣比例 $storeBrokerageTwo = sys_config('store_brokerage_two'); - //二级上浮之后的反佣比例 $storeBrokerageTwo = $two_agent_level ? bcadd($storeBrokerageTwo, bcmul($storeBrokerageTwo, bcdiv(($this->getLevelInfo($two_agent_level)['two_brokerage'] ?? 0), 100, 2), 2), 2) : $storeBrokerageTwo; return [$storeBrokerageRatio, $storeBrokerageTwo, $spread_uid, $spread_two_uid]; } + /** + * 根据 agent_level ID 获取等级 grade(HJF 会员等级数字 0-4) + * + * @param int $agentLevelId eb_user.agent_level 值 + * @return int grade(0=普通会员, 1=创客, 2=云店, 3=服务商, 4=分公司) + */ + public function getGradeByLevelId(int $agentLevelId): int + { + if ($agentLevelId <= 0) { + return 0; + } + $levelInfo = $this->getLevelInfo($agentLevelId); + return (int)($levelInfo['grade'] ?? 0); + } + + /** + * 根据 agent_level ID 获取直推奖励积分 + */ + public function getDirectRewardPoints(int $agentLevelId): int + { + if ($agentLevelId <= 0) { + return 0; + } + $levelInfo = $this->getLevelInfo($agentLevelId); + return (int)($levelInfo['direct_reward_points'] ?? 0); + } + + /** + * 根据 agent_level ID 获取伞下奖励积分 + */ + public function getUmbrellaRewardPoints(int $agentLevelId): int + { + if ($agentLevelId <= 0) { + return 0; + } + $levelInfo = $this->getLevelInfo($agentLevelId); + return (int)($levelInfo['umbrella_reward_points'] ?? 0); + } + + /** + * 根据 grade 获取 agent_level ID + * + * @param int $grade 等级数字 (1=创客, 2=云店, 3=服务商, 4=分公司) + * @return int agent_level ID,找不到返回 0 + */ + public function getLevelIdByGrade(int $grade): int + { + if ($grade <= 0) { + return 0; + } + return (int)$this->dao->value(['grade' => $grade, 'is_del' => 0, 'status' => 1], 'id') ?: 0; + } + /** * 计算一二级返佣比率上浮 * @param $ratio @@ -338,10 +470,7 @@ class AgentLevelServices extends BaseServices } /** - * 添加等级表单 - * @param int $id - * @return array - * @throws \FormBuilder\Exception\FormBuilderException + * 添加等级表单(改造后包含积分奖励字段) */ public function createForm() { @@ -352,17 +481,16 @@ class AgentLevelServices extends BaseServices $field[] = Form::number('grade', '等级:', 0)->min(0)->precision(0); $field[] = Form::frameImage('image', '背景图:', Url::buildUrl('admin/widget.images/index', array('fodder' => 'image')))->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景图')); $field[] = Form::color('color', '字体颜色:')->required('请选择字体颜色'); + $field[] = Form::number('direct_reward_points', '直推奖励积分:', 0)->info('该等级会员每直推1单报单商品获得的冻结积分数')->min(0); + $field[] = Form::number('umbrella_reward_points', '伞下奖励积分:', 0)->info('该等级会员伞下每入1单报单商品获得的冻结积分数(级差基数)')->min(0); $field[] = Form::number('one_brokerage', '一级上浮:', 0)->info('在分销一级佣金基础上浮(0-1000之间整数)百分比,目前一级返佣比率:' . $store_brokerage_ratio . '%,例如上浮10%,则返佣比率:一级返佣比率 * (1 + 一级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_ratio, 10) . '%')->min(0)->max(1000); $field[] = Form::number('two_brokerage', '二级上浮:', 0)->info('在分销二级佣金基础上浮(0-1000之间整数)百分比,目前二级返佣比率:' . $store_brokerage_two . '%,例如上浮10%,则返佣比率:二级返佣比率 * (1 + 二级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_two, 10) . '%')->min(0)->max(1000); $field[] = Form::radio('status', '是否显示:', 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); - return create_form('添加分销员等级', $field, Url::buildUrl('/agent/level'), 'POST'); + return create_form('添加会员等级', $field, Url::buildUrl('/agent/level'), 'POST'); } /** - * 获取修改等级表单 - * @param int $id - * @return array - * @throws \FormBuilder\Exception\FormBuilderException + * 获取修改等级表单(改造后包含积分奖励字段) */ public function editForm(int $id) { @@ -377,11 +505,13 @@ class AgentLevelServices extends BaseServices $field[] = Form::number('grade', '等级', $levelInfo['grade'])->min(0)->precision(0); $field[] = Form::frameImage('image', '背景图', Url::buildUrl('admin/widget.images/index', array('fodder' => 'image')), $levelInfo['image'])->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景图')); $field[] = Form::color('color', '字体颜色', $levelInfo['color'] ?? '')->required('请选择字体颜色'); + $field[] = Form::number('direct_reward_points', '直推奖励积分', $levelInfo['direct_reward_points'] ?? 0)->info('该等级会员每直推1单报单商品获得的冻结积分数')->min(0); + $field[] = Form::number('umbrella_reward_points', '伞下奖励积分', $levelInfo['umbrella_reward_points'] ?? 0)->info('该等级会员伞下每入1单报单商品获得的冻结积分数(级差基数)')->min(0); $field[] = Form::number('one_brokerage', '一级上浮', $levelInfo['one_brokerage'])->info('在分销一级佣金基础上浮(0-1000之间整数)百分比,目前一级返佣比率:' . $store_brokerage_ratio . '%,上浮' . $levelInfo['one_brokerage'] . '%,则返佣比率:一级返佣比率 * (1 + 一级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_ratio, $levelInfo['one_brokerage']) . '%')->min(0)->max(1000); $field[] = Form::number('two_brokerage', '二级上浮', $levelInfo['two_brokerage'])->info('在分销二级佣金基础上浮(0-1000之间整数)百分比,目前二级返佣比率:' . $store_brokerage_two . '%,上浮' . $levelInfo['two_brokerage'] . '%,则返佣比率:二级返佣比率 * (1 + 二级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_two, $levelInfo['two_brokerage']) . '%')->min(0)->max(1000); $field[] = Form::radio('status', '是否显示', $levelInfo['status'])->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); - return create_form('编辑分销员等级', $field, Url::buildUrl('/agent/level/' . $id), 'PUT'); + return create_form('编辑会员等级', $field, Url::buildUrl('/agent/level/' . $id), 'PUT'); } /** diff --git a/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php b/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php index c79ecbcc..e9d52e96 100644 --- a/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php +++ b/pro_v3.5.1/app/services/agent/AgentLevelTaskServices.php @@ -20,10 +20,12 @@ use crmeb\services\FormBuilder as Form; use FormBuilder\Factory\Iview; use think\annotation\Inject; use think\exception\ValidateException; +use think\facade\Db; use think\facade\Route as Url; /** + * 分销等级任务(改造后同时支持 HJF 会员等级升级任务) * * Class AgentLevelTaskServices * @package app\services\agent @@ -33,12 +35,13 @@ class AgentLevelTaskServices extends BaseServices { /** * 任务类型 - * type 记录在数据库中用来区分任务 - * name 任务名 (任务名中的{$num}会自动替换成设置的数字 + 单位) - * max_number 最大设定数值 0为不限定 - * min_number 最小设定数值 - * unit 单位 - * */ + * + * type 1-5: 原 CRMEB 分销任务类型 + * type 6-8: HJF 会员等级升级任务类型(改造新增) + * 6 = 直推报单单数(直推下级购买报单商品的订单数) + * 7 = 伞下报单业绩(含业绩分离逻辑) + * 8 = 最低直推人数 + */ protected array $TaskType = [ [ 'type' => 1, @@ -90,6 +93,36 @@ class AgentLevelTaskServices extends BaseServices 'unit' => '单', 'image' => '/uploads/system/agent_spread_order.png' ], + [ + 'type' => 6, + 'method' => 'directQueueOrderCount', + 'name' => '直推报单满{$num}', + 'real_name' => '直推报单单数', + 'max_number' => 0, + 'min_number' => 1, + 'unit' => '单', + 'image' => '/uploads/system/agent_spread_order.png' + ], + [ + 'type' => 7, + 'method' => 'umbrellaQueueOrderCount', + 'name' => '伞下报单满{$num}', + 'real_name' => '伞下报单业绩', + 'max_number' => 0, + 'min_number' => 1, + 'unit' => '单', + 'image' => '/uploads/system/agent_spread_order.png' + ], + [ + 'type' => 8, + 'method' => 'directSpreadCount', + 'name' => '至少{$num}个直推', + 'real_name' => '最低直推人数', + 'max_number' => 0, + 'min_number' => 1, + 'unit' => '人', + 'image' => '/uploads/system/agent_spread.png' + ], ]; /** @@ -299,12 +332,15 @@ class AgentLevelTaskServices extends BaseServices /** * 检测某个任务完成情况 + * + * type 1-5: 原 CRMEB 分销任务 + * type 6: 直推报单单数(HJF 改造) + * type 7: 伞下报单业绩含业绩分离(HJF 改造) + * type 8: 最低直推人数(HJF 改造) + * * @param int $uid * @param int $task_id * @return array|false - * @throws \think\db\exception\DataNotFoundException - * @throws \think\db\exception\DbException - * @throws \think\db\exception\ModelNotFoundException */ public function checkLevelTaskFinish(int $uid, int $task_id, $levelTaskInfo = []) { @@ -356,6 +392,17 @@ class AgentLevelTaskServices extends BaseServices $userNumber = $storeOrderServices->count($where); } break; + case 6: + $userNumber = $this->getDirectQueueOrderCount($uid); + break; + case 7: + $userNumber = $this->getUmbrellaQueueOrderCount($uid); + break; + case 8: + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userNumber = $userServices->count(['spread_uid' => $uid]); + break; default: return false; } @@ -372,6 +419,104 @@ class AgentLevelTaskServices extends BaseServices return [$msg, $userNumber, $isComplete]; } + /** + * 获取指定等级的升级任务列表(不分页,供外部服务/控制器调用,避免直接访问 protected $dao) + * + * @param int $level_id eb_agent_level.id + * @return array + */ + public function getUpgradeTasksForLevel(int $level_id): array + { + if ($level_id <= 0) { + return []; + } + return $this->dao->getTaskList(['level_id' => $level_id, 'is_del' => 0, 'status' => 1]) ?: []; + } + + /** + * 统计直推下级的报单订单数(type=6 任务) + * + * @param int $uid 用户 ID + * @return int + */ + public function getDirectQueueOrderCount(int $uid): int + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $directUids = $userServices->getColumn(['spread_uid' => $uid], 'uid'); + if (empty($directUids)) { + return 0; + } + return (int)Db::name('store_order') + ->whereIn('uid', $directUids) + ->where('is_queue_goods', 1) + ->where('paid', 1) + ->where('is_del', 0) + ->count(); + } + + /** + * 统计伞下报单业绩(type=7 任务,含业绩分离逻辑) + * + * 业绩分离:若某直推下级已升级为云店或更高(grade≥2), + * 则该下级及其团队的订单不计入本用户的伞下业绩。 + * + * @param int $uid 用户 ID + * @param int $maxDepth 递归最大深度 + * @return int + */ + public function getUmbrellaQueueOrderCount(int $uid, int $maxDepth = 8): int + { + return $this->recursiveUmbrellaCount($uid, $maxDepth); + } + + /** + * 递归统计伞下业绩(DFS),云店及以上等级的下级团队业绩被分离 + */ + private function recursiveUmbrellaCount(int $uid, int $remainDepth): int + { + if ($remainDepth <= 0) { + return 0; + } + + $directChildren = Db::name('user') + ->where('spread_uid', $uid) + ->field('uid,agent_level') + ->select() + ->toArray(); + + if (empty($directChildren)) { + return 0; + } + + /** @var AgentLevelServices $levelServices */ + $levelServices = app()->make(AgentLevelServices::class); + $total = 0; + + foreach ($directChildren as $child) { + $childGrade = 0; + if (!empty($child['agent_level'])) { + $childLevelInfo = $levelServices->getLevelInfo((int)$child['agent_level']); + $childGrade = (int)($childLevelInfo['grade'] ?? 0); + } + + if ($childGrade >= 2) { + continue; + } + + $total += (int)Db::name('store_order') + ->where('uid', $child['uid']) + ->where('is_queue_goods', 1) + ->where('paid', 1) + ->where('is_del', 0) + ->count(); + + $total += $this->recursiveUmbrellaCount((int)$child['uid'], $remainDepth - 1); + } + + return $total; + } + /** * 检测等级任务 * @param int $id diff --git a/pro_v3.5.1/app/services/hjf/MemberLevelServices.php b/pro_v3.5.1/app/services/hjf/MemberLevelServices.php index 6f929cad..9fd556c4 100644 --- a/pro_v3.5.1/app/services/hjf/MemberLevelServices.php +++ b/pro_v3.5.1/app/services/hjf/MemberLevelServices.php @@ -3,24 +3,23 @@ declare(strict_types=1); namespace app\services\hjf; -use app\dao\user\UserDao; +use app\services\agent\AgentLevelServices; +use app\services\agent\AgentLevelTaskServices; use app\services\BaseServices; -use crmeb\services\SystemConfigService; +use app\services\user\UserServices; use think\annotation\Inject; use think\facade\Db; use think\facade\Log; /** - * 会员等级升级服务 + * 会员等级升级服务(改造复用版) * - * 升级条件(PRD 3.2.1): - * - 普通会员 → 创客:直推3单(hjf_level_direct_require_1,默认3) - * - 创客 → 云店:伞下业绩30单 + 至少3个直推(hjf_level_umbrella_require_2,默认30) - * - 云店 → 服务商:伞下业绩100单 + 至少3个直推(hjf_level_umbrella_require_3,默认100) - * - 服务商 → 分公司:伞下业绩1000单 + 至少3个直推(hjf_level_umbrella_require_4,默认1000) + * 基于 CRMEB Pro 的团队分销等级功能进行改造: + * - 使用 eb_user.agent_level (FK → eb_agent_level.id) 代替独立的 member_level + * - 升级条件通过 eb_agent_level_task 的 type 6/7/8 定义 + * - 升级逻辑委托给 AgentLevelServices::checkUserLevelFinish() * - * 伞下业绩分离:当某直推下级已升级到云店(level≥2)后, - * 该下级及其整个团队的业绩不再计入本级的伞下业绩。 + * 本服务保留为薄封装层,提供 HJF 特有的查询方法供控制器调用。 * * Class MemberLevelServices * @package app\services\hjf @@ -28,111 +27,70 @@ use think\facade\Log; class MemberLevelServices extends BaseServices { #[Inject] - protected UserDao $userDao; + protected AgentLevelServices $agentLevelServices; - /** - * 各等级升级所需直推单数(0→1升级条件) - */ - const DIRECT_REQUIRE_KEYS = [ - 1 => 'hjf_level_direct_require_1', // 普通→创客:直推N单 - ]; - - /** - * 各等级升级所需伞下单数(n-1→n升级条件,n≥2) - */ - const UMBRELLA_REQUIRE_KEYS = [ - 2 => 'hjf_level_umbrella_require_2', // 创客→云店 - 3 => 'hjf_level_umbrella_require_3', // 云店→服务商 - 4 => 'hjf_level_umbrella_require_4', // 服务商→分公司 - ]; - - /** - * 默认升级门槛 - */ - const DEFAULT_DIRECT_REQUIRE = [1 => 3]; - const DEFAULT_UMBRELLA_REQUIRE = [2 => 30, 3 => 100, 4 => 1000]; - - /** - * 最低直推人数要求(云店及以上需要至少3个直推) - */ - const MIN_DIRECT_SPREAD_COUNT = 3; + #[Inject] + protected AgentLevelTaskServices $agentLevelTaskServices; /** * 检查并执行升级(异步触发入口) * - * @param int $uid 被检查的用户 ID + * 委托给 CRMEB 的 AgentLevelServices 复用原有升级检测流程, + * 该流程已支持 type 6/7/8 的 HJF 任务类型。 */ public function checkUpgrade(int $uid): void { try { - $user = $this->userDao->get($uid); - if (!$user) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserCacheInfo($uid); + if (!$userInfo) { return; } - $currentLevel = (int)($user['member_level'] ?? 0); - $nextLevel = $currentLevel + 1; - - if ($nextLevel > 4) { - return; // 已是最高等级 + $spreadUid = $userServices->getSpreadUid($uid, $userInfo); + $twoSpreadUid = 0; + if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) { + $twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false); } + $uids = array_unique([$uid, $spreadUid, $twoSpreadUid]); - $qualified = $this->checkLevelCondition($uid, $currentLevel, $nextLevel); - if ($qualified) { - $this->upgrade($uid, $nextLevel); - - // 升级后继续检查是否可连续升级 - $this->checkUpgrade($uid); - } + $this->agentLevelServices->checkUserLevelFinish($uid, $uids); } catch (\Throwable $e) { Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage()); } } /** - * 检查用户是否满足从 currentLevel 升到 nextLevel 的条件 + * 获取用户当前会员等级 grade(0=普通, 1=创客, 2=云店, 3=服务商, 4=分公司) */ - private function checkLevelCondition(int $uid, int $currentLevel, int $nextLevel): bool + public function getUserGrade(int $uid): int { - if ($nextLevel === 1) { - // 普通→创客:统计直推报单数 - $require = $this->getDirectRequire(1); - $count = $this->getDirectQueueOrderCount($uid); - return $count >= $require; - } - - // 创客/云店/服务商→更高等级:伞下业绩 + 至少3个直推 - $umbrellaRequire = $this->getUmbrellaRequire($nextLevel); - $umbrellaCount = $this->getUmbrellaQueueOrderCount($uid); - - if ($umbrellaCount < $umbrellaRequire) { - return false; - } - - // 需要至少3个直推(对 level≥2 的升级) - $directCount = $this->getDirectSpreadCount($uid); - return $directCount >= self::MIN_DIRECT_SPREAD_COUNT; + $agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level'); + return $this->agentLevelServices->getGradeByLevelId($agentLevel); } /** - * 获取直推用户的报单订单数(直推层级 = 1 层) - * - * 报单商品标记:`is_queue_goods = 1`(eb_store_order 中的字段) + * 获取用户当前等级名称 + */ + public function getUserLevelName(int $uid): string + { + $agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level'); + if ($agentLevel <= 0) { + return '普通会员'; + } + $maps = $this->agentLevelServices->loadHjfUserListLevelMaps(); + $info = $this->agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevel, $maps); + + return $info['name'] ?? '普通会员'; + } + + /** + * 获取直推用户的报单订单数 */ public function getDirectQueueOrderCount(int $uid): int { - // 查询直推用户 uid 列表 - $directUids = $this->userDao->getColumn(['spread_uid' => $uid], 'uid'); - if (empty($directUids)) { - return 0; - } - - return (int)Db::name('store_order') - ->whereIn('uid', $directUids) - ->where('is_queue_goods', 1) - ->where('paid', 1) - ->where('is_del', 0) - ->count(); + return $this->agentLevelTaskServices->getDirectQueueOrderCount($uid); } /** @@ -140,107 +98,39 @@ class MemberLevelServices extends BaseServices */ public function getDirectSpreadCount(int $uid): int { - return (int)$this->userDao->count(['spread_uid' => $uid]); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + return (int)$userServices->count(['spread_uid' => $uid]); } /** * 获取伞下总报单订单数(含业绩分离逻辑) - * - * 业绩分离:若某直推下级已升级为云店(level≥2), - * 则该下级及其团队的订单不计入本用户的伞下业绩。 - * - * @param int $uid 统计对象用户 ID - * @param int $maxDepth 递归最大深度,防止死循环 */ - public function getUmbrellaQueueOrderCount(int $uid, int $maxDepth = 8): int + public function getUmbrellaQueueOrderCount(int $uid): int { - return $this->recursiveUmbrellaCount($uid, $maxDepth); + return $this->agentLevelTaskServices->getUmbrellaQueueOrderCount($uid); } /** - * 递归统计伞下业绩(DFS) - */ - private function recursiveUmbrellaCount(int $uid, int $remainDepth): int - { - if ($remainDepth <= 0) { - return 0; - } - - $directChildren = $this->userDao->selectList( - ['spread_uid' => $uid], - 'uid,member_level', - 0, 0, 'uid', 'asc' - ); - - if (empty($directChildren)) { - return 0; - } - - $total = 0; - foreach ($directChildren as $child) { - $childLevel = (int)($child['member_level'] ?? 0); - - // 业绩分离:直推下级已是云店或以上(level≥2),其团队业绩不计入本级 - if ($childLevel >= 2) { - continue; - } - - // 统计该下级自身的报单订单数 - $total += (int)Db::name('store_order') - ->where('uid', $child['uid']) - ->where('is_queue_goods', 1) - ->where('paid', 1) - ->where('is_del', 0) - ->count(); - - // 递归统计该下级的伞下 - $total += $this->recursiveUmbrellaCount((int)$child['uid'], $remainDepth - 1); - } - - return $total; - } - - /** - * 执行升级 + * 手动设置会员等级(管理后台使用) * - * @param int $uid 用户 ID - * @param int $newLevel 新等级 + * @param int $uid 用户 ID + * @param int $grade 目标等级 grade (0-4) */ - public function upgrade(int $uid, int $newLevel): void + public function setUserLevel(int $uid, int $grade): void { - Db::transaction(function () use ($uid, $newLevel) { - $this->userDao->update($uid, ['member_level' => $newLevel], 'uid'); - - Log::info("[MemberLevel] uid={$uid} 升级到 level={$newLevel}"); - }); - - // 升级后通知推荐链上级重新检查 - $user = $this->userDao->get($uid); - if ($user && $user['spread_uid']) { - // 异步检查上级升级(防止递归过深直接调用) - try { - app(\app\jobs\hjf\MemberLevelCheckJob::class)::dispatch($user['spread_uid']); - } catch (\Throwable $e) { - Log::warning("[MemberLevel] 无法派发上级检查 Job: " . $e->getMessage()); + $agentLevelId = 0; + if ($grade > 0) { + $agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade); + if ($agentLevelId <= 0) { + throw new \think\exception\ValidateException("等级 grade={$grade} 在 eb_agent_level 中不存在"); } } - } - private function getDirectRequire(int $level): int - { - $key = self::DIRECT_REQUIRE_KEYS[$level] ?? ''; - if (!$key) { - return self::DEFAULT_DIRECT_REQUIRE[$level] ?? 3; - } - return (int)SystemConfigService::get($key, self::DEFAULT_DIRECT_REQUIRE[$level] ?? 3); - } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userServices->update($uid, ['agent_level' => $agentLevelId]); - private function getUmbrellaRequire(int $level): int - { - $key = self::UMBRELLA_REQUIRE_KEYS[$level] ?? ''; - if (!$key) { - return self::DEFAULT_UMBRELLA_REQUIRE[$level] ?? 9999; - } - return (int)SystemConfigService::get($key, self::DEFAULT_UMBRELLA_REQUIRE[$level] ?? 9999); + Log::info("[MemberLevel] 手动设置 uid={$uid} agent_level={$agentLevelId} (grade={$grade})"); } } diff --git a/pro_v3.5.1/app/services/hjf/PointsRewardServices.php b/pro_v3.5.1/app/services/hjf/PointsRewardServices.php index a8fa745e..c1cdfa23 100644 --- a/pro_v3.5.1/app/services/hjf/PointsRewardServices.php +++ b/pro_v3.5.1/app/services/hjf/PointsRewardServices.php @@ -5,22 +5,19 @@ namespace app\services\hjf; use app\dao\hjf\PointsReleaseLogDao; use app\dao\user\UserDao; +use app\services\agent\AgentLevelServices; use app\services\BaseServices; -use crmeb\services\SystemConfigService; use think\annotation\Inject; use think\facade\Db; use think\facade\Log; /** - * 积分奖励服务(级差计算) + * 积分奖励服务(级差计算)—— 改造复用版 * - * 触发时机:报单商品订单支付回调成功后调用 reward($orderUid, $orderId)。 - * - * 奖励规则(PRD 3.2): - * - 推荐人(直推上级)获得 直推奖励积分(按推荐人等级) - * - 更上级获得 级差积分(上级积分 - 直接下级已获得的积分) - * - 所有奖励积分写入 frozen_points(待释放状态) - * - 同时写 points_release_log 记录明细 + * 改造要点(PRD 3.2.2): + * - 使用 eb_user.agent_level (FK → eb_agent_level.id) 获取会员等级 + * - 从 eb_agent_level 表的 direct_reward_points / umbrella_reward_points 字段读取奖励积分 + * - 不再使用独立的 member_level 字段和系统配置表中的 hjf_reward_* 键 * * Class PointsRewardServices * @package app\services\hjf @@ -33,50 +30,19 @@ class PointsRewardServices extends BaseServices #[Inject] protected UserDao $userDao; - /** - * 各等级直推奖励积分配置键 - */ - const DIRECT_REWARD_KEYS = [ - 0 => 0, // 普通会员:无直推奖励 - 1 => 'hjf_reward_direct_1', // 创客 - 2 => 'hjf_reward_direct_2', // 云店 - 3 => 'hjf_reward_direct_3', // 服务商 - 4 => 'hjf_reward_direct_4', // 分公司 - ]; - - /** - * 各等级伞下奖励积分配置键 - */ - const UMBRELLA_REWARD_KEYS = [ - 0 => 0, - 1 => 'hjf_reward_umbrella_1', - 2 => 'hjf_reward_umbrella_2', - 3 => 'hjf_reward_umbrella_3', - 4 => 'hjf_reward_umbrella_4', - ]; - - /** - * 默认积分奖励(当系统配置未初始化时使用) - */ - const DEFAULT_DIRECT = [0 => 0, 1 => 500, 2 => 800, 3 => 1000, 4 => 1300]; - const DEFAULT_UMBRELLA = [0 => 0, 1 => 0, 2 => 300, 3 => 200, 4 => 300]; + #[Inject] + protected AgentLevelServices $agentLevelServices; /** * 对一笔报单订单发放积分奖励 - * - * @param int $orderUid 下单用户 ID - * @param string $orderId 订单号 */ public function reward(int $orderUid, string $orderId): void { try { - // 获取下单用户信息 $buyer = $this->userDao->get($orderUid); if (!$buyer || !$buyer['spread_uid']) { - return; // 无推荐人,不发奖励 + return; } - - // 沿推荐链向上遍历,计算级差奖励 $this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0); } catch (\Throwable $e) { Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage()); @@ -86,11 +52,11 @@ class PointsRewardServices extends BaseServices /** * 向上递归发放级差积分 * - * @param int $uid 当前被奖励用户 - * @param int $fromUid 触发方(下级)用户 ID - * @param string $orderId 来源订单号 - * @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减) - * @param int $depth 递归深度(最多遍历10层) + * @param int $uid 当前被奖励用户 + * @param int $fromUid 触发方(下级)用户 ID + * @param string $orderId 来源订单号 + * @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减) + * @param int $depth 递归深度 */ private function propagateReward( int $uid, @@ -108,22 +74,21 @@ class PointsRewardServices extends BaseServices return; } - $level = (int)($user['member_level'] ?? 0); - if ($level === 0) { - // 普通会员不获得奖励,但继续向上传递 + $agentLevelId = (int)($user['agent_level'] ?? 0); + $grade = $this->agentLevelServices->getGradeByLevelId($agentLevelId); + + if ($grade === 0) { if ($user['spread_uid']) { $this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1); } return; } - // 判断是直推还是伞下(depth=0 说明是第一个上级,即直推) $isDirect = ($depth === 0); - $reward = $isDirect - ? $this->getDirectReward($level) - : $this->getUmbrellaReward($level); + $reward = $isDirect + ? $this->agentLevelServices->getDirectRewardPoints($agentLevelId) + : $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId); - // 级差:本次实发 = 本等级应得 - 下级已获得 $actual = max(0, $reward - $lowerReward); if ($actual > 0) { @@ -136,13 +101,12 @@ class PointsRewardServices extends BaseServices ); } - // 继续向上传递(使用本级应得的 reward 作为下一级的 lowerReward) if ($user['spread_uid']) { $this->propagateReward( (int)$user['spread_uid'], $uid, $orderId, - $reward, // 传递本级"应得"(而非实发)给上级做级差 + $reward, $depth + 1 ); } @@ -154,10 +118,8 @@ class PointsRewardServices extends BaseServices private function grantFrozenPoints(int $uid, int $points, string $orderId, string $type, string $mark): void { Db::transaction(function () use ($uid, $points, $orderId, $type, $mark) { - // 增加 frozen_points $this->userDao->bcInc($uid, 'frozen_points', $points, 'uid'); - // 写明细日志 $this->logDao->save([ 'uid' => $uid, 'points' => $points, @@ -170,22 +132,4 @@ class PointsRewardServices extends BaseServices ]); }); } - - private function getDirectReward(int $level): int - { - $key = self::DIRECT_REWARD_KEYS[$level] ?? 0; - if (!$key) { - return self::DEFAULT_DIRECT[$level] ?? 0; - } - return (int)SystemConfigService::get($key, self::DEFAULT_DIRECT[$level] ?? 0); - } - - private function getUmbrellaReward(int $level): int - { - $key = self::UMBRELLA_REWARD_KEYS[$level] ?? 0; - if (!$key) { - return self::DEFAULT_UMBRELLA[$level] ?? 0; - } - return (int)SystemConfigService::get($key, self::DEFAULT_UMBRELLA[$level] ?? 0); - } } diff --git a/pro_v3.5.1/app/services/user/UserServices.php b/pro_v3.5.1/app/services/user/UserServices.php index b321ad36..9781544b 100644 --- a/pro_v3.5.1/app/services/user/UserServices.php +++ b/pro_v3.5.1/app/services/user/UserServices.php @@ -766,23 +766,31 @@ class UserServices extends BaseServices */ public function index(array $where) { + // 添加过滤条件 + $where['is_filter_del'] = 1; + + /** @var UserWechatuserServices $userWechatUser */ + $userWechatUser = app()->make(UserWechatuserServices::class); + $fields = 'u.*,w.country,w.province,w.city,w.sex,w.unionid,w.openid,w.user_type as w_user_type,w.groupid,w.tagid_list,w.subscribe,w.subscribe_time'; + try { - // 添加过滤条件 - $where['is_filter_del'] = 1; - - /** @var UserWechatuserServices $userWechatUser */ - $userWechatUser = app()->make(UserWechatuserServices::class); - $fields = 'u.*,w.country,w.province,w.city,w.sex,w.unionid,w.openid,w.user_type as w_user_type,w.groupid,w.tagid_list,w.subscribe,w.subscribe_time'; - - // 获取用户列表 [$list, $count] = $userWechatUser->getWhereUserList($where, $fields); + } catch (\Throwable $e) { + Log::error('User index list query failed: ' . $e->getMessage()); - if ($list) { - // 提取唯一 UID 列表 - $uids = array_unique(array_column($list, 'uid')); + return ['count' => 0, 'list' => []]; + } - // 获取相关服务数据 - $userlabel = $this->getUserLablel($uids); + if (!$list) { + return compact('count', 'list'); + } + + try { + // 提取唯一 UID 列表 + $uids = array_unique(array_column($list, 'uid')); + + // 获取相关服务数据 + $userlabel = $this->getUserLablel($uids); $groupIds = array_unique(array_column($list, 'group_id')); $userGroupService = app()->make(UserGroupServices::class); $userGroup = $userGroupService->getUsersGroupName($groupIds); @@ -799,6 +807,11 @@ class UserServices extends BaseServices $clientData = $workClientService->getList(['uid' => $uids], ['id', 'uid', 'name', 'external_userid', 'corp_id', 'unionid'], false); $clientlist = $clientData['list'] ?? []; + /** HJF:分销等级展示索引(is_del=0,按 id/grade 双索引,优先 HJF 官方等级名称) */ + /** @var AgentLevelServices $agentLevelServices */ + $agentLevelServices = app()->make(AgentLevelServices::class); + $hjfLevelMaps = $agentLevelServices->loadHjfUserListLevelMaps(); + // 补充信息 $extendInfo = SystemConfigService::get('user_extend_info', []); $is_extend_info = false; @@ -826,6 +839,13 @@ class UserServices extends BaseServices // 补充每个用户的详细信息 foreach ($list as &$item) { + $agentLevelId = (int)($item['agent_level'] ?? 0); + $hjfLevelInfo = $agentLevelServices->pickHjfLevelRowForUserListDisplay($agentLevelId, $hjfLevelMaps); + $item['member_level'] = $hjfLevelInfo ? (int)$hjfLevelInfo['grade'] : 0; + $item['member_level_name'] = $hjfLevelInfo ? ($hjfLevelInfo['name'] ?? '') : '普通会员'; + $item['available_points'] = (int)($item['available_points'] ?? 0); + $item['frozen_points'] = (int)($item['frozen_points'] ?? 0); + // 地址补充 if (empty($item['addres'])) { if (!empty($item['country']) || !empty($item['province']) || !empty($item['city'])) { @@ -895,13 +915,13 @@ class UserServices extends BaseServices // 扩展信息标志 $item['is_extend_info'] = $is_extend_info; } - } return compact('count', 'list'); - } catch (\Exception $e) { - // 异常处理 - Log::error('Error in user index: ' . $e->getMessage()); - return ['count' => 0, 'list' => []]; + } catch (\Throwable $e) { + // 加工阶段失败时仍返回查询结果,避免整页空白(常见:企微/等级等扩展服务异常) + Log::error('User index enrichment failed: ' . $e->getMessage()); + + return compact('count', 'list'); } } diff --git a/pro_v3.5.1/app/services/user/UserWechatuserServices.php b/pro_v3.5.1/app/services/user/UserWechatuserServices.php index 36007c08..df6b26fd 100644 --- a/pro_v3.5.1/app/services/user/UserWechatuserServices.php +++ b/pro_v3.5.1/app/services/user/UserWechatuserServices.php @@ -12,6 +12,7 @@ declare (strict_types=1); namespace app\services\user; +use app\services\agent\AgentLevelServices; use app\services\BaseServices; use app\dao\user\UserWechatUserDao; use think\annotation\Inject; @@ -48,6 +49,7 @@ class UserWechatuserServices extends BaseServices */ public function getWhereUserList(array $where, string $field): array { + $where = $this->normalizeHjfMemberLevelWhere($where); [$page, $limit] = $this->getPageValue(); $order_string = ''; $order_arr = ['asc', 'desc']; @@ -58,4 +60,40 @@ class UserWechatuserServices extends BaseServices $count = $this->dao->getCountByWhere($where); return [$list, $count]; } + + /** + * 将会员列表筛选「HJF 等级(grade)」转为 eb_user.agent_level 条件,供 UserWechatUserDao 使用。 + */ + protected function normalizeHjfMemberLevelWhere(array $where): array + { + if (!array_key_exists('hjf_member_level', $where)) { + return $where; + } + $raw = $where['hjf_member_level']; + if ($raw === null) { + unset($where['hjf_member_level']); + + return $where; + } + if (is_string($raw)) { + $raw = trim($raw); + } + // 空串/仅空白:不按分销等级筛选(避免 (int)' '=>0 误加 agent_level=0) + if ($raw === '') { + unset($where['hjf_member_level']); + + return $where; + } + $grade = (int)$raw; + /** @var AgentLevelServices $agentLevel */ + $agentLevel = app()->make(AgentLevelServices::class); + if ($grade === 0) { + $where['hjf_agent_level_id'] = 0; + } else { + $where['hjf_agent_level_id'] = $agentLevel->getLevelIdByGrade($grade) ?: -1; + } + unset($where['hjf_member_level']); + + return $where; + } } diff --git a/pro_v3.5.1/crmeb/basic/BaseController.php b/pro_v3.5.1/crmeb/basic/BaseController.php index c483656b49941c93d97845fb2d1e7726df7cd276..d444c8cb7f1b615331054c7b31d6c1e1089054df 100755 GIT binary patch literal 85544 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 literal 3505 zcmc&$ZBN@)6#kxHaX&;#!hir<*$OR%Qrf}VqOw)p1_&AFLQEXn*bYg$GEpHY&^B}$ zpl%sMdtqgjT7=p$D0KXnDcA8n^)KvP-)oyB4c$wm`UB*i_vf5*&$(yLX>lzOjiPrN zYVuK2w@~-Mm7eoxko~uRyKB$BB!@F(`73*7?$zNvvh*ui$`=lA7xumdBH39*!?lk= z)v~%Y9yd`~jG&gL=GHG;np#>>+%z?#EgDUy(~Y8@#G{R(nnYc)j2vkL>Da*f6pl64 zJfzE#h!q3JSYF8fZej1f^>Ew1^SHROW53)k9PC1NtnVHd50>qxdt`YV1ZeJZ2Uc#; zNop~_N9LXt7ZY88ydiqDQuXG!1x28uR=~iDOy(-Ky}ENAUVBl_5F?Cgzu}?%YY4ThUS2tq}Sh7Q0)Pv|U%=N#Y< zXY)i-wkO}v!Kv*m2wz%KBvTS(=_U?G7<0NJUu0pOJi1Hnt-U&2DEyHpPaZ(o?O*1L zd7vzo!t&DZAv^Bj7@$fw_R$K-e{U^3AWIvl@8kZfhkC1w1UpzA^w z1);qSXs|LF&Ws7B;Bj=XQq39=&i4QS#rOVfVa~@w!&MPM%_atn3zu9nAD96nGY^DU77g& z626wbF?HFP#1}tJr^DsT3_6t79OjYQpt2oaxe^LT>*UG9D)G1rAt_~G-3LYYIqG&8 zU%%z--sCu!I04Y4aq0c$s{=i~P0dqMrlmhI@nP8a0Mm$0QMl5cQ?SaZ{|Tt=v=ReK z=I6-M{fhK^&F`#~H$%ADn#yGRuDzG}OgM#cb24*^HkGgJFCyOhgCP$MeZ}F7$vOvS zuFvUB!nWn~VKxXW%f`^Ki-AlvftA;wVUHtT9WAaZ{u1W_u}%MGq=}~3by4|fV&J-Q iS|00><;nijec_;E(=AFHbQ^s)IMynlhe9rt>A>Gh7Cl1% diff --git a/pro_v3.5.1/database/eb_agent_level.sql b/pro_v3.5.1/database/eb_agent_level.sql new file mode 100644 index 00000000..b5925a3e --- /dev/null +++ b/pro_v3.5.1/database/eb_agent_level.sql @@ -0,0 +1,54 @@ +/* + Navicat Premium Dump SQL + + Source Server : jxy-hjf-db + Source Server Type : MySQL + Source Server Version : 50740 (5.7.40-log) + Source Host : 182.92.142.158:3306 + Source Schema : hjfshop + + Target Server Type : MySQL + Target Server Version : 50740 (5.7.40-log) + File Encoding : 65001 + + Date: 21/03/2026 22:47:01 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for eb_agent_level +-- ---------------------------- +DROP TABLE IF EXISTS `eb_agent_level`; +CREATE TABLE `eb_agent_level` ( + `id` int(10) NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '等级名称', + `image` varchar(255) NOT NULL DEFAULT '' COMMENT '背景图', + `color` varchar(32) NOT NULL DEFAULT '' COMMENT ' 字体颜色', + `one_brokerage` smallint(5) NOT NULL DEFAULT '0' COMMENT '一级分拥比例', + `two_brokerage` smallint(5) NOT NULL DEFAULT '0' COMMENT '二级分拥比例', + `grade` smallint(5) NOT NULL DEFAULT '0' COMMENT '等级', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态', + `is_del` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除', + `add_time` int(10) NOT NULL DEFAULT '0' COMMENT '添加时间', + `direct_reward_points` int(11) NOT NULL DEFAULT '0' COMMENT '直推奖励积分(每单)', + `umbrella_reward_points` int(11) NOT NULL DEFAULT '0' COMMENT '伞下奖励积分(每单,级差基数)', + PRIMARY KEY (`id`), + KEY `status` (`status`,`is_del`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='分销员等级表'; + +-- ---------------------------- +-- Records of eb_agent_level +-- ---------------------------- +BEGIN; +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (1, '创客', '/uploads/system/agent_level_1.png', '#D97E1D', 1, 0, 1, 1, 0, 1700126550, 500, 0); +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (2, '云店', '/uploads/system/agent_level_2.png', '#5D7DAC', 5, 3, 2, 1, 0, 1700126572, 800, 300); +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (3, '服务商', '/uploads/system/agent_level_3.png', '#5856D6', 10, 5, 3, 1, 0, 1700126595, 1000, 200); +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (4, '分公司', '/uploads/system/agent_level_4.png', '#1DB0FC', 12, 7, 4, 1, 0, 1700126621, 1300, 300); +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (5, '等级五', '/uploads/system/agent_level_5.png', '#AF52DE', 19, 12, 5, 0, 1, 1701764897, 0, 0); +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (6, '服务商1', '', '#9C27B0', 0, 0, 3, 0, 1, 1774091023, 1000, 200); +INSERT INTO `eb_agent_level` (`id`, `name`, `image`, `color`, `one_brokerage`, `two_brokerage`, `grade`, `status`, `is_del`, `add_time`, `direct_reward_points`, `umbrella_reward_points`) VALUES (7, '分公司2', '', '#F44336', 0, 0, 4, 0, 1, 1774091023, 1300, 300); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/pro_v3.5.1/database/hjf_migration.sql b/pro_v3.5.1/database/hjf_migration.sql index 2b9f5197..76676965 100644 --- a/pro_v3.5.1/database/hjf_migration.sql +++ b/pro_v3.5.1/database/hjf_migration.sql @@ -1,15 +1,17 @@ -- ============================================================ -- 黄精粉健康商城 HJF 数据库迁移脚本 --- 版本:Phase 2 --- 日期:2026-03-15 +-- 版本:Phase 3(改造复用版) +-- 日期:2026-03-21 -- 执行说明: -- 1. 兼容 MySQL 5.7+,数据库前缀为 eb_ --- 2. 按顺序执行 P2-01 ~ P2-05 +-- 2. 按顺序执行 P3-01 ~ P3-07 -- 3. 所有操作均做幂等处理,可重复执行 +-- 4. 遵循 PRD 改造复用原则:会员等级复用 eb_agent_level 体系, +-- 使用 eb_user.agent_level (FK) 代替独立的 member_level 字段 -- ============================================================ -- ============================================================ --- P2-01: 公排池表 +-- P3-01: 公排池表 -- ============================================================ CREATE TABLE IF NOT EXISTS `eb_queue_pool` ( @@ -31,7 +33,7 @@ CREATE TABLE IF NOT EXISTS `eb_queue_pool` ( -- ============================================================ --- P2-02: 积分释放日志表 +-- P3-02: 积分释放日志表 -- ============================================================ CREATE TABLE IF NOT EXISTS `eb_points_release_log` ( @@ -54,10 +56,42 @@ CREATE TABLE IF NOT EXISTS `eb_points_release_log` ( -- ============================================================ --- P2-03 / P2-04: eb_user / eb_store_product / eb_store_order 扩展字段 +-- P3-03: eb_agent_level 扩展字段(改造复用:增加积分奖励字段) +-- ============================================================ + +DROP PROCEDURE IF EXISTS `hjf_migrate_agent_level`; + +DELIMITER $$ +CREATE PROCEDURE `hjf_migrate_agent_level`() +BEGIN + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_agent_level' AND COLUMN_NAME = 'direct_reward_points' + ) THEN + ALTER TABLE `eb_agent_level` + ADD COLUMN `direct_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '直推奖励积分(每单)'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_agent_level' AND COLUMN_NAME = 'umbrella_reward_points' + ) THEN + ALTER TABLE `eb_agent_level` + ADD COLUMN `umbrella_reward_points` int(11) NOT NULL DEFAULT 0 COMMENT '伞下奖励积分(每单,级差基数)'; + END IF; + +END$$ +DELIMITER ; + +CALL `hjf_migrate_agent_level`(); +DROP PROCEDURE IF EXISTS `hjf_migrate_agent_level`; + + +-- ============================================================ +-- P3-04: eb_user / eb_store_product / eb_store_order 扩展字段 -- --- MySQL 5.7 不支持 "ADD COLUMN IF NOT EXISTS", --- 改用存储过程 + information_schema 实现幂等检查。 +-- 注意:不再新增 member_level 字段,复用已有的 agent_level (FK→eb_agent_level.id) -- ============================================================ DROP PROCEDURE IF EXISTS `hjf_migrate_columns`; @@ -66,15 +100,7 @@ DELIMITER $$ CREATE PROCEDURE `hjf_migrate_columns`() BEGIN - -- ---- eb_user 字段 ---- - IF NOT EXISTS ( - SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'member_level' - ) THEN - ALTER TABLE `eb_user` - ADD COLUMN `member_level` tinyint(1) NOT NULL DEFAULT 0 COMMENT '会员等级:0普通 1创客 2云店 3服务商 4分公司'; - END IF; - + -- ---- eb_user 字段(不含 member_level,复用 agent_level)---- IF NOT EXISTS ( SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'no_assess' @@ -99,14 +125,6 @@ BEGIN ADD COLUMN `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '可用积分'; END IF; - -- eb_user 索引:idx_member_level - IF NOT EXISTS ( - SELECT 1 FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND INDEX_NAME = 'idx_member_level' - ) THEN - ALTER TABLE `eb_user` ADD INDEX `idx_member_level` (`member_level`); - END IF; - -- ---- eb_store_product 字段 ---- IF NOT EXISTS ( SELECT 1 FROM information_schema.COLUMNS @@ -124,7 +142,6 @@ BEGIN ADD COLUMN `allow_pay_types` varchar(255) NOT NULL DEFAULT '' COMMENT '允许积分支付类型(JSON数组)'; END IF; - -- eb_store_product 索引:idx_is_queue_goods IF NOT EXISTS ( SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' AND INDEX_NAME = 'idx_is_queue_goods' @@ -141,7 +158,6 @@ BEGIN ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否报单商品订单:1=是'; END IF; - -- eb_store_order 索引:idx_is_queue_goods IF NOT EXISTS ( SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_order' AND INDEX_NAME = 'idx_is_queue_goods' @@ -157,115 +173,221 @@ DROP PROCEDURE IF EXISTS `hjf_migrate_columns`; -- ============================================================ --- P2-05: eb_system_config 初始化配置项 +-- P3-05: 初始化会员等级数据到 eb_agent_level(改造复用) -- --- 字段说明(与 CRMEB 原表保持一致): --- menu_name = 配置键名(代码中 SystemConfigService::get() 读取) --- value = 默认值(字符串) --- info = 后台显示名称 --- desc = 说明文字 --- config_tab_id = 0(不归属某分组,便于独立管理) --- status = 1(启用) +-- 将原分销员等级改为五级会员等级体系: +-- grade=1 → 创客 (direct=500, umbrella=0) +-- grade=2 → 云店 (direct=800, umbrella=300) +-- grade=3 → 服务商 (direct=1000, umbrella=200) +-- grade=4 → 分公司 (direct=1300, umbrella=300) +-- +-- 注意:普通会员 = agent_level=0(无记录),不需要插入 +-- +-- 先将 CRMEB 原有 demo 等级软删除,然后插入 HJF 会员等级 +-- ============================================================ + +UPDATE `eb_agent_level` + SET `is_del` = 1 + WHERE `name` NOT IN ('创客', '云店', '服务商', '分公司') + AND `is_del` = 0; + +INSERT INTO `eb_agent_level` + (`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`, + `direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`) +SELECT '创客', 1, '', '#FF9800', 0, 0, 500, 0, 1, 0, UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `eb_agent_level` WHERE `name` = '创客' AND `is_del` = 0 +); + +INSERT INTO `eb_agent_level` + (`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`, + `direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`) +SELECT '云店', 2, '', '#2196F3', 0, 0, 800, 300, 1, 0, UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `eb_agent_level` WHERE `name` = '云店' AND `is_del` = 0 +); + +INSERT INTO `eb_agent_level` + (`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`, + `direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`) +SELECT '服务商', 3, '', '#9C27B0', 0, 0, 1000, 200, 1, 0, UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `eb_agent_level` WHERE `name` = '服务商' AND `is_del` = 0 +); + +INSERT INTO `eb_agent_level` + (`name`, `grade`, `image`, `color`, `one_brokerage`, `two_brokerage`, + `direct_reward_points`, `umbrella_reward_points`, `status`, `is_del`, `add_time`) +SELECT '分公司', 4, '', '#F44336', 0, 0, 1300, 300, 1, 0, UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `eb_agent_level` WHERE `name` = '分公司' AND `is_del` = 0 +); + + +-- ============================================================ +-- P3-06: 初始化等级升级任务到 eb_agent_level_task(改造复用) +-- +-- 新增任务类型: +-- type=6 → 直推报单单数 +-- type=7 → 伞下报单业绩(含业绩分离) +-- type=8 → 最低直推人数 +-- +-- 各等级任务配置: +-- 创客(grade=1): type=6, number=3 (直推3单) +-- 云店(grade=2): type=7, number=30 (伞下30单) + type=8, number=3 (至少3直推) +-- 服务商(grade=3): type=7, number=100 + type=8, number=3 +-- 分公司(grade=4): type=7, number=1000 + type=8, number=3 +-- ============================================================ + +DROP PROCEDURE IF EXISTS `hjf_init_agent_tasks`; + +DELIMITER $$ +CREATE PROCEDURE `hjf_init_agent_tasks`() +BEGIN + DECLARE v_level_id_1 INT DEFAULT 0; + DECLARE v_level_id_2 INT DEFAULT 0; + DECLARE v_level_id_3 INT DEFAULT 0; + DECLARE v_level_id_4 INT DEFAULT 0; + + SELECT id INTO v_level_id_1 FROM eb_agent_level WHERE grade = 1 AND is_del = 0 LIMIT 1; + SELECT id INTO v_level_id_2 FROM eb_agent_level WHERE grade = 2 AND is_del = 0 LIMIT 1; + SELECT id INTO v_level_id_3 FROM eb_agent_level WHERE grade = 3 AND is_del = 0 LIMIT 1; + SELECT id INTO v_level_id_4 FROM eb_agent_level WHERE grade = 4 AND is_del = 0 LIMIT 1; + + -- 创客:直推报单3单 + IF v_level_id_1 > 0 AND NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_1 AND type = 6 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_1, '直推报单满3单', 6, 3, '直推下级购买报单商品满3单升级为创客', 1, 1, 0, UNIX_TIMESTAMP()); + END IF; + + -- 云店:伞下报单30单 + 至少3个直推 + IF v_level_id_2 > 0 THEN + IF NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_2 AND type = 7 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_2, '伞下报单满30单', 7, 30, '伞下业绩(含分离)达到30单升级为云店', 1, 1, 0, UNIX_TIMESTAMP()); + END IF; + IF NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_2 AND type = 8 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_2, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为云店', 2, 1, 0, UNIX_TIMESTAMP()); + END IF; + END IF; + + -- 服务商:伞下报单100单 + 至少3个直推 + IF v_level_id_3 > 0 THEN + IF NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_3 AND type = 7 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_3, '伞下报单满100单', 7, 100, '伞下业绩(含分离)达到100单升级为服务商', 1, 1, 0, UNIX_TIMESTAMP()); + END IF; + IF NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_3 AND type = 8 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_3, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为服务商', 2, 1, 0, UNIX_TIMESTAMP()); + END IF; + END IF; + + -- 分公司:伞下报单1000单 + 至少3个直推 + IF v_level_id_4 > 0 THEN + IF NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_4 AND type = 7 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_4, '伞下报单满1000单', 7, 1000, '伞下业绩(含分离)达到1000单升级为分公司', 1, 1, 0, UNIX_TIMESTAMP()); + END IF; + IF NOT EXISTS ( + SELECT 1 FROM eb_agent_level_task WHERE level_id = v_level_id_4 AND type = 8 AND is_del = 0 + ) THEN + INSERT INTO eb_agent_level_task (level_id, `name`, type, number, `desc`, sort, status, is_del, add_time) + VALUES (v_level_id_4, '至少3个直推', 8, 3, '需至少3个直推下级才可升级为分公司', 2, 1, 0, UNIX_TIMESTAMP()); + END IF; + END IF; + +END$$ +DELIMITER ; + +CALL `hjf_init_agent_tasks`(); +DROP PROCEDURE IF EXISTS `hjf_init_agent_tasks`; + + +-- ============================================================ +-- P3-07: eb_system_config 初始化配置项 -- ============================================================ --- 防止重复执行报错,使用 INSERT IGNORE INSERT IGNORE INTO `eb_system_config` (`is_store`, `menu_name`, `type`, `input_type`, `config_tab_id`, `parameter`, `upload_type`, `required`, `width`, `high`, `value`, `info`, `desc`, `sort`, `status`) VALUES --- 公排触发倍数:每入 N 单退款第1单(默认 4) (0, 'hjf_trigger_multiple', 'text', 'input', 0, '', 0, '', 100, 0, '4', '公排触发倍数', '每进入N单公排触发退款第1单,默认4', 10, 1), --- 积分每日释放比例(‰,默认 4,即 4‰) (0, 'hjf_release_rate', 'text', 'input', 0, '', 0, '', 100, 0, '4', '积分每日释放比例(‰)', '每日释放:frozen_points × N / 1000,默认4(即4‰)', 20, 1), --- 提现手续费率(%,默认 7,即 7%) (0, 'hjf_fee_rate', 'text', 'input', 0, '', 0, '', 100, 0, - '7', '提现手续费率(%)', '申请提现时收取的手续费比例,默认7%', 30, 1), + '7', '提现手续费率(%)', '申请提现时收取的手续费比例,默认7%', 30, 1); --- 等级升级门槛:普通→创客(直推N单) -(0, 'hjf_level_direct_require_1', 'text', 'input', 0, - '', 0, '', 100, 0, - '3', '创客升级所需直推单数', '普通会员直推N单报单商品后升级为创客,默认3', 40, 1), --- 等级升级门槛:创客→云店(伞下N单) -(0, 'hjf_level_umbrella_require_2', 'text', 'input', 0, - '', 0, '', 100, 0, - '30', '云店升级所需伞下单数', '创客伞下业绩达到N单后升级为云店,默认30', 50, 1), +-- ============================================================ +-- P3-08: 如果已有旧的 member_level 字段,将数据迁移到 agent_level +-- ============================================================ --- 等级升级门槛:云店→服务商(伞下N单) -(0, 'hjf_level_umbrella_require_3', 'text', 'input', 0, - '', 0, '', 100, 0, - '100', '服务商升级所需伞下单数', '云店伞下业绩达到N单后升级为服务商,默认100', 60, 1), +DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`; --- 等级升级门槛:服务商→分公司(伞下N单) -(0, 'hjf_level_umbrella_require_4', 'text', 'input', 0, - '', 0, '', 100, 0, - '1000', '分公司升级所需伞下单数', '服务商伞下业绩达到N单后升级为分公司,默认1000', 70, 1), +DELIMITER $$ +CREATE PROCEDURE `hjf_migrate_member_to_agent_level`() +BEGIN --- 直推奖励积分:创客直推可得N积分 -(0, 'hjf_reward_direct_1', 'text', 'input', 0, - '', 0, '', 100, 0, - '500', '创客直推奖励积分', '创客等级直推一单报单商品可获得的冻结积分,默认500', 80, 1), + IF EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'member_level' + ) THEN + -- 将 member_level 数值映射到 agent_level (FK → eb_agent_level.id) + UPDATE eb_user u + INNER JOIN eb_agent_level al ON al.grade = u.member_level AND al.is_del = 0 + SET u.agent_level = al.id + WHERE u.member_level > 0 AND (u.agent_level = 0 OR u.agent_level IS NULL); + END IF; --- 直推奖励积分:云店 -(0, 'hjf_reward_direct_2', 'text', 'input', 0, - '', 0, '', 100, 0, - '800', '云店直推奖励积分', '云店等级直推一单报单商品可获得的冻结积分,默认800', 90, 1), +END$$ +DELIMITER ; --- 直推奖励积分:服务商 -(0, 'hjf_reward_direct_3', 'text', 'input', 0, - '', 0, '', 100, 0, - '1000', '服务商直推奖励积分', '服务商等级直推一单报单商品可获得的冻结积分,默认1000', 100, 1), - --- 直推奖励积分:分公司 -(0, 'hjf_reward_direct_4', 'text', 'input', 0, - '', 0, '', 100, 0, - '1300', '分公司直推奖励积分', '分公司等级直推一单报单商品可获得的冻结积分,默认1300', 110, 1), - --- 伞下奖励积分:创客(无伞下奖励) -(0, 'hjf_reward_umbrella_1', 'text', 'input', 0, - '', 0, '', 100, 0, - '0', '创客伞下奖励积分', '创客等级伞下奖励积分(级差),默认0(无伞下奖励)', 120, 1), - --- 伞下奖励积分:云店 -(0, 'hjf_reward_umbrella_2', 'text', 'input', 0, - '', 0, '', 100, 0, - '300', '云店伞下奖励积分', '云店等级伞下奖励积分(级差),默认300', 130, 1), - --- 伞下奖励积分:服务商 -(0, 'hjf_reward_umbrella_3', 'text', 'input', 0, - '', 0, '', 100, 0, - '200', '服务商伞下奖励积分', '服务商等级伞下奖励积分(级差),默认200', 140, 1), - --- 伞下奖励积分:分公司 -(0, 'hjf_reward_umbrella_4', 'text', 'input', 0, - '', 0, '', 100, 0, - '300', '分公司伞下奖励积分', '分公司等级伞下奖励积分(级差),默认300', 150, 1); +CALL `hjf_migrate_member_to_agent_level`(); +DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`; -- ============================================================ -- 迁移完成校验(可手动执行检查) -- ============================================================ --- SELECT TABLE_NAME FROM information_schema.TABLES --- WHERE TABLE_SCHEMA = DATABASE() --- AND TABLE_NAME IN ('eb_queue_pool', 'eb_points_release_log'); +-- SELECT id, name, grade, direct_reward_points, umbrella_reward_points +-- FROM eb_agent_level WHERE is_del = 0 ORDER BY grade; + +-- SELECT alt.id, al.name AS level_name, alt.type, alt.number, alt.name AS task_name +-- FROM eb_agent_level_task alt +-- JOIN eb_agent_level al ON al.id = alt.level_id +-- WHERE alt.is_del = 0 AND al.is_del = 0 +-- ORDER BY al.grade, alt.type; -- SELECT COLUMN_NAME FROM information_schema.COLUMNS -- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' --- AND COLUMN_NAME IN ('member_level','no_assess','frozen_points','available_points'); - --- SELECT COLUMN_NAME FROM information_schema.COLUMNS --- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' --- AND COLUMN_NAME IN ('is_queue_goods','allow_pay_types'); +-- AND COLUMN_NAME IN ('no_assess','frozen_points','available_points'); -- SELECT menu_name, value FROM eb_system_config -- WHERE menu_name LIKE 'hjf_%' ORDER BY sort; diff --git a/pro_v3.5.1/help/PHP-Setup.md b/pro_v3.5.1/help/PHP-Setup.md index dc2f32de..51eef4d6 100644 --- a/pro_v3.5.1/help/PHP-Setup.md +++ b/pro_v3.5.1/help/PHP-Setup.md @@ -60,6 +60,64 @@ --- +## 长期固定使用 PHP 8.0(推荐) + +CRMEB Pro v3.5 官方以 **PHP 8.0** 为基准;与 **Swoole Loader**、扩展版本一一对应,建议本机 CLI 与文档统一为 **8.0**,避免混用 8.1 导致 Loader/扩展不匹配。 + +### 1. 安装并链接 Homebrew `php@8.0` + +```bash +brew install php@8.0 +``` + +**Intel Mac(前缀一般为 `/usr/local`)** — 让终端默认 `php` 指向 8.0: + +```bash +brew unlink php@8.1 2>/dev/null || true +brew link php@8.0 --force --overwrite +``` + +若不想改全局 link,只在当前用户把 8.0 放在 PATH 最前(写入 `~/.zshrc` 后 `source ~/.zshrc`): + +```bash +# Intel +export PATH="/usr/local/opt/php@8.0/bin:/usr/local/opt/php@8.0/sbin:$PATH" +``` + +**Apple Silicon(前缀一般为 `/opt/homebrew`)**: + +```bash +export PATH="/opt/homebrew/opt/php@8.0/bin:/opt/homebrew/opt/php@8.0/sbin:$PATH" +``` + +### 2. 验证 + +```bash +# `which php` 应指向 php@8.0,例如: +# Intel: /usr/local/opt/php@8.0/bin/php +# Apple 硅: /opt/homebrew/opt/php@8.0/bin/php +which php +php -v # 应显示 PHP 8.0.x +php -m | grep -E 'swoole|swoole_loader' +``` + +- **swoole**、**swoole_loader**(非企业版)均应出现;Loader 只需在 **PHP 8.0** 的 `php.ini` / `conf.d` 里配置 **一处**,避免重复加载告警。 + +### 3. 启动 API + +始终在**项目根目录**执行: + +```bash +cd /path/to/pro_v3.5.1 +# 建议写死 8.0 路径,避免 PATH 里仍是 8.1: +/usr/local/opt/php@8.0/bin/php -d memory_limit=300M think swoole +# Apple 硅用:/opt/homebrew/opt/php@8.0/bin/php ... +``` + +或使用 `./help/start-api.sh`(**仅**使用上述 php@8.0 路径或环境变量 `CRMEB_PHP_BIN`,不再回退到任意 `php`)。 + +--- + ## 验证与本地启动 ### 一键检查(项目根目录执行) diff --git a/pro_v3.5.1/help/start-api.sh b/pro_v3.5.1/help/start-api.sh index ed130f60..ed1faffb 100755 --- a/pro_v3.5.1/help/start-api.sh +++ b/pro_v3.5.1/help/start-api.sh @@ -1,7 +1,29 @@ #!/usr/bin/env bash # 按 help/PHP-Setup.md 要求,以 memory_limit=300M 启动 Swoole API 服务 -# 用法:在项目根目录执行 ./help/start-api.sh,或先 cd pro_v3.5.1 再执行 +# 长期固定 PHP 8.0:优先使用 Homebrew php@8.0,避免 PATH 里误用 8.1 +# 用法:在项目根目录执行 ./help/start-api.sh set -e cd "$(dirname "$0")/.." -php -d memory_limit=300M think swoole + +# 固定使用 PHP 8.0,不回退到 PATH 里的 `php`(避免误用 8.1 等) +resolve_php80() { + if [[ -n "${CRMEB_PHP_BIN:-}" && -x "${CRMEB_PHP_BIN}" ]]; then + echo "${CRMEB_PHP_BIN}" + return 0 + fi + for candidate in \ + "/usr/local/opt/php@8.0/bin/php" \ + "/opt/homebrew/opt/php@8.0/bin/php"; do + if [[ -x "${candidate}" ]]; then + echo "${candidate}" + return 0 + fi + done + echo "start-api.sh: 未找到 PHP 8.0,请安装: brew install php@8.0" >&2 + echo "或指定: CRMEB_PHP_BIN=/你的路径/php ./help/start-api.sh" >&2 + exit 1 +} + +PHP_BIN="$(resolve_php80)" +exec "${PHP_BIN}" -d memory_limit=300M think swoole diff --git a/pro_v3.5.1/route/api.php b/pro_v3.5.1/route/api.php index a7ec9f5c..f580d009 100644 --- a/pro_v3.5.1/route/api.php +++ b/pro_v3.5.1/route/api.php @@ -891,18 +891,18 @@ Route::group('api', function () { * HJF 黄精粉模块路由 */ Route::group('hjf', function () { - // 需要登录的接口 Route::group(function () { - // 公排队列 - Route::get('queue/status', 'v1.hjf.QueueController/status'); // 公排状态 - Route::get('queue/history', 'v1.hjf.QueueController/history'); // 入队历史 + Route::get('queue/status', 'v1.hjf.QueueController/status'); + Route::get('queue/history', 'v1.hjf.QueueController/history'); - // 积分明细 - Route::get('points/detail', 'v1.hjf.PointsController/detail'); // 积分明细 + Route::get('points/detail', 'v1.hjf.PointsController/detail'); - // 资产总览 & 现金流水 - Route::get('assets/overview', 'v1.hjf.AssetsController/overview'); // 资产总览 - Route::get('assets/cash/detail', 'v1.hjf.AssetsController/cashDetail'); // 现金流水 + Route::get('assets/overview', 'v1.hjf.AssetsController/overview'); + Route::get('assets/cash/detail', 'v1.hjf.AssetsController/cashDetail'); + + Route::get('member/info', 'v1.hjf.MemberController/info'); + Route::get('member/team', 'v1.hjf.MemberController/team'); + Route::get('member/income', 'v1.hjf.MemberController/income'); })->middleware(AuthTokenMiddleware::class, true); })->middleware(StationOpenMiddleware::class); diff --git a/pro_v3.5.1/view/admin/src/pages/product/productList/index.vue b/pro_v3.5.1/view/admin/src/pages/product/productList/index.vue index 63f555a2..9c5b1723 100644 --- a/pro_v3.5.1/view/admin/src/pages/product/productList/index.vue +++ b/pro_v3.5.1/view/admin/src/pages/product/productList/index.vue @@ -1150,7 +1150,9 @@ export default { this.goodHeade(); }, activated() { - this.artFrom.page = Number(this.$route.params.from_page) || 1; + const p = Number(this.$route.params.from_page) || 1; + this.artFrom.page = p; + this.tableForm.page = p; this.goodHeade(); this.getDataList(); }, @@ -1438,12 +1440,18 @@ export default { }); }, getPath() { + const tabType = + this.$route.query.type != null + ? String(this.$route.query.type) + : "1"; this.columns2 = [...this.columns]; - if (name !== "1" && name !== "2") { + if (tabType !== "1" && tabType !== "2") { this.columns2.shift(); } this.artFrom.page = 1; - this.artFrom.type = this.$route.query.type.toString(); + this.artFrom.type = tabType; + this.tableForm.page = 1; + this.tableForm.type = tabType; this.getDataList(); }, changeMenu(row, name, index) { @@ -1774,22 +1782,26 @@ export default { } return newObj; }, - // 商品列表; + // 商品列表(与 goodHeade 一致使用 artFrom,避免 tableForm 未同步导致列表为空或与 Tab 不一致) getDataList() { // this.loading = true; - let tableForm = {...this.tableForm}; - tableForm.sales_range = tableForm.sales_range - ? tableForm.sales_range.join("-") - : ""; - tableForm.price_range = tableForm.price_range - ? tableForm.price_range.join("-") - : ""; - tableForm.stock_range = tableForm.stock_range - ? tableForm.stock_range.join("-") - : ""; - tableForm.collect_range = tableForm.collect_range - ? tableForm.collect_range.join("-") - : ""; + let tableForm = this.deepClone(this.artFrom); + tableForm.sales_range = + tableForm.sales_range && tableForm.sales_range.length + ? tableForm.sales_range.join("-") + : ""; + tableForm.price_range = + tableForm.price_range && tableForm.price_range.length + ? tableForm.price_range.join("-") + : ""; + tableForm.stock_range = + tableForm.stock_range && tableForm.stock_range.length + ? tableForm.stock_range.join("-") + : ""; + tableForm.collect_range = + tableForm.collect_range && tableForm.collect_range.length + ? tableForm.collect_range.join("-") + : ""; // tableForm.create_range = this.timeVal.length ? this.timeVal.join("-") : ""; getGoods(tableForm) .then((res) => { diff --git a/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue b/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue index 0f9df107..f4c2ad23 100644 --- a/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue +++ b/pro_v3.5.1/view/admin/src/pages/setting/membershipLevel/index.vue @@ -219,6 +219,16 @@ export default { minWidth: 80, title: "等级", }, + { + key: "direct_reward_points", + minWidth: 120, + title: "直推奖励积分", + }, + { + key: "umbrella_reward_points", + minWidth: 120, + title: "伞下奖励积分", + }, { key: "one_brokerage", minWidth: 120, diff --git a/pro_v3.5.1/view/admin/src/pages/user/list/index.vue b/pro_v3.5.1/view/admin/src/pages/user/list/index.vue index 4a08b50e..84b3ed8e 100644 --- a/pro_v3.5.1/view/admin/src/pages/user/list/index.vue +++ b/pro_v3.5.1/view/admin/src/pages/user/list/index.vue @@ -54,15 +54,15 @@
- +