Files
root 3117146281 feat: 打通 HBuilderX CLI 构建 + 修复文章闭环静态盲区
构建:
- 新增 tools/build.js:junction HBuilderX 工具链,CLI 构建 h5/mp-weixin
- vue 指向补丁版 @dcloudio/uni-h5-vue(官方 npm vue 不导出 isInSSRComponentSetup)
- 设 HX_APP_ROOT 避免退化成 H5 空壳产物;产物完整性校验

校验工具:
- 新增 check-cloud-methods.js:acorn 解析云对象方法,比对 94 处调用点
- 新增 check-android-contract.js:Kotlin 侧云对象契约校验
- audit-project.js 修 downloadFile 误报(注释未剥离);tools/ 排除出扫描
- package.json 声明此前隐式依赖的 acorn

功能:
- 补 uni-cms-articles.getPublishedArticles(安卓端依赖但此前不存在)
- 修 u-parse <audio> 引用已移除组件导致 H5 构建失败
2026-09-12 01:02:45 +08:00

333 lines
9.9 KiB
JavaScript
Raw Permalink 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.
const db = uniCloud.database()
const collection = db.collection('uni-cms-articles')
const uniID = require('uni-id-common')
const {
normalizeCmsList,
cmsLstToDelta,
createVoteId,
hasRenderableContent
} = require('./content-adapter')
const isAdmin = (payload) => (payload.role || []).includes('admin')
function normalizeArticlePayload(query = {}, existing = {}) {
const edit_type = query.edit_type !== undefined ? query.edit_type : existing.edit_type
const mobile = edit_type === 'mobile'
const cmsLst = query.cmsLst !== undefined
? normalizeCmsList(query.cmsLst)
: (Array.isArray(existing.cmsLst) ? normalizeCmsList(existing.cmsLst) : [])
const content = mobile
? cmsLstToDelta(cmsLst)
: (query.content && Array.isArray(query.content.ops) ? query.content : existing.content || { ops: [] })
if (query.article_status === 1 && !hasRenderableContent(content, cmsLst)) {
throw new Error('正文不能为空')
}
const normalizedList = cmsLst.map(item => {
if (item.type !== 'vote' || !item.vote) return item
return {
...item,
vote: {
...item.vote,
vote_id: item.vote.vote_id || createVoteId()
}
}
})
return { edit_type, cmsLst: normalizedList, content }
}
function valueOrExisting(query, existing, key, fallback) {
return query[key] !== undefined ? query[key] : (existing[key] !== undefined ? existing[key] : fallback)
}
/** 转义正则元字符,避免用户输入被当作正则执行 */
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function toPositiveInt(value, fallback, max) {
const n = Number(value)
if (!Number.isFinite(n) || n < 0) return fallback
const i = Math.floor(n)
return max !== undefined ? Math.min(i, max) : i
}
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 oldRes = await collection.doc(id).get()
const existing = oldRes.data && oldRes.data[0]
if (!existing) throw new Error('文章不存在')
const { edit_type, cmsLst, content } = normalizeArticlePayload(query, existing)
const now = new Date()
const r = await collection.doc(id).update({
title: valueOrExisting(query, existing, 'title', ''),
// 作者不可被客户端改写,始终以数据库中的归属为准
title_html: valueOrExisting(query, existing, 'title_html', ''),
title_delta: valueOrExisting(query, existing, 'title_delta', {}),
thumbnail: valueOrExisting(query, existing, 'thumbnail', []),
p_type: valueOrExisting(query, existing, 'p_type', ''),
category_id: valueOrExisting(query, existing, 'category_id', ''),
temp_id: valueOrExisting(query, existing, 'temp_id', ''),
edit_type,
excerpt: valueOrExisting(query, existing, 'excerpt', ''),
cmsLst,
content,
music_url_id: valueOrExisting(query, existing, 'music_url_id', {}),
// 修改不应重置发布时间,只更新最后修改时间
last_modify_date: now,
last_modify_ip: this.getClientInfo().clientIP,
article_status: valueOrExisting(query, existing, 'article_status', 0)
})
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 { edit_type, cmsLst, content } = normalizeArticlePayload(query)
const title = typeof query.title === 'string' ? query.title.trim() : ''
if (!title) throw new Error('标题不能为空')
const now = new Date()
const r = await collection.add({
title,
// 作者一律取登录态,不接受客户端传入
user_id: payload.uid,
title_html: query.title_html || '',
title_delta: query.title_delta || {},
thumbnail: Array.isArray(query.thumbnail) ? query.thumbnail : [],
p_type: query.p_type || '',
category_id: query.category_id || '',
temp_id: query.temp_id || '',
edit_type,
excerpt: query.excerpt || '',
music_url_id: query.music_url_id || {},
// 计数类字段由服务端维护,初始为 0
view_count: 0,
like_count: 0,
collect_count: 0,
cmsLst,
content,
publish_date: now,
last_modify_date: now,
last_modify_ip: this.getClientInfo().clientIP,
article_status: query.article_status === 1 ? 1 : 0
})
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 {
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 }
}
},
/**
* 已发布文章流(公开,无需登录)。
*
* 原生安卓端与游客首页依赖此方法。没有它时匿名用户只能拿到空列表,
* 因为 get_cms_articles_for_userId 强制登录且只返回本人文章。
*
* 只投影列表所需字段:cmsLst / content 体积大且列表页用不到。
* 另外 schema.ext 的 afterRead 在读取 content 字段时会顺带给 view_count 加 1,
* 列表页不该把「出现在首页」算成一次阅读,因此这里不取 content。
*/
async getPublishedArticles(query = {}) {
try {
const offset = toPositiveInt(query.offset, 0)
const limit = toPositiveInt(query.limit, 20, 100)
const where = { article_status: 1 }
if (typeof query.category_id === 'string' && query.category_id) {
where.category_id = query.category_id
}
const keyword = typeof query.keyword === 'string' ? query.keyword.trim() : ''
if (keyword) where.title = new RegExp(escapeRegExp(keyword), 'i')
const res = await collection
.where(where)
.field({
title: true,
title_html: true,
title_delta: true,
user_id: true,
thumbnail: true,
excerpt: true,
category_id: true,
temp_id: true,
edit_type: true,
p_type: true,
music_url_id: true,
article_status: true,
view_count: true,
like_count: true,
collect_count: true,
publish_date: true,
last_modify_date: true
})
.orderBy('publish_date', 'desc')
.skip(offset)
.limit(limit)
.get()
const total = await collection.where(where).count()
return { code: 200, data: res.data, total: total.total, offset, limit }
} catch (e) {
console.error('getPublishedArticles 失败:', e.message)
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: '系统异常,请重试' }
}
}
}