首次完整推送,
V:1.20240808.006
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -0,0 +1,257 @@
|
||||
const {
|
||||
QuillDeltaToHtmlConverter: QuillDeltaToHtmlConverterBase,
|
||||
BlockGroup,
|
||||
ListGroup,
|
||||
BlotBlock,
|
||||
} = require('./core')
|
||||
|
||||
const jsonRules = {
|
||||
list(list) {
|
||||
const firstItem = list.items[0];
|
||||
|
||||
return {
|
||||
type: "list",
|
||||
data: {
|
||||
type: firstItem.item.op.attributes.list,
|
||||
items: list.items.map((item) => jsonRules.listItem(item)),
|
||||
},
|
||||
attributes: firstItem.item.op.attributes,
|
||||
class: attributes2class(firstItem.item.op.attributes, 'block'),
|
||||
style: attributes2style(firstItem.item.op.attributes, 'block'),
|
||||
};
|
||||
},
|
||||
listItem(listItem) {
|
||||
// listItem.item.op.attributes.indent = 0;
|
||||
const inlines = jsonRules.inlines(listItem.item.op, listItem.item.ops, false).map((v) => v.ops)
|
||||
|
||||
return {
|
||||
data: inlines[0] || [],
|
||||
children: listItem.innerList ? jsonRules.list(listItem.innerList): [],
|
||||
}
|
||||
},
|
||||
block (op, ops) {
|
||||
const type = ops[0].insert.type
|
||||
|
||||
if (type === 'text') {
|
||||
return jsonRules.inlines(op, ops)
|
||||
}
|
||||
|
||||
return {
|
||||
type: ops[0].insert.type,
|
||||
data: ops.map(bop => jsonRules.inline(bop)),
|
||||
attributes: op.attributes,
|
||||
class: attributes2class(op.attributes, 'block'),
|
||||
style: attributes2style(op.attributes, 'block'),
|
||||
}
|
||||
},
|
||||
inlines(op = {}, ops, isInlineGroup = true) {
|
||||
const opsLen = ops.length - 1;
|
||||
const br = {
|
||||
type: 'br'
|
||||
}
|
||||
const texts = ops.reduce((acc, op, i) => {
|
||||
if (i > 0 && i === opsLen && op.isJustNewline()) {
|
||||
acc[acc.length - 1].op = op
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[acc.length - 1]) {
|
||||
acc.push({ops: [], op: {}});
|
||||
}
|
||||
|
||||
if (op.isJustNewline()) {
|
||||
const nextOp = ops[i + 1];
|
||||
acc[acc.length - 1].op = op
|
||||
if (nextOp && nextOp.isJustNewline()) {
|
||||
acc.push({ops: [br], op: {}});
|
||||
} else {
|
||||
acc.push({ops: [], op: {}});
|
||||
}
|
||||
return acc;
|
||||
} else {
|
||||
acc[acc.length - 1].ops.push(jsonRules.inline(op));
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isInlineGroup) {
|
||||
return texts;
|
||||
}
|
||||
|
||||
return texts.map((v) => {
|
||||
return {
|
||||
type: "paragraph",
|
||||
data: v.ops,
|
||||
class: attributes2class(op.attributes || v.op.attributes, 'block'),
|
||||
style: attributes2style(op.attributes || v.op.attributes, 'block'),
|
||||
};
|
||||
});
|
||||
},
|
||||
inline (op) {
|
||||
const data = {
|
||||
value: op.insert.value,
|
||||
attributes: op.attributes,
|
||||
class: attributes2class(op.attributes, 'inline'),
|
||||
style: attributes2style(op.attributes, 'inline'),
|
||||
}
|
||||
|
||||
if (op.isCustomEmbed()) {
|
||||
return {
|
||||
type: op.insert.type,
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
if (op.isLink()) {
|
||||
return {
|
||||
type: 'link',
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: op.isImage() ? "image": "text",
|
||||
data
|
||||
}
|
||||
},
|
||||
blotBlock (op) {
|
||||
return {
|
||||
type: op.insert.type,
|
||||
data: {
|
||||
value: op.insert.value,
|
||||
attributes: op.attributes,
|
||||
class: attributes2class(op.attributes, 'block'),
|
||||
style: attributes2style(op.attributes, 'block'),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function attributes2style(attributes, type) {
|
||||
if (!attributes) return ''
|
||||
// 定义允许的属性
|
||||
const allowAttr = {
|
||||
inline: ['align', 'color', 'background', 'font', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight', 'fontFamily', 'lineHeight', 'letterSpacing', 'textDecoration', 'textIndent', 'wordWrap', 'wordBreak', 'whiteSpace'],
|
||||
block: ['align', 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'lineHeight', 'textIndent']
|
||||
}[type]
|
||||
|
||||
// 定义属性适配器
|
||||
const adp = {
|
||||
align: 'text-align',
|
||||
}
|
||||
|
||||
// 如果不支持该类型的属性,则抛出错误
|
||||
if (!allowAttr) throw new Error('type not supported')
|
||||
|
||||
// 将属性转换为style
|
||||
return allowAttr.reduce((res, item) => {
|
||||
// 如果属性为空,则返回空字符串
|
||||
if (!attributes) return res
|
||||
// 如果属性不为undefined,则将属性名和属性值添加到style列表中
|
||||
if (attributes[item] !== undefined) {
|
||||
// 如果属性适配器中存在该属性,则使用适配器中的属性名
|
||||
// 否则,将属性名转换为短横线连接的形式
|
||||
res.push(`${adp[item] || item.replace(/([A-Z])/g, (s, m) => `-${m.toLowerCase()}`)}: ${attributes[item]}`)
|
||||
}
|
||||
return res
|
||||
|
||||
}, []).join(';')
|
||||
}
|
||||
function attributes2class(attributes, type) {
|
||||
if (!attributes) return ''
|
||||
// 定义允许的属性
|
||||
const allowAttr = {
|
||||
inline: ['bold', 'italic', 'underline', 'strike', 'ins', 'link'],
|
||||
block: ['header', 'list']
|
||||
}[type]
|
||||
|
||||
// 如果不支持该类型的属性,则抛出错误
|
||||
if (!allowAttr) throw new Error('type not supported')
|
||||
|
||||
// 将属性转换为class列表
|
||||
const classList = allowAttr.reduce((res, item) => {
|
||||
// 如果属性为空,则返回空字符串
|
||||
if (!attributes || item === "link") return res
|
||||
// 如果属性为true,则将属性名添加到class列表中
|
||||
if (attributes[item] === true) {
|
||||
res.push(item)
|
||||
// 如果属性不为undefined,则将属性名和属性值添加到class列表中
|
||||
} else if (attributes[item] !== undefined) {
|
||||
res.push(`${item}-${attributes[item]}`)
|
||||
}
|
||||
return res
|
||||
|
||||
}, [])
|
||||
|
||||
// 如果属性中包含link,则添加link类
|
||||
if ('link' in attributes) {
|
||||
classList.push('link')
|
||||
}
|
||||
|
||||
return classList.join(' ')
|
||||
}
|
||||
class QuillDeltaToJSONConverter {
|
||||
constructor(deltaOps) {
|
||||
|
||||
this.deltaPreHandler(deltaOps)
|
||||
|
||||
this.deltaOps = deltaOps
|
||||
this.converter = new QuillDeltaToHtmlConverterBase(this.deltaOps, {
|
||||
multiLineParagraph: false,
|
||||
});
|
||||
}
|
||||
|
||||
deltaPreHandler (deltaOps) {
|
||||
for (const op of deltaOps) {
|
||||
if (typeof op.insert === 'string') continue
|
||||
|
||||
if (!op.attributes) {
|
||||
op.attributes = {}
|
||||
}
|
||||
|
||||
const insertType = Object.keys(op.insert)
|
||||
const blockRenderList = ['divider', 'unlockContent', 'mediaVideo']
|
||||
|
||||
if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) {
|
||||
op.attributes.renderAsBlock = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convert () {
|
||||
const opsGroups = this.converter.getGroupedOps();
|
||||
|
||||
return [].concat.apply(
|
||||
[],
|
||||
opsGroups.map((group) => {
|
||||
// console.log(JSON.stringify(group), '--------------------group------------------------------')
|
||||
switch (group.constructor) {
|
||||
case ListGroup:
|
||||
return jsonRules.list(group);
|
||||
case BlockGroup:
|
||||
return jsonRules.block(group.op, group.ops)
|
||||
case BlotBlock:
|
||||
return jsonRules.blotBlock(group.op, group.ops)
|
||||
default:
|
||||
return jsonRules.inlines(group.op, group.ops);
|
||||
}
|
||||
})
|
||||
).filter(op => op.data instanceof Array ? op.data.length : op.data);
|
||||
}
|
||||
}
|
||||
|
||||
class QuillDeltaToHtmlConverter extends QuillDeltaToHtmlConverterBase {
|
||||
constructor(deltaOps) {
|
||||
super(deltaOps, {
|
||||
multiLineParagraph: false,
|
||||
inlineStyles: true,
|
||||
});
|
||||
|
||||
// this.renderCustomWith(function (customOp, contextOp) {
|
||||
// console.log(customOp, contextOp)
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
exports.QuillDeltaToHtmlConverter = QuillDeltaToHtmlConverter
|
||||
exports.QuillDeltaToJSONConverter = QuillDeltaToJSONConverter
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "quill-delta-converter",
|
||||
"description": "quill编辑器delta格式转换器",
|
||||
"main": "index.js"
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
const crypto = require('crypto')
|
||||
const createConfig = require('uni-config-center')
|
||||
const config = createConfig({
|
||||
pluginId: 'uni-cms'
|
||||
}).config()
|
||||
|
||||
const unlockRecordDBName = 'uni-cms-unlock-record'
|
||||
|
||||
// 定义云函数
|
||||
exports.main = async function (event) {
|
||||
// 解构 event 对象
|
||||
const {trans_id, extra: _extra, sign} = event
|
||||
let extra = {}
|
||||
try {
|
||||
extra = JSON.parse(_extra)
|
||||
} catch (e) {}
|
||||
|
||||
// 如果 adConfig 或 securityKey 配置项不存在,则抛出错误引导用户配置参数
|
||||
if (!config.adConfig || !config.adConfig.securityKey) throw new Error('请先配置adConfig.securityKey')
|
||||
// 如果 extra.article_id 不存在,则返回 null
|
||||
if (!extra.article_id) return null
|
||||
|
||||
// 签名验证
|
||||
const reSign = crypto.createHash('sha256').update(`${config.adConfig.securityKey}:${trans_id}`).digest('hex')
|
||||
if (sign !== reSign) {
|
||||
console.log('签名错误', `${config.adConfig.securityKey}:${trans_id}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取数据库实例
|
||||
const db = uniCloud.database()
|
||||
// 查询解锁记录
|
||||
const unlockRecord = await db.collection(unlockRecordDBName).where({
|
||||
trans_id
|
||||
}).get()
|
||||
|
||||
// 如果已经解锁过了,则返回 null
|
||||
if (unlockRecord.data.length) {
|
||||
console.log('已经解锁过了')
|
||||
return null // 已经解锁过了
|
||||
}
|
||||
|
||||
// 添加解锁记录
|
||||
await db.collection(unlockRecordDBName).add({
|
||||
unique_id: extra.unique_id,
|
||||
unique_type: extra.unique_type,
|
||||
article_id: extra.article_id,
|
||||
trans_id,
|
||||
create_date: Date.now()
|
||||
})
|
||||
|
||||
console.log('解锁成功')
|
||||
|
||||
// 应广告规范,需返回 isValid 为 true
|
||||
return {
|
||||
isValid: true
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "uni-cms-unlock-callback",
|
||||
"dependencies": {
|
||||
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
|
||||
},
|
||||
"extensions": {}
|
||||
}
|
23
uni_modules/uni-cms-article/uniCloud/database/db_init.json
Normal file
23
uni_modules/uni-cms-article/uniCloud/database/db_init.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"uni-cms-unlock-record": {
|
||||
"data": [],
|
||||
"index": [
|
||||
{
|
||||
"IndexName": "unique_article_trans_",
|
||||
"MgoKeySchema": {
|
||||
"MgoIndexKeys": [{
|
||||
"Name": "unique_id",
|
||||
"Direction": "1"
|
||||
},{
|
||||
"Name": "article_id",
|
||||
"Direction": "1"
|
||||
},{
|
||||
"Name": "trans_id",
|
||||
"Direction": "1"
|
||||
}],
|
||||
"MgoIsUnique": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"content",
|
||||
"count"
|
||||
],
|
||||
"permission": {
|
||||
"read": true,
|
||||
"create": false,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "ID,系统自动生成"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索内容"
|
||||
},
|
||||
"count": {
|
||||
"bsonType": "int",
|
||||
"description": "搜索次数"
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "统计时间"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"permission": {
|
||||
"read": false,
|
||||
"create": true,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "ID,系统自动生成"
|
||||
},
|
||||
"user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索人id,参考uni-id-users表"
|
||||
},
|
||||
"device_id": {
|
||||
"bsonType": "string",
|
||||
"description": "设备id"
|
||||
},
|
||||
"platform": {
|
||||
"bsonType": "string",
|
||||
"description": "设备平台,如:mp-weixin、app-plus等"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索内容"
|
||||
},
|
||||
"ip": {
|
||||
"bsonType": "string",
|
||||
"description": "客户端IP地址",
|
||||
"forceDefaultValue": {
|
||||
"$env": "clientIP"
|
||||
}
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "统计时间"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"permission": {
|
||||
"read": false,
|
||||
"create": true,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "ID,系统自动生成"
|
||||
},
|
||||
"user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索人id,参考uni-id-users表"
|
||||
},
|
||||
"device_id": {
|
||||
"bsonType": "string",
|
||||
"description": "设备id"
|
||||
},
|
||||
"platform": {
|
||||
"bsonType": "string",
|
||||
"description": "设备平台,如:mp-weixin、app-plus等"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索内容"
|
||||
},
|
||||
"ip": {
|
||||
"bsonType": "string",
|
||||
"description": "客户端IP地址",
|
||||
"forceDefaultValue": {
|
||||
"$env": "clientIP"
|
||||
}
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "统计时间"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
@ -0,0 +1,350 @@
|
||||
// 获取配置
|
||||
const createConfig = safeRequire('uni-config-center')
|
||||
const {QuillDeltaToHtmlConverter, QuillDeltaToJSONConverter} = safeRequire('quill-delta-converter')
|
||||
const config = createConfig({
|
||||
pluginId: 'uni-cms'
|
||||
}).config()
|
||||
|
||||
// 获取数据库实例
|
||||
const db = uniCloud.database()
|
||||
|
||||
// 文章数据库名称
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
|
||||
// 解锁内容数据库名称
|
||||
const unlockContentDBName = 'uni-cms-unlock-record'
|
||||
|
||||
// 安全检测文本内容
|
||||
async function checkContentSec(content, requestId, errorMsg) {
|
||||
// 安全引入内容安全检测模块
|
||||
const UniSecCheck = safeRequire('uni-sec-check')
|
||||
// 创建内容安全检测实例
|
||||
const uniSecCheck = new UniSecCheck({
|
||||
provider: 'mp-weixin',
|
||||
requestId
|
||||
})
|
||||
// 调用文本安全检测接口
|
||||
const res = await uniSecCheck.textSecCheck({
|
||||
content, // 待检测的文本内容
|
||||
scene: 1, // 表示资料类场景
|
||||
version: 1 // 调用检测API的版本号
|
||||
})
|
||||
|
||||
// 如果存在敏感词,抛出异常
|
||||
if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
|
||||
throw new Error(errorMsg || '存在敏感词,请修改后提交')
|
||||
} else if (res.errCode !== 0) {
|
||||
console.error(res)
|
||||
throw new Error('内容安全检测异常:' + res.errCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 安全检测图片内容
|
||||
async function checkImageSec(image, requestId, errorMsg) {
|
||||
// 安全引入内容安全检测模块
|
||||
const UniSecCheck = safeRequire('uni-sec-check')
|
||||
// 创建内容安全检测实例
|
||||
const uniSecCheck = new UniSecCheck({
|
||||
provider: 'mp-weixin',
|
||||
requestId
|
||||
})
|
||||
|
||||
const images = typeof image === "string" ? [image]: image
|
||||
|
||||
for (let item of images) {
|
||||
// 处理cloud://开头的链接
|
||||
if (item.startsWith('cloud://')) {
|
||||
const res = await uniCloud.getTempFileURL({
|
||||
fileList: [item]
|
||||
})
|
||||
|
||||
if (res.fileList && res.fileList.length > 0) {
|
||||
item = res.fileList[0].tempFileURL
|
||||
}
|
||||
}
|
||||
|
||||
// 调用图片安全检测接口
|
||||
const res = await uniSecCheck.imgSecCheck({
|
||||
image: item, // 待检测的图片URL
|
||||
scene: 1, // 表示资料类场景
|
||||
version: 1 // 调用检测API的版本号
|
||||
})
|
||||
|
||||
// 如果存在违规内容,抛出异常
|
||||
if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
|
||||
throw new Error(errorMsg || '图片违规,请修改后提交')
|
||||
} else if (res.errCode !== 0) {
|
||||
console.error(res)
|
||||
throw new Error('内容安全检测异常:' + res.errCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测内容安全开关
|
||||
function checkContentSecurityEnable(field) {
|
||||
// 1. 从配置中心获取配置
|
||||
return config.contentSecurity && config.contentSecurity.allowCheckType && config.contentSecurity.allowCheckType.includes(field)
|
||||
}
|
||||
|
||||
// 安全require
|
||||
function safeRequire(module) {
|
||||
try {
|
||||
return require(module)
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(`${module} 公共模块不存在,请在 uniCloud/database 目录右击"配置schema扩展公共模块"添加 ${module} 模块`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
trigger: {
|
||||
// 创建文章前触发
|
||||
beforeCreate: async function ({clientInfo, addDataList}) {
|
||||
// addDataList 是一个数组,因为可以一次性创建多条数据
|
||||
if (addDataList.length <= 0) return
|
||||
|
||||
// 检测内容安全开关
|
||||
const allowCheckContent = checkContentSecurityEnable('content')
|
||||
const allowCheckImage = checkContentSecurityEnable('image')
|
||||
|
||||
// 遍历数组,对每一条数据进行安全检测
|
||||
for (const addData of addDataList) {
|
||||
// 如果是草稿,不检测
|
||||
if (addData.article_status !== 1) continue
|
||||
|
||||
// 并行检测
|
||||
const parallel = []
|
||||
// 检测标题
|
||||
if (allowCheckContent && addData.title) {
|
||||
parallel.push(checkContentSec(addData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测摘要
|
||||
if (allowCheckContent && addData.excerpt) {
|
||||
parallel.push(checkContentSec(addData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测内容
|
||||
if (allowCheckContent && addData.content) {
|
||||
parallel.push(checkContentSec(JSON.stringify(addData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测封面图
|
||||
if (allowCheckImage && addData.thumbnail) {
|
||||
parallel.push(checkImageSec(addData.thumbnail, clientInfo.requestId, '封面图存在违规,请修改后提交'))
|
||||
}
|
||||
// 等待所有并行检测完成
|
||||
await Promise.all(parallel)
|
||||
}
|
||||
},
|
||||
// 更新文章前触发
|
||||
beforeUpdate: async function ({clientInfo, where, updateData}) {
|
||||
const id = where && where._id
|
||||
|
||||
if (!id) return
|
||||
|
||||
// 如果是草稿,不检测
|
||||
if (updateData.article_status !== 1) return
|
||||
|
||||
// 检测内容安全开关
|
||||
const allowCheckContent = checkContentSecurityEnable('content')
|
||||
const allowCheckImage = checkContentSecurityEnable('image')
|
||||
|
||||
// 并行检测
|
||||
const parallel = []
|
||||
|
||||
// 检测标题
|
||||
if (allowCheckContent && updateData.title) {
|
||||
parallel.push(checkContentSec(updateData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测摘要
|
||||
if (allowCheckContent && updateData.excerpt) {
|
||||
parallel.push(checkContentSec(updateData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测内容
|
||||
if (allowCheckContent && updateData.content) {
|
||||
parallel.push(checkContentSec(JSON.stringify(updateData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测封面图
|
||||
if (allowCheckImage && updateData.thumbnail) {
|
||||
parallel.push(checkImageSec(updateData.thumbnail, clientInfo.requestId, '封面图存在违规,请修改后提交'))
|
||||
}
|
||||
|
||||
// 等待所有并行检测完成
|
||||
await Promise.all(parallel)
|
||||
|
||||
},
|
||||
// 读取文章后触发
|
||||
afterRead: async function ({userInfo, clientInfo, result, where, field}) {
|
||||
const isAdmin = field && field.length && field.includes('is_admin')
|
||||
// 检查是否配置了clientAppIds字段,如果没有则抛出错误
|
||||
if ((!config.clientAppIds || !config.clientAppIds.length) && !isAdmin) {
|
||||
throw new Error('请在 uni-cms 配置文件中配置 clientAppIds 字段后访问,详见:https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#uni-cms-config')
|
||||
}
|
||||
|
||||
// 如果clientAppIds字段未配置或当前appId不在clientAppIds中,则返回
|
||||
if (!config.clientAppIds || !config.clientAppIds.includes(clientInfo.appId)) return
|
||||
|
||||
// 获取广告配置
|
||||
const adConfig = config.adConfig || {}
|
||||
|
||||
// 获取文章id
|
||||
const id = where && where._id
|
||||
|
||||
// 如果id不存在或者field不包含content,则返回
|
||||
if (id && field.includes('content')) {
|
||||
// 读取了content字段后view_count加1
|
||||
await db.collection(articleDBName).where(where).update({
|
||||
view_count: db.command.inc(1)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果查询结果为空,则返回
|
||||
if (!result.data || result.data.length <= 0) return
|
||||
|
||||
// 获取文章
|
||||
const article = result.data[0]
|
||||
|
||||
// 如果文章内容不存在,则返回
|
||||
if (!article.content) return
|
||||
|
||||
let needUnlock = false
|
||||
let unlockContent = []
|
||||
|
||||
// 获取文章内容中的图片
|
||||
article.content_images = article.content.ops.reduce((imageBlocks, block) => {
|
||||
if (!block.insert.image) return imageBlocks
|
||||
|
||||
const {attributes} = block
|
||||
const {'data-custom': custom = ""} = attributes || {}
|
||||
const parseCustom = custom.split('&').reduce((obj, item) => {
|
||||
const [key, value] = item.split('=')
|
||||
obj[key] = value
|
||||
return obj
|
||||
})
|
||||
|
||||
return imageBlocks.concat(
|
||||
parseCustom.source ||
|
||||
block.insert.image
|
||||
)
|
||||
}, [])
|
||||
|
||||
for (const op of article.content.ops) {
|
||||
unlockContent.push(op)
|
||||
// 遍历文章内容,找到解锁内容
|
||||
if (op.insert.unlockContent) {
|
||||
needUnlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果文章不需要解锁,则返回
|
||||
if (!needUnlock) {
|
||||
article.content = getRenderableArticleContent(article.content, clientInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取唯一标识符
|
||||
const uniqueId = adConfig.watchAdUniqueType === 'user' ? userInfo.uid : clientInfo.deviceId
|
||||
|
||||
// 如果未登录或者文章未解锁,则返回解锁内容
|
||||
if (!uniqueId || !article._id) {
|
||||
article.content = getRenderableArticleContent({
|
||||
ops: unlockContent
|
||||
}, clientInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询解锁记录
|
||||
const unlockRecord = await db.collection(unlockContentDBName).where({
|
||||
unique_id: uniqueId,
|
||||
article_id: article._id
|
||||
}).get()
|
||||
|
||||
// 如果未解锁,则返回解锁内容
|
||||
if (unlockRecord.data && unlockRecord.data.length <= 0) {
|
||||
article.content = getRenderableArticleContent({
|
||||
ops: unlockContent
|
||||
}, clientInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// 将文章解锁替换为行结束符 \n
|
||||
article.content = getRenderableArticleContent({
|
||||
ops: article.content.ops.map(op => {
|
||||
if (op.insert.unlockContent) {
|
||||
op.insert = "\n"
|
||||
}
|
||||
return op
|
||||
})
|
||||
}, clientInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRenderableArticleContent (rawArticleContent, clientInfo) {
|
||||
const isUniAppX = /uni-app-x/i.test(clientInfo.userAgent)
|
||||
|
||||
if (!isUniAppX) {
|
||||
const quillDeltaConverter = new QuillDeltaToJSONConverter(rawArticleContent.ops)
|
||||
return quillDeltaConverter.convert()
|
||||
}
|
||||
|
||||
const deltaOps = []
|
||||
|
||||
for (let i = 0; i < rawArticleContent.ops.length; i++) {
|
||||
const op = rawArticleContent.ops[i]
|
||||
|
||||
if (typeof op.insert === 'object') {
|
||||
const insertType = Object.keys(op.insert)
|
||||
const blockRenderList = ['image', 'divider', 'unlockContent', 'mediaVideo']
|
||||
if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) {
|
||||
deltaOps.push({
|
||||
type: insertType[0],
|
||||
ops: [op]
|
||||
})
|
||||
|
||||
// 一般块级节点后面都跟一个换行,需要把这个换行给去掉
|
||||
const nextOps = rawArticleContent.ops[i + 1]
|
||||
if (nextOps && nextOps.insert === '\n') {
|
||||
i ++
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const currentIndex = deltaOps.length > 0 ? deltaOps.length - 1: 0
|
||||
if (
|
||||
typeof deltaOps[currentIndex] !== "object" ||
|
||||
(deltaOps[currentIndex] && deltaOps[currentIndex].type !== 'rich-text')
|
||||
) {
|
||||
deltaOps.push({
|
||||
type: 'rich-text',
|
||||
ops: []
|
||||
})
|
||||
}
|
||||
|
||||
deltaOps[deltaOps.length - 1].ops.push(op)
|
||||
}
|
||||
|
||||
return deltaOps.reduce((content, item) => {
|
||||
const isRichText = item.type === 'rich-text'
|
||||
let block = {
|
||||
type: item.type,
|
||||
data: isRichText ? item.ops: item.ops[0]
|
||||
}
|
||||
|
||||
if (item.type === 'rich-text') {
|
||||
const lastOp = item.ops.length > 0 ? item.ops[item.ops.length - 1]: null
|
||||
|
||||
if (lastOp !== null && lastOp.insert === "\n") {
|
||||
item.ops.pop()
|
||||
}
|
||||
|
||||
const quillDeltaConverter = new QuillDeltaToHtmlConverter(item.ops)
|
||||
|
||||
block.data = quillDeltaConverter.convert()
|
||||
}
|
||||
|
||||
return content.concat(block)
|
||||
}, [])
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": ["user_id", "title", "content"],
|
||||
"permission": {
|
||||
"read": true,
|
||||
"create": "'admin' in auth.role || 'CREATE_UNI_CMS_ARTICLE' in auth.permission",
|
||||
"update": "'admin' in auth.role || 'UPDATE_UNI_CMS_ARTICLE' in auth.permission",
|
||||
"delete": "'admin' in auth.role || 'DELETE_UNI_CMS_ARTICLE' in auth.permission"
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "存储文档 ID(用户 ID),系统自动生成"
|
||||
},
|
||||
"user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "文章作者ID, 参考`uni-id-users` 表",
|
||||
"foreignKey": "uni-id-users._id",
|
||||
"defaultValue": {
|
||||
"$env": "uid"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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": "已发布"
|
||||
}
|
||||
]
|
||||
},
|
||||
"view_count": {
|
||||
"bsonType": "int",
|
||||
"title": "阅读数量",
|
||||
"description": "阅读数量",
|
||||
"defaultValue": 0,
|
||||
"permission": {
|
||||
"write": false
|
||||
}
|
||||
},
|
||||
"like_count": {
|
||||
"bsonType": "int",
|
||||
"description": "喜欢数、点赞数",
|
||||
"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": "文章预览过期时间"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"user_id",
|
||||
"trans_id",
|
||||
"content_id"
|
||||
],
|
||||
"permission": {
|
||||
"read": true,
|
||||
"create": true,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "存储文档 ID(用户 ID),系统自动生成"
|
||||
},
|
||||
"unique_id": {
|
||||
"bsonType": "string",
|
||||
"description": "用于标识观看广告的唯一标识"
|
||||
},
|
||||
"unique_type": {
|
||||
"bsonType": "string",
|
||||
"description": "观看广告的唯一标识类型;user 用户;device 设备"
|
||||
},
|
||||
"trans_id": {
|
||||
"bsonType": "string",
|
||||
"title": "交易ID",
|
||||
"description": "广告回调传回的交易ID",
|
||||
"label": "内容id",
|
||||
"trim": "both"
|
||||
},
|
||||
"content_id": {
|
||||
"bsonType": "string",
|
||||
"title": "内容id",
|
||||
"description": "内容(文章)ID",
|
||||
"label": "内容id",
|
||||
"trim": "both"
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"title": "创建时间",
|
||||
"description": "创建时间",
|
||||
"defaultValue": {
|
||||
"$env": "now"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user