Files
xk-doctor-wx/common/js/content-security.js
2026-07-13 12:52:36 +08:00

262 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 微信内容安全检查(医生端小程序)
* 调用 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;
}
}