文件
fenleixinxi/application/admin/command/FcMenu.php
MiAn d871e73d23 feat: 重构会员权益与分销配置,新增套餐维度权益管理
1. 新增会员权益配置控制器 Memberbenefit 和 Publish,拆分运营配置模块
2. 新增套餐表字段 free_view_quota、post_discount,支持按套餐配置免费看和发帖折扣
3. 优化前端页面,新增套餐编辑表单、区域选择器、发布配置交互
4. 新增调试支持,支持 mock openid、本地 IP 绕过权限,便于自动化测试
5. 新增数据库工具脚本,用于检查配置、修复数据、验证会员功能
6. 优化会员接口,返回套餐维度的权益信息,支持无限次免费看场景
2026-10-26 09:21:51 +08:00

319 行
13 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\admin\command;
use think\Cache;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
/**
* 同城业务后台菜单结构重建(幂等)
*
* 分类原则:按运营岗位职责划分,而不是按控制器目录。
* - 数据看板:业务总览
* - 信息管理:信息列表、信息分类、草稿、评价、举报(内容审核闭环)
* - 商家会员:商家、用户、消息
* - 交易财务:订单、资金流水、付费查看、佣金、提现
* - 运营推广:轮播、套餐、优惠券、分销关系
* - 权限配置:管理员、角色、菜单规则、操作日志
* - 系统设置:平台概况、系统配置(含小程序/发布/分销分组)、服务区域、附件、个人资料、插件
*
* 命名规范:菜单文字不超过 5 个汉字,尽量统一为 4 个字。
*
* 用法:
* php think fc:menu
*
* 说明:
* - 以 name 为唯一键 upsert,重复执行不会产生重复菜单;
* - 只调整「菜单/规则」的归属、标题、图标、排序、显隐,不改任何业务数据;
* - 清理 FastAdmin 演示菜单与已下线的公众号菜单;
* - 执行后清理 __menu__ 缓存,左侧菜单立即生效。
*/
class FcMenu extends Command
{
/** 一级菜单:name => [标题, 图标, 权重] */
const GROUPS = [
'console' => ['数据看板', 'fa fa-dashboard', 100],
'fc/content' => ['信息管理', 'fa fa-file-text-o', 90],
'fc/user' => ['商家会员', 'fa fa-users', 80],
'fc/trade' => ['交易财务', 'fa fa-shopping-cart', 70],
'fc/marketing' => ['运营推广', 'fa fa-share-alt', 60],
'fc/auth' => ['权限配置', 'fa fa-group', 50],
'fc/system' => ['系统设置', 'fa fa-cogs', 40],
];
/** 功能菜单:name => [一级菜单, 标题, 图标, 权重] */
const ITEMS = [
// 数据看板
'fc/dashboard' => ['console', '业务总览', 'fa fa-dashboard', 98],
// 信息管理:信息 + 分类 + 审核
'fc/postaudit' => ['fc/content', '信息审核', 'fa fa-check-square-o', 96],
'fc/post' => ['fc/content', '信息列表', 'fa fa-file-text', 95],
'fc/category' => ['fc/content', '分类管理', 'fa fa-list', 90],
'fc/draft' => ['fc/content', '草稿管理', 'fa fa-folder-o', 85],
'fc/comment' => ['fc/content', '评价管理', 'fa fa-star', 80],
'fc/report' => ['fc/content', '举报处理', 'fa fa-flag', 75],
// 商家会员
'fc/merchant' => ['fc/user', '商家管理', 'fa fa-building', 95],
'fc/userext' => ['fc/user', '用户管理', 'fa fa-user-circle', 90],
'fc/message' => ['fc/user', '消息管理', 'fa fa-bell', 85],
// 交易财务
'fc/order' => ['fc/trade', '订单管理', 'fa fa-file-text-o', 95],
'fc/financeflow' => ['fc/trade', '资金流水', 'fa fa-money', 90],
'fc/payview' => ['fc/trade', '付费查看', 'fa fa-eye', 85],
'fc/commission' => ['fc/trade', '佣金明细', 'fa fa-cny', 80],
'fc/withdraw' => ['fc/trade', '提现审核', 'fa fa-credit-card', 75],
// 运营推广
'fc/banner' => ['fc/marketing', '轮播管理', 'fa fa-image', 95],
'fc/plan' => ['fc/marketing', '会员套餐', 'fa fa-diamond', 90],
'fc/publish' => ['fc/marketing', '运营配置', 'fa fa-sliders', 88],
'fc/coupon' => ['fc/marketing', '优惠券', 'fa fa-ticket', 85],
'fc/distrelation' => ['fc/marketing', '分销关系', 'fa fa-sitemap', 80],
// 权限配置
'auth/admin' => ['fc/auth', '管理员', 'fa fa-user', 95],
'auth/group' => ['fc/auth', '角色管理', 'fa fa-group', 90],
'auth/rule' => ['fc/auth', '菜单规则', 'fa fa-bars', 85],
'auth/adminlog' => ['fc/auth', '操作日志', 'fa fa-list-alt', 80],
// 系统设置
'general/config' => ['fc/system', '系统配置', 'fa fa-cog', 99],
'dashboard' => ['fc/system', '平台概况', 'fa fa-dashboard', 95],
'fc/area' => ['fc/system', '服务区域', 'fa fa-map-o', 90],
'general/attachment' => ['fc/system', '附件管理', 'fa fa-file-image-o', 80],
'general/profile' => ['fc/system', '个人资料', 'fa fa-user', 70],
'addon' => ['fc/system', '插件管理', 'fa fa-rocket', 60],
];
/** 已并入对应主菜单、不再单独显示的历史菜单 */
const RETIRED = ['fc/setting', 'fc/memberbenefit'];
/** 权限节点:name => [上级菜单, 标题] */
const NODES = [
'fc/dashboard/index' => ['fc/dashboard', '查看'],
'fc/publish/index' => ['fc/publish', '查看'],
'fc/plan/savebenefit' => ['fc/plan', '保存特权'],
'fc/distrelation/saveconfig' => ['fc/distrelation', '保存配置'],
'fc/post/audit' => ['fc/post', '审核'],
'fc/post/top' => ['fc/post', '置顶'],
'fc/post/offline' => ['fc/post', '下架'],
'fc/post/online' => ['fc/post', '上架'],
'fc/coupon/index' => ['fc/coupon', '查看'],
'fc/coupon/add' => ['fc/coupon', '添加'],
'fc/coupon/edit' => ['fc/coupon', '编辑'],
'fc/coupon/del' => ['fc/coupon', '删除'],
'fc/coupon/multi' => ['fc/coupon', '批量更新'],
];
/** 需要整体下线的 FastAdmin 内置菜单(公众号模块 / 框架会员模块) */
const OFFLINE = [
'wechat' => '公众号模块与本项目小程序无关,下线',
'user' => '框架会员模块与「商家会员 → 用户管理」重复,下线',
];
/** 下线菜单统一改为 4 字中文,避免规则列表里出现英文/超长标题 */
const OFFLINE_TITLES = [
'user' => '会员管理',
'user/user' => '会员管理',
'user/group' => '会员分组',
'user/rule' => '会员规则',
'wechat' => '微信管理',
'wechat/autoreply' => '自动回复',
'wechat/response' => '资源管理',
'wechat/config' => '配置管理',
'wechat/menu' => '菜单管理',
];
/** 需要清理的演示菜单 */
const DEMO = ['category'];
protected function configure()
{
$this->setName('fc:menu')
->setDescription('重建同城业务后台菜单结构(幂等)');
}
protected function execute(Input $input, Output $output)
{
self::apply($output);
}
/**
* 应用目标菜单树
*
* @param Output|null $output
* @return void
*/
public static function apply(Output $output = null)
{
$now = time();
$table = Db::name('auth_rule');
// 0) 旧分组更名:分销管理 → 运营推广,避免按 name 定位时新建一个空分组
if (!$table->where('name', 'fc/marketing')->find()) {
$old = $table->where('name', 'fc/dist')->find();
if ($old) {
$table->where('id', $old['id'])->update([
'name' => 'fc/marketing',
'updatetime' => $now,
]);
}
}
// 1) 一级菜单
$groupIds = [];
foreach (self::GROUPS as $name => $item) {
$groupIds[$name] = self::put($table, $name, [
'type' => 'menu',
'pid' => 0,
'title' => $item[0],
'icon' => $item[1],
'ismenu' => 1,
'weigh' => $item[2],
], $now);
}
// 2) 功能菜单
$itemIds = [];
foreach (self::ITEMS as $name => $item) {
$parent = $groupIds[$item[0]] ?? ($groupIds['fc/system'] ?? 0);
$itemIds[$name] = self::put($table, $name, [
'type' => 'file',
'pid' => $parent,
'title' => $item[1],
'icon' => $item[2],
'ismenu' => 1,
'weigh' => $item[3],
], $now);
}
// 3) 权限节点(只补缺,不改业务已用的标题)
$nodeCount = 0;
foreach (self::NODES as $name => $node) {
$parent = $itemIds[$node[0]] ?? ($groupIds[$node[0]] ?? 0);
if (!$parent) {
continue;
}
self::put($table, $name, [
'type' => 'file',
'pid' => $parent,
'title' => $node[1],
'icon' => 'fa fa-circle-o',
'ismenu' => 0,
'weigh' => 0,
], $now);
$nodeCount++;
}
// 4) 菜单页说明(build_heading 读取 auth_rule.remark,而不是控制器注释)
$table->where('name', 'auth/rule')->update([
'remark' => '左侧菜单与权限节点都在这里维护;修改后左侧导航自动刷新,删除菜单会连同其子级一起删除',
]);
// 5) 演示菜单清理
$removed = 0;
foreach (self::DEMO as $name) {
$row = $table->where('name', $name)->find();
if (!$row) {
continue;
}
$ids = array_merge([(int)$row['id']], $table->where('pid', $row['id'])->column('id'));
$removed += $table->where('id', 'in', $ids)->delete();
}
// 6) 下线框架/公众号菜单(父级隐藏后,子级不会再出现在侧边栏)
$offline = 0;
foreach (array_keys(self::OFFLINE) as $name) {
$row = $table->where('name', $name)->find();
if (!$row) {
continue;
}
$table->where('id', $row['id'])->update(['status' => 'hidden', 'updatetime' => $now]);
$table->where('pid', $row['id'])->update(['status' => 'hidden', 'updatetime' => $now]);
$offline++;
}
foreach (self::OFFLINE_TITLES as $name => $title) {
$table->where('name', $name)->update(['title' => $title]);
}
// 6.1) 已并入系统配置页的历史菜单,不再单独显示
foreach (self::RETIRED as $name) {
$table->where('name', $name)->update(['status' => 'hidden', 'updatetime' => $now]);
}
// 7) 清理已搬空的历史分组(同城业务 / 基础数据 / 常规管理 / 权限管理)
$emptyRemoved = 0;
foreach (['fc', 'fc/base', 'general', 'auth'] as $name) {
$row = $table->where('name', $name)->find();
if ($row && !$table->where('pid', $row['id'])->count()) {
$table->where('id', $row['id'])->delete();
$emptyRemoved++;
}
}
// 8) 菜单缓存
try {
Cache::rm('__menu__');
} catch (\Exception $e) {
// 缓存清理失败不影响菜单数据,过期后会自动重建
}
if ($output) {
// 菜单命名规范:不超过 5 个汉字
$tooLong = [];
foreach (self::GROUPS as $group) {
if (mb_strlen($group[0], 'UTF-8') > 5) {
$tooLong[] = $group[0];
}
}
foreach (self::ITEMS as $item) {
if (mb_strlen($item[1], 'UTF-8') > 5) {
$tooLong[] = $item[1];
}
}
$output->writeln(' - 一级菜单 ' . count(self::GROUPS) . ' / 功能菜单 ' . count(self::ITEMS));
$output->writeln(' - 权限节点 ' . $nodeCount . ' 个;清理演示菜单 ' . $removed . ' 条;下线框架菜单 ' . $offline . ' 组;移除空分组 ' . $emptyRemoved . ' 个');
if ($tooLong) {
$output->writeln(' - 警告:以下菜单超过 5 个汉字:' . implode('、', $tooLong));
}
$output->writeln(' - 已清理后台菜单缓存 __menu__');
}
}
/**
* 按 name 写入一条规则:存在则更新,不存在则插入
*
* @param \think\db\Query $table
* @param string $name
* @param array $data
* @param int $now
* @return int
*/
protected static function put($table, $name, array $data, $now)
{
$data = array_merge([
'type' => 'file',
'pid' => 0,
'title' => $name,
'icon' => 'fa fa-circle-o',
'url' => '',
'ismenu' => 1,
'menutype' => 'addtabs',
'weigh' => 0,
'status' => 'normal',
], $data);
$data['updatetime'] = $now;
$row = $table->where('name', $name)->find();
if ($row) {
$table->where('id', $row['id'])->update($data);
return (int)$row['id'];
}
$data['name'] = $name;
$data['createtime'] = $now;
return (int)$table->insertGetId($data);
}
}