feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(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(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,154 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user