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:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

+26 -5
View File
@@ -16,6 +16,11 @@ YwxApp 的数据库结构自愈引擎收敛在 `ywxapp/model/BaseModel.php` 的
所有方法内部都包了 `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。
## 二、模型壳约定
每个需要自愈的模型都自带一个静态方法:
@@ -38,6 +43,21 @@ public static function ensureSchema(): void
**幂等性**:所有自愈都先探测(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)
自愈**不会定时 / 不会自动**,全部是「业务请求时按需触发」。
@@ -56,11 +76,12 @@ public static function ensureSchema(): void
| `ywxapp/model/Notice.php` | `getActiveTopNotices()` / `getList()` | 前台公告条渲染、公告列表页 |
| `ywxapp/model/Ad.php` | `getByPosition()` / `getSlots()` | 广告位渲染 |
### 3. 中间件 / 蜘蛛统计
### 3. 中间件
| 入口 | 调用 | 触发场景 |
| --- | --- | --- |
| `SpiderStat` 中间件 | `ensureTables()` | 每次请求(蜘蛛日志 / 统计表) |
| `ywxapp/middleware/SpiderStat`(全局) | `ensureTables()` | 每次请求(蜘蛛日志 / 统计表) |
| `app/appmall/middleware/AppmallSchemaMiddleware`(应用级) | `AppmallSchema::ensure()` | **任意** `/appmall/*` 请求(前台 / 后台 / 对外 API) |
### 4. 插件安装 / 升级
@@ -68,9 +89,9 @@ public static function ensureSchema(): void
| --- | --- | --- |
| blog | `BlogSchema::ensure*` | 插件业务方法 |
| forum | `ForumSchema::ensure*` | 插件业务方法 |
| appmall(中心站) | `AppmallSchema::ensure*` | 开发者中心业务方法 |
| appmall(中心站) | 应用中间件 `AppmallSchema::ensure()` + 各模型 `ensureSchema()`(`initialize()` 自动触发) | 任意 `/appmall/*` 请求,或直接使用任一 appmall 模型 |
| haonav / seo | `Addon.php::upgrade()` / `ensureTablesFromInstallSql()` | 插件升级 / 安装 |
| wxchat / mqttbroker / mqpay 等 | 各自 Schema 类 | 插件初始化 |
| wxchat / iot / mqpay 等 | 各自 Schema 类 | 插件初始化 |
## 四、线上老库迁移不在自愈范围
@@ -85,7 +106,7 @@ public static function ensureSchema(): void
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` / `appmall` 控制器。
- **修复**:所有模型壳 / 控制器一律改用 `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 用户授予建表 / 改表权限,自愈会自动补齐;