安全(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(市场 / 开发者中心 / 审核 / 分润)
155 lines
6.5 KiB
PHP
155 lines
6.5 KiB
PHP
<?php
|
|
namespace app\appmall\library;
|
|
|
|
use think\facade\Log;
|
|
use ywxapp\model\BaseModel;
|
|
|
|
/**
|
|
* 应用市场(appmall)专属表结构自愈。
|
|
*
|
|
* 事实源:app/appmall/install.sql(本模块自带,随模块一起分发)。
|
|
* 本类不再内联任何建表 DDL —— 早期版本把 DDL 抄写在本类与多个控制器里,
|
|
* 彼此与 install.sql 逐份漂移(漏列 / 漏索引 / 缺 UK),现统一改为「解析 install.sql 执行」。
|
|
*
|
|
* 无版本号:缺表建表、缺列补列全部是幂等探测(SHOW TABLES / SHOW COLUMNS),
|
|
* 不依赖 schema 版本表、不需要迁移脚本,重复调用安全。
|
|
*
|
|
* 分工(谁的表谁兜底):
|
|
* - 本模块 13 张表**全部有模型**(见 self::MODELS,与 install.sql 一一对应):
|
|
* 各模型 ensureSchema() 提供「缺表 + 老库扩展列」,并在 initialize() 中被自动触发,
|
|
* 因此任何入口(控制器 / 服务 / 静态查询)都不会再撞 1146(缺表)/ 1054(缺列)。
|
|
* - 本类 ensure() 汇总「全部模型 + install.sql 里暂无模型的表」,供应用级中间件 / 控制器入口调用一次。
|
|
*/
|
|
class AppmallSchema
|
|
{
|
|
/**
|
|
* 本模块全部表的模型清单(表 ↔ 模型 一一对应,勿漏)。
|
|
* 新增表时按序:install.sql 建表 → 加模型(含 ensureSchema)→ 登记到此处。
|
|
*
|
|
* @var array<class-string>
|
|
*/
|
|
public const MODELS = [
|
|
\app\appmall\model\AddonList::class, // appmall_addon_list
|
|
\app\appmall\model\AddonSubmission::class, // appmall_addon_submissions
|
|
\app\appmall\model\AddonReviewLog::class, // appmall_addon_review_log
|
|
\app\appmall\model\Developer::class, // appmall_developers
|
|
\app\appmall\model\Revenue::class, // appmall_addon_revenues
|
|
\app\appmall\model\Withdrawal::class, // appmall_addon_withdrawals
|
|
\app\appmall\model\AddonOrder::class, // appmall_addon_orders
|
|
\app\appmall\model\AddonLicense::class, // appmall_addon_licenses
|
|
\app\appmall\model\AddonDownloadLog::class, // appmall_addon_download_logs
|
|
\app\appmall\model\Site::class, // appmall_sites
|
|
\app\appmall\model\FrameworkList::class, // appmall_framework_list
|
|
\app\appmall\model\FrameworkDownloadLog::class, // appmall_framework_download_log
|
|
\app\appmall\model\Throttle::class, // appmall_throttle
|
|
\app\appmall\model\AddonReview::class, // appmall_addon_reviews(用户评价)
|
|
];
|
|
|
|
/** 进程内已确保建表的表(不含前缀),避免每请求重复 SHOW TABLES */
|
|
private static $tableDone = [];
|
|
|
|
/** 进程内已确保过的扩展列 "表|列" */
|
|
private static $colDone = [];
|
|
|
|
/** install.sql 解析缓存:[不含前缀表名 => CREATE 语句] */
|
|
private static $ddlMap = null;
|
|
|
|
/**
|
|
* 确保单张 appmall 表就绪:缺表建表 + 补齐扩展列(幂等,进程内只探测一次)。
|
|
* 模型 ensureSchema() 直接委托这里,控制器无需再手写 DDL。
|
|
*
|
|
* @param string $table 不含前缀的表名,如 appmall_addon_list
|
|
* @param array<string,string> $columns 该表的老库扩展列 [列名 => 列定义]
|
|
*/
|
|
public static function ensureTable(string $table, array $columns = []): void
|
|
{
|
|
$table = self::bare($table);
|
|
$prefix = BaseModel::currentPrefix();
|
|
|
|
if (!isset(self::$tableDone[$table])) {
|
|
self::$tableDone[$table] = true;
|
|
$map = self::ddlMap();
|
|
if (isset($map[$table])) {
|
|
BaseModel::ensureTable($prefix . $table, str_replace('__PREFIX__', $prefix, $map[$table]));
|
|
}
|
|
}
|
|
|
|
foreach ($columns as $col => $def) {
|
|
$key = $table . '|' . $col;
|
|
if (isset(self::$colDone[$key])) {
|
|
continue;
|
|
}
|
|
self::$colDone[$key] = true;
|
|
BaseModel::ensureColumn($prefix . $table, $col, $def);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 确保全部 appmall 表就绪(幂等,进程内每表只探测一次)。
|
|
*
|
|
* 触发点:
|
|
* - app/appmall/middleware.php(应用级中间件)→ 任何 /appmall/* 请求先建全表;
|
|
* - 任意模型被使用 → 该模型 initialize() 自触发对应单表兜底。
|
|
*/
|
|
public static function ensure(): void
|
|
{
|
|
// 1) 模型清单:缺表建表 + 老库扩展列,随模型一起维护,不会漏
|
|
foreach (self::MODELS as $model) {
|
|
try {
|
|
if (class_exists($model) && method_exists($model, 'ensureSchema')) {
|
|
$model::ensureSchema();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// 单表自愈失败不阻断其余表(由业务暴露)
|
|
}
|
|
}
|
|
|
|
// 2) install.sql 里存在、但暂未建模型的表也兜底(防止「只加 SQL 忘了加模型」而漏表)
|
|
foreach (array_keys(self::ddlMap()) as $table) {
|
|
self::ensureTable($table);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 去掉可能已带上的表前缀,统一成 install.sql 里的裸表名。
|
|
*/
|
|
private static function bare(string $table): string
|
|
{
|
|
$prefix = BaseModel::currentPrefix();
|
|
if ($prefix !== '' && strpos($table, $prefix) === 0) {
|
|
return substr($table, strlen($prefix));
|
|
}
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* 解析 install.sql,得到 [裸表名 => CREATE 语句](语句内保留 __PREFIX__ 占位符)。
|
|
* 结果进程内缓存一次;文件缺失时记日志并返回空表(自愈降级为不动作,不阻断业务)。
|
|
*/
|
|
private static function ddlMap(): array
|
|
{
|
|
if (self::$ddlMap !== null) {
|
|
return self::$ddlMap;
|
|
}
|
|
self::$ddlMap = [];
|
|
|
|
$file = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'install.sql';
|
|
if (!is_file($file)) {
|
|
try {
|
|
Log::error('[appmall] 建表事实源 install.sql 缺失,表结构自愈已跳过:' . $file);
|
|
} catch (\Throwable $e) {
|
|
// 日志不可用则忽略
|
|
}
|
|
return self::$ddlMap;
|
|
}
|
|
|
|
$content = (string) file_get_contents($file);
|
|
if (preg_match_all('/CREATE TABLE IF NOT EXISTS `__PREFIX__([a-z0-9_]+)`\s*\(.*?\)\s*ENGINE=[^;]*;/is', $content, $rows, PREG_SET_ORDER)) {
|
|
foreach ($rows as $row) {
|
|
self::$ddlMap[$row[1]] = $row[0];
|
|
}
|
|
}
|
|
return self::$ddlMap;
|
|
}
|
|
}
|