uni-cms-articles.schema.ext.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // 获取配置
  2. const createConfig = safeRequire('uni-config-center')
  3. const {QuillDeltaToHtmlConverter, QuillDeltaToJSONConverter} = safeRequire('quill-delta-converter')
  4. const config = createConfig({
  5. pluginId: 'uni-cms'
  6. }).config()
  7. // 获取数据库实例
  8. const db = uniCloud.database()
  9. // 文章数据库名称
  10. const articleDBName = 'uni-cms-articles'
  11. // 解锁内容数据库名称
  12. const unlockContentDBName = 'uni-cms-unlock-record'
  13. // 安全检测文本内容
  14. async function checkContentSec(content, requestId, errorMsg) {
  15. // 安全引入内容安全检测模块
  16. const UniSecCheck = safeRequire('uni-sec-check')
  17. // 创建内容安全检测实例
  18. const uniSecCheck = new UniSecCheck({
  19. provider: 'mp-weixin',
  20. requestId
  21. })
  22. // 调用文本安全检测接口
  23. const res = await uniSecCheck.textSecCheck({
  24. content, // 待检测的文本内容
  25. scene: 1, // 表示资料类场景
  26. version: 1 // 调用检测API的版本号
  27. })
  28. // 如果存在敏感词,抛出异常
  29. if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
  30. throw new Error(errorMsg || '存在敏感词,请修改后提交')
  31. } else if (res.errCode !== 0) {
  32. console.error(res)
  33. throw new Error('内容安全检测异常:' + res.errCode)
  34. }
  35. }
  36. // 安全检测图片内容
  37. async function checkImageSec(image, requestId, errorMsg) {
  38. // 安全引入内容安全检测模块
  39. const UniSecCheck = safeRequire('uni-sec-check')
  40. // 创建内容安全检测实例
  41. const uniSecCheck = new UniSecCheck({
  42. provider: 'mp-weixin',
  43. requestId
  44. })
  45. const images = typeof image === "string" ? [image]: image
  46. for (let item of images) {
  47. // 处理cloud://开头的链接
  48. if (item.startsWith('cloud://')) {
  49. const res = await uniCloud.getTempFileURL({
  50. fileList: [item]
  51. })
  52. if (res.fileList && res.fileList.length > 0) {
  53. item = res.fileList[0].tempFileURL
  54. }
  55. }
  56. // 调用图片安全检测接口
  57. const res = await uniSecCheck.imgSecCheck({
  58. image: item, // 待检测的图片URL
  59. scene: 1, // 表示资料类场景
  60. version: 1 // 调用检测API的版本号
  61. })
  62. // 如果存在违规内容,抛出异常
  63. if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
  64. throw new Error(errorMsg || '图片违规,请修改后提交')
  65. } else if (res.errCode !== 0) {
  66. console.error(res)
  67. throw new Error('内容安全检测异常:' + res.errCode)
  68. }
  69. }
  70. }
  71. // 检测内容安全开关
  72. function checkContentSecurityEnable(field) {
  73. // 1. 从配置中心获取配置
  74. return config.contentSecurity && config.contentSecurity.allowCheckType && config.contentSecurity.allowCheckType.includes(field)
  75. }
  76. // 安全require
  77. function safeRequire(module) {
  78. try {
  79. return require(module)
  80. } catch (e) {
  81. if (e.code === 'MODULE_NOT_FOUND') {
  82. throw new Error(`${module} 公共模块不存在,请在 uniCloud/database 目录右击"配置schema扩展公共模块"添加 ${module} 模块`)
  83. }
  84. }
  85. }
  86. module.exports = {
  87. trigger: {
  88. // 创建文章前触发
  89. beforeCreate: async function ({clientInfo, addDataList}) {
  90. // addDataList 是一个数组,因为可以一次性创建多条数据
  91. if (addDataList.length <= 0) return
  92. // 检测内容安全开关
  93. const allowCheckContent = checkContentSecurityEnable('content')
  94. const allowCheckImage = checkContentSecurityEnable('image')
  95. // 遍历数组,对每一条数据进行安全检测
  96. for (const addData of addDataList) {
  97. // 如果是草稿,不检测
  98. if (addData.article_status !== 1) continue
  99. // 并行检测
  100. const parallel = []
  101. // 检测标题
  102. if (allowCheckContent && addData.title) {
  103. parallel.push(checkContentSec(addData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交'))
  104. }
  105. // 检测摘要
  106. if (allowCheckContent && addData.excerpt) {
  107. parallel.push(checkContentSec(addData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交'))
  108. }
  109. // 检测内容
  110. if (allowCheckContent && addData.content) {
  111. parallel.push(checkContentSec(JSON.stringify(addData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
  112. }
  113. // 检测封面图
  114. if (allowCheckImage && addData.thumbnail) {
  115. parallel.push(checkImageSec(addData.thumbnail, clientInfo.requestId, '封面图存在违规,请修改后提交'))
  116. }
  117. // 等待所有并行检测完成
  118. await Promise.all(parallel)
  119. }
  120. },
  121. // 更新文章前触发
  122. beforeUpdate: async function ({clientInfo, where, updateData}) {
  123. const id = where && where._id
  124. if (!id) return
  125. // 如果是草稿,不检测
  126. if (updateData.article_status !== 1) return
  127. // 检测内容安全开关
  128. const allowCheckContent = checkContentSecurityEnable('content')
  129. const allowCheckImage = checkContentSecurityEnable('image')
  130. // 并行检测
  131. const parallel = []
  132. // 检测标题
  133. if (allowCheckContent && updateData.title) {
  134. parallel.push(checkContentSec(updateData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交'))
  135. }
  136. // 检测摘要
  137. if (allowCheckContent && updateData.excerpt) {
  138. parallel.push(checkContentSec(updateData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交'))
  139. }
  140. // 检测内容
  141. if (allowCheckContent && updateData.content) {
  142. parallel.push(checkContentSec(JSON.stringify(updateData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
  143. }
  144. // 检测封面图
  145. if (allowCheckImage && updateData.thumbnail) {
  146. parallel.push(checkImageSec(updateData.thumbnail, clientInfo.requestId, '封面图存在违规,请修改后提交'))
  147. }
  148. // 等待所有并行检测完成
  149. await Promise.all(parallel)
  150. },
  151. // 读取文章后触发
  152. afterRead: async function ({userInfo, clientInfo, result, where, field}) {
  153. const isAdmin = field && field.length && field.includes('is_admin')
  154. // 检查是否配置了clientAppIds字段,如果没有则抛出错误
  155. if ((!config.clientAppIds || !config.clientAppIds.length) && !isAdmin) {
  156. throw new Error('请在 uni-cms 配置文件中配置 clientAppIds 字段后访问,详见:https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#uni-cms-config')
  157. }
  158. // 如果clientAppIds字段未配置或当前appId不在clientAppIds中,则返回
  159. if (!config.clientAppIds || !config.clientAppIds.includes(clientInfo.appId)) return
  160. // 获取广告配置
  161. const adConfig = config.adConfig || {}
  162. // 获取文章id
  163. const id = where && where._id
  164. // 如果id不存在或者field不包含content,则返回
  165. if (id && field.includes('content')) {
  166. // 读取了content字段后view_count加1
  167. await db.collection(articleDBName).where(where).update({
  168. view_count: db.command.inc(1)
  169. })
  170. }
  171. // 如果查询结果为空,则返回
  172. if (!result.data || result.data.length <= 0) return
  173. // 获取文章
  174. const article = result.data[0]
  175. // 如果文章内容不存在,则返回
  176. if (!article.content) return
  177. let needUnlock = false
  178. let unlockContent = []
  179. // 获取文章内容中的图片
  180. article.content_images = article.content.ops.reduce((imageBlocks, block) => {
  181. if (!block.insert.image) return imageBlocks
  182. const {attributes} = block
  183. const {'data-custom': custom = ""} = attributes || {}
  184. const parseCustom = custom.split('&').reduce((obj, item) => {
  185. const [key, value] = item.split('=')
  186. obj[key] = value
  187. return obj
  188. })
  189. return imageBlocks.concat(
  190. parseCustom.source ||
  191. block.insert.image
  192. )
  193. }, [])
  194. for (const op of article.content.ops) {
  195. unlockContent.push(op)
  196. // 遍历文章内容,找到解锁内容
  197. if (op.insert.unlockContent) {
  198. needUnlock = true
  199. break
  200. }
  201. }
  202. // 如果文章不需要解锁,则返回
  203. if (!needUnlock) {
  204. article.content = getRenderableArticleContent(article.content, clientInfo)
  205. return
  206. }
  207. // 获取唯一标识符
  208. const uniqueId = adConfig.watchAdUniqueType === 'user' ? userInfo.uid : clientInfo.deviceId
  209. // 如果未登录或者文章未解锁,则返回解锁内容
  210. if (!uniqueId || !article._id) {
  211. article.content = getRenderableArticleContent({
  212. ops: unlockContent
  213. }, clientInfo)
  214. return
  215. }
  216. // 查询解锁记录
  217. const unlockRecord = await db.collection(unlockContentDBName).where({
  218. unique_id: uniqueId,
  219. article_id: article._id
  220. }).get()
  221. // 如果未解锁,则返回解锁内容
  222. if (unlockRecord.data && unlockRecord.data.length <= 0) {
  223. article.content = getRenderableArticleContent({
  224. ops: unlockContent
  225. }, clientInfo)
  226. return
  227. }
  228. // 将文章解锁替换为行结束符 \n
  229. article.content = getRenderableArticleContent({
  230. ops: article.content.ops.map(op => {
  231. if (op.insert.unlockContent) {
  232. op.insert = "\n"
  233. }
  234. return op
  235. })
  236. }, clientInfo)
  237. }
  238. }
  239. }
  240. function getRenderableArticleContent (rawArticleContent, clientInfo) {
  241. const isUniAppX = /uni-app-x/i.test(clientInfo.userAgent)
  242. if (!isUniAppX) {
  243. const quillDeltaConverter = new QuillDeltaToJSONConverter(rawArticleContent.ops)
  244. return quillDeltaConverter.convert()
  245. }
  246. const deltaOps = []
  247. for (let i = 0; i < rawArticleContent.ops.length; i++) {
  248. const op = rawArticleContent.ops[i]
  249. if (typeof op.insert === 'object') {
  250. const insertType = Object.keys(op.insert)
  251. const blockRenderList = ['image', 'divider', 'unlockContent', 'mediaVideo']
  252. if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) {
  253. deltaOps.push({
  254. type: insertType[0],
  255. ops: [op]
  256. })
  257. // 一般块级节点后面都跟一个换行,需要把这个换行给去掉
  258. const nextOps = rawArticleContent.ops[i + 1]
  259. if (nextOps && nextOps.insert === '\n') {
  260. i ++
  261. }
  262. continue
  263. }
  264. }
  265. const currentIndex = deltaOps.length > 0 ? deltaOps.length - 1: 0
  266. if (
  267. typeof deltaOps[currentIndex] !== "object" ||
  268. (deltaOps[currentIndex] && deltaOps[currentIndex].type !== 'rich-text')
  269. ) {
  270. deltaOps.push({
  271. type: 'rich-text',
  272. ops: []
  273. })
  274. }
  275. deltaOps[deltaOps.length - 1].ops.push(op)
  276. }
  277. return deltaOps.reduce((content, item) => {
  278. const isRichText = item.type === 'rich-text'
  279. let block = {
  280. type: item.type,
  281. data: isRichText ? item.ops: item.ops[0]
  282. }
  283. if (item.type === 'rich-text') {
  284. const lastOp = item.ops.length > 0 ? item.ops[item.ops.length - 1]: null
  285. if (lastOp !== null && lastOp.insert === "\n") {
  286. item.ops.pop()
  287. }
  288. const quillDeltaConverter = new QuillDeltaToHtmlConverter(item.ops)
  289. block.data = quillDeltaConverter.convert()
  290. }
  291. return content.concat(block)
  292. }, [])
  293. }