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

20 KiB
Raw Blame History

AGI 接力规划 · 批次 B1–B6

本文件是给执行模型的施工单。每一批都标注了精确文件、改动点、验收命令。 上一轮(B0)已完成 38 个文件的修复 + 4 个门禁脚本,全绿。

当前基线(执行前请先复现):

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*=/ 匹配。

真实例子(已核实):

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 缺字段」

对每条漂移,输出三态而不是一刀切:

drift-kind: missing-in-schema | page-field-typo | aggregate-expression

聚合表达式(B1-1 已过滤)不应再出现在报告。

验收

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

const roleArr = item.role.map(item => item.role_name)

item.role 在联表未命中、或用户无角色时为 undefined → TypeError。

改法(沿用本轮 tags 的写法):

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

item.permission = item.permission.map(pItem => pItem.permission_name).join('、')

permission 为空数组或缺失即崩。改为:

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 生产日志

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)

验收

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 计数)

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 后什么都不做:

// 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 已有现成写法,照抄):

<uni-table :loading="loading" :emptyText="errorMessage || '暂无数据'">

B3-3 复用入口

不要在 6 个页面各写一份。先在 js_sdk/uni-stat/util.js 增加:

export function createStatQueryState(extra = {}) {
  return {
    loading: false,
    errorMessage: '',
    ...extra
  }
}

页面 data() 展开即可。若执行模型判断收益不足,可退化为「6 页各自统一命名」,但命名必须一致,否则后续无法批量替换。

验收

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 = [],点搜索只是刷新列表:

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,筛选/排序后会串行状态:

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 等业务主键)。

验收

npm run check:vue3          # 必须保持 passed
rg -n ':key="index"' pages --glob '*.vue'   # 期望大幅减少

B5 — 数据与 schema 合同落地(P2)

B5-1 真实漂移(非误报,已核实)

pages/uni-stat/error/app/app.vue:47        collection: uni-stat-app-crash-logs

页面 field 中的以下字段在 schema 里需逐个核对:

_id, name, uni_platform, create_date, count, app_launch_count, start_time, day_count

处理原则:schema 是合同,页面是消费方。若字段确实由统计跑批写入但 schema 漏声明 → 补 schema;若页面写了不存在的字段 → 删页面字段。

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(列表来自实测):

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 个。索引字段依据代码实际查询条件推导,不要凭空造:

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 已引用:

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:

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 生成物。

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 集中管理。


执行顺序与并行建议

B1  ──► B2 ──► B3 ──► B4 ──► B5 ──► B6
门禁   崩溃点  错误态  可用性  数据合同  仓库卫生
  • B1 必须单线程先做,它是其他批次的可信度前提。
  • B2 / B4 可并行(不同文件)。
  • B3 涉及 6+ 页面,建议一个执行模型专做,避免风格分裂。
  • B5-3 遇阻立即停下出报告,不要自行决策。

每批完成后的强制动作

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:
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 永远命中,死循环。

  1. 该函数被 buildMenus 的 sort 比较器调用 → 排序期间触发,页面直接卡死,连报错都没有。

  2. pages/system/menu/add.vue / edit.vue 的 parent_id 是自由文本输入:

js_sdk/validator/opendb-admin-menus.js  → parent_id 只校验 format: string
opendb-admin-menus.schema.json          → parentKey 只是表单渲染提示,不是 DB 约束

即:在菜单管理里把某菜单的父级填成它自己,保存,刷新左侧菜单 → 假死。全程无需任何特殊权限。

改动点

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)

: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 显式关闭转义:

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 渲染前对字段做上下文相关处理:

文本节点(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:

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 全仓零命中)。内容:

getChartData(query) { }        // 空方法
watch: { query: { handler() { this.options.pageCurrent = 1 } } }  // options 未定义 → 一旦挂载必崩

data() 里也没有 options。

改动:直接删除文件(无引用,零风险)。若判定要保留,必须补 options 并实现 getChartData。

B7-6 js.vue sourceMap 入口判定(P2)

: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

更新后的执行顺序

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 是唯一会让用户浏览器卡死的缺陷,建议紧接着做。