安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
167 lines
6.2 KiB
PHP
167 lines
6.2 KiB
PHP
<?php
|
|
/**
|
|
* 一次性脚本:清理内置「电子商务」(Shop)模块在已装好的库里的残留数据。
|
|
*
|
|
* 背景:内置商城已整体下线(控制器 / 验证器 / 视图 / 静态 JS / CommonShop 模型全部删除,
|
|
* install.sql 里的菜单种子与 common_shop 表定义也一并移除)。但已经装好的库不会被这些改动影响:
|
|
* MenuRegistry::ensure() 只会「缺了就补」,从来不删,于是「交易营销 → 电子商务」会变成一个
|
|
* 点了就报错的僵尸菜单;wxapp_common_shop 表也会一直挂着。
|
|
*
|
|
* 本脚本处理三样东西(幂等,找不到就什么都不做):
|
|
* 1. backend_power 里 shop / shop:* 的权限节点(含挂在这些节点下的子按钮)
|
|
* 2. backend_role_power 里指向它们的角色授权记录(不删会留下孤儿行)
|
|
* 3. wxapp_common_shop 数据表
|
|
*
|
|
* 用法:
|
|
* php scripts/purge_builtin_shop.php # 预演:只统计、不动库(默认)
|
|
* php scripts/purge_builtin_shop.php --force # 真正执行
|
|
*
|
|
* 依赖:只需 PDO + mysql 扩展,不加载框架(里的 .env / config/database.php 自己读)。
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
$root = dirname(__DIR__) . DIRECTORY_SEPARATOR;
|
|
$force = in_array('--force', $argv ?? [], true);
|
|
|
|
echo $force
|
|
? "[模式] 执行:以下动作会真实落库\n"
|
|
: "[模式] 预演(dry-run):只统计不改动,加 --force 才执行\n";
|
|
|
|
/* ------------------------- 1. 读配置:极简 env() + include database.php ------------------------- */
|
|
|
|
$GLOBALS['__env'] = [];
|
|
foreach ([$root . '.env', $root . '.env.local'] as $envFile) {
|
|
if (! is_file($envFile)) {
|
|
continue;
|
|
}
|
|
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || $line[0] === '#') {
|
|
continue;
|
|
}
|
|
$pos = strpos($line, '=');
|
|
if ($pos === false) {
|
|
continue;
|
|
}
|
|
$GLOBALS['__env'][trim(substr($line, 0, $pos))] = trim(trim(substr($line, $pos + 1)), "\"'");
|
|
}
|
|
}
|
|
|
|
if (! function_exists('env')) {
|
|
function env(string $key = '', $default = null)
|
|
{
|
|
return $GLOBALS['__env'][$key] ?? $default;
|
|
}
|
|
}
|
|
|
|
$dbConfFile = $root . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
|
if (! is_file($dbConfFile)) {
|
|
fwrite(STDERR, "找不到配置文件: {$dbConfFile}\n");
|
|
exit(1);
|
|
}
|
|
$dbConf = include $dbConfFile;
|
|
$conn = $dbConf['connections'][$dbConf['default'] ?? 'mysql'] ?? [];
|
|
|
|
/* ------------------------- 2. 连库 ------------------------- */
|
|
|
|
$dsn = sprintf(
|
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
|
$conn['hostname'] ?? '127.0.0.1',
|
|
(int) ($conn['hostport'] ?? 3306),
|
|
$conn['database'] ?? '',
|
|
$conn['charset'] ?: 'utf8mb4'
|
|
);
|
|
|
|
try {
|
|
$pdo = new PDO($dsn, $conn['username'] ?? 'root', $conn['password'] ?? '', [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
fwrite(STDERR, '数据库连接失败: ' . $e->getMessage() . "\n");
|
|
exit(1);
|
|
}
|
|
|
|
$prefix = (string) ($conn['prefix'] ?? '');
|
|
$powerTable = $prefix . 'backend_power';
|
|
$rolePowerTable = $prefix . 'backend_role_power';
|
|
$shopTable = $prefix . 'common_shop';
|
|
|
|
echo "[连接] {$conn['hostname']}:" . ($conn['hostport'] ?? 3306)
|
|
. " / {$conn['database']} (表前缀 {$prefix})\n";
|
|
|
|
/* ------------------------- 3. 菜单 / 权限节点 + 角色授权 ------------------------- */
|
|
|
|
$tableExists = static function (string $table) use ($pdo): bool {
|
|
return (bool) $pdo->query("SHOW TABLES LIKE '{$table}'")->fetchColumn();
|
|
};
|
|
|
|
if (! $tableExists($powerTable)) {
|
|
echo "[权限] {$powerTable} 不存在,跳过\n";
|
|
echo "[数据表] {$shopTable} 不存在,跳过\n";
|
|
exit(0);
|
|
}
|
|
|
|
$stmt = $pdo->prepare(
|
|
"SELECT `id`, `name`, `title` FROM `{$powerTable}`
|
|
WHERE `name` = 'shop' OR `name` LIKE 'shop:%'
|
|
ORDER BY `id`"
|
|
);
|
|
$stmt->execute();
|
|
$powers = $stmt->fetchAll();
|
|
|
|
if (! $powers) {
|
|
echo "[权限] 没有 shop 相关权限节点,跳过\n";
|
|
} else {
|
|
echo '[权限] 命中 ' . count($powers) . " 个节点:\n";
|
|
foreach ($powers as $row) {
|
|
echo sprintf(" #%d %s (%s)\n", (int) $row['id'], $row['name'], $row['title']);
|
|
}
|
|
|
|
$ids = array_map('intval', array_column($powers, 'id'));
|
|
$in = implode(',', $ids);
|
|
|
|
$hasRolePower = $tableExists($rolePowerTable);
|
|
$grantRows = $hasRolePower
|
|
? (int) $pdo->query("SELECT COUNT(*) FROM `{$rolePowerTable}` WHERE `power_id` IN ({$in})")->fetchColumn()
|
|
: 0;
|
|
$powerRows = (int) $pdo->query("SELECT COUNT(*) FROM `{$powerTable}` WHERE `id` IN ({$in}) OR `pid` IN ({$in})")->fetchColumn();
|
|
|
|
echo "[授权] {$rolePowerTable} 待删 {$grantRows} 条\n";
|
|
echo "[权限] {$powerTable} 待删 {$powerRows} 条(含挂在它们下面的子按钮)\n";
|
|
|
|
if ($force) {
|
|
$pdo->beginTransaction();
|
|
try {
|
|
if ($hasRolePower) {
|
|
$pdo->exec("DELETE FROM `{$rolePowerTable}` WHERE `power_id` IN ({$in})");
|
|
}
|
|
$affected = $pdo->exec("DELETE FROM `{$powerTable}` WHERE `id` IN ({$in}) OR `pid` IN ({$in})");
|
|
$pdo->commit();
|
|
echo "[执行] 已删授权 {$grantRows} 条、权限节点 {$affected} 条\n";
|
|
} catch (Throwable $e) {
|
|
$pdo->rollBack();
|
|
fwrite(STDERR, '[失败] 权限清理回滚: ' . $e->getMessage() . "\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------- 4. 数据表 ------------------------- */
|
|
|
|
if ($pdo->query("SHOW TABLES LIKE '{$shopTable}'")->fetchColumn()) {
|
|
$rows = (int) $pdo->query("SELECT COUNT(*) FROM `{$shopTable}`")->fetchColumn();
|
|
echo "[数据表] {$shopTable} 存在,共 {$rows} 行\n";
|
|
if ($force) {
|
|
$pdo->exec("DROP TABLE `{$shopTable}`");
|
|
echo "[执行] 已 DROP TABLE `{$shopTable}`\n";
|
|
}
|
|
} else {
|
|
echo "[数据表] {$shopTable} 不存在,跳过\n";
|
|
}
|
|
|
|
echo $force
|
|
? "[完成] 内置商城残留已清理。后台菜单即时生效(MenuRegistry 每次请求读库,无缓存)\n"
|
|
: "[提示] 以上为预演结果,未做任何改动。确认后执行: php scripts/purge_builtin_shop.php --force\n";
|