## 阻断性缺陷 - 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:模板与图标资源生成脚本
245 lines
5.7 KiB
JavaScript
245 lines
5.7 KiB
JavaScript
const db = uniCloud.database()
|
|
const collection = db.collection('uni-cms-articles')
|
|
const uniID = require('uni-id-common')
|
|
|
|
const isAdmin = (payload) => (payload.role || []).includes('admin')
|
|
|
|
module.exports = {
|
|
|
|
_before: function () { // 通用预处理器
|
|
this.uniID = uniID.createInstance({
|
|
context: this.getClientInfo()
|
|
})
|
|
},
|
|
|
|
/** 校验登录并返回 payload;未登录抛错 */
|
|
async _requireLogin() {
|
|
const payload = await this.uniID.checkToken(this.getUniIdToken())
|
|
if (payload.errCode) throw new Error('登录状态失效,请重新登录')
|
|
return payload
|
|
},
|
|
|
|
/** 校验当前用户是该文章作者(或管理员) */
|
|
async _requireOwner(id, payload) {
|
|
if (!id) throw new Error('缺少文章 id')
|
|
const res = await collection.doc(id).field({ user_id: true }).get()
|
|
const doc = res.data && res.data[0]
|
|
if (!doc) throw new Error('文章不存在')
|
|
if (doc.user_id !== payload.uid && !isAdmin(payload)) {
|
|
throw new Error('无权操作他人文章')
|
|
}
|
|
return doc
|
|
},
|
|
|
|
async del_cms_articles(query) {
|
|
try {
|
|
const payload = await this._requireLogin()
|
|
const { id } = query || {}
|
|
await this._requireOwner(id, payload)
|
|
await collection.doc(id).remove()
|
|
return { code: 200 }
|
|
} catch (e) {
|
|
return { code: 500, msg: e.message }
|
|
}
|
|
},
|
|
|
|
async get_cms_articles_for_userId(query) {
|
|
try {
|
|
// 只能查自己的文章列表;管理员可指定他人
|
|
const payload = await this._requireLogin()
|
|
const requested = query && query.user_id
|
|
if (requested && requested !== payload.uid && !isAdmin(payload)) {
|
|
return { code: 403, msg: '无权查看他人文章' }
|
|
}
|
|
const user_id = isAdmin(payload) && requested ? requested : payload.uid
|
|
const res = await collection.where({
|
|
user_id: user_id,
|
|
"edit_type": "mobile"
|
|
}).get()
|
|
return { code: 200, data: res.data }
|
|
} catch (e) {
|
|
console.error(e.message)
|
|
return { code: 500, msg: e.message }
|
|
}
|
|
},
|
|
|
|
async update_cms_articles(query) {
|
|
try {
|
|
const payload = await this._requireLogin()
|
|
const { id } = query || {}
|
|
await this._requireOwner(id, payload)
|
|
|
|
const {
|
|
title,
|
|
title_html,
|
|
title_delta,
|
|
thumbnail,
|
|
p_type,
|
|
category_id,
|
|
temp_id,
|
|
edit_type,
|
|
excerpt,
|
|
cmsLst,
|
|
article_status
|
|
} = query
|
|
|
|
const now = new Date()
|
|
const r = await collection.doc(id).update({
|
|
title,
|
|
// 作者不可被客户端改写,始终以数据库中的归属为准
|
|
title_html,
|
|
title_delta,
|
|
thumbnail,
|
|
p_type,
|
|
category_id,
|
|
temp_id,
|
|
edit_type,
|
|
excerpt,
|
|
cmsLst,
|
|
// 修改不应重置发布时间,只更新最后修改时间
|
|
last_modify_date: now,
|
|
last_modify_ip: this.getClientInfo().clientIP,
|
|
article_status
|
|
})
|
|
if (!r.updated) {
|
|
return { code: 500, msg: '更新失败' }
|
|
}
|
|
const res = await collection.doc(id).get()
|
|
return {
|
|
code: 200,
|
|
res: {
|
|
data: res.data[0],
|
|
_id: id,
|
|
user_id: payload.uid
|
|
}
|
|
}
|
|
} catch (e) {
|
|
return { code: 500, msg: e.message }
|
|
}
|
|
},
|
|
|
|
async add_cms_articles(query) {
|
|
try {
|
|
const payload = await this._requireLogin()
|
|
|
|
const {
|
|
title,
|
|
title_html,
|
|
title_delta,
|
|
thumbnail,
|
|
p_type,
|
|
category_id,
|
|
temp_id,
|
|
edit_type,
|
|
excerpt,
|
|
cmsLst,
|
|
article_status
|
|
} = query
|
|
|
|
const now = new Date()
|
|
const r = await collection.add({
|
|
title,
|
|
// 作者一律取登录态,不接受客户端传入
|
|
user_id: payload.uid,
|
|
title_html,
|
|
title_delta,
|
|
thumbnail,
|
|
p_type,
|
|
category_id,
|
|
temp_id,
|
|
edit_type,
|
|
excerpt,
|
|
// 计数类字段由服务端维护,初始为 0
|
|
view_count: 0,
|
|
like_count: 0,
|
|
collect_count: 0,
|
|
cmsLst,
|
|
publish_date: now,
|
|
last_modify_date: now,
|
|
last_modify_ip: this.getClientInfo().clientIP,
|
|
article_status
|
|
})
|
|
const res = await collection.doc(r.id).get()
|
|
return {
|
|
code: 200,
|
|
res: {
|
|
data: res.data[0],
|
|
_id: r.id,
|
|
user_id: payload.uid
|
|
}
|
|
}
|
|
} catch (e) {
|
|
return { code: 500, msg: e.message }
|
|
}
|
|
},
|
|
|
|
async getTemp(query) {
|
|
try {
|
|
await this._requireLogin()
|
|
const { temp_id } = query || {}
|
|
if (!temp_id) return { code: 400, msg: '缺少 temp_id' }
|
|
const res = await db.collection("cms-temp").doc(temp_id).get()
|
|
return {
|
|
code: 200,
|
|
data: res.data[0]
|
|
}
|
|
} catch (e) {
|
|
return { code: 500, msg: e.message }
|
|
}
|
|
},
|
|
|
|
async getTotal() {
|
|
try {
|
|
const res = await collection.where({ article_status: 1 }).count()
|
|
return {
|
|
code: 200,
|
|
message: 'success',
|
|
total: res.total
|
|
}
|
|
} catch (err) {
|
|
return {
|
|
code: 500,
|
|
message: err.message
|
|
}
|
|
}
|
|
},
|
|
|
|
// 文章访问日志服务
|
|
async detailLog(query) {
|
|
try {
|
|
const payload = await this._requireLogin()
|
|
const articleId = query && query.articleId
|
|
if (!articleId) return { code: -1, msg: '缺少文章 ID' }
|
|
|
|
// 必须按「用户 + 文章」定位记录。只按 article_id 查询会命中别人的行,
|
|
// 导致多人共用一条日志、自己的阅读时间永远不更新。
|
|
const userId = payload.uid
|
|
const where = { user_id: userId, article_id: articleId }
|
|
const { data: existingLog } = await db.collection('cms-articles-log').where(where).get()
|
|
const now = new Date()
|
|
|
|
if (existingLog.length > 0) {
|
|
await db.collection('cms-articles-log')
|
|
.doc(existingLog[0]._id)
|
|
.update({
|
|
last_view_time: now,
|
|
view_count: db.command.inc(1)
|
|
})
|
|
} else {
|
|
await db.collection('cms-articles-log').add({
|
|
user_id: userId,
|
|
article_id: articleId,
|
|
view_count: 1,
|
|
first_view_time: now,
|
|
last_view_time: now
|
|
})
|
|
}
|
|
|
|
return { code: 0, msg: '日志记录成功' }
|
|
} catch (err) {
|
|
console.error('日志记录失败:', err)
|
|
return { code: -1, msg: '系统异常,请重试' }
|
|
}
|
|
}
|
|
}
|