fix: 修复首页空白/失效外链/云函数越权,补齐缺失页面与种子数据

## 阻断性缺陷
- list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在,
  而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。
  按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。
- parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。
- 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record
  schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。

## 越权与数据一致性
- uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、
  改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。
- comments.likeComment/relikeComment:直接采信客户端传入的 user_id,
  可冒名点赞刷计数。改为以令牌为准,并纳入事务。
- comments.updateComment:对数组取 .author_id,权限判断恒失败;
  字段名 updateTime 与 ip_location 类型与 schema 不符。
- comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减;
  且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。
- cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效,
  可无限重复刷计数;补唯一索引并事务化。
- cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。
- cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。
- article_info:get() 使用未定义变量必崩;读接口全部无鉴权。
- user-info:公开资料接口可查任意用户 last_login_ip。

## 资源与数据
- 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403),
  改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。
- 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记,
  同时补上点赞、收藏、投票、浏览日志的唯一索引。

## 功能
- 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。
  原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。
- 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。

## 工具
- tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查
- tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译)
- tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过"
- tools/gen-*.py:模板与图标资源生成脚本
This commit is contained in:
2026-09-11 17:48:27 +08:00
parent 50f8a89d51
commit 4f5893f87a
62 changed files with 5565 additions and 1104 deletions
@@ -0,0 +1,236 @@
{
"bsonType": "object",
"required": [
"user_id",
"title",
"content"
],
"permission": {
"read": true,
"create": "auth.uid != null",
"update": "'admin' in auth.role || doc.user_id == auth.uid",
"delete": "'admin' in auth.role || doc.user_id == auth.uid"
},
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "文章作者ID, 参考`uni-id-users` 表",
"foreignKey": "uni-id-users._id",
"defaultValue": {
"$env": "uid"
}
},
"edit_type": {
"bsonType": "string",
"description": "编辑器类型(pc/mobile)"
},
"title_delta": {
"bsonType": "object",
"description": "标题html片段"
},
"p_type": {
"bsonType": "string",
"description": "兵种选择"
},
"temp_id": {
"bsonType": "string",
"description": "模板ID, 参考`cms-temp` 表",
"foreignKey": "cms-temp._id"
},
"music_url_id": {
"bsonType": "object",
"description": "音乐文件"
},
"category_id": {
"bsonType": "string",
"title": "分类",
"description": "分类 id,参考`uni-news-categories`表",
"foreignKey": "uni-cms-categories._id",
"enum": {
"collection": "uni-cms-categories",
"field": "name as text, _id as value"
}
},
"title": {
"bsonType": "string",
"title": "标题",
"description": "标题",
"label": "标题",
"trim": "both"
},
"title_html": {
"bsonType": "string",
"title": "标题html片段",
"description": "标题html片段",
"label": "标题html片段",
"trim": "both"
},
"cmsLst": {
"bsonType": "array",
"title": "文章内容列表"
},
"content": {
"bsonType": "object",
"title": "文章内容",
"description": "文章内容; 格式为Quill编辑器的Delta格式",
"label": "文章内容"
},
"excerpt": {
"bsonType": "string",
"title": "文章摘录",
"description": "文章摘录",
"label": "摘要",
"trim": "both"
},
"article_status": {
"bsonType": "int",
"title": "文章状态",
"description": "文章状态:0 草稿箱 1 已发布",
"defaultValue": 0,
"enum": [
{
"value": 0,
"text": "草稿箱"
},
{
"value": 1,
"text": "已发布"
}
]
},
"reply_count": {
"bsonType": "int",
"title": "回复数量",
"description": "回复数量",
"defaultValue": 0
},
"like_count": {
"bsonType": "int",
"title": "点赞数",
"description": "喜欢数、点赞数",
"defaultValue": 0,
"permission": {
"write": false
}
},
"collect_count": {
"bsonType": "int",
"title": "收藏数量",
"description": "收藏数量",
"defaultValue": 0,
"permission": {
"write": false
}
},
"gift_count": {
"bsonType": "int",
"title": "送礼数量",
"description": "送礼数量",
"defaultValue": 0,
"permission": {
"write": false
}
},
"view_count": {
"bsonType": "int",
"title": "阅读数量",
"description": "阅读数量",
"defaultValue": 0,
"permission": {
"write": false
}
},
"is_sticky": {
"bsonType": "bool",
"title": "是否置顶",
"description": "是否置顶",
"permission": {
"write": false
}
},
"is_essence": {
"bsonType": "bool",
"title": "阅读加精",
"description": "阅读加精",
"permission": {
"write": false
}
},
"comment_status": {
"bsonType": "int",
"title": "开放评论",
"description": "评论状态:0 关闭 1 开放",
"enum": [
{
"value": 0,
"text": "关闭"
},
{
"value": 1,
"text": "开放"
}
]
},
"comment_count": {
"bsonType": "int",
"description": "评论数量",
"permission": {
"write": false
}
},
"last_comment_user_id": {
"bsonType": "string",
"description": "最后回复用户 id,参考`uni-id-users` 表",
"foreignKey": "uni-id-users._id"
},
"thumbnail": {
"bsonType": "array",
"title": "封面大图",
"description": "缩略图地址",
"label": "封面大图",
"defaultValue": []
},
"publish_date": {
"bsonType": "timestamp",
"title": "发表时间",
"description": "发表时间",
"defaultValue": {
"$env": "now"
}
},
"publish_ip": {
"bsonType": "string",
"title": "发布文章时IP地址",
"description": "发表时 IP 地址",
"forceDefaultValue": {
"$env": "clientIP"
}
},
"last_modify_date": {
"bsonType": "timestamp",
"title": "最后修改时间",
"description": "最后修改时间",
"defaultValue": {
"$env": "now"
}
},
"last_modify_ip": {
"bsonType": "string",
"description": "最后修改时 IP 地址",
"forceDefaultValue": {
"$env": "clientIP"
}
},
"preview_secret": {
"bsonType": "string",
"description": "文章预览密钥"
},
"preview_expired": {
"bsonType": "timestamp",
"description": "文章预览过期时间"
}
}
}