2026-07-13 16:08:29 +08:00
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 微信内容安全检查(患者端小程序)
|
|
|
|
|
|
|
|
|
|
|
|
* 调用 xk-api /api/mobile/content-security/* 接口
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { post } from '@/request/api/http.js';
|
|
|
|
|
|
|
|
|
|
|
|
import { checkDev } from '@/utils/utils';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 资料类场景(个人信息编辑) */
|
|
|
|
|
|
|
|
|
|
|
|
export const SCENE_PROFILE = 1;
|
|
|
|
|
|
|
|
|
|
|
|
/** 社交日志场景(聊天) */
|
|
|
|
|
|
|
|
|
|
|
|
export const SCENE_CHAT = 4;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 微信 msg_sec_check 单条内容上限 */
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_TEXT_LEN = 2500;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 获取当前环境 xkApi 基址
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
function getXkApiBase() {
|
|
|
|
|
|
|
|
|
|
|
|
const env = checkDev();
|
|
|
|
|
|
|
|
|
|
|
|
const map = {
|
|
|
|
|
|
|
|
|
|
|
|
prod: 'https://api.xiaokang88.com/api/mobile',
|
|
|
|
|
|
|
|
|
|
|
|
test: 'https://test.xk.api.nailaoyun.cn/api/mobile',
|
|
|
|
|
|
|
|
|
|
|
|
dev: 'http://127.0.0.1:18001/api/mobile',
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return map[env] || map.prod;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getAuthHeader() {
|
|
|
|
|
|
|
|
|
|
|
|
let token = uni.getStorageSync('token') || '';
|
|
|
|
|
|
|
|
|
|
|
|
token = String(token).replace(/^Bearer\s+/i, '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
return token ? { authorization: `Bearer ${token}` } : {};
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 统一展示内容安全错误
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
export function handleSecurityError(err) {
|
|
|
|
|
|
|
|
|
|
|
|
const msg = (err && (err.msg || err.message)) || '内容含有违规信息,请修改后重试';
|
|
|
|
|
|
|
|
|
|
|
|
uni.showToast({ title: msg, icon: 'none' });
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function assertCheckOk(res) {
|
|
|
|
|
|
|
2026-07-19 16:08:04 +08:00
|
|
|
|
if (res && res.data.code === 0) {
|
2026-07-13 16:08:29 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:08:04 +08:00
|
|
|
|
throw { msg: (res && res.data.message) || '内容含有违规信息,请修改后重试' };
|
2026-07-13 16:08:29 +08:00
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 将字段值规范为可比较的字符串(数组用逗号拼接)
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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 对比,返回有变动且非空的文本列表
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 文本内容安全检查
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
export async function checkText(content, scene = SCENE_PROFILE) {
|
|
|
|
|
|
|
|
|
|
|
|
const text = String(content || '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const res = await post('/content-security/check-text', { content: text, scene }, 3);
|
|
|
|
|
|
|
|
|
|
|
|
return assertCheckOk(res);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 批量文本检查(按顺序,每条独立请求)
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 本地图片安全检查(选图后、上传前)
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
export function checkImage(filePath, scene = SCENE_PROFILE) {
|
|
|
|
|
|
|
|
|
|
|
|
if (!filePath) {
|
|
|
|
|
|
|
|
|
|
|
|
return Promise.resolve(true);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const baseUrl = getXkApiBase();
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
|
|
|
|
|
|
uni.uploadFile({
|
|
|
|
|
|
|
|
|
|
|
|
url: `${baseUrl}/content-security/check-image`,
|
|
|
|
|
|
|
|
|
|
|
|
filePath,
|
|
|
|
|
|
|
|
|
|
|
|
name: 'file',
|
|
|
|
|
|
|
|
|
|
|
|
formData: { scene: String(scene) },
|
|
|
|
|
|
|
|
|
|
|
|
header: getAuthHeader(),
|
|
|
|
|
|
|
|
|
|
|
|
success: (uploadRes) => {
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
|
|
const result = JSON.parse(uploadRes.data);
|
|
|
|
|
|
|
|
|
|
|
|
assertCheckOk(result);
|
|
|
|
|
|
|
|
|
|
|
|
resolve(true);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
|
|
|
|
reject(e.msg ? e : { msg: '图片安全检查失败' });
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
fail: () => reject({ msg: '图片安全检查失败' }),
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
|
|
* 公网图片 URL 检查(OSS 直传登记前可选)
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
export async function checkImageUrl(url, scene = SCENE_PROFILE) {
|
|
|
|
|
|
|
|
|
|
|
|
const imageUrl = String(url || '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!imageUrl) {
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const res = await post('/content-security/check-image-url', { url: imageUrl, scene }, 3);
|
|
|
|
|
|
|
|
|
|
|
|
return assertCheckOk(res);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|