Files
t-admin/reports/next-batch-plan.md
T

577 lines
20 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.
# AGI 接力规划 · 批次 B1–B6
本文件是给执行模型的施工单。每一批都标注了**精确文件、改动点、验收命令**。
上一轮(B0)已完成 38 个文件的修复 + 4 个门禁脚本,全绿。
当前基线(执行前请先复现):
```bash
npm test
# {"errors":0,"warnings":0,"status":"passed"}
# {"schemaCount":59,"collectionCount":27,"missingSchemaCount":3,"fieldDriftCount":73}
# {"source":1,"mapped":1,"conflicts":0}
# {"status":"passed","checks":7}
```
---
## B1 — 让门禁可信(P0,必须最先做)
**为什么最先做**:现在 73 条 fieldDrift 里混着大量误报,执行模型照着修会改错文件。门禁不可信,后面全是盲改。
### B1-1 `scripts/data-contract-report.js` 修正字段归属
现状 `collectFields` 只认 `collectionList: "x"` 一种写法,且用 `/field\s*=/` 匹配。
真实例子(已核实):
```text
pages/uni-stat/pay-order/ranking/ranking.vue:35 group-field="sum(total_fee) as total_fee,..."
```
`group-field=` 里含 `field=`,被正则抓成「字段」,于是产出 `uni-pay-orders.sum(total_fee)` 这类假漂移。
改动点:
1. 正则改为 `/[\s"]field\s*=\s*["']/g`,用前置边界排除 `group-field` / `groupField`。
2. 跳过聚合表达式:字段名含 `(` 或 `)` 的直接丢弃。
3. 集合归属按优先级取:`collectionList: "x"` → `collection: "x"` → `collectionList: [db.collection('x')...]`。
4. 同一页面若出现多个 collection(如 `uni-data-select` 的辅助集合),只把**主 `unicloud-db` 绑定的集合**用于 field 归属;辅助集合只登记不比对字段。
### B1-2 `scripts/quality-check.js` 同步修正
`checkCollectionContracts` 用了同类正则,会把辅助集合也算进去。同步 B1-1 的集合归属规则,避免产出噪音 warning。
### B1-3 区分「真漂移」与「schema 缺字段」
对每条漂移,输出三态而不是一刀切:
```text
drift-kind: missing-in-schema | page-field-typo | aggregate-expression
```
聚合表达式(B1-1 已过滤)不应再出现在报告。
**验收**
```bash
npm run check:contracts
# 期望 fieldDriftCount 从 73 显著下降;uni-pay-orders.sum(*) 全部消失
rg 'sum\(' reports/data-contract-report.md # 期望无输出
```
---
## B2 — 会崩的真实缺陷(P1)
以下 3 条是**已确认的空值崩溃点**,与本轮报告真伪无关。
### B2-1 `pages/system/user/list.vue:251`
```js
const roleArr = item.role.map(item => item.role_name)
```
`item.role` 在联表未命中、或用户无角色时为 `undefined` → `TypeError`。
改法(沿用本轮 `tags` 的写法):
```js
const roleArr = Array.isArray(item.role) ? item.role.map(r => r && r.role_name).filter(Boolean) : []
```
同时 `item.tags` 映射后要保证是数组,避免模板 `v-for` 报错。
### B2-2 `pages/system/role/list.vue:131`
```js
item.permission = item.permission.map(pItem => pItem.permission_name).join('、')
```
`permission` 为空数组或缺失即崩。改为:
```js
const list = Array.isArray(item.permission) ? item.permission : []
item.permission = list.map(p => p && p.permission_name).filter(Boolean).join('、')
```
### B2-3 `pages/system/user/list.vue:283` 生产日志
```js
console.log(JSON.stringify(db.command.or(...)))
```
删除;若需排障,包进 `process.env.NODE_ENV !== 'production'`。
**另外两处调试日志**(同类,一并清理):
- `pages/uni-stat/device/activity/activity.vue:210` `console.log('query: ', query)`
- `pages/uni-stat/device/trend/trend.vue:328` 同上
- `pages/uni-stat/pay-order/ranking/ranking.vue:188` `console.log('where: ', where)`
**验收**
```bash
rg -n 'console\.log' pages --glob '*.vue' | grep -v '//' | grep -v NODE_ENV
# 期望只剩 cms-temp/add.vue 的上传日志(B4-2 处理)
```
---
## B3 — 统计页错误态统一(P1,工作量最大)
### B3-1 仍在吞错的页面(已核实 `console.error` 计数)
```text
pages/uni-stat/page-ent/page-ent.vue
pages/uni-stat/page-res/page-res.vue
pages/uni-stat/page-content/page-content.vue
pages/uni-stat/event/event.vue
pages/uni-stat/channel/channel.vue
pages/uni-stat/scene/scene.vue
```
### B3-2 统一契约
每个查询区块必须落到四态,禁止 `console.error` 后什么都不做:
```js
// data
loading: false,
errorMessage: '',
// 查询前
this.loading = true
this.errorMessage = ''
// catch
this.errorMessage = err && (err.message || err.errMsg) || '数据加载失败'
this.tableData = [] // 必须清空,否则旧数据显示成新结果
// finally
this.loading = false
```
模板侧把 `errorMessage` 喂给表格与图表组件(`uni-stat/device/overview` 已有现成写法,照抄):
```vue
<uni-table :loading="loading" :emptyText="errorMessage || '暂无数据'">
```
### B3-3 复用入口
不要在 6 个页面各写一份。先在 `js_sdk/uni-stat/util.js` 增加:
```js
export function createStatQueryState(extra = {}) {
return {
loading: false,
errorMessage: '',
...extra
}
}
```
页面 `data()` 展开即可。若执行模型判断收益不足,可退化为「6 页各自统一命名」,但**命名必须一致**,否则后续无法批量替换。
**验收**
```bash
rg -c 'errorMessage' pages/uni-stat/**/*.vue | wc -l # 覆盖页面数应增加
rg -n "console\.error" pages/uni-stat --glob '*.vue' # 期望只剩兜底日志,且都伴随 errorMessage 赋值
```
---
## B4 — 搜索与表单可用性(P1/P2)
### B4-1 搜索框存在但搜不到东西
以下页面渲染了搜索输入,但 `dbSearchFields = []`,点搜索只是刷新列表:
```text
pages/system/app/list.vue → ['appid', 'name', 'description']
pages/uni-id-log/list.vue → ['username', 'device_uuid', 'ip', 'appid']
pages/comment/list.vue → ['content']
pages/comment_like/list.vue → ['comment_id', 'user_id']
pages/comment_operation_logs/list.vue → ['content', 'user_id']
pages/opendb-search-hot/list.vue → ['content']
pages/opendb-search-log/list.vue → ['content', 'device_id', 'user_id']
```
若某页判定不该支持全文搜索(例如纯日志页),则**移除搜索框**,不要留假入口。
### B4-2 `pages/cms-temp/add.vue` 图片上传链路
现状 8 处 `console.log`,且 `updateImage` 用 `success` 回调 + 未捕获异常。
`edit.vue` 已在 B0 改成 `try/catch + await` 写法,**照 `edit.vue` 的写法重写 `add.vue` 的 `updateImage`**,并删除全部调试日志。
### B4-3 `topWindow.vue` 死代码
`windows/topWindow.vue:146-151` 的 `showPasswordPopup()` 调用了不存在的 `this.$refs.passwordPopup`。
模板中只有 `errorLogsPopup`。删除该死方法(修改密码入口已由 `changePassword()` 承担)。
### B4-4 `leftWindow.vue` 路由行为
`windows/leftWindow.vue:71-97`:
- 内链用 `uni.redirectTo` 破坏返回栈 → 改 `uni.navigateTo`
- H5 外链 `window.open(url)` 缺 `noopener` → `window.open(url, '_blank', 'noopener,noreferrer')`
- 跳转失败提示补充菜单名与目标 URL,便于定位配置错误
### B4-5 列表 key
以下页面仍在用数组下标作 key,筛选/排序后会串行状态:
```text
pages/system/role/list.vue:40
pages/system/permission/list.vue:45
pages/system/user/list.vue:52
pages/system/tag/list.vue:37
pages/opendb-search-hot/list.vue:29
pages/opendb-search-log/list.vue:30
pages/comment*/list.vue
pages/uni-id-log/list.vue:37
pages/uni-stat/pay-order/ranking/ranking.vue:45
```
统一改 `:key="item._id"`(无 `_id` 的聚合结果用 `item.user_id` 等业务主键)。
**验收**
```bash
npm run check:vue3 # 必须保持 passed
rg -n ':key="index"' pages --glob '*.vue' # 期望大幅减少
```
---
## B5 — 数据与 schema 合同落地(P2)
### B5-1 真实漂移(非误报,已核实)
```text
pages/uni-stat/error/app/app.vue:47 collection: uni-stat-app-crash-logs
```
页面 field 中的以下字段在 schema 里需逐个核对:
```text
_id, name, uni_platform, create_date, count, app_launch_count, start_time, day_count
```
处理原则:**schema 是合同,页面是消费方**。若字段确实由统计跑批写入但 schema 漏声明 → 补 schema;若页面写了不存在的字段 → 删页面字段。
```text
pages/uni-stat/page-rule/page-rule.vue:17 collection: uni-stat-pages
field="title,path,page_rules,appid"
```
同样逐个核对 `uni-stat-pages.schema.json`。
### B5-2 缺失索引的集合
有 schema 但无 `*.index.json`(列表来自实测):
```text
opendb-search-hot ← 搜索热词高频查询
opendb-search-log ← 日志量大
opendb-sms-log ← 短信日志,按 task_id/status 查
opendb-sms-task
cms-temp ← 文章外键关联
opendb-news-articles / opendb-news-categories
read-news-log
uni-id-scores
uni-stat-run-errors
opendb-tempdata
```
优先补前 5 个。索引字段依据代码实际查询条件推导,不要凭空造:
```text
opendb-search-log → content? 否;建 create_date desc、device_id、user_id
opendb-sms-log → task_id、status、mobile
opendb-sms-task → create_date desc
cms-temp → 无强需求,可延后
```
### B5-3 `comment*` 三集合决策(**需要人工确认,禁止自动执行**)
`comment` / `comment_like` / `comment_operation_logs` 在仓库内**没有任何 schema**,
但页面完整实现了增删改查。二选一:
| 方案 | 动作 | 风险 |
|---|---|---|
| A. 新建 schema | 按页面字段补 3 份 schema + 权限 + 索引 | 若云端实际用的是 `opendb-news-comments`,会造成双写分裂 |
| B. 改用官方表 | 页面 field 映射到 `opendb-news-comments` 等 | 字段语义差异大,`content`↔`comment_content`、`create_time`↔`comment_date`,改完需重建数据 |
**执行模型不得自行选择**。先在 `reports/` 输出决策报告,列出两方案的字段映射表,等确认。
### B5-4 `cms-temp` 权限种子缺口
schema 已引用:
```text
CREATE_CMS_TEMP / UPDATE_CMS_TEMP / DELETE_CMS_TEMP
```
但 `uniCloud-alipay/database/uni-id-permissions.init_data.json` 是空数组 `[]`,
即非 admin 角色**没有任何途径获得这些权限**。
处理:
- 若只允许超管维护模板 → 把 schema 简化为 `'admin' in auth.role`,去掉悬空权限点;
- 若要支持授权运营 → 在 `init_data.json` 补 3 条权限记录。
当前 schema 用 `'admin' in auth.role || 'CREATE_CMS_TEMP' in auth.permission`,
admin 可用、权限点悬空但无害。**建议明确化**,避免后续误判为已支持细粒度授权。
### B5-5 `opendb-sms-log` 部署影响
本轮已修 `ccreate_date` → `create_date`、`mobile` int → string、`task_id` 外键表名。
这些是**破坏性 schema 变更**:线上若已有 `ccreate_date` 数据,改名后旧数据不可见。
补齐迁移脚本或在 changelog 明确标注「需人工核对线上数据后再上传 schema」。
---
## B6 — 仓库与依赖卫生(P3)
### B6-1 云函数版本元数据
7 个云函数 `package.json` 仍是 `2.4.25`,根项目已 `2.5.1`:
```text
uni-upgrade-center / ext-storage-co / uni-stat-receiver / uni-sms-co
uni-analyse-searchhot / uni-portal / uni-stat-cron
```
先确认这是官方插件兼容元数据还是漏同步。若是漏同步 → 统一 2.5.1;若语义是「基于哪个版本的插件」→ **不要动**,只在 README 说明。
### B6-2 `unpackage/` 被纳入版本控制
约 1367 个已跟踪文件位于 `unpackage/`,属 HBuilderX 生成物。
```bash
git rm -r --cached unpackage
printf 'unpackage/\nreports/\n.zcode/\n' >> .gitignore
```
> 注意:`reports/` 若希望留痕(便于对比各轮门禁结果),可只忽略 `reports/*.json`,保留 `.md`。
### B6-3 `App.vue` 扩展存储占位域名
`App.vue:66` `domain: "cdn.example.com"` 未替换时开启扩展存储会上传失败。
至少加一行注释指向配置项,或在 `admin.config.js` 集中管理。
---
## 执行顺序与并行建议
```text
B1 ──► B2 ──► B3 ──► B4 ──► B5 ──► B6
门禁 崩溃点 错误态 可用性 数据合同 仓库卫生
```
- **B1 必须单线程先做**,它是其他批次的可信度前提。
- B2 / B4 可并行(不同文件)。
- B3 涉及 6+ 页面,建议一个执行模型专做,避免风格分裂。
- B5-3 遇阻立即停下出报告,不要自行决策。
## 每批完成后的强制动作
```bash
npm test # 四项全绿才允许进入下一批
git diff --check # 无空白错误
```
并在 `changelog.md` 追加对应条目。
## 交付物清单
| 批次 | 主要文件 |
|---|---|
| B1 | `scripts/data-contract-report.js`、`scripts/quality-check.js` |
| B2 | `pages/system/user/list.vue`、`pages/system/role/list.vue`、4 处日志 |
| B3 | `js_sdk/uni-stat/util.js` + 6–12 个 `pages/uni-stat/**` |
| B4 | `pages/cms-temp/add.vue`、`windows/topWindow.vue`、`windows/leftWindow.vue`、7 个 list 页 |
| B5 | `uniCloud-alipay/database/*.index.json`、`*.schema.json`、`reports/*decision*.md` |
| B6 | `.gitignore`、7 个云函数 `package.json`(待确认)、`App.vue` |
---
# 补遗 · B7(二轮勘察新增,优先级高于 B3)
以下 6 条是二轮实测确认的缺陷,**B7-1 与 B7-4 应插到 B2 之前或并列 B2**。
## B7-1 菜单环导致浏览器假死(P0,已复现)
**已用 Node 复现 `INFINITE LOOP`。**
成因链完整,三个环节都缺防护:
1. `components/uni-data-menu/util.js` 的 `getParentIds`:
```js
while (currentItem && currentItem.parent_id) {
parentArr.push(currentItem.parent_id)
currentItem = menuList.find(item => item.menu_id === currentItem.parent_id)
}
```
无 `visited` 集合。数据一旦成环(A→B、B→A),`find` 永远命中,死循环。
2. 该函数被 `buildMenus` 的 `sort` 比较器调用 → **排序期间**触发,页面直接卡死,连报错都没有。
3. `pages/system/menu/add.vue` / `edit.vue` 的 `parent_id` 是**自由文本输入**:
```text
js_sdk/validator/opendb-admin-menus.js → parent_id 只校验 format: string
opendb-admin-menus.schema.json → parentKey 只是表单渲染提示,不是 DB 约束
```
即:在菜单管理里把某菜单的父级填成它自己,保存,刷新左侧菜单 → 假死。全程无需任何特殊权限。
**改动点**
```text
components/uni-data-menu/util.js
· getParentIds 加 visited Set,命中已访问节点立即 return []
· buildMenu 递归加 visited 参数,同一 menu_id 不重复下钻
· buildMenus 的 sort 比较器不要每次重算 getParentIds —— 预先算一次存 Map,
否则 O(n² log n)(配合 B7-2 的 pageSize=20000 是灾难)
pages/system/menu/add.vue + edit.vue
· 保存前校验 parent_id !== menu_id
· 保存前用已加载的 menus 走一遍祖先链,判定是否会成环,成环则阻止提交并提示
pages/system/menu/list.vue
· getParents / getChildren 两个递归同样加 visited
```
**验收**:构造 A→B、B→A 的两条数据,菜单页必须能正常渲染并提示数据异常,不得卡死。
## B7-2 `menu/list.vue` 性能与一致性(P1)
```text
:126 const pageSize = 20000
:211 getParents 递归无环保护
:221 getChildren 递归无环保护
:277 enableChange 乐观更新:
item.enable = !item.enable
db.collection(...).update({enable}) ← 无 await、无 catch、无回滚
```
`enableChange` 失败时 UI 已切换,形成假成功;刷新后又变回去,用户会反复点。
**改动**:先 `await` 成功再改 UI,或失败回滚 + toast。递归函数加 visited。
## B7-3 发布页注入面(P1)
`uni-portal/createPublishHtml/index.js:8` 显式关闭转义:
```js
TE.defaults.escape = false
```
模板中以下插值**未经转义直接进输出**:
| 模板位置 | 上下文 | 风险 |
|---|---|---|
| `{@h5.url@}` ×2 | `href="{@h5.url@}"` | 未做协议白名单,`javascript:` 可注入 |
| `{@name@}` | `<title>` / `<h2>` | HTML 注入 |
| `{@introduction@}` | `<p>` | HTML 注入 |
| `{@description@}` | `<pre>` | HTML 注入 |
| `{@android_url@}` `{@ios_url@}` | `href` | 同 h5.url |
| `{@icon_url@}` `{@$data[$value].qrcode_url@}` | `src` | 属性逃逸 |
数据源是应用管理表单(`pages/system/app/add.vue` 的 `name` / `introduction` / `description` / `h5.url`),**由后台账号填写**。所以这是**管理员权限后的持久化注入**,不是匿名入口——但多管理员/低权运营场景下依然是真实攻击面,且发布页是对外公开的。
**改动**:在 `createPublishHtml` 渲染前对字段做上下文相关处理:
```text
文本节点(title/h2/p/pre) → HTML 实体转义 & < > " '
属性值(href/src) → 属性转义 + URL 协议白名单(仅 http/https/cloud)
JS 字符串(若有 window.$app)→ JSON.stringify
```
不要全局打开 `TE.defaults.escape = true`(模板本身含合法 HTML),要按字段处理。
## B7-4 `batch-sms` watcher 崩溃 + 变量语义(P1)
`components/batch-sms/batch-sms.vue:177-190`:
```js
const template = this.smsTemplate.find(t => t.value === smsDataModel.templateId)
let content = smsDataModel.templateData.reduce((res, param) => {
const reg = new RegExp(`\\$\\{${param.field}\\}`)
return res.replace(reg, ($1) => param.value || $1) // ← 同 B0 已修的后端问题
}, template.content) // ← template 可能 undefined
```
两处缺陷:
1. `template` 未命中 → `template.content` 直接 `TypeError`。模板被删/切库/加载失败即触发。
2. `param.value || $1` 把 `0`、`false`、空字符串当作缺失 —— B0 已在 `build-template-data.js` 修了同类问题,**前端这份漏了**,两端语义必须一致。
**改动**:`template` 判空提前 return;`param.value === undefined || param.value === null ? $1 : String(param.value)`。
## B7-5 死文件 `pay-order/components/test.vue`(P1)
已实测**无任何引用**(`rg` 全仓零命中)。内容:
```js
getChartData(query) { } // 空方法
watch: { query: { handler() { this.options.pageCurrent = 1 } } } // options 未定义 → 一旦挂载必崩
```
`data()` 里也没有 `options`。
**改动**:直接删除文件(无引用,零风险)。若判定要保留,必须补 `options` 并实现 `getChartData`。
## B7-6 `js.vue` sourceMap 入口判定(P2)
```text
:312 sourceMapEnabled() { return true } ← method,非 computed
:40 v-if="sourceMapEnabled" ← 模板取到的是函数对象,恒 truthy
:66 v-if="sourceMapEnabled"
:81 v-if="sourceMapEnabled"
:395 getAllData(query) { if (query.indexOf("appid") === -1) ... }
```
两个问题:
1. `sourceMapEnabled` 是 method 却被当 computed 用,恒真。原意(注释掉的 `return !!this.uniStat.uploadSourceMapCloudSpaceId`)应是**按配置决定**。结果是所有用户都看到上传入口,点了才失败。
2. `query.indexOf("appid")` 是**字符串包含判断**,不是字段存在判断。`query` 是拼接串,若某字段值里含 `appid` 子串会误通过;`appid` 存在但为空串也可能漏判。
**改动**:`sourceMapEnabled` 改为 computed,同时判断配置 + `$hasPermission`;`getAllData` 改为 `if (!this.query.appid) return`(其他统计页已是此写法)。
---
## 已核对:不是问题(执行模型勿动,避免白干)
| 项 | 实测结论 |
|---|---|
| i18n 缺失 | **无缺失**。`i18n/{en,zh-Hans,zh-Hant}.json` 共 47 key,三语齐全,缺翻译 0 条 |
| `pages/demo/icons/icons.vue` | **不可删**。`pages/system/menu/edit.vue:53` 依赖它做图标选择器 |
| `pages/demo/table/table` | 在 `pages.json` 注册,静态菜单注释掉了;保留即可 |
| `uni-stat-app-crash-logs` 大量字段漂移 | 部分为 B1 误报;真实部分见 B5-1,需逐字段核对 schema |
| `unpackage/` 体积 | 确为生成物,见 B6-2 |
## 更新后的执行顺序
```text
B1 门禁可信
├─► B7-1 菜单环假死 ← P0,插到最前
├─► B2 空值崩溃
└─► B7-4 batch-sms 崩溃
└─► B7-3 发布页转义
└─► B7-2 菜单性能
└─► B3 统计错误态
└─► B4 可用性
└─► B5 数据合同
└─► B6/B7-5/B7-6 卫生
```
B1 仍必须最先行(否则 B5 全是盲改);B7-1 是唯一会让**用户浏览器卡死**的缺陷,建议紧接着做。