1. 特色方功能迭代

This commit is contained in:
李琦
2026-07-13 16:08:29 +08:00
parent b66f47dfbb
commit cae91485f6
10 changed files with 523 additions and 15 deletions

View File

@@ -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统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化

1
.mimocode/.cron-lock Normal file
View File

@@ -0,0 +1 @@
{"pid":36108,"startedAt":1783929676073}

View File

@@ -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' }); }

View File

@@ -9,6 +9,16 @@
<view class="head_cancle" v-if="orderInfo.ProductOrder.status!=0 && orderInfo.ProductOrder.cancel_status==1">
订单已取消
</view>
<!-- 就诊人信息 -->
<view class="head patient-head" v-if="patientInfo && patientInfo.name">
<view class="state">
<text>就诊人</text>
</view>
<view class="name">
{{ patientInfo.name }}
{{ patientInfo.mobile || patientInfo.mobile_tm || '' }}
</view>
</view>
<!-- 快递自提 -->
<view class="_choose"
v-if="orderInfo.ProductOrder.status==0&&orderInfo.ProductOrder.is_pay==0 && orderInfo.ProductOrder.cancel_status==0">
@@ -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;

View File

@@ -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,
// 内容安全 baselinegetInfo 后快照 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: {

View File

@@ -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',

View File

@@ -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 || [])],
};
}
},
}

View File

@@ -9,6 +9,16 @@
<view class="head_cancle" v-if="orderInfo.ProductOrder.status!=0 && orderInfo.ProductOrder.cancel_status==1">
订单已取消
</view>
<!-- 就诊人信息 -->
<view class="head patient-head" v-if="patientInfo && patientInfo.name">
<view class="state">
<text>就诊人</text>
</view>
<view class="name">
{{ patientInfo.name }}
{{ patientInfo.mobile || patientInfo.mobile_tm || '' }}
</view>
</view>
<!-- 快递自提 -->
<view class="_choose" v-if="orderInfo.ProductOrder.status==0&&orderInfo.ProductOrder.is_pay==0 && orderInfo.ProductOrder.cancel_status==0">
<text>配送方式</text>
@@ -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;

390
utils/content-security.js Normal file
View File

@@ -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);
}

View File

@@ -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;