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 构建失败
This commit is contained in:
@@ -1,9 +1,59 @@
|
||||
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 () { // 通用预处理器
|
||||
@@ -63,44 +113,36 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
async update_cms_articles(query) {
|
||||
try {
|
||||
const payload = await this._requireLogin()
|
||||
const { id } = query || {}
|
||||
await this._requireOwner(id, payload)
|
||||
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 {
|
||||
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
|
||||
})
|
||||
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: '更新失败' }
|
||||
}
|
||||
@@ -118,47 +160,38 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
async add_cms_articles(query) {
|
||||
try {
|
||||
const payload = await this._requireLogin()
|
||||
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 {
|
||||
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 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,
|
||||
@@ -175,7 +208,6 @@ module.exports = {
|
||||
|
||||
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()
|
||||
@@ -188,6 +220,62 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 已发布文章流(公开,无需登录)。
|
||||
*
|
||||
* 原生安卓端与游客首页依赖此方法。没有它时匿名用户只能拿到空列表,
|
||||
* 因为 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()
|
||||
|
||||
Reference in New Issue
Block a user