From cae91485f61bc312a482a9f9caa77528ee5e0ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Mon, 13 Jul 2026 16:08:29 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=89=B9=E8=89=B2=E6=96=B9=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/Code-Standards.md | 10 + .mimocode/.cron-lock | 1 + subPackages/chat/chat.vue | 27 +- subPackages/my/my-drug/drug-info.vue | 17 ++ subPackages/my/mycollection.vue | 27 +- subPackages/my/myinfo-add.vue | 17 +- subPackages/my/myinfo-edit.vue | 28 +- subPackages/my/record-pay.vue | 17 ++ utils/content-security.js | 390 +++++++++++++++++++++++++++ utils/utils.js | 4 +- 10 files changed, 523 insertions(+), 15 deletions(-) create mode 100644 .cursor/rules/Code-Standards.md create mode 100644 .mimocode/.cron-lock create mode 100644 utils/content-security.js diff --git a/.cursor/rules/Code-Standards.md b/.cursor/rules/Code-Standards.md new file mode 100644 index 0000000..fa8db67 --- /dev/null +++ b/.cursor/rules/Code-Standards.md @@ -0,0 +1,10 @@ +--- +description: +alwaysApply: true +--- + +1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则 +2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行 +3. 有封装好的方法、组件需要复用,不要重复造轮子 +4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show) +5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化 diff --git a/.mimocode/.cron-lock b/.mimocode/.cron-lock new file mode 100644 index 0000000..a59dec9 --- /dev/null +++ b/.mimocode/.cron-lock @@ -0,0 +1 @@ +{"pid":36108,"startedAt":1783929676073} \ No newline at end of file diff --git a/subPackages/chat/chat.vue b/subPackages/chat/chat.vue index 732baad..8cc7dab 100644 --- a/subPackages/chat/chat.vue +++ b/subPackages/chat/chat.vue @@ -208,6 +208,7 @@ import {getChatRegisterInfoApi, sendToUserApi, upLoadChatFileApi, getDoctorInfoA import {getTransferAssistantConfigApi, getTransferConsultationAgreementInfoApi, getAgreementDetailApi} from "@/request/api/transferPrescription"; import { checkDev } from '@/utils/utils'; import { prepareImagePath } from '@/utils/image-compress.js'; +import { checkText, checkImage, handleSecurityError, SCENE_CHAT } from '@/utils/content-security.js'; // 根据环境获取上传URL const isDev = checkDev('dev'); @@ -902,9 +903,16 @@ export default { return `${hours}:${minutes}`; }, - sendTextMessage() { + async sendTextMessage() { if (!this.newMessage.trim()) return; - this.sendMessage({ type: 'text', content: this.newMessage }); + const content = this.newMessage; + try { + await checkText(content, SCENE_CHAT); + } catch (e) { + handleSecurityError(e); + return; + } + this.sendMessage({ type: 'text', content }); this.newMessage = ''; }, @@ -954,7 +962,16 @@ export default { if (type === 'image') { uni.chooseImage({ count: 9, sourceType: ['album', 'camera'], - success: res => { res.tempFilePaths.forEach(file => { this.uploadAndSendFile(file, 'image'); }); } + success: async (res) => { + for (const file of res.tempFilePaths) { + try { + await checkImage(file, SCENE_CHAT); + this.uploadAndSendFile(file, 'image'); + } catch (e) { + handleSecurityError(e); + } + } + } }); } // if (type === 'image') { @@ -990,7 +1007,9 @@ export default { const result = JSON.parse(uploadFileRes.data); if (result.code === 0 && result.result && result.result.url) { this.sendMessage({ type: type, content: result.result.url }); - } else { uni.showToast({ title: '上传失败', icon: 'none' }); } + } else { + uni.showToast({ title: result.message || '上传失败', icon: 'none' }); + } } catch (e) { uni.showToast({ title: '上传失败', icon: 'none' }); } }, fail: () => { uni.hideLoading(); uni.showToast({ title: '上传失败', icon: 'none' }); } diff --git a/subPackages/my/my-drug/drug-info.vue b/subPackages/my/my-drug/drug-info.vue index e3ec2c0..d36b33e 100644 --- a/subPackages/my/my-drug/drug-info.vue +++ b/subPackages/my/my-drug/drug-info.vue @@ -9,6 +9,16 @@ 订单已取消 + + + + 就诊人 + + + {{ patientInfo.name }} + {{ patientInfo.mobile || patientInfo.mobile_tm || '' }} + + @@ -244,6 +254,7 @@ export default { openshow: false, // 弹窗是否展示 is_decoct: 1, // 代煎服务 1确定代煎 0不代煎 infoList: [], // 门店信息 + patientInfo: null, // 就诊人信息 payWay: "", flag: true, timer: '', @@ -478,6 +489,7 @@ export default { }).then((res) => { if (res.data.errcode == 0) { this.orderInfo = res.data.data + this.patientInfo = res.data.data.patient || null this.order_id = res.data.data.ProductOrder.id uni.setStorageSync('userWay_order_id', res.data.data.ProductOrder.id) uni.setStorageSync('delivery_method', res.data.data.ProductOrder.delivery_method) @@ -844,6 +856,11 @@ export default { justify-content: center; padding: 0 40rpx; + &.patient-head { + height: auto; + min-height: 120rpx; + } + .state { font-size: 28rpx; font-family: PingFang SC-Bold, PingFang SC; diff --git a/subPackages/my/mycollection.vue b/subPackages/my/mycollection.vue index 6cabe4d..db583f6 100644 --- a/subPackages/my/mycollection.vue +++ b/subPackages/my/mycollection.vue @@ -56,6 +56,7 @@ getEditUser, getuserInfo } from '../../request/api/api' + import { checkProfileTexts, checkImage, handleSecurityError, SCENE_PROFILE } from '@/utils/content-security.js'; export default { data() { return { @@ -67,7 +68,9 @@ sex: "", mobile: "", type: 'text', - border: false + border: false, + // 内容安全 baseline:getInfo 后快照 nickname + _securityBaseline: {}, } }, onLoad(e) { @@ -87,6 +90,7 @@ this.sex = res.data.data.sex this.mobile = res.data.data.user.mobile this.idCard = res.data.data.user.idcard + this._securityBaseline = { name: this.name } } }) }, @@ -118,7 +122,7 @@ count: 1, //默认9 sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], - success: (res) => { + success: async (res) => { console.log('图片选择成功') uni.showLoading({ title: '请稍后...' @@ -129,10 +133,6 @@ console.log(resSize) if (resSize > 1048576) { uni.hideLoading() - // uni.showToast({ - // title: "上传图片大小不能超过1MB", - // icon: 'error' - // }); this.$refs.uToast.show({ title:"上传图片大小不能超过1MB", type: 'default', @@ -141,6 +141,13 @@ return } + try { + await checkImage(tempFiles[0]['path'], SCENE_PROFILE); + } catch (e) { + uni.hideLoading(); + handleSecurityError(e); + return; + } uni.uploadFile({ url: 'https://app.xiaokang88.com/member/v1/attachment/upload', filePath: res.tempFiles[0]['path'], @@ -195,7 +202,13 @@ } }) }, - toAppion() { + async toAppion() { + try { + await checkProfileTexts({ name: this.name }, this._securityBaseline, SCENE_PROFILE); + } catch (e) { + handleSecurityError(e); + return; + } getEditUser({ method: "post", data: { diff --git a/subPackages/my/myinfo-add.vue b/subPackages/my/myinfo-add.vue index 6b6d5d9..a88aed7 100644 --- a/subPackages/my/myinfo-add.vue +++ b/subPackages/my/myinfo-add.vue @@ -268,6 +268,7 @@ userList, userRelation } from '../../request/api/api'; + import { checkProfileTexts, handleSecurityError, SCENE_PROFILE } from '@/utils/content-security.js'; export default { data() { return { @@ -416,6 +417,8 @@ allergic_history: [], person_history: [], family_history: [], + // 内容安全 baseline:新增页为空对象,仅提交变动字段 + _securityBaseline: {}, is_default: 0, guardianType: '1', guardianPatientName: '', @@ -677,10 +680,22 @@ return res.data.message || res.data.msg || '保存失败'; }, // 添加就诊人 - getAdduser() { + async getAdduser() { if (!this.validatePatientAge() || !this.validateGuardian()) { return; } + try { + await checkProfileTexts({ + name: this.names, + visitDesc: this.visitDesc, + allergic_history: this.allergic_history, + person_history: this.person_history, + family_history: this.family_history, + }, this._securityBaseline, SCENE_PROFILE); + } catch (e) { + handleSecurityError(e); + return; + } const payload = this.buildGuardianPayload({ store_id: uni.getStorageSync('store_id') || '11001', is_default: this.checked ? '1' : '0', diff --git a/subPackages/my/myinfo-edit.vue b/subPackages/my/myinfo-edit.vue index 8692ac3..0d06777 100644 --- a/subPackages/my/myinfo-edit.vue +++ b/subPackages/my/myinfo-edit.vue @@ -245,6 +245,7 @@ userList, userRelation } from '../../request/api/api'; + import { checkProfileTexts, handleSecurityError, SCENE_PROFILE } from '@/utils/content-security.js'; export default { data() { return { @@ -394,6 +395,8 @@ allergic_history: [], person_history: [], family_history: [], + // 内容安全 baseline:详情加载完成后快照 + _securityBaseline: {}, heathList: [], // 健康信息 guardianType: '1', guardianPatientName: '', @@ -817,10 +820,22 @@ }, // 添加就诊人 - getAdduser() { + async getAdduser() { if (!this.validatePatientAge() || !this.validateGuardian()) { return; } + try { + await checkProfileTexts({ + name: this.names, + visitDesc: this.visitDesc, + allergic_history: this.allergic_history, + person_history: this.person_history, + family_history: this.family_history, + }, this._securityBaseline, SCENE_PROFILE); + } catch (e) { + handleSecurityError(e); + return; + } const payload = this.buildGuardianPayload({ store_id: uni.getStorageSync('store_id') || '11001', id: this.addid, @@ -927,7 +942,18 @@ } }) } + this.updateSecurityBaseline() }) + }, + // 快照当前表单,供内容安全变动检测使用 + updateSecurityBaseline() { + this._securityBaseline = { + name: this.names, + visitDesc: this.visitDesc, + allergic_history: [...(this.allergic_history || [])], + person_history: [...(this.person_history || [])], + family_history: [...(this.family_history || [])], + }; } }, } diff --git a/subPackages/my/record-pay.vue b/subPackages/my/record-pay.vue index 45bb7a9..57d602d 100644 --- a/subPackages/my/record-pay.vue +++ b/subPackages/my/record-pay.vue @@ -9,6 +9,16 @@ 订单已取消 + + + + 就诊人 + + + {{ patientInfo.name }} + {{ patientInfo.mobile || patientInfo.mobile_tm || '' }} + + 配送方式 @@ -252,6 +262,7 @@ openshow: false, // 弹窗是否展示 is_decoct: 1, // 代煎服务 1确定代煎 0不代煎 infoList: [], // 门店信息 + patientInfo: null, // 就诊人信息 payWay: "", flag: true, // 新增:物流提示信息 @@ -456,6 +467,7 @@ // console.log(res, 'info'); if (res.data.errcode == 0) { this.orderInfo = res.data.data + this.patientInfo = res.data.data.patient || null this.chinese = res.data.data.chinese this.west = res.data.data.west this.granular = res.data.data.granular @@ -829,6 +841,11 @@ justify-content: center; padding: 0 40rpx; + &.patient-head { + height: auto; + min-height: 120rpx; + } + .state { font-size: 28rpx; font-family: PingFang SC-Bold, PingFang SC; diff --git a/utils/content-security.js b/utils/content-security.js new file mode 100644 index 0000000..1fb0664 --- /dev/null +++ b/utils/content-security.js @@ -0,0 +1,390 @@ +/** + + * 微信内容安全检查(患者端小程序) + + * 调用 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) { + + if (res && res.code === 0) { + + return true; + + } + + throw { msg: (res && res.message) || '内容含有违规信息,请修改后重试' }; + +} + + + +/** + + * 将字段值规范为可比较的字符串(数组用逗号拼接) + + */ + +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); + +} + diff --git a/utils/utils.js b/utils/utils.js index 62c7394..d0b9d95 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -6,8 +6,8 @@ export function checkDev(key = '') { // 判断某些模块是否开启 switch (key) { case 'dev': - return false; - // return true; + // return false; + return true; case 'open-im': // return false; return true;