262 lines
6.8 KiB
JavaScript
262 lines
6.8 KiB
JavaScript
/**
|
||
* 微信内容安全检查(医生端小程序)
|
||
* 调用 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;
|
||
}
|
||
}
|