feat(hjf): H5路由修复、分销等级显示优化、个人中心等级徽章

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
This commit is contained in:
apple
2026-03-22 01:43:36 +08:00
parent 590eca8c22
commit 8592243d36
34 changed files with 1467 additions and 745 deletions

View File

@@ -14,33 +14,37 @@
### 1.2 技术底座说明 ### 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 (后台) | | 前端框架 | uni-app (Vue 3) + iView Admin (后台) |
| 数据库 | MySQL 8.0 | | 数据库 | MySQL 5.7 |
| 消息队列 | think-queue (Redis驱动) | | 消息队列 | think-queue (Redis驱动) |
| 定时任务 | Swoole Timer / Linux Crontab | | 定时任务 | Swoole Timer / Linux Crontab |
| 小程序端 | 微信小程序 + H5 | | 小程序端 | 微信小程序 + H5 |
### 1.3 术语定义 ### 1.3 术语定义
| 术语 | 定义 |
|---|---| | 术语 | 定义 |
| 公排池 | 所有购买报单商品的订单按付款时间顺序进入的全局排队队列 | | ----- | ---------------------------------- |
| 进四退一 | 默认每进入4单触发退还最早入队第1单的购买款项数量后台可配置 | | 公排池 | 所有购买报单商品的订单按付款时间顺序进入的全局排队队列 |
| 报单商品 | 参与公排机制的指定商品当前主要为3600元黄精粉套餐 | | 进四退一 | 默认每进入4单触发退还最早入队第1单的购买款项数量后台可配置 |
| 普通商品 | 参与公排机制的商品,可使用积分购买 | | 报单商品 | 参与公排机制的指定商品,当前主要为3600元黄精粉套餐 |
| 待释放积分 | 已奖励但尚在冻结期的积分,按千分之四/天速率解冻 | | 普通商品 | 不参与公排机制的商品,可使用积分购买 |
| 释放积分 | 已完成解冻的可用积分,可用于购买普通商品 | | 释放积分 | 已奖励但尚在冻结期的积分,按千分之四/天速率解冻 |
| 伞下 | 某会员通过裂变推荐关系树中,其直推及直推以下的所有下级成员 | | 已释放积分 | 已完成解冻的可用积分,可用于购买普通商品 |
| 直推 | 某会员直接邀请加入的一级下级成员 | | 伞下 | 某会员通过裂变推荐关系树中,其直推及直推以下的所有下级成员 |
| 创客 | 直推3单后自动升级的会员等级 | | 直推 | 某会员直接邀请加入的一级下级成员 |
| 云店 | 伞下业绩30单后自动升级的会员等级至少3个直推 | | 创客 | 直推3单后自动升级的会员等级 |
| 服务商 | 伞下业绩100单后自动升级的会员等级至少3个直推 | | 云店 | 伞下业绩30单后自动升级的会员等级至少3个直推 |
| 分公司 | 伞下业绩1000单后自动升级的会员等级至少3个直推 | | 服务商 | 伞下业绩100单后自动升级的会员等级至少3个直推 |
| 级差 | 上级享受的积分奖励为下级等级对应奖励与该下级自身等级所扣除部分的差额 | | 分公司 | 伞下业绩1000单后自动升级的会员等级至少3个直推 |
| 级差 | 上级享受的积分奖励为下级等级对应奖励与该下级自身等级所扣除部分的差额 |
--- ---
@@ -52,21 +56,25 @@
### 2.2 产品定位 ### 2.2 产品定位
| 维度 | 描述 |
|---|---| | 维度 | 描述 |
| 产品类型 | 微信小程序 + PC管理后台基于CRMEB Pro v3.5 | | ----- | -------------------------------- |
| 核心商品 | 黄精粉套餐3600元/单)及周边健康产品 | | 产品类型 | 微信小程序 + PC管理后台基于CRMEB Pro v3.5 |
| 商业模式 | 社交裂变电商 + 公排返利 + 会员积分分销 | | 核心商品 | 黄精粉套餐3600元/单)及周边健康产品 |
| 目标市场 | 健康消费意识强、具备社交传播意愿的中青年用户群体 | | 商业模式 | 社交裂变电商 + 公排返利 + 会员积分分销 |
| 核心差异化 | 公排退款机制降低用户试错成本,积分分级奖励激励持续推广 | | 目标市场 | 健康消费意识强、具备社交传播意愿的中青年用户群体 |
| 核心差异化 | 公排退款机制降低用户试错成本,积分分级奖励激励持续推广 |
### 2.3 CRMEB Pro 功能复用与改造策略 ### 2.3 CRMEB Pro 功能复用与改造策略
| 策略 | 涉及模块 |
|---|---| | 策略 | 涉及模块 |
| **直接复用** | 微信登录/手机号授权、商品CRUD及上下架、订单管理及状态流转、微信支付/支付宝支付、优惠券管理、Banner/文章/公告管理、首页DIY装修、后台权限管理、数据统计看板、活动管理及核销、用户管理及标签 | | -------- | ------------------------------------------------------------------------------------------------------------ |
| **直接复用** | 微信登录/手机号授权、商品CRUD及上下架、订单管理及状态流转、微信支付/支付宝支付、优惠券管理、Banner/文章/公告管理、首页DIY装修、后台权限管理、数据统计看板、活动管理及核销、用户管理及标签 |
| **改造复用** | 分销推荐关系绑定 → 加入公排关联、团队分销等级 → 改为五级会员等级体系、分销佣金冻结 → 改为积分待释放/按日释放、余额账户 → 增加公排退款入口、商品分类 → 增加报单商品标记、提现功能 → 调整手续费计算逻辑 | | **改造复用** | 分销推荐关系绑定 → 加入公排关联、团队分销等级 → 改为五级会员等级体系、分销佣金冻结 → 改为积分待释放/按日释放、余额账户 → 增加公排退款入口、商品分类 → 增加报单商品标记、提现功能 → 调整手续费计算逻辑 |
| **全新开发** | 公排池引擎进N退1、积分每日释放定时任务、级差计算引擎、伞下业绩统计含级别隔离、公排状态展示页面 | | **全新开发** | 公排池引擎进N退1、积分每日释放定时任务、级差计算引擎、伞下业绩统计含级别隔离、公排状态展示页面 |
--- ---
@@ -100,19 +108,22 @@
#### 3.2.1 等级定义与升级条件 #### 3.2.1 等级定义与升级条件
| 等级 | 升级条件 | 直推奖励(积分/单) | 伞下奖励(积分/单) | 备注 |
|---|---|---|---|---| | 等级 | 升级条件 | 直推奖励(积分/单) | 伞下奖励(积分/单) | 备注 |
| 普通会员 | 注册即获得 | — | — | 可参与公排 | | ---- | ---------------- | ---------- | ---------- | -------- |
| 创客 | 直推3单 | 500 | — | 直推单数可配置 | | 普通会员 | 注册即获得 | — | — | 可参与公排 |
| 云店 | 伞下业绩30单至少3直推 | 800 | 300 | 伞下云店分离计算 | | 创客 | 直推3单 | 500 | — | 直推单数可配置 |
| 服务商 | 伞下业绩100单至少3直推 | 1000 | 200 | — | | 云店 | 伞下业绩30单至少3直推 | 800 | 300 | 伞下云店分离计算 |
| 分公司 | 伞下业绩1000至少3直推 | 1300 | 300 | — | | 服务商 | 伞下业绩100单至少3直推 | 1000 | 200 | — |
| 分公司 | 伞下业绩1000单至少3直推 | 1300 | 300 | — |
> 所有奖励数值和升级门槛均支持后台配置(复用 CRMEB 系统配置表) > 所有奖励数值和升级门槛均支持后台配置(复用 CRMEB 系统配置表)
#### 3.2.2 改造说明 #### 3.2.2 改造说明
基于 CRMEB Pro 的团队分销等级功能进行改造: 基于 CRMEB Pro 的团队分销等级功能进行改造:
- 将原有的"分销员等级"概念替换为"会员等级" - 将原有的"分销员等级"概念替换为"会员等级"
- 升级条件从"推广订单数/消费金额"改为"直推单数 + 伞下业绩单数" - 升级条件从"推广订单数/消费金额"改为"直推单数 + 伞下业绩单数"
- 佣金计算从"按比例返佣"改为"按等级发放固定积分" - 佣金计算从"按比例返佣"改为"按等级发放固定积分"
@@ -122,11 +133,13 @@
#### 3.3.1 账户类型 #### 3.3.1 账户类型
| 账户类型 | 来源 | 用途 | 提现 |
|---|---|---|---| | 账户类型 | 来源 | 用途 | 提现 |
| 现金余额 | 公排退款、后台手动充值 | 购物、申请提现 | 可提现扣除7%手续费 | | ----- | ----------------- | --------------- | ----------- |
| 待释放积分 | 会员推荐奖励 | 按天解冻后转为已释放积分 | 可提现 | | 现金余额 | 公排退款、后台手动充值 | 购物、申请提现 | 可提现扣除7%手续费 |
| 释放积分 | 待释放积分每日解冻0.4%/天) | 购买普通商品(不可买报单商品) | 不可提现 | | 释放积分 | 会员推荐奖励 | 按天解冻后转为已释放积分 | 不可提现 |
| 已释放积分 | 待释放积分每日解冻0.4%/天) | 购买普通商品(不可买报单商品) | 不可提现 |
#### 3.3.2 改造说明 #### 3.3.2 改造说明
@@ -142,21 +155,25 @@
### 4.1 登录与注册【直接复用】 ### 4.1 登录与注册【直接复用】
| 功能点 | 详细说明 | 优先级 | 复用/改造 |
|---|---|---|---| | 功能点 | 详细说明 | 优先级 | 复用/改造 |
| 微信授权登录 | 使用微信OAuth授权获取用户基本信息 | P0 | 直接复用 | | ------- | ------------------------------ | --- | ----- |
| 手机号一键登录 | 微信手机号授权组件,获取用户手机号完成绑定 | P0 | 直接复用 | | 微信授权登录 | 使用微信OAuth授权获取用户基本信息 | P0 | 直接复用 |
| 推荐关系绑定 | 首次进入小程序时携带推荐人参数,自动绑定上下级关系,不可更改 | P0 | 改造复用 | | 手机号一键登录 | 微信手机号授权组件,获取用户手机号完成绑定 | P0 | 直接复用 |
| 新用户引导 | 首次登录展示平台介绍、公排规则说明页面 | P1 | 新开发 | | 推荐关系绑定 | 首次进入小程序时携带推荐人参数,自动绑定上下级关系,不可更改 | P0 | 改造复用 |
| 新用户引导 | 首次登录展示平台介绍、公排规则说明页面 | P1 | 新开发 |
### 4.2 首页【改造复用DIY】 ### 4.2 首页【改造复用DIY】
| 模块 | 内容说明 | 优先级 | 复用/改造 |
|---|---|---|---| | 模块 | 内容说明 | 优先级 | 复用/改造 |
| Banner轮播图 | 展示主推套餐、最新活动,后台可更换图片和跳转链接 | P0 | 直接复用 | | --------- | ------------------------ | --- | ----- |
| 活动专区 | 展示最新线下活动卡片(品鉴会报名入口) | P0 | 直接复用 | | Banner轮播图 | 展示主推套餐、最新活动,后台可更换图片和跳转链接 | P0 | 直接复用 |
| 商品推荐区 | 展示主推商品列表(报单商品+热门普通商品) | P1 | 改造复用 | | 活动专区 | 展示最新线下活动卡片(品鉴会报名入口) | P0 | 直接复用 |
| 公告通知 | 平台公告或活动通知 | P2 | 直接复用 | | 商品推荐区 | 展示主推商品列表(报单商品+热门普通商品) | P1 | 改造复用 |
| 公告通知 | 平台公告或活动通知 | P2 | 直接复用 |
### 4.3 商品与购买【改造复用】 ### 4.3 商品与购买【改造复用】
@@ -166,14 +183,16 @@
#### 4.3.2 支付方式 #### 4.3.2 支付方式
| 支付方式 | 适用商品 | 配置权限 |
|---|---|---| | 支付方式 | 适用商品 | 配置权限 |
| 微信支付 | 所有商品 | 系统默认支持(复用) | | ----- | ----- | ----------- |
| 支付 | 所有商品 | 系统默认支持(复用) | | 微信支付 | 所有商品 | 系统默认支持(复用) |
| 现金余额 | 指定商品 | 后台按商品设置(改造) | | 支付宝 | 所有商品 | 系统默认支持(复用) |
| 现金余额 | 指定商品 | 后台按商品设置(改造) |
| 待释放积分 | 仅普通商品 | 后台按商品设置(新增) | | 待释放积分 | 仅普通商品 | 后台按商品设置(新增) |
| 已释放积分 | 仅普通商品 | 后台按商品设置(新增) | | 已释放积分 | 仅普通商品 | 后台按商品设置(新增) |
> 报单商品不支持积分支付;普通商品支持哪种支付方式由后台商品管理中单独配置 > 报单商品不支持积分支付;普通商品支持哪种支付方式由后台商品管理中单独配置
#### 4.3.3 购买流程改造 #### 4.3.3 购买流程改造
@@ -187,6 +206,7 @@
### 4.4 裂变推荐机制【改造复用】 ### 4.4 裂变推荐机制【改造复用】
复用 CRMEB Pro 的分销推广海报/链接功能,改造推荐成功后的奖励逻辑: 复用 CRMEB Pro 的分销推广海报/链接功能,改造推荐成功后的奖励逻辑:
- 保持推荐关系绑定机制不变 - 保持推荐关系绑定机制不变
- 将"按比例返佣金"改为"按等级发固定积分" - 将"按比例返佣金"改为"按等级发固定积分"
- 积分入账为"待释放"状态 - 积分入账为"待释放"状态
@@ -199,16 +219,19 @@
#### 4.5.2 我的资产【改造复用】 #### 4.5.2 我的资产【改造复用】
| 资产项 | 展示内容 | 可操作项 | 复用/改造 |
|---|---|---|---| | 资产项 | 展示内容 | 可操作项 | 复用/改造 |
| 现金余额 | 当前可用余额金额 | 申请提现(填写金额,显示到账金额) | 改造复用 | | ----- | --------------- | ----------------- | ----- |
| 待释放积分 | 待解冻积分总量、预计今日释放量 | 查看释放明细 | 新开发 | | 现金余额 | 当前可用余额金额 | 申请提现(填写金额,显示到账金额) | 改造复用 |
| 释放积分 | 可用积分总量 | 查看使用记录 | 新开发 | | 释放积分 | 待解冻积分总量、预计今日释放量 | 查看释放明细 | 新开发 |
| 优惠券 | 我的优惠券列表 | 使用(购物时选择) | 直接复用 | | 已释放积分 | 可用积分总量 | 查看使用记录 | 新开发 |
| 优惠券 | 我的优惠券列表 | 使用(购物时选择) | 直接复用 |
#### 4.5.3 我的推荐【改造复用】 #### 4.5.3 我的推荐【改造复用】
基于 CRMEB Pro 团队分销的推广人管理进行改造: 基于 CRMEB Pro 团队分销的推广人管理进行改造:
- 推荐关系树:可视化展示自己的直推成员及伞下成员(显示等级、入团时间) - 推荐关系树:可视化展示自己的直推成员及伞下成员(显示等级、入团时间)
- 推荐收益明细:每笔积分奖励的来源、时间、金额 - 推荐收益明细:每笔积分奖励的来源、时间、金额
- 推荐数据统计:直推人数、伞下总人数、伞下总单数 - 推荐数据统计:直推人数、伞下总人数、伞下总单数
@@ -226,6 +249,7 @@
### 5.1 概览仪表盘【改造复用】 ### 5.1 概览仪表盘【改造复用】
在 CRMEB Pro 数据统计基础上增加公排相关数据: 在 CRMEB Pro 数据统计基础上增加公排相关数据:
- 今日数据:新增用户数、今日订单数、今日销售额、公排触发次数 - 今日数据:新增用户数、今日订单数、今日销售额、公排触发次数
- 趋势图:用户增长趋势、销售额趋势 - 趋势图:用户增长趋势、销售额趋势
- 实时公排状态:当前公排池总单数、待退款订单数 - 实时公排状态:当前公排池总单数、待退款订单数
@@ -233,6 +257,7 @@
### 5.2 用户管理【改造复用】 ### 5.2 用户管理【改造复用】
在 CRMEB 用户管理基础上增加: 在 CRMEB 用户管理基础上增加:
- 等级管理:手动调整用户等级(含降级),设置/取消"不考核"标记 - 等级管理:手动调整用户等级(含降级),设置/取消"不考核"标记
- 上下级关系树:可视化查看任意用户的推荐关系树 - 上下级关系树:可视化查看任意用户的推荐关系树
- 账户操作:手动增减余额或积分 - 账户操作:手动增减余额或积分
@@ -240,6 +265,7 @@
### 5.3 商品管理【改造复用】 ### 5.3 商品管理【改造复用】
在 CRMEB 商品管理基础上增加: 在 CRMEB 商品管理基础上增加:
- 报单商品设置:标记某商品为报单商品(参与公排机制) - 报单商品设置:标记某商品为报单商品(参与公排机制)
- 支付方式设置:为每个商品独立配置允许的支付方式(含积分支付选项) - 支付方式设置:为每个商品独立配置允许的支付方式(含积分支付选项)
@@ -250,6 +276,7 @@
### 5.5 财务管理【改造复用】 ### 5.5 财务管理【改造复用】
在 CRMEB 财务管理基础上增加: 在 CRMEB 财务管理基础上增加:
- 公排退款流水记录 - 公排退款流水记录
- 积分发放记录(来源订单、受益人、发放时间、积分类型) - 积分发放记录(来源订单、受益人、发放时间、积分类型)
- 积分释放日志 - 积分释放日志
@@ -263,17 +290,19 @@
在 CRMEB 系统配置基础上新增以下配置项: 在 CRMEB 系统配置基础上新增以下配置项:
| 配置项 | 说明 | 默认值 |
|---|---|---| | 配置项 | 说明 | 默认值 |
| 公排触发倍数 | 进N单退1单N值配置 | 4 | | --------- | ----------------- | ------- |
| 积分日释放比例 | 待释放积分每日解冻比例(‰) | 4千分之四 | | 公排触发倍数 | 进N单退1单N值配置 | 4 |
| 提现手续费率 | 提现时扣除的手续费百分比 | 7% | | 积分日释放比例 | 待释放积分每日解冻比例(‰) | 4千分之四 |
| 创客升级门槛 | 直推满N单升级创客 | 3 | | 提现手续费率 | 提现时扣除的手续费百分比 | 7% |
| 云店升级门槛 | 伞下满N单升级云店 | 30 | | 创客升级门槛 | 直推满N单升级创客 | 3 |
| 服务商升级门槛 | 伞下满N单升级服务商 | 100 | | 云店升级门槛 | 伞下满N单升级云店 | 30 |
| 分公司升级门槛 | 伞下满N单升级分公司 | 1000 | | 服务商升级门槛 | 伞下满N单升级服务商 | 100 |
| 各等级直推积分奖励 | 各等级会员每直推1单获得的积分数 | 见等级表 | | 分公司升级门槛 | 伞下满N单升级分公司 | 1000 |
| 各等级伞下积分奖励 | 各等级会员伞下每入1单获得的积分数 | 见等级表 | | 各等级直推积分奖励 | 各等级会员每直推1单获得的积分数 | 见等级表 |
| 各等级伞下积分奖励 | 各等级会员伞下每入1单获得的积分数 | 见等级表 |
### 5.8 内容管理【直接复用】 ### 5.8 内容管理【直接复用】
@@ -282,6 +311,7 @@
### 5.9 数据统计【改造复用】 ### 5.9 数据统计【改造复用】
在 CRMEB 数据统计基础上增加: 在 CRMEB 数据统计基础上增加:
- 公排统计:公排池当前状态、历史退款总额、触发频率分析 - 公排统计:公排池当前状态、历史退款总额、触发频率分析
- 积分统计:总发放积分、总释放积分、积分使用情况 - 积分统计:总发放积分、总释放积分、积分使用情况
@@ -293,40 +323,45 @@
#### eb_queue_pool公排池表 #### eb_queue_pool公排池表
| 字段 | 类型 | 说明 |
|---|---|---| | 字段 | 类型 | 说明 |
| id | INT UNSIGNED AUTO_INCREMENT | 主键 | | ------------- | --------------------------- | --------------- |
| uid | INT UNSIGNED | 用户ID关联eb_user | | id | INT UNSIGNED AUTO_INCREMENT | 主键 |
| order_id | VARCHAR(64) | 原始订单号 | | uid | INT UNSIGNED | 用户ID关联eb_user |
| amount | DECIMAL(10,2) | 金额默认3600.00 | | order_id | VARCHAR(64) | 原始订单号 |
| queue_no | BIGINT UNSIGNED | 全局排队序号 | | amount | DECIMAL(10,2) | 金额默认3600.00 |
| status | TINYINT | 0排队中 1已退款 | | queue_no | BIGINT UNSIGNED | 全局排队序号 |
| refund_time | INT UNSIGNED | 退款时间戳 | | status | TINYINT | 0排队中 1已退款 |
| trigger_batch | INT UNSIGNED | 触发退款的批次号 | | refund_time | INT UNSIGNED | 退款时间戳 |
| add_time | INT UNSIGNED | 入队时间戳 | | trigger_batch | INT UNSIGNED | 触发退款的批次号 |
| add_time | INT UNSIGNED | 入队时间戳 |
#### eb_points_release_log积分释放日志表 #### eb_points_release_log积分释放日志表
| 字段 | 类型 | 说明 |
|---|---|---| | 字段 | 类型 | 说明 |
| id | INT UNSIGNED AUTO_INCREMENT | 主键 | | -------------- | --------------------------- | -------- |
| uid | INT UNSIGNED | 用户ID | | id | INT UNSIGNED AUTO_INCREMENT | 主键 |
| frozen_before | BIGINT | 释放前待释放积分 | | uid | INT UNSIGNED | 用户ID |
| release_amount | BIGINT | 本次释放积分 | | frozen_before | BIGINT | 释放前待释放积分 |
| frozen_after | BIGINT | 释放后待释放积分 | | release_amount | BIGINT | 本次释放积分 |
| release_date | DATE | 释放日期 | | frozen_after | BIGINT | 释放后待释放积分 |
| add_time | INT UNSIGNED | 记录时间 | | release_date | DATE | 释放日期 |
| add_time | INT UNSIGNED | 记录时间 |
### 6.2 修改表 ### 6.2 修改表
#### eb_user用户表新增字段 #### eb_user用户表新增字段
| 字段 | 类型 | 说明 |
|---|---|---| | 字段 | 类型 | 说明 |
| member_level | TINYINT DEFAULT 0 | 会员等级0普通 1创客 2云店 3服务商 4分公司 | | ---------------- | ----------------- | -------------------------- |
| no_assess | TINYINT DEFAULT 0 | 不考核标记0正常 1不考核 | | no_assess | TINYINT DEFAULT 0 | 不考核标记0正常 1不考核 |
| frozen_points | BIGINT DEFAULT 0 | 待释放积分 | | frozen_points | BIGINT DEFAULT 0 | 待释放积分 |
| available_points | BIGINT DEFAULT 0 | 已释放积分 | | available_points | BIGINT DEFAULT 0 | 已释放积分 |
#### eb_system_config系统配置表 #### eb_system_config系统配置表
@@ -349,3 +384,4 @@
- 积分操作使用数据库事务,保证一致性 - 积分操作使用数据库事务,保证一致性
- 所有金额计算使用 bcmath 扩展,避免浮点误差 - 所有金额计算使用 bcmath 扩展,避免浮点误差
- 支付回调验签,防止伪造 - 支付回调验签,防止伪造

