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;