/** * 微信内容安全检查(医生端小程序) * 调用 xk-api /api/doctor/content-security/* 接口 */ import { req } from '@/common/js/index.js'; import { promptDoctorWxBind } from '@/common/js/wx-bind-helper.js'; /** 资料类场景(个人信息编辑) */ export const SCENE_PROFILE = 1; /** 社交日志场景(聊天) */ export const SCENE_CHAT = 4; /** 微信 msg_sec_check 单条内容上限 */ const MAX_TEXT_LEN = 2500; /** 需先绑定微信的错误文案片段 */ const NEED_BIND_MSGS = ['请先绑定微信', '用户未绑定微信', '无法获取用户手机号', '绑定微信']; /** * 从多种 reject 结构中提取错误文案(兼容 HTTP 4xx 拦截器固定 msg) */ function extractErrorMessage(err) { if (!err) { return ''; } const direct = err.msg || err.message; if (direct && direct !== 'response 300-499') { return String(direct); } const resData = err.res && err.res.data; if (resData == null) { return ''; } if (typeof resData === 'object') { return String(resData.message || resData.msg || ''); } if (typeof resData === 'string') { try { const parsed = JSON.parse(resData); return String((parsed && (parsed.message || parsed.msg)) || ''); } catch (e) { return ''; } } return ''; } /** * 判断是否为未绑定微信导致的检测失败 */ export function isNeedBindError(err) { const msg = extractErrorMessage(err); return NEED_BIND_MSGS.some((item) => msg.includes(item)); } /** * 未绑定时弹窗确认,用户同意则绑定并重试检测 */ async function handleNeedBindAndRetry(retryFn) { const confirmed = await promptDoctorWxBind(); if (!confirmed) { throw { msg: '已取消绑定,无法进行内容安全校验' }; } return retryFn(); } /** * 统一展示内容安全错误 */ export function handleSecurityError(err) { const msg = extractErrorMessage(err) || '内容含有违规信息,请修改后重试'; uni.showToast({ title: msg, icon: 'none' }); } function assertCheckOk(res) { if (res && (res.code === 0 || res.errcode === 0)) { return true; } throw { msg: (res && (res.message || res.msg)) || '内容含有违规信息,请修改后重试' }; } /** * 将字段值规范为可比较的字符串(数组用逗号拼接) */ function normalizeFieldValue(val) { if (val == null) { return ''; } if (Array.isArray(val)) { return val.filter(Boolean).map((item) => String(item).trim()).filter(Boolean).join(','); } return String(val).trim(); } /** * 与 baseline 对比,返回有变动且非空的文本列表 * @param {Object} fields 当前表单字段 { key: value } * @param {Object} baseline 初始快照 */ export function pickChangedTexts(fields, baseline = {}) { const texts = []; const base = baseline || {}; Object.keys(fields || {}).forEach((key) => { const current = normalizeFieldValue(fields[key]); const original = normalizeFieldValue(base[key]); if (current && current !== original) { texts.push(current); } }); return texts; } /** * 将多条文本用换行拼接;超过 2500 字则按字段拆成多批请求 */ export function joinTextsForCheck(texts) { const list = (Array.isArray(texts) ? texts : []) .map((item) => String(item || '').trim()) .filter(Boolean); if (!list.length) { return []; } const batches = []; let current = ''; list.forEach((text) => { const merged = current ? `${current}\n${text}` : text; if (merged.length <= MAX_TEXT_LEN) { current = merged; return; } if (current) { batches.push(current); current = ''; } if (text.length <= MAX_TEXT_LEN) { current = text; } else { // 单字段超长时按上限切分(就诊人场景几乎不会触发) for (let i = 0; i < text.length; i += MAX_TEXT_LEN) { batches.push(text.slice(i, i + MAX_TEXT_LEN)); } } }); if (current) { batches.push(current); } return batches; } /** * 表单提交前:仅检测变动字段,合并为一次或少量 msg_sec_check 调用 */ export async function checkProfileTexts(fields, baseline, scene = SCENE_PROFILE) { const changed = pickChangedTexts(fields, baseline); if (!changed.length) { return true; } const batches = joinTextsForCheck(changed); for (let i = 0; i < batches.length; i++) { await checkText(batches[i], scene); } return true; } async function requestCheckText(content, scene) { const text = String(content || '').trim(); if (!text) { return true; } const res = await req.request({ url: '/newApi/content-security/check-text', method: 'POST', data: { content: text, scene }, }); return assertCheckOk(res); } /** * 文本内容安全检查(遇未绑定微信时弹窗确认后绑定并重试一次) */ export async function checkText(content, scene = SCENE_PROFILE) { try { return await requestCheckText(content, scene); } catch (e) { if (isNeedBindError(e)) { return handleNeedBindAndRetry(() => requestCheckText(content, scene)); } throw e; } } /** * 批量文本检查(按顺序,每条独立请求;聊天等场景不适用合并检测) */ export async function checkTexts(texts, scene = SCENE_PROFILE) { const list = Array.isArray(texts) ? texts : [texts]; for (let i = 0; i < list.length; i++) { const item = list[i]; if (item) { await checkText(item, scene); } } return true; } async function requestCheckImage(filePath, scene) { return true; const res = await req.request({ url: '/newApi/content-security/check-image', type: 'upload', method: 'POST', filePath, name: 'file', formData: { scene: String(scene) }, }); return assertCheckOk(res); } /** * 本地图片安全检查(遇未绑定微信时弹窗确认后绑定并重试一次) */ export async function checkImage(filePath, scene = SCENE_PROFILE) { if (!filePath) { return true; } try { return await requestCheckImage(filePath, scene); } catch (e) { if (isNeedBindError(e)) { return handleNeedBindAndRetry(() => requestCheckImage(filePath, scene)); } throw e; } } async function requestCheckImageUrl(url, scene) { const res = await req.request({ url: '/newApi/content-security/check-image-url', method: 'POST', data: { url, scene }, }); return assertCheckOk(res); } /** * 公网图片 URL 检查(遇未绑定微信时弹窗确认后绑定并重试一次) */ export async function checkImageUrl(url, scene = SCENE_PROFILE) { const imageUrl = String(url || '').trim(); if (!imageUrl) { return true; } try { return await requestCheckImageUrl(imageUrl, scene); } catch (e) { if (isNeedBindError(e)) { return handleNeedBindAndRetry(() => requestCheckImageUrl(imageUrl, scene)); } throw e; } }