Files
YwxAppThink/docs/guide/model_selfheal.md
T
yangwx 18101cf11a 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(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

115 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模型自愈机制(Schema Self-Heal)
YwxApp 的数据库结构自愈引擎收敛在 `ywxapp/model/BaseModel.php` 的静态方法中。
它的定位是 **「缺表 / 缺列兜底」**,不是迁移工具——旧表改名、跨前缀重整等问题仍需手动执行迁移 SQL(见 `docs/migrations/`)。
## 一、自愈引擎能力(BaseModel 静态方法)
| 方法 | 作用 | 说明 |
| --- | --- | --- |
| `ensureTable($table, $sql)` | 表不存在则 CREATE | `$table` 为已带前缀的完整表名;内部先 `SHOW TABLES LIKE` 探测 |
| `ensureTableFromInstall($prefix, $table)` | 从 `public/install/install.sql` 正则提取建表 DDL 执行 | **install.sql 是唯一事实源**;`$table` 为不含前缀的表名 |
| `ensureColumn($table, $col, $def)` | 列不存在则 ALTER ADD | MySQL 不支持 `ADD COLUMN IF NOT EXISTS`,故先探测 |
| `ensureAutoIncrementPk($table, $col='id')` | 修复 id 非自增主键 | 老库 `id` 定义成 NOT NULL 但无 AUTO_INCREMENT 时,create() 会报 1364 |
| `tableExists($table)` | 探测表是否存在 | `SHOW TABLES LIKE`,异常时返回 false |
| `currentPrefix()` | 取运行时表前缀 | 优先 `config/database.php`,回退 `Db::getConfig('prefix')` |
所有方法内部都包了 `try/catch`:自愈失败(如数据库账号无 `CREATE/ALTER` 权限、`NO_BACKSLASH_ESCAPES` 导致 SQL 出错)会**被静默吞掉并记录日志**,不会阻断业务——但意味着「自愈没起作用」时不会立刻报错,需要查日志才能发现。
> **模块自带 SQL 的情况**:`ensureTableFromInstall()` 只认核心的 `public/install/install.sql`。
> 中心站专用模块 `appmall` 有自己的一份事实源 `app/appmall/install.sql`,由
> `app/appmall/library/AppmallSchema` 用同样的「解析 SQL → `ensureTable()`」方式执行,
> 模块内不再内联重复 DDL。
## 二、模型壳约定
每个需要自愈的模型都自带一个静态方法:
```php
public static function ensureSchema(): void
{
try {
$prefix = \think\facade\Db::getConfig('prefix') ?: '';
$table = $prefix . 'notice';
// 缺表建表(优先 ensureTableFromInstall,或从 install.sql 同款 DDL 兜底)
self::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$prefix}notice` (...)");
// 老库扩展列兜底(幂等)
self::ensureColumn($table, 'type', "tinyint(1) NOT NULL DEFAULT 1");
} catch (\Throwable $e) {
// 忽略自愈失败,由业务暴露
}
}
```
**幂等性**:所有自愈都先探测(SHOW TABLES / SHOW COLUMNS)再操作,已存在则跳过,重复调用安全。
**自动触发(appmall 采用)**:`app/appmall/model/` 下的 **13 个模型与本模块 13 张表一一对应**
(清单登记在 `AppmallSchema::MODELS`,加表必查),每个都在 `initialize()` 里调用
`self::ensureSchema()` —— 模型一被使用(含 `Model::where()` 这类静态查询)就先兜底建表 + 补老库扩展列,
业务入口无需记得手动调用,不会再撞 1146(缺表)/ 1054(缺列)。
`AppmallSchema::ensureTable()` 内部按「表」和「表+列」做了进程内 memo,每请求每表最多一次 `SHOW TABLES`,
开销可忽略。(核心库同样有先例:`MemberProp::initialize()` / `CommonCard::initialize()`。)
**应用级中间件兜底(appmall 采用)**:`app/appmall/middleware.php` 注册
`AppmallSchemaMiddleware`,任何进入 `/appmall/*` 的请求(前台页 / 后台页 / 对外 API / 未注册路由)
在业务之前先 `AppmallSchema::ensure()` 一次(见第三节第 3 小节)。
> 为什么要「模型 + 中间件」双层:模型兜底保证**直接使用模型**的代码路径不缺表;
> 中间件兜底保证**任何入口**(含控制器里大量 `Db::name('appmall_xxx')` 直查、
> 以及将来新增的页面 / 接口)不会因为某处忘了调用而缺表。两者都幂等,重复执行无副作用。
## 三、触发时机(谁在什么时候调用 ensureSchema)
自愈**不会定时 / 不会自动**,全部是「业务请求时按需触发」。
### 1. 核心库(每次相关请求都跑一次,开销极小)
| 入口 | 调用 | 触发场景 |
| --- | --- | --- |
| `ywxapp/library/AdminAuth.php` (`login`/`register`) | `BackendAdmin::ensureSchema()` | 后台登录 / 注册 |
| `ywxapp/controller/MemberBase.php` (`_initialize`) | `MemberUser::ensureSchema()` | 访问任意会员中心控制器 |
### 2. 业务方法内触发
| 模型 / 类 | 触发方法 | 触发场景 |
| --- | --- | --- |
| `ywxapp/model/Notice.php` | `getActiveTopNotices()` / `getList()` | 前台公告条渲染、公告列表页 |
| `ywxapp/model/Ad.php` | `getByPosition()` / `getSlots()` | 广告位渲染 |
### 3. 中间件
| 入口 | 调用 | 触发场景 |
| --- | --- | --- |
| `ywxapp/middleware/SpiderStat`(全局) | `ensureTables()` | 每次请求(蜘蛛日志 / 统计表) |
| `app/appmall/middleware/AppmallSchemaMiddleware`(应用级) | `AppmallSchema::ensure()` | **任意** `/appmall/*` 请求(前台 / 后台 / 对外 API) |
### 4. 插件安装 / 升级
| 插件 | 触发入口 | 触发场景 |
| --- | --- | --- |
| blog | `BlogSchema::ensure*` | 插件业务方法 |
| forum | `ForumSchema::ensure*` | 插件业务方法 |
| appmall(中心站) | 应用中间件 `AppmallSchema::ensure()` + 各模型 `ensureSchema()`(`initialize()` 自动触发) | 任意 `/appmall/*` 请求,或直接使用任一 appmall 模型 |
| haonav / seo | `Addon.php::upgrade()` / `ensureTablesFromInstallSql()` | 插件升级 / 安装 |
| wxchat / iot / mqpay 等 | 各自 Schema 类 | 插件初始化 |
## 四、线上老库迁移不在自愈范围
自愈只负责「缺表 / 缺列兜底」,**不会做 RENAME、不会跨前缀重整**。旧库改名必须手动执行迁移 SQL:
- `docs/migrations/migrate_admin_user_to_backend_member_wxapp.sql` — admin/user → backend/member
- `docs/migrations/rename_table_prefix.sql` — 批量改前缀
- `docs/migrations/fix_missing_wxapp_notice.sql` — 自愈因权限被吞时手动补表
## 五、排错:遇到 1146 Table doesn't exist
1. 确认表是否在 `install.sql` 中有建表语句(事实源)。
2. 若是核心 / 插件表,确认对应 `ensureSchema()` 是否被调用(见第三节入口)。
3. ⚠️ **高频根因:运行时没取到表前缀**。`ensureSchema()` 若用 `Db::getConfig('prefix')` 取前缀,在部分线上环境(多应用路由 / CLI / Db 配置未就绪)会返回空字符串,导致自愈建出**无前缀的表**(如 `notice`),而模型 `name` 拼前缀后查的是 `wxapp_notice` → 1146。
- **修复**:所有模型壳 / 控制器一律改用 `BaseModel::currentPrefix()` 取前缀(优先 `config/database.php`,回退 `Db::getConfig`),并已统一修复 `Notice` / `Ad`;`appmall` 的控制器 / 服务原先各自内联 DDL 且用 `Db::getConfig('prefix')`,现已全部收敛到 `AppmallSchema`(事实源 `app/appmall/install.sql`,统一走 `currentPrefix()`),并由应用中间件统一触发。
- 自查:`grep "Db::getConfig('prefix')"` 应只剩 0 处(全部换成 `currentPrefix`)。
4. 若前缀正确、`ensureSchema()` 已调用仍 1146 → **多半是线上 DB 账号无 CREATE/ALTER 权限**,自愈被静默吞掉。
- 根治:给 DB 用户授予建表 / 改表权限,自愈会自动补齐;
- 兜底:手动执行 `docs/migrations/` 下对应修复 SQL(如 `fix_missing_wxapp_notice.sql`)。
5. 若是改名类(admin_* / user_*)→ 必须手动跑迁移 SQL,自愈无法处理。