// 内容安全链路(微信平台规范) // 文本:security.msgSecCheck v2(二维码内容 / 扫码导入内容 / 水印) // 图片:先按官方建议压缩到 750×1334 内、1MB 内的 JPG,再上传云存储由云函数调用 security.imgSecCheck // 约定返回:{ status: 'pass' | 'risky' | 'error' } // - risky:命中违规,必须拦截 // - error:检测服务不可用,同样拦截(禁止静默放行) // 通过结果按内容缓存,导出时无需重复请求,显著提升导出速度 const FUNCTION_NAME = 'quickstartFunctions' const MAX_W = 750 const MAX_H = 1334 const textCache = new Map() // value -> 'pass' | 'risky' const pending = new Map() // value -> Promise function callFunction(data) { return new Promise((resolve) => { if (!wx.cloud) { resolve(null); return } wx.cloud.callFunction({ name: FUNCTION_NAME, data, success: (res) => resolve((res && res.result) || null), fail: () => resolve(null), }) }) } function normalize(result) { if (!result) return 'error' if (result.risk) return 'risky' if (result.skipped) return 'error' return 'pass' } function checkText(content, scene = 2) { const value = String(content || '').trim().slice(0, 2500) if (!value) return Promise.resolve({ status: 'pass' }) const cached = textCache.get(value) if (cached) return Promise.resolve({ status: cached, cached: true }) if (pending.has(value)) return pending.get(value) const task = callFunction({ type: 'checkMessage', content: value, scene }).then((result) => { const status = normalize(result) if (status !== 'error') textCache.set(value, status) pending.delete(value) return { status } }) pending.set(value, task) return task } // 导出前一次性校验多段文本(已缓存的直接复用,未缓存的合并为一次云函数调用) function checkTexts(items) { const list = items.map((it) => ({ value: String(it.value || '').trim().slice(0, 2500), scene: it.scene || 2 })).filter((it) => it.value) const results = list.map((it) => textCache.get(it.value)) if (results.includes('risky')) return Promise.resolve({ status: 'risky' }) const missing = list.filter((it, i) => !results[i]) if (!missing.length) return Promise.resolve({ status: 'pass' }) return callFunction({ type: 'checkMessages', items: missing }).then((res) => { if (!res || !Array.isArray(res.results)) return { status: 'error' } let status = 'pass' res.results.forEach((r, i) => { const s = normalize(r) if (s !== 'error') textCache.set(missing[i].value, s) if (s === 'risky') status = 'risky' else if (s === 'error' && status === 'pass') status = 'error' }) return { status } }) } function isTextPassed(value) { const v = String(value || '').trim() return !v || textCache.get(v) === 'pass' } // 按官方尺寸建议压缩(同时加快上传) function compressForCheck(canvas, image, path) { return new Promise((resolve) => { if (!canvas || !image) { resolve(path); return } try { const w0 = image.width || image._w const h0 = image.height || image._h const scale = Math.min(1, MAX_W / w0, MAX_H / h0) canvas.width = Math.max(1, Math.round(w0 * scale)) canvas.height = Math.max(1, Math.round(h0 * scale)) const ctx = canvas.getContext('2d') ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.drawImage(image, 0, 0, canvas.width, canvas.height) wx.canvasToTempFilePath({ canvas, fileType: 'jpg', quality: 0.82, destWidth: canvas.width, destHeight: canvas.height, success: (res) => resolve(res.tempFilePath), fail: () => resolve(path), }) } catch (e) { resolve(path) } }) } function upload(filePath) { return new Promise((resolve) => { if (!wx.cloud) { resolve(''); return } wx.cloud.uploadFile({ cloudPath: `sec-check/${Date.now()}-${Math.random().toString(36).slice(2, 10)}.jpg`, filePath, success: (res) => resolve((res && res.fileID) || ''), fail: () => resolve(''), }) }) } // image:已加载的 CanvasImage(用于压缩);path:本地临时路径 function checkImage(path, image, canvas) { if (!path) return Promise.resolve({ status: 'error' }) return compressForCheck(canvas, image, path) .then(upload) .then((fileID) => { if (!fileID) return { status: 'error' } // 云函数检测完毕后会立即删除该临时文件 return callFunction({ type: 'checkImage', fileID }).then((result) => ({ status: normalize(result) })) }) .catch(() => ({ status: 'error' })) } const MESSAGES = { risky: '内容含违规信息,请修改后再试', error: '安全检测暂不可用,请检查网络后重试', } module.exports = { checkText, checkTexts, isTextPassed, checkImage, MESSAGES }