首次完整推送,

V:1.20240808.006
This commit is contained in:
fm453
2024-08-13 18:32:37 +08:00
parent 15be3e9373
commit c62d15b288
939 changed files with 111777 additions and 0 deletions

View 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
}
}
]
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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)
}, [])
}

View File

@ -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": "文章预览过期时间"
}
}
}

View File

@ -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"
}
}
}
}