14
docs/issues-0321-1.md Normal file
View File

@@ -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`

View File

@@ -61,7 +61,14 @@ class Common extends AuthController
*/ */
public function auth() 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 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('保存成功'); return $this->success('保存成功');
} }
@@ -88,7 +103,11 @@ class Common extends AuthController
*/ */
public function getCopyright(): Response public function getCopyright(): Response
{ {
$copyright = ['copyrightContext' => '', 'copyrightImage' => '']; try {
$copyright = $this->__z6uxyJQ4xYa5ee1mx5();
} catch (\Throwable $e) {
$copyright = ['copyrightContext' => '', 'copyrightImage' => ''];
}
$copyright['version'] = get_crmeb_version(); $copyright['version'] = get_crmeb_version();
return $this->success($copyright); return $this->success($copyright);
} }
@@ -100,48 +119,7 @@ class Common extends AuthController
*/ */
public function auth_apply(SystemAuthServices $services): Response 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("申请授权成功!"); return $this->success("申请授权成功!");
} }
/** /**

View File

@@ -74,6 +74,8 @@ class AgentLevel extends AuthController
['grade', 0], ['grade', 0],
['image', ''], ['image', ''],
['color', ''], ['color', ''],
['direct_reward_points', 0],
['umbrella_reward_points', 0],
['one_brokerage', 0], ['one_brokerage', 0],
['two_brokerage', 0], ['two_brokerage', 0],
['status', 0]]); ['status', 0]]);
@@ -123,6 +125,8 @@ class AgentLevel extends AuthController
['grade', 0], ['grade', 0],
['image', ''], ['image', ''],
['color', ''], ['color', ''],
['direct_reward_points', 0],
['umbrella_reward_points', 0],
['one_brokerage', 0], ['one_brokerage', 0],
['two_brokerage', 0], ['two_brokerage', 0],
['status', 0]]); ['status', 0]]);
@@ -145,6 +149,8 @@ class AgentLevel extends AuthController
$levelInfo->grade = $data['grade']; $levelInfo->grade = $data['grade'];
$levelInfo->image = $data['image']; $levelInfo->image = $data['image'];
$levelInfo->color = $data['color']; $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->one_brokerage = $data['one_brokerage'];
$levelInfo->two_brokerage = $data['two_brokerage']; $levelInfo->two_brokerage = $data['two_brokerage'];
$levelInfo->status = $data['status']; $levelInfo->status = $data['status'];

View File

@@ -5,18 +5,19 @@ namespace app\controller\admin\v1\hjf;
use app\controller\admin\AuthController; use app\controller\admin\AuthController;
use app\dao\user\UserDao; use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
use app\services\hjf\MemberLevelServices; use app\services\hjf\MemberLevelServices;
use app\services\system\SystemConfigServices;
use crmeb\services\SystemConfigService;
use think\annotation\Inject; use think\annotation\Inject;
/** /**
* Admin · 会员管理接口 * Admin · 会员管理接口(改造复用版)
* *
* GET /adminapi/hjf/member/list — 会员列表(分页,支持按等级筛选) * 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。
* PUT /adminapi/hjf/member/level/:uid — 手动调整会员等级 *
* GET /adminapi/hjf/member/config获取会员等级配置 * GET /adminapi/hjf/member/list 会员列表
* POST /adminapi/hjf/member/config 保存会员等级配置 * PUT /adminapi/hjf/member/level/:uid手动调整会员等级
* GET /adminapi/hjf/member/config — 获取会员等级配置(从 eb_agent_level 读取)
* POST /adminapi/hjf/member/config — 保存会员等级配置(写入 eb_agent_level
* *
* Class MemberController * Class MemberController
* @package app\controller\admin\v1\hjf * @package app\controller\admin\v1\hjf
@@ -29,6 +30,9 @@ class MemberController extends AuthController
#[Inject] #[Inject]
protected MemberLevelServices $levelServices; protected MemberLevelServices $levelServices;
#[Inject]
protected AgentLevelServices $agentLevelServices;
/** /**
* 会员列表(分页) * 会员列表(分页)
*/ */
@@ -47,25 +51,38 @@ class MemberController extends AuthController
if ($where['keyword'] !== '') { if ($where['keyword'] !== '') {
$condition['uid|nickname|phone'] = ['like', '%' . $where['keyword'] . '%']; $condition['uid|nickname|phone'] = ['like', '%' . $where['keyword'] . '%'];
} }
if ($where['member_level'] !== '') { 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); $count = $this->userDao->count($condition);
$list = $this->userDao->selectList( $list = $this->userDao->selectList(
$condition, $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, $page,
$limit, $limit,
'uid', 'uid',
'desc' 'desc'
); );
// 附加直推单数 & 伞下单数 $levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]);
$levelMap = array_column($levelList, null, 'id');
foreach ($list as &$item) { 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['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); unset($item);
@@ -74,17 +91,15 @@ class MemberController extends AuthController
/** /**
* 手动调整会员等级 * 手动调整会员等级
*
* @param int $uid
*/ */
public function updateLevel(int $uid): mixed public function updateLevel(int $uid): mixed
{ {
$data = $this->request->getMore([ $data = $this->request->getMore([
['member_level', 0], ['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'); return $this->fail('等级范围 0-4');
} }
@@ -93,78 +108,51 @@ class MemberController extends AuthController
return $this->fail('用户不存在'); return $this->fail('用户不存在');
} }
$this->userDao->update($uid, ['member_level' => $newLevel], 'uid'); $this->levelServices->setUserLevel($uid, $grade);
return $this->success('更新成功'); return $this->success('更新成功');
} }
/** /**
* 获取会员等级配置 * 获取会员等级配置(从 eb_agent_level 表读取)
*/ */
public function getConfig(): mixed public function getConfig(): mixed
{ {
$keys = [ $levelList = $this->agentLevelServices->dao->getList(['is_del' => 0, 'status' => 1]);
'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',
];
$config = []; $config = [];
$defaults = [ foreach ($levelList as $level) {
'hjf_level_direct_require_1' => 3, $config[] = [
'hjf_level_umbrella_require_2' => 30, 'id' => $level['id'],
'hjf_level_umbrella_require_3' => 100, 'name' => $level['name'],
'hjf_level_umbrella_require_4' => 1000, 'grade' => $level['grade'],
'hjf_reward_direct_1' => 500, 'direct_reward_points' => $level['direct_reward_points'] ?? 0,
'hjf_reward_direct_2' => 800, 'umbrella_reward_points' => $level['umbrella_reward_points'] ?? 0,
'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);
} }
return $this->success($config); return $this->success($config);
} }
/** /**
* 保存会员等级配置 * 保存会员等级配置(写入 eb_agent_level 表)
*/ */
public function saveConfig(SystemConfigServices $configServices): mixed public function saveConfig(): mixed
{ {
$allowedKeys = [ $levels = $this->request->post('levels', []);
'hjf_level_direct_require_1', if (!is_array($levels)) {
'hjf_level_umbrella_require_2', return $this->fail('参数格式错误');
'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',
];
$data = $this->request->post(); foreach ($levels as $item) {
foreach ($data as $key => $value) { if (empty($item['id'])) continue;
if (in_array($key, $allowedKeys, true)) { $updateData = [];
$configServices->setConfig($key, (string)$value); 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);
} }
} }

View File

@@ -66,6 +66,7 @@ class StoreProduct extends AuthController
['stock_range', ''],//库存区间 ['stock_range', ''],//库存区间
['collect_range', ''],//收藏区间 ['collect_range', ''],//收藏区间
['product_clear', ''],//适用群体 ['product_clear', ''],//适用群体
['is_queue_goods', ''],//报单商品1/0空=不限
]); ]);
if ($this->adminType == 4) { if ($this->adminType == 4) {
@@ -206,6 +207,7 @@ class StoreProduct extends AuthController
['stock_range', ''],//库存区间 ['stock_range', ''],//库存区间
['collect_range', ''],//收藏区间 ['collect_range', ''],//收藏区间
['product_clear', ''],//适用群体 ['product_clear', ''],//适用群体
['is_queue_goods', ''],//报单商品1/0空=不限(须配合模型 searchIsQueueGoodsAttr
]); ]);
if ($this->adminType == 4) { if ($this->adminType == 4) {
$where['supplier_id'] = $this->adminId; $where['supplier_id'] = $this->adminId;

View File

@@ -85,6 +85,8 @@ class User extends AuthController
['isMember', ''], ['isMember', ''],
['label_ids', ''], ['label_ids', ''],
['is_channel', ''], ['is_channel', ''],
/** HJF按分销等级 grade04筛选对应 eb_user.agent_level */
['hjf_member_level', ''],
]); ]);
if ($where['label_ids']) { if ($where['label_ids']) {
$where['label_id'] = stringToIntArray($where['label_ids']); $where['label_id'] = stringToIntArray($where['label_ids']);

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace app\controller\api\v1\hjf;
use app\Request;
use app\services\agent\AgentLevelServices;
use app\services\agent\AgentLevelTaskServices;
use app\services\hjf\MemberLevelServices;
use app\services\hjf\PointsRewardServices;
use app\dao\hjf\PointsReleaseLogDao;
use app\services\user\UserServices;
use think\annotation\Inject;
use think\facade\Db;
/**
* 用户端 · 会员信息接口(改造复用版)
*
* 复用 eb_agent_level 体系,使用 eb_user.agent_level 字段。
*
* GET /api/hjf/member/info — 当前用户等级信息
* GET /api/hjf/member/team — 团队成员列表
* GET /api/hjf/member/income — 团队收益明细
*
* Class MemberController
* @package app\controller\api\v1\hjf
*/
class MemberController
{
#[Inject]
protected MemberLevelServices $memberLevelServices;
#[Inject]
protected AgentLevelServices $agentLevelServices;
#[Inject]
protected AgentLevelTaskServices $agentLevelTaskServices;
/**
* 获取当前用户会员信息
*/
public function info(Request $request): \think\Response
{
$uid = (int)$request->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'));
}
}

View File

@@ -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']) { if (isset($where['level']) && $where['level']) {
$model = $model->where($userAlias . 'level', $where['level']); $model = $model->where($userAlias . 'level', $where['level']);

View File

@@ -3,9 +3,10 @@ declare(strict_types=1);
namespace app\jobs\hjf; namespace app\jobs\hjf;
use app\services\hjf\MemberLevelServices; use app\services\agent\AgentLevelServices;
use app\services\hjf\PointsRewardServices; use app\services\hjf\PointsRewardServices;
use app\services\hjf\QueuePoolServices; use app\services\hjf\QueuePoolServices;
use app\services\user\UserServices;
use crmeb\basic\BaseJobs; use crmeb\basic\BaseJobs;
use crmeb\traits\QueueTrait; use crmeb\traits\QueueTrait;
use think\exception\ValidateException; use think\exception\ValidateException;
@@ -16,11 +17,11 @@ use think\facade\Log;
* *
* 触发时机Pay 监听器检测到 is_queue_goods=1 时派发。 * 触发时机Pay 监听器检测到 is_queue_goods=1 时派发。
* *
* 执行流程: * 执行流程(改造复用版)
* 1. 调用 QueuePoolServices::enqueue() 将订单写入公排池 * 1. 调用 QueuePoolServices::enqueue() 将订单写入公排池
* (内部含 Redis 分布式锁 + 退款触发检测)
* 2. 调用 PointsRewardServices::reward() 沿推荐链发放级差积分 * 2. 调用 PointsRewardServices::reward() 沿推荐链发放级差积分
* 3. 调用 MemberLevelServices::checkUpgrade() 检查下单用户上级链是否触发等级升级 * 3. 调用 AgentLevelServices::checkUserLevelFinish() 检查升级
* (复用 CRMEB 分销等级升级流程,已支持 HJF 任务类型 6/7/8
* *
* Class HjfOrderPayJob * Class HjfOrderPayJob
* @package app\jobs\hjf * @package app\jobs\hjf
@@ -29,22 +30,14 @@ class HjfOrderPayJob extends BaseJobs
{ {
use QueueTrait; 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 public function doJob(int $uid, string $orderId, float $amount = 3600.00): bool
{ {
try { try {
// 1. 公排入队
/** @var QueuePoolServices $queueServices */ /** @var QueuePoolServices $queueServices */
$queueServices = app()->make(QueuePoolServices::class); $queueServices = app()->make(QueuePoolServices::class);
$queueServices->enqueue($uid, $orderId, $amount); $queueServices->enqueue($uid, $orderId, $amount);
Log::info("[HjfOrderPay] 公排入队成功 uid={$uid} orderId={$orderId}"); Log::info("[HjfOrderPay] 公排入队成功 uid={$uid} orderId={$orderId}");
} catch (ValidateException $e) { } catch (ValidateException $e) {
// 锁竞争导致入队失败重新投递到队列延迟5秒
Log::warning("[HjfOrderPay] 入队被锁,延迟重试 uid={$uid} orderId={$orderId}: " . $e->getMessage()); Log::warning("[HjfOrderPay] 入队被锁,延迟重试 uid={$uid} orderId={$orderId}: " . $e->getMessage());
static::dispatchSece(5, [$uid, $orderId, $amount]); static::dispatchSece(5, [$uid, $orderId, $amount]);
return true; return true;
@@ -54,29 +47,29 @@ class HjfOrderPayJob extends BaseJobs
} }
try { try {
// 2. 积分奖励(级差发放)
/** @var PointsRewardServices $pointsServices */ /** @var PointsRewardServices $pointsServices */
$pointsServices = app()->make(PointsRewardServices::class); $pointsServices = app()->make(PointsRewardServices::class);
$pointsServices->reward($uid, $orderId); $pointsServices->reward($uid, $orderId);
Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId}"); Log::info("[HjfOrderPay] 积分奖励发放完成 uid={$uid} orderId={$orderId}");
} catch (\Throwable $e) { } catch (\Throwable $e) {
// 积分发放失败不阻塞主流程,记录错误即可
Log::error("[HjfOrderPay] 积分奖励失败 uid={$uid} orderId={$orderId}: " . $e->getMessage()); Log::error("[HjfOrderPay] 积分奖励失败 uid={$uid} orderId={$orderId}: " . $e->getMessage());
} }
try { try {
// 3. 触发推荐链等级升级检查(对买家本人及其直推上级) /** @var UserServices $userServices */
/** @var MemberLevelServices $levelServices */ $userServices = app()->make(UserServices::class);
$levelServices = app()->make(MemberLevelServices::class); $userInfo = $userServices->getUserCacheInfo($uid);
$levelServices->checkUpgrade($uid); $spreadUid = $userInfo ? (int)($userInfo['spread_uid'] ?? 0) : 0;
$twoSpreadUid = 0;
// 同时检查直推上级(支付行为可能满足上级的伞下业绩门槛) if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
$spreadUid = (int)\think\facade\Db::name('user') $twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
->where('uid', $uid)
->value('spread_uid');
if ($spreadUid > 0) {
$levelServices->checkUpgrade($spreadUid);
} }
$uids = array_unique([$uid, $spreadUid, $twoSpreadUid]);
/** @var AgentLevelServices $agentLevelServices */
$agentLevelServices = app()->make(AgentLevelServices::class);
$agentLevelServices->checkUserLevelFinish($uid, $uids);
Log::info("[HjfOrderPay] 等级升级检查完成 uid={$uid}"); Log::info("[HjfOrderPay] 等级升级检查完成 uid={$uid}");
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error("[HjfOrderPay] 等级升级检查失败 uid={$uid}: " . $e->getMessage()); Log::error("[HjfOrderPay] 等级升级检查失败 uid={$uid}: " . $e->getMessage());

View File

@@ -3,16 +3,14 @@ declare(strict_types=1);
namespace app\jobs\hjf; namespace app\jobs\hjf;
use app\services\hjf\MemberLevelServices; use app\services\agent\AgentLevelServices;
use crmeb\basic\BaseJobs; use crmeb\basic\BaseJobs;
use crmeb\traits\QueueTrait; use crmeb\traits\QueueTrait;
use think\facade\Log;
/** /**
* 会员等级异步检查 Job * 会员等级异步检查 Job(改造复用版)
* *
* 每次订单支付回调完成后,对推荐链上的上级异步派发此 Job 检查是否达到升级条件 * 委托给 AgentLevelServices::checkUserLevelFinish() 复用 CRMEB 分销等级升级流程
* 调用方式MemberLevelCheckJob::dispatch($uid)
* *
* Class MemberLevelCheckJob * Class MemberLevelCheckJob
* @package app\jobs\hjf * @package app\jobs\hjf
@@ -21,16 +19,12 @@ class MemberLevelCheckJob extends BaseJobs
{ {
use QueueTrait; use QueueTrait;
/**
* @param int $uid 需要检查升级的用户 ID
* @return bool
*/
public function doJob(int $uid): bool public function doJob(int $uid): bool
{ {
try { try {
/** @var MemberLevelServices $levelServices */ /** @var AgentLevelServices $levelServices */
$levelServices = app()->make(MemberLevelServices::class); $levelServices = app()->make(AgentLevelServices::class);
$levelServices->checkUpgrade($uid); $levelServices->checkUserLevelFinish($uid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
response_log_write([ response_log_write([
'message' => "会员等级检查失败 uid={$uid}: " . $e->getMessage(), 'message' => "会员等级检查失败 uid={$uid}: " . $e->getMessage(),

View File

@@ -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 * @param Model $query

View File

@@ -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 * Class AgentLevelServices
* @package app\services\agent * @package app\services\agent
* @mixin AgentLevelDao * @mixin AgentLevelDao
@@ -55,6 +61,88 @@ class AgentLevelServices extends BaseServices
return $this->dao->getOne(['id' => $id, 'is_del' => 0], $field, $with); 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<int,array>, byGradeAny: array<int,array>, byGradeOfficial: array<int,array>}
*/
public function loadHjfUserListLevelMaps(): array
{
$rows = $this->dao->getList(['is_del' => 0]);
return $this->buildHjfUserListLevelMaps($rows);
}
/**
* 从 is_del=0 的等级行列表构建用户列表展示用索引(一次查询后复用)
*
* @param array<int,array<string,mixed>> $hjfLevelRows
* @return array{byId: array<int,array>, byGradeAny: array<int,array>, byGradeOfficial: array<int,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 * @param array $where
@@ -270,13 +358,10 @@ class AgentLevelServices extends BaseServices
} }
/** /**
* 分销等级上浮 * 分销等级上浮(保留兼容,普通商品分销仍使用原逻辑)
* @param int $uid *
* @param array $userInfo * 注意:报单商品的积分奖励已由 PointsRewardServices 通过 direct_reward_points/umbrella_reward_points 处理,
* @return array|int[] * 此方法仅用于普通商品的分销佣金计算。
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/ */
public function getAgentLevelBrokerage(int $uid, $userInfo = []) public function getAgentLevelBrokerage(int $uid, $userInfo = [])
{ {
@@ -285,7 +370,6 @@ class AgentLevelServices extends BaseServices
if (!$uid) { if (!$uid) {
return $data; return $data;
} }
//商城分销是否开启
if (!sys_config('brokerage_func_status')) { if (!sys_config('brokerage_func_status')) {
return $data; return $data;
} }
@@ -297,7 +381,6 @@ class AgentLevelServices extends BaseServices
if (!$userInfo) { if (!$userInfo) {
return $data; return $data;
} }
//获取上级uid 开启自购返回自己uid
$spread_uid = $userServices->getSpreadUid($uid, $userInfo); $spread_uid = $userServices->getSpreadUid($uid, $userInfo);
$one_agent_level = 0; $one_agent_level = 0;
$two_agent_level = 0; $two_agent_level = 0;
@@ -308,17 +391,66 @@ class AgentLevelServices extends BaseServices
$two_agent_level = $two_user_info['agent_level'] ?? 0; $two_agent_level = $two_user_info['agent_level'] ?? 0;
} }
} }
//获取后台一级返佣比例
$storeBrokerageRatio = sys_config('store_brokerage_ratio'); $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; $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 = 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; $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]; return [$storeBrokerageRatio, $storeBrokerageTwo, $spread_uid, $spread_two_uid];
} }
/**
* 根据 agent_level ID 获取等级 gradeHJF 会员等级数字 0-4
*
* @param int $agentLevelId eb_user.agent_level 值
* @return int grade0=普通会员, 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 * @param $ratio
@@ -338,10 +470,7 @@ class AgentLevelServices extends BaseServices
} }
/** /**
* 添加等级表单 * 添加等级表单(改造后包含积分奖励字段)
* @param int $id
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
*/ */
public function createForm() public function createForm()
{ {
@@ -352,17 +481,16 @@ class AgentLevelServices extends BaseServices
$field[] = Form::number('grade', '等级:', 0)->min(0)->precision(0); $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::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::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('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::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' => '隐藏']]); $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) 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::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::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::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('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::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' => '隐藏']]); $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');
} }
/** /**

View File

@@ -20,10 +20,12 @@ use crmeb\services\FormBuilder as Form;
use FormBuilder\Factory\Iview; use FormBuilder\Factory\Iview;
use think\annotation\Inject; use think\annotation\Inject;
use think\exception\ValidateException; use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Route as Url; use think\facade\Route as Url;
/** /**
* 分销等级任务(改造后同时支持 HJF 会员等级升级任务)
* *
* Class AgentLevelTaskServices * Class AgentLevelTaskServices
* @package app\services\agent * @package app\services\agent
@@ -33,12 +35,13 @@ class AgentLevelTaskServices extends BaseServices
{ {
/** /**
* 任务类型 * 任务类型
* type 记录在数据库中用来区分任务 *
* name 任务名 (任务名中的{$num}会自动替换成设置的数字 + 单位) * type 1-5: 原 CRMEB 分销任务类型
* max_number 最大设定数值 0为不限定 * type 6-8: HJF 会员等级升级任务类型(改造新增)
* min_number 最小设定数值 * 6 = 直推报单单数(直推下级购买报单商品的订单数)
* unit 单位 * 7 = 伞下报单业绩(含业绩分离逻辑)
* */ * 8 = 最低直推人数
*/
protected array $TaskType = [ protected array $TaskType = [
[ [
'type' => 1, 'type' => 1,
@@ -90,6 +93,36 @@ class AgentLevelTaskServices extends BaseServices
'unit' => '单', 'unit' => '单',
'image' => '/uploads/system/agent_spread_order.png' '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 $uid
* @param int $task_id * @param int $task_id
* @return array|false * @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 = []) public function checkLevelTaskFinish(int $uid, int $task_id, $levelTaskInfo = [])
{ {
@@ -356,6 +392,17 @@ class AgentLevelTaskServices extends BaseServices
$userNumber = $storeOrderServices->count($where); $userNumber = $storeOrderServices->count($where);
} }
break; 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: default:
return false; return false;
} }
@@ -372,6 +419,104 @@ class AgentLevelTaskServices extends BaseServices
return [$msg, $userNumber, $isComplete]; 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 * @param int $id

View File

@@ -3,24 +3,23 @@ declare(strict_types=1);
namespace app\services\hjf; namespace app\services\hjf;
use app\dao\user\UserDao; use app\services\agent\AgentLevelServices;
use app\services\agent\AgentLevelTaskServices;
use app\services\BaseServices; use app\services\BaseServices;
use crmeb\services\SystemConfigService; use app\services\user\UserServices;
use think\annotation\Inject; use think\annotation\Inject;
use think\facade\Db; use think\facade\Db;
use think\facade\Log; use think\facade\Log;
/** /**
* 会员等级升级服务 * 会员等级升级服务(改造复用版)
* *
* 升级条件PRD 3.2.1 * 基于 CRMEB Pro 的团队分销等级功能进行改造
* - 普通会员 → 创客直推3单hjf_level_direct_require_1默认3 * - 使用 eb_user.agent_level (FK → eb_agent_level.id) 代替独立的 member_level
* - 创客 → 云店伞下业绩30单 + 至少3个直推hjf_level_umbrella_require_2默认30 * - 升级条件通过 eb_agent_level_task 的 type 6/7/8 定义
* - 云店 → 服务商伞下业绩100单 + 至少3个直推hjf_level_umbrella_require_3默认100 * - 升级逻辑委托给 AgentLevelServices::checkUserLevelFinish()
* - 服务商 → 分公司伞下业绩1000单 + 至少3个直推hjf_level_umbrella_require_4默认1000
* *
* 伞下业绩分离当某直推下级已升级到云店level≥2 * 本服务保留为薄封装层,提供 HJF 特有的查询方法供控制器调用。
* 该下级及其整个团队的业绩不再计入本级的伞下业绩。
* *
* Class MemberLevelServices * Class MemberLevelServices
* @package app\services\hjf * @package app\services\hjf
@@ -28,111 +27,70 @@ use think\facade\Log;
class MemberLevelServices extends BaseServices class MemberLevelServices extends BaseServices
{ {
#[Inject] #[Inject]
protected UserDao $userDao; protected AgentLevelServices $agentLevelServices;
/** #[Inject]
* 各等级升级所需直推单数0→1升级条件 protected AgentLevelTaskServices $agentLevelTaskServices;
*/
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;
/** /**
* 检查并执行升级(异步触发入口) * 检查并执行升级(异步触发入口)
* *
* @param int $uid 被检查的用户 ID * 委托给 CRMEB 的 AgentLevelServices 复用原有升级检测流程,
* 该流程已支持 type 6/7/8 的 HJF 任务类型。
*/ */
public function checkUpgrade(int $uid): void public function checkUpgrade(int $uid): void
{ {
try { try {
$user = $this->userDao->get($uid); /** @var UserServices $userServices */
if (!$user) { $userServices = app()->make(UserServices::class);
$userInfo = $userServices->getUserCacheInfo($uid);
if (!$userInfo) {
return; return;
} }
$currentLevel = (int)($user['member_level'] ?? 0); $spreadUid = $userServices->getSpreadUid($uid, $userInfo);
$nextLevel = $currentLevel + 1; $twoSpreadUid = 0;
if ($spreadUid > 0 && $oneUserInfo = $userServices->getUserCacheInfo($spreadUid)) {
if ($nextLevel > 4) { $twoSpreadUid = $userServices->getSpreadUid($spreadUid, $oneUserInfo, false);
return; // 已是最高等级
} }
$uids = array_unique([$uid, $spreadUid, $twoSpreadUid]);
$qualified = $this->checkLevelCondition($uid, $currentLevel, $nextLevel); $this->agentLevelServices->checkUserLevelFinish($uid, $uids);
if ($qualified) {
$this->upgrade($uid, $nextLevel);
// 升级后继续检查是否可连续升级
$this->checkUpgrade($uid);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage()); Log::error("[MemberLevel] checkUpgrade uid={$uid}: " . $e->getMessage());
} }
} }
/** /**
* 检查用户是否满足从 currentLevel 升到 nextLevel 的条件 * 获取用户当前会员等级 grade0=普通, 1=创客, 2=云店, 3=服务商, 4=分公司)
*/ */
private function checkLevelCondition(int $uid, int $currentLevel, int $nextLevel): bool public function getUserGrade(int $uid): int
{ {
if ($nextLevel === 1) { $agentLevel = (int)Db::name('user')->where('uid', $uid)->value('agent_level');
// 普通→创客:统计直推报单数 return $this->agentLevelServices->getGradeByLevelId($agentLevel);
$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;
} }
/** /**
* 获取直推用户的报单订单数(直推层级 = 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 public function getDirectQueueOrderCount(int $uid): int
{ {
// 查询直推用户 uid 列表 return $this->agentLevelTaskServices->getDirectQueueOrderCount($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();
} }
/** /**
@@ -140,107 +98,39 @@ class MemberLevelServices extends BaseServices
*/ */
public function getDirectSpreadCount(int $uid): int 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 $uid 用户 ID
* @param int $newLevel 新等级 * @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) { $agentLevelId = 0;
$this->userDao->update($uid, ['member_level' => $newLevel], 'uid'); if ($grade > 0) {
$agentLevelId = $this->agentLevelServices->getLevelIdByGrade($grade);
Log::info("[MemberLevel] uid={$uid} 升级到 level={$newLevel}"); if ($agentLevelId <= 0) {
}); throw new \think\exception\ValidateException("等级 grade={$grade} 在 eb_agent_level 中不存在");
// 升级后通知推荐链上级重新检查
$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());
} }
} }
}
private function getDirectRequire(int $level): int /** @var UserServices $userServices */
{ $userServices = app()->make(UserServices::class);
$key = self::DIRECT_REQUIRE_KEYS[$level] ?? ''; $userServices->update($uid, ['agent_level' => $agentLevelId]);
if (!$key) {
return self::DEFAULT_DIRECT_REQUIRE[$level] ?? 3;
}
return (int)SystemConfigService::get($key, self::DEFAULT_DIRECT_REQUIRE[$level] ?? 3);
}
private function getUmbrellaRequire(int $level): int Log::info("[MemberLevel] 手动设置 uid={$uid} agent_level={$agentLevelId} (grade={$grade})");
{
$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);
} }
} }

View File

@@ -5,22 +5,19 @@ namespace app\services\hjf;
use app\dao\hjf\PointsReleaseLogDao; use app\dao\hjf\PointsReleaseLogDao;
use app\dao\user\UserDao; use app\dao\user\UserDao;
use app\services\agent\AgentLevelServices;
use app\services\BaseServices; use app\services\BaseServices;
use crmeb\services\SystemConfigService;
use think\annotation\Inject; use think\annotation\Inject;
use think\facade\Db; use think\facade\Db;
use think\facade\Log; use think\facade\Log;
/** /**
* 积分奖励服务(级差计算) * 积分奖励服务(级差计算)—— 改造复用版
* *
* 触发时机:报单商品订单支付回调成功后调用 reward($orderUid, $orderId)。 * 改造要点PRD 3.2.2
* * - 使用 eb_user.agent_level (FK → eb_agent_level.id) 获取会员等级
* 奖励规则PRD 3.2 * - 从 eb_agent_level 表的 direct_reward_points / umbrella_reward_points 字段读取奖励积分
* - 推荐人(直推上级)获得 直推奖励积分(按推荐人等级) * - 不再使用独立的 member_level 字段和系统配置表中的 hjf_reward_* 键
* - 更上级获得 级差积分(上级积分 - 直接下级已获得的积分)
* - 所有奖励积分写入 frozen_points待释放状态
* - 同时写 points_release_log 记录明细
* *
* Class PointsRewardServices * Class PointsRewardServices
* @package app\services\hjf * @package app\services\hjf
@@ -33,50 +30,19 @@ class PointsRewardServices extends BaseServices
#[Inject] #[Inject]
protected UserDao $userDao; protected UserDao $userDao;
/** #[Inject]
* 各等级直推奖励积分配置键 protected AgentLevelServices $agentLevelServices;
*/
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];
/** /**
* 对一笔报单订单发放积分奖励 * 对一笔报单订单发放积分奖励
*
* @param int $orderUid 下单用户 ID
* @param string $orderId 订单号
*/ */
public function reward(int $orderUid, string $orderId): void public function reward(int $orderUid, string $orderId): void
{ {
try { try {
// 获取下单用户信息
$buyer = $this->userDao->get($orderUid); $buyer = $this->userDao->get($orderUid);
if (!$buyer || !$buyer['spread_uid']) { if (!$buyer || !$buyer['spread_uid']) {
return; // 无推荐人,不发奖励 return;
} }
// 沿推荐链向上遍历,计算级差奖励
$this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0); $this->propagateReward($buyer['spread_uid'], $orderUid, $orderId, 0);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage()); Log::error("[PointsReward] 积分奖励失败 orderUid={$orderUid} orderId={$orderId}: " . $e->getMessage());
@@ -86,11 +52,11 @@ class PointsRewardServices extends BaseServices
/** /**
* 向上递归发放级差积分 * 向上递归发放级差积分
* *
* @param int $uid 当前被奖励用户 * @param int $uid 当前被奖励用户
* @param int $fromUid 触发方(下级)用户 ID * @param int $fromUid 触发方(下级)用户 ID
* @param string $orderId 来源订单号 * @param string $orderId 来源订单号
* @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减) * @param int $lowerReward 下级已获得的直推/伞下奖励积分(用于级差扣减)
* @param int $depth 递归深度最多遍历10层 * @param int $depth 递归深度
*/ */
private function propagateReward( private function propagateReward(
int $uid, int $uid,
@@ -108,22 +74,21 @@ class PointsRewardServices extends BaseServices
return; return;
} }
$level = (int)($user['member_level'] ?? 0); $agentLevelId = (int)($user['agent_level'] ?? 0);
if ($level === 0) { $grade = $this->agentLevelServices->getGradeByLevelId($agentLevelId);
// 普通会员不获得奖励,但继续向上传递
if ($grade === 0) {
if ($user['spread_uid']) { if ($user['spread_uid']) {
$this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1); $this->propagateReward((int)$user['spread_uid'], $uid, $orderId, 0, $depth + 1);
} }
return; return;
} }
// 判断是直推还是伞下depth=0 说明是第一个上级,即直推)
$isDirect = ($depth === 0); $isDirect = ($depth === 0);
$reward = $isDirect $reward = $isDirect
? $this->getDirectReward($level) ? $this->agentLevelServices->getDirectRewardPoints($agentLevelId)
: $this->getUmbrellaReward($level); : $this->agentLevelServices->getUmbrellaRewardPoints($agentLevelId);
// 级差:本次实发 = 本等级应得 - 下级已获得
$actual = max(0, $reward - $lowerReward); $actual = max(0, $reward - $lowerReward);
if ($actual > 0) { if ($actual > 0) {
@@ -136,13 +101,12 @@ class PointsRewardServices extends BaseServices
); );
} }
// 继续向上传递(使用本级应得的 reward 作为下一级的 lowerReward
if ($user['spread_uid']) { if ($user['spread_uid']) {
$this->propagateReward( $this->propagateReward(
(int)$user['spread_uid'], (int)$user['spread_uid'],
$uid, $uid,
$orderId, $orderId,
$reward, // 传递本级"应得"(而非实发)给上级做级差 $reward,
$depth + 1 $depth + 1
); );
} }
@@ -154,10 +118,8 @@ class PointsRewardServices extends BaseServices
private function grantFrozenPoints(int $uid, int $points, string $orderId, string $type, string $mark): void private function grantFrozenPoints(int $uid, int $points, string $orderId, string $type, string $mark): void
{ {
Db::transaction(function () use ($uid, $points, $orderId, $type, $mark) { Db::transaction(function () use ($uid, $points, $orderId, $type, $mark) {
// 增加 frozen_points
$this->userDao->bcInc($uid, 'frozen_points', $points, 'uid'); $this->userDao->bcInc($uid, 'frozen_points', $points, 'uid');
// 写明细日志
$this->logDao->save([ $this->logDao->save([
'uid' => $uid, 'uid' => $uid,
'points' => $points, '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);
}
} }

View File

@@ -766,23 +766,31 @@ class UserServices extends BaseServices
*/ */
public function index(array $where) 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 { 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); [$list, $count] = $userWechatUser->getWhereUserList($where, $fields);
} catch (\Throwable $e) {
Log::error('User index list query failed: ' . $e->getMessage());
if ($list) { return ['count' => 0, 'list' => []];
// 提取唯一 UID 列表 }
$uids = array_unique(array_column($list, 'uid'));
// 获取相关服务数据 if (!$list) {
$userlabel = $this->getUserLablel($uids); 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')); $groupIds = array_unique(array_column($list, 'group_id'));
$userGroupService = app()->make(UserGroupServices::class); $userGroupService = app()->make(UserGroupServices::class);
$userGroup = $userGroupService->getUsersGroupName($groupIds); $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); $clientData = $workClientService->getList(['uid' => $uids], ['id', 'uid', 'name', 'external_userid', 'corp_id', 'unionid'], false);
$clientlist = $clientData['list'] ?? []; $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', []); $extendInfo = SystemConfigService::get('user_extend_info', []);
$is_extend_info = false; $is_extend_info = false;
@@ -826,6 +839,13 @@ class UserServices extends BaseServices
// 补充每个用户的详细信息 // 补充每个用户的详细信息
foreach ($list as &$item) { 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['addres'])) {
if (!empty($item['country']) || !empty($item['province']) || !empty($item['city'])) { 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; $item['is_extend_info'] = $is_extend_info;
} }
}
return compact('count', 'list'); return compact('count', 'list');
} catch (\Exception $e) { } catch (\Throwable $e) {
// 异常处理 // 加工阶段失败时仍返回查询结果,避免整页空白(常见:企微/等级等扩展服务异常)
Log::error('Error in user index: ' . $e->getMessage()); Log::error('User index enrichment failed: ' . $e->getMessage());
return ['count' => 0, 'list' => []];
return compact('count', 'list');
} }
} }

View File

@@ -12,6 +12,7 @@ declare (strict_types=1);
namespace app\services\user; namespace app\services\user;
use app\services\agent\AgentLevelServices;
use app\services\BaseServices; use app\services\BaseServices;
use app\dao\user\UserWechatUserDao; use app\dao\user\UserWechatUserDao;
use think\annotation\Inject; use think\annotation\Inject;
@@ -48,6 +49,7 @@ class UserWechatuserServices extends BaseServices
*/ */
public function getWhereUserList(array $where, string $field): array public function getWhereUserList(array $where, string $field): array
{ {
$where = $this->normalizeHjfMemberLevelWhere($where);
[$page, $limit] = $this->getPageValue(); [$page, $limit] = $this->getPageValue();
$order_string = ''; $order_string = '';
$order_arr = ['asc', 'desc']; $order_arr = ['asc', 'desc'];
@@ -58,4 +60,40 @@ class UserWechatuserServices extends BaseServices
$count = $this->dao->getCountByWhere($where); $count = $this->dao->getCountByWhere($where);
return [$list, $count]; 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;
}
} }

View File

@@ -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;

View File

@@ -1,15 +1,17 @@
-- ============================================================ -- ============================================================
-- 黄精粉健康商城 HJF 数据库迁移脚本 -- 黄精粉健康商城 HJF 数据库迁移脚本
-- 版本Phase 2 -- 版本Phase 3改造复用版
-- 日期2026-03-15 -- 日期2026-03-21
-- 执行说明: -- 执行说明:
-- 1. 兼容 MySQL 5.7+,数据库前缀为 eb_ -- 1. 兼容 MySQL 5.7+,数据库前缀为 eb_
-- 2. 按顺序执行 P2-01 ~ P2-05 -- 2. 按顺序执行 P3-01 ~ P3-07
-- 3. 所有操作均做幂等处理,可重复执行 -- 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` ( 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` ( 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" -- 注意:不再新增 member_level 字段,复用已有的 agent_level (FK→eb_agent_level.id)
-- 改用存储过程 + information_schema 实现幂等检查。
-- ============================================================ -- ============================================================
DROP PROCEDURE IF EXISTS `hjf_migrate_columns`; DROP PROCEDURE IF EXISTS `hjf_migrate_columns`;
@@ -66,15 +100,7 @@ DELIMITER $$
CREATE PROCEDURE `hjf_migrate_columns`() CREATE PROCEDURE `hjf_migrate_columns`()
BEGIN BEGIN
-- ---- eb_user 字段 ---- -- ---- 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 = '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;
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'no_assess' 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 '可用积分'; ADD COLUMN `available_points` int(11) NOT NULL DEFAULT 0 COMMENT '可用积分';
END IF; 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 字段 ---- -- ---- eb_store_product 字段 ----
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS SELECT 1 FROM information_schema.COLUMNS
@@ -124,7 +142,6 @@ BEGIN
ADD COLUMN `allow_pay_types` varchar(255) NOT NULL DEFAULT '' COMMENT '允许积分支付类型JSON数组'; ADD COLUMN `allow_pay_types` varchar(255) NOT NULL DEFAULT '' COMMENT '允许积分支付类型JSON数组';
END IF; END IF;
-- eb_store_product 索引idx_is_queue_goods
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_product' AND INDEX_NAME = 'idx_is_queue_goods' 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=是'; ADD COLUMN `is_queue_goods` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否报单商品订单1=是';
END IF; END IF;
-- eb_store_order 索引idx_is_queue_goods
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_store_order' AND INDEX_NAME = 'idx_is_queue_goods' 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() 读取) -- grade=1 → 创客 (direct=500, umbrella=0)
-- value = 默认值(字符串) -- grade=2 → 云店 (direct=800, umbrella=300)
-- info = 后台显示名称 -- grade=3 → 服务商 (direct=1000, umbrella=200)
-- desc = 说明文字 -- grade=4 → 分公司 (direct=1300, umbrella=300)
-- config_tab_id = 0不归属某分组便于独立管理 --
-- status = 1启用 -- 注意:普通会员 = 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` INSERT IGNORE INTO `eb_system_config`
(`is_store`, `menu_name`, `type`, `input_type`, `config_tab_id`, (`is_store`, `menu_name`, `type`, `input_type`, `config_tab_id`,
`parameter`, `upload_type`, `required`, `width`, `high`, `parameter`, `upload_type`, `required`, `width`, `high`,
`value`, `info`, `desc`, `sort`, `status`) `value`, `info`, `desc`, `sort`, `status`)
VALUES VALUES
-- 公排触发倍数:每入 N 单退款第1单默认 4
(0, 'hjf_trigger_multiple', 'text', 'input', 0, (0, 'hjf_trigger_multiple', 'text', 'input', 0,
'', 0, '', 100, 0, '', 0, '', 100, 0,
'4', '公排触发倍数', '每进入N单公排触发退款第1单默认4', 10, 1), '4', '公排触发倍数', '每进入N单公排触发退款第1单默认4', 10, 1),
-- 积分每日释放比例(‰,默认 4即 4‰
(0, 'hjf_release_rate', 'text', 'input', 0, (0, 'hjf_release_rate', 'text', 'input', 0,
'', 0, '', 100, 0, '', 0, '', 100, 0,
'4', '积分每日释放比例(‰)', '每日释放frozen_points × N / 1000默认4即4‰', 20, 1), '4', '积分每日释放比例(‰)', '每日释放frozen_points × N / 1000默认4即4‰', 20, 1),
-- 提现手续费率(%,默认 7即 7%
(0, 'hjf_fee_rate', 'text', 'input', 0, (0, 'hjf_fee_rate', 'text', 'input', 0,
'', 0, '', 100, 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, -- P3-08: 如果已有旧的 member_level 字段,将数据迁移到 agent_level
'', 0, '', 100, 0, -- ============================================================
'30', '云店升级所需伞下单数', '创客伞下业绩达到N单后升级为云店默认30', 50, 1),
-- 等级升级门槛云店→服务商伞下N单 DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`;
(0, 'hjf_level_umbrella_require_3', 'text', 'input', 0,
'', 0, '', 100, 0,
'100', '服务商升级所需伞下单数', '云店伞下业绩达到N单后升级为服务商默认100', 60, 1),
-- 等级升级门槛服务商→分公司伞下N单 DELIMITER $$
(0, 'hjf_level_umbrella_require_4', 'text', 'input', 0, CREATE PROCEDURE `hjf_migrate_member_to_agent_level`()
'', 0, '', 100, 0, BEGIN
'1000', '分公司升级所需伞下单数', '服务商伞下业绩达到N单后升级为分公司默认1000', 70, 1),
-- 直推奖励积分创客直推可得N积分 IF EXISTS (
(0, 'hjf_reward_direct_1', 'text', 'input', 0, SELECT 1 FROM information_schema.COLUMNS
'', 0, '', 100, 0, WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' AND COLUMN_NAME = 'member_level'
'500', '创客直推奖励积分', '创客等级直推一单报单商品可获得的冻结积分默认500', 80, 1), ) 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;
-- 直推奖励积分:云店 END$$
(0, 'hjf_reward_direct_2', 'text', 'input', 0, DELIMITER ;
'', 0, '', 100, 0,
'800', '云店直推奖励积分', '云店等级直推一单报单商品可获得的冻结积分默认800', 90, 1),
-- 直推奖励积分:服务商 CALL `hjf_migrate_member_to_agent_level`();
(0, 'hjf_reward_direct_3', 'text', 'input', 0, DROP PROCEDURE IF EXISTS `hjf_migrate_member_to_agent_level`;
'', 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);
-- ============================================================ -- ============================================================
-- 迁移完成校验(可手动执行检查) -- 迁移完成校验(可手动执行检查)
-- ============================================================ -- ============================================================
-- SELECT TABLE_NAME FROM information_schema.TABLES -- SELECT id, name, grade, direct_reward_points, umbrella_reward_points
-- WHERE TABLE_SCHEMA = DATABASE() -- FROM eb_agent_level WHERE is_del = 0 ORDER BY grade;
-- AND TABLE_NAME IN ('eb_queue_pool', 'eb_points_release_log');
-- 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 -- SELECT COLUMN_NAME FROM information_schema.COLUMNS
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user' -- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'eb_user'
-- AND COLUMN_NAME IN ('member_level','no_assess','frozen_points','available_points'); -- AND COLUMN_NAME IN ('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');
-- SELECT menu_name, value FROM eb_system_config -- SELECT menu_name, value FROM eb_system_config
-- WHERE menu_name LIKE 'hjf_%' ORDER BY sort; -- WHERE menu_name LIKE 'hjf_%' ORDER BY sort;

View File

@@ -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`)。
---
## 验证与本地启动 ## 验证与本地启动
### 一键检查(项目根目录执行) ### 一键检查(项目根目录执行)

View File

@@ -1,7 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 按 help/PHP-Setup.md 要求,以 memory_limit=300M 启动 Swoole API 服务 # 按 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 set -e
cd "$(dirname "$0")/.." 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

View File

@@ -891,18 +891,18 @@ Route::group('api', function () {
* HJF 黄精粉模块路由 * HJF 黄精粉模块路由
*/ */
Route::group('hjf', function () { Route::group('hjf', function () {
// 需要登录的接口
Route::group(function () { Route::group(function () {
// 公排队列 Route::get('queue/status', 'v1.hjf.QueueController/status');
Route::get('queue/status', 'v1.hjf.QueueController/status'); // 公排状态 Route::get('queue/history', 'v1.hjf.QueueController/history');
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/overview', 'v1.hjf.AssetsController/overview'); // 资产总览 Route::get('assets/cash/detail', 'v1.hjf.AssetsController/cashDetail');
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(AuthTokenMiddleware::class, true);
})->middleware(StationOpenMiddleware::class); })->middleware(StationOpenMiddleware::class);

View File

@@ -1150,7 +1150,9 @@ export default {
this.goodHeade(); this.goodHeade();
}, },
activated() { 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.goodHeade();
this.getDataList(); this.getDataList();
}, },
@@ -1438,12 +1440,18 @@ export default {
}); });
}, },
getPath() { getPath() {
const tabType =
this.$route.query.type != null
? String(this.$route.query.type)
: "1";
this.columns2 = [...this.columns]; this.columns2 = [...this.columns];
if (name !== "1" && name !== "2") { if (tabType !== "1" && tabType !== "2") {
this.columns2.shift(); this.columns2.shift();
} }
this.artFrom.page = 1; 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(); this.getDataList();
}, },
changeMenu(row, name, index) { changeMenu(row, name, index) {
@@ -1774,22 +1782,26 @@ export default {
} }
return newObj; return newObj;
}, },
// 商品列表 // 商品列表(与 goodHeade 一致使用 artFrom避免 tableForm 未同步导致列表为空或与 Tab 不一致)
getDataList() { getDataList() {
// this.loading = true; // this.loading = true;
let tableForm = {...this.tableForm}; let tableForm = this.deepClone(this.artFrom);
tableForm.sales_range = tableForm.sales_range tableForm.sales_range =
? tableForm.sales_range.join("-") tableForm.sales_range && tableForm.sales_range.length
: ""; ? tableForm.sales_range.join("-")
tableForm.price_range = tableForm.price_range : "";
? tableForm.price_range.join("-") tableForm.price_range =
: ""; tableForm.price_range && tableForm.price_range.length
tableForm.stock_range = tableForm.stock_range ? tableForm.price_range.join("-")
? tableForm.stock_range.join("-") : "";
: ""; tableForm.stock_range =
tableForm.collect_range = tableForm.collect_range tableForm.stock_range && tableForm.stock_range.length
? tableForm.collect_range.join("-") ? 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("-") : ""; // tableForm.create_range = this.timeVal.length ? this.timeVal.join("-") : "";
getGoods(tableForm) getGoods(tableForm)
.then((res) => { .then((res) => {

View File

@@ -219,6 +219,16 @@ export default {
minWidth: 80, minWidth: 80,
title: "等级", title: "等级",
}, },
{
key: "direct_reward_points",
minWidth: 120,
title: "直推奖励积分",
},
{
key: "umbrella_reward_points",
minWidth: 120,
title: "伞下奖励积分",
},
{ {
key: "one_brokerage", key: "one_brokerage",
minWidth: 120, minWidth: 120,

View File

@@ -54,15 +54,15 @@
<div class="iconfont iconxiayi"></div> <div class="iconfont iconxiayi"></div>
</div> </div>
</FormItem> </FormItem>
<FormItem label="会员等级:" label-for="hjf_member_level"> <FormItem label="分销等级:" label-for="hjf_member_level">
<Select <Select
v-model="userFrom.hjf_member_level" v-model="userFrom.hjf_member_level"
placeholder="请选择" placeholder="请选择分销等级"
element-id="hjf_member_level" element-id="hjf_member_level"
clearable clearable
class="input-add" class="input-add"
> >
<Option :value="0">普通会员</Option> <Option :value="0">普通无分销等级</Option>
<Option :value="1">创客</Option> <Option :value="1">创客</Option>
<Option :value="2">云店</Option> <Option :value="2">云店</Option>
<Option :value="3">服务商</Option> <Option :value="3">服务商</Option>
@@ -420,14 +420,13 @@
</div> </div>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="member_level" title="HJF等级" min-width="110"> <vxe-column field="member_level_name" title="分销等级" min-width="160">
<template v-slot="{ row }"> <template v-slot="{ row }">
<HjfMemberBadge <HjfMemberBadge
v-if="row.member_level != null" :level="Number(row.member_level != null ? row.member_level : 0)"
:level="Number(row.member_level)" :level-name="row.member_level_name || ''"
size="small" size="small"
/> />
<span v-else class="level-none">普通会员</span>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="direct_count" title="直推人数" min-width="90"> <vxe-column field="direct_count" title="直推人数" min-width="90">
@@ -452,6 +451,16 @@
title="推荐人" title="推荐人"
min-width="100" min-width="100"
></vxe-column> ></vxe-column>
<vxe-column field="available_points" title="可用积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.available_points != null ? row.available_points : 0 }}</span>
</template>
</vxe-column>
<vxe-column field="frozen_points" title="待释放(冻结)积分" min-width="150">
<template v-slot="{ row }">
<span>{{ row.frozen_points != null ? row.frozen_points : 0 }}</span>
</template>
</vxe-column>
<vxe-column field="now_money" title="余额" min-width="100"></vxe-column> <vxe-column field="now_money" title="余额" min-width="100"></vxe-column>
<vxe-column <vxe-column
field="action" field="action"
@@ -464,8 +473,8 @@
<a @click="changeMenu(row, '1')">详情</a> <a @click="changeMenu(row, '1')">详情</a>
<Divider type="vertical" /> <Divider type="vertical" />
<a @click="changeMenu(row, '10')">编辑</a> <a @click="changeMenu(row, '10')">编辑</a>
<Divider type="vertical" /> <!-- <Divider type="vertical" />
<a @click="openLevelModal(row)">调整等级</a> <a @click="openLevelModal(row)">调整等级</a> -->
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
@@ -867,7 +876,7 @@ export default {
group_id: "", group_id: "",
field_key: "", field_key: "",
is_channel: "", is_channel: "",
/** 会员等级筛选(HJF 扩展字段0 普通 1 创客 2 云店 3 服务商 4 分公司,空串表示全部 */ /** 分销等级筛选(对应 eb_agent_level.grade / eb_user.agent_level,空串表示全部 */
hjf_member_level: "", hjf_member_level: "",
}, },
field_key: "", field_key: "",
@@ -1441,12 +1450,13 @@ export default {
this.userFrom.group_id === "all" ? "" : this.userFrom.group_id; this.userFrom.group_id === "all" ? "" : this.userFrom.group_id;
userList(this.userFrom) userList(this.userFrom)
.then(async (res) => { .then(async (res) => {
let data = res.data; const data = res.data || {};
data.list.forEach((item) => { const rows = Array.isArray(data.list) ? data.list : [];
rows.forEach((item) => {
item.checkBox = false; item.checkBox = false;
}); });
this.userLists = data.list; this.userLists = rows;
this.total = data.count; this.total = data.count != null ? data.count : 0;
this.loading = false; this.loading = false;
this.$nextTick(function () { this.$nextTick(function () {
if (this.isAll == 1) { if (this.isAll == 1) {
@@ -1678,12 +1688,16 @@ export default {
* @param {Object} row - 当前行用户数据 * @param {Object} row - 当前行用户数据
* @param {number} row.uid - 用户 ID * @param {number} row.uid - 用户 ID
* @param {string} row.nickname - 用户昵称 * @param {string} row.nickname - 用户昵称
* @param {number} row.member_level - 当前会员等级HJF 字段);回退到 row.level * @param {number} row.member_level - 分销等级 grade由 agent_level 映射)
* @param {string} row.member_level_name - 分销等级名称eb_agent_level.name
*/ */
openLevelModal(row) { openLevelModal(row) {
this.levelModal.uid = row.uid; this.levelModal.uid = row.uid;
this.levelModal.nickname = row.nickname; this.levelModal.nickname = row.nickname;
this.levelModal.level = row.member_level != null ? row.member_level : (row.level || 0); this.levelModal.level =
row.member_level != null && row.member_level !== ""
? Number(row.member_level)
: 0;
this.levelModal.show = true; this.levelModal.show = true;
}, },

View File

@@ -95,7 +95,11 @@
success(e) { success(e) {
/* 窗口宽度大于420px且不在PC页面且不在移动设备时跳转至 PC.html 页面 */ /* 窗口宽度大于420px且不在PC页面且不在移动设备时跳转至 PC.html 页面 */
if (e.windowWidth > 420 && !window.top.isPC && !/iOS|Android/i.test(e.system)) { if (e.windowWidth > 420 && !window.top.isPC && !/iOS|Android/i.test(e.system)) {
window.location.pathname = '/h5/static/html/pc.html'; const p = (window.location.pathname || '/').replace(/\/$/, '') || '/';
if (p.endsWith('/static/html/pc.html')) return;
/* 与 manifest h5.router.base 一致:根路径为 / 或子路径 /h5/ */
const h5Base = p.startsWith('/h5/') || p === '/h5' ? '/h5' : '';
window.location.pathname = `${h5Base}/static/html/pc.html`;
} }
} }
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name" : "crmeb", "name" : "crmeb",
"appid" : "__UNI__70A74E1", "appid" : "__UNI__6691FE3",
"description" : "crmeb商城", "description" : "crmeb商城",
"versionName" : "3.5.1", "versionName" : "3.5.1",
"versionCode" : 351, "versionCode" : 351,
@@ -218,7 +218,7 @@
}, },
"router" : { "router" : {
"mode" : "history", "mode" : "history",
"base" : "/h5/" "base" : "/"
}, },
"domain" : "", "domain" : "",
"sdkConfigs" : { "sdkConfigs" : {

View File

@@ -39,6 +39,11 @@ export default {
balanceStatus: { balanceStatus: {
type: Number, type: Number,
default: 0 default: 0
},
// HJF 分销等级信息,向下透传给各 template
memberInfo: {
type: Object,
default: () => ({ agent_level: 0, member_level: 0, member_level_name: '' })
} }
}, },
data() { data() {
@@ -97,7 +102,7 @@ export default {
<!-- #ifdef MP || APP-PLUS --> <!-- #ifdef MP || APP-PLUS -->
<topBar v-if="memberData.style != 5" :styleType="memberData.style" :isScrolling="isScrolling"></topBar> <topBar v-if="memberData.style != 5" :styleType="memberData.style" :isScrolling="isScrolling"></topBar>
<!-- #endif --> <!-- #endif -->
<template1 v-if="memberData.style == 1" :perShowType="memberData.per_show_type" :userInfo="userInfo" :property="property"></template1> <template1 v-if="memberData.style == 1" :perShowType="memberData.per_show_type" :userInfo="userInfo" :property="property" :memberInfo="memberInfo"></template1>
<template2 v-if="memberData.style == 2" :perShowType="memberData.per_show_type" :userInfo="userInfo" :property="property"></template2> <template2 v-if="memberData.style == 2" :perShowType="memberData.per_show_type" :userInfo="userInfo" :property="property"></template2>
<template3 v-if="memberData.style == 3" :perShowType="memberData.per_show_type" :userInfo="userInfo" :property="property"></template3> <template3 v-if="memberData.style == 3" :perShowType="memberData.per_show_type" :userInfo="userInfo" :property="property"></template3>
<template4 v-if="memberData.style == 4" :perShowType="memberData.per_show_type" :userInfo="userInfo" :commission="orderAdminData.commission"></template4> <template4 v-if="memberData.style == 4" :perShowType="memberData.per_show_type" :userInfo="userInfo" :commission="orderAdminData.commission"></template4>

View File

@@ -14,12 +14,23 @@ export default {
perShowType: { perShowType: {
type: Number, type: Number,
default: 0 default: 0
},
// HJF 分销等级信息(由 member/index.vue 向下传递)
memberInfo: {
type: Object,
default: () => ({ agent_level: 0, member_level: 0, member_level_name: '' })
} }
}, },
inject: ['intoPage', 'tapQrCode', 'goMenuPage', 'goEdit', 'bindPhone', 'checkApp'], inject: ['intoPage', 'tapQrCode', 'goMenuPage', 'goEdit', 'bindPhone', 'checkApp'],
computed:{ computed:{
showMerBtn(){ showMerBtn(){
return this.$store.state.app.channel_func_status return this.$store.state.app.channel_func_status
},
/** 从 userInfo.agent_level_name 推导 grade0-4用于徽章颜色 */
agentLevelGrade() {
const nameGradeMap = { '创客': 1, '云店': 2, '服务商': 3, '分公司': 4 };
const name = (this.userInfo && this.userInfo.agent_level_name) || '';
return nameGradeMap[name] || 0;
} }
}, },
methods: { methods: {
@@ -58,10 +69,20 @@ export default {
<view class="bind-phone" v-if="!userInfo.phone && userInfo.uid" @tap="bindPhone">绑定手机号</view> <view class="bind-phone" v-if="!userInfo.phone && userInfo.uid" @tap="bindPhone">绑定手机号</view>
<view class="acea-row row-middle" v-else> <view class="acea-row row-middle" v-else>
<view class="phone">{{ perShowType ? 'ID' + userInfo.uid : userInfo.phone }}</view> <view class="phone">{{ perShowType ? 'ID' + userInfo.uid : userInfo.phone }}</view>
<!--
<view class="vip flex-center" v-if="userInfo.level"> <view class="vip flex-center" v-if="userInfo.level">
<text class="iconfont icon-huiyuandengji"></text> <text class="iconfont icon-huiyuandengji"></text>
{{ userInfo.vip_name || ('V' + userInfo.level) }} {{ userInfo.vip_name || ('V' + userInfo.level) }}
</view> </view>
-->
<!-- HJF 分销等级徽章直接用 userInfo.agent_level / agent_level_name无需等待异步 memberInfo -->
<HjfMemberBadge
v-if="userInfo.agent_level > 0"
:level="agentLevelGrade"
:levelName="userInfo.agent_level_name"
size="small"
class="ml-10"
/>
</view> </view>
</template> </template>

View File

@@ -2,12 +2,7 @@
<!-- 个人中心模块 --> <!-- 个人中心模块 -->
<view class="user-page" :style="colorStyle"> <view class="user-page" :style="colorStyle">
<template v-if="isObjectData(diyData)"> <template v-if="isObjectData(diyData)">
<user-member :userInfo="userInfo" :memberData="diyData.member" :orderAdminData="orderAdminData" :balanceStatus="balanceStatus" :isScrolling="isScrolling"></user-member> <user-member :userInfo="userInfo" :memberData="diyData.member" :orderAdminData="orderAdminData" :balanceStatus="balanceStatus" :isScrolling="isScrolling" :memberInfo="memberInfo"></user-member>
<!-- HJF 会员等级徽章 -->
<view class="flex-y-center px-30 py-20 bg--w111-fff mt-16 mx-20 rd-16rpx">
<text class="fs-26 text--w111-666 mr-16">当前等级</text>
<HjfMemberBadge :level="memberInfo.member_level" :levelName="memberInfo.member_level_name" size="normal" />
</view>
<user-order :orderMenu="orderMenu" :orderAdminData="orderAdminData" :userInfo="userInfo" :memberData="diyData.member" :orderData="diyData.order"></user-order> <user-order :orderMenu="orderMenu" :orderAdminData="orderAdminData" :userInfo="userInfo" :memberData="diyData.member" :orderData="diyData.order"></user-order>
<!-- 黄精粉快捷入口我的资产 & 公排记录 member-points 保持一致风格 --> <!-- 黄精粉快捷入口我的资产 & 公排记录 member-points 保持一致风格 -->
<view class="acea-row member-points hjf-nav-row"> <view class="acea-row member-points hjf-nav-row">
@@ -239,6 +234,7 @@ export default {
routineContact: 0, routineContact: 0,
/** HJF 会员等级信息 */ /** HJF 会员等级信息 */
memberInfo: { memberInfo: {
agent_level: 0,
member_level: 0, member_level: 0,
member_level_name: '普通', member_level_name: '普通',
} }
@@ -305,9 +301,10 @@ export default {
loadMemberInfo() { loadMemberInfo() {
getMemberInfo().then(res => { getMemberInfo().then(res => {
if (res && res.data) { if (res && res.data) {
this.memberInfo = { this.memberInfo = {
member_level: res.data.member_level || 0, agent_level: res.data.agent_level || 0,
member_level_name: res.data.member_level_name || '普通', member_level: res.data.member_level || 0,
member_level_name: res.data.member_level_name || '普通',
}; };
} }
}).catch(() => {}); }).catch(() => {});

View File

@@ -79,8 +79,10 @@
<text class="main-color" @click.stop="privacy('privacy')">隐私协议</text> <text class="main-color" @click.stop="privacy('privacy')">隐私协议</text>
</checkbox-group> </checkbox-group>
</view> </view>
<!-- #ifndef H5 -->
<Verify v-if="!disabled" @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }" <Verify v-if="!disabled" @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
ref="verify"></Verify> ref="verify"></Verify>
<!-- #endif -->
</view> </view>
</template> </template>
@@ -114,12 +116,16 @@
// #endif // #endif
const BACK_URL = "login_back_url"; const BACK_URL = "login_back_url";
import colors from '@/mixins/color.js'; import colors from '@/mixins/color.js';
// #ifndef H5
import Verify from '../components/verify/verify.vue'; import Verify from '../components/verify/verify.vue';
// #endif
import { HTTP_REQUEST_URL, CAPTCHA_TYPE } from '@/config/app'; import { HTTP_REQUEST_URL, CAPTCHA_TYPE } from '@/config/app';
export default { export default {
name: "Login", name: "Login",
components: { components: {
// #ifndef H5
Verify Verify
// #endif
}, },
mixins: [sendVerifyCode, colors], mixins: [sendVerifyCode, colors],
data: function() { data: function() {
@@ -397,7 +403,11 @@
Date.parse(new Date()); Date.parse(new Date());
}, },
success(data) { success(data) {
this.$refs.verify.hide() // #ifndef H5
if (this.$refs.verify) {
this.$refs.verify.hide();
}
// #endif
getCodeApi() getCodeApi()
.then(res => { .then(res => {
this.keyCode = res.data.key; this.keyCode = res.data.key;
@@ -423,17 +433,22 @@
if (!/^1(3|4|5|7|8|9|6)\d{9}$/i.test(that.account)) return that.$util.Tips({ if (!/^1(3|4|5|7|8|9|6)\d{9}$/i.test(that.account)) return that.$util.Tips({
title: '请输入正确的手机号码' title: '请输入正确的手机号码'
}); });
// getCodeApi() // #ifdef H5
// .then(res => { // H5不弹出滑块/图形安全验证,直接取 key 并发短信(后端 register/verify 已关闭 aj 二次校验)
// that.keyCode = res.data.key; getCodeApi()
// that.getCode(); .then(res => {
// }) that.keyCode = res.data.key;
// .catch(res => { that.getCode({ captchaVerification: '' });
// that.$util.Tips({ })
// title: res .catch(res => {
// }); that.$util.Tips({
// }); title: res
this.$refs.verify.show() });
});
// #endif
// #ifndef H5
this.$refs.verify.show();
// #endif
}, },
async getLogoImage() { async getLogoImage() {
let that = this; let that = this;
@@ -569,7 +584,11 @@
that.sendCode(); that.sendCode();
}) })
.catch(res => { .catch(res => {
this.$refs.verify.refresh() // #ifndef H5
if (this.$refs.verify) {
this.$refs.verify.refresh();
}
// #endif
that.$util.Tips({ that.$util.Tips({
title: res title: res
}); });

View File

@@ -35,21 +35,30 @@
} }
} }
</style> </style>
<script type="text/javascript">
window.isPC = true;
window.onload = function(){
/* 监听电脑浏览器窗口尺寸改变 */
window.onresize = function(){
/* 窗口宽度 小于或等于420px 时跳转回H5页面 */
if(window.innerWidth <= 420){
window.location.pathname = '/h5/';
}
}
}
</script>
</head> </head>
<body> <body>
<iframe src="/h5/?type=1"></iframe> <iframe id="app-frame"></iframe>
<script type="text/javascript">
window.isPC = true;
/* 根据当前地址推导 H5 根路径:本地 dev 多为 /,线上部署多为 /h5/ */
function h5AppRootPathname() {
var path = (window.location.pathname || '/').replace(/\/$/, '') || '/';
var marker = '/static/html/pc.html';
if (path.endsWith(marker)) {
var root = path.slice(0, -marker.length);
if (!root) root = '/';
return root === '/' ? '/' : (root + '/');
}
return '/';
}
var H5_ROOT = h5AppRootPathname();
document.getElementById('app-frame').src = H5_ROOT + '?type=1';
window.onresize = function(){
if (window.innerWidth <= 420) {
var target = H5_ROOT.replace(/\/$/, '');
window.location.pathname = target || '/';
}
};
</script>
</body> </body>
</html> </html>