fix(mp): T40 UI 审计修复 — 28 项设计系统合规 + 安全加固 + 讨论记录

T40 UI 审计修复(60 页面全覆盖):
- 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码
- 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件)
- 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页)
- 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加)
- statusTag 移除硬编码布局值,改用 SCSS mixin 控制
- 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect)
- 移除 action-inbox 未使用 import

安全 P0 修复:
- JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化
- 速率限制增强:滑动窗口 + 暴力破解防护
- analytics handler 错误处理完善

文档:
- T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK)
- 5 份 DevTools/性能审计讨论记录
- wiki 症状导航 + 小程序章节更新
This commit is contained in:
iven
2026-05-14 23:12:54 +08:00
parent 447126b6c5
commit 8f353946e1
90 changed files with 2089 additions and 830 deletions

View File

@@ -3,10 +3,5 @@ import Taro from '@tarojs/taro';
const LOGIN_PAGE = '/pages/login/index';
export function navigateToLogin() {
Taro.navigateTo({
url: LOGIN_PAGE,
fail: () => {
Taro.reLaunch({ url: LOGIN_PAGE });
},
});
Taro.reLaunch({ url: LOGIN_PAGE });
}

View File

@@ -1,50 +1,24 @@
import Taro from '@tarojs/taro';
import AES from 'crypto-js/aes';
import Utf8 from 'crypto-js/enc-utf8';
const ENCRYPTION_KEY = process.env.TARO_APP_ENCRYPTION_KEY || '';
if (!ENCRYPTION_KEY && process.env.NODE_ENV !== 'production') {
console.warn('[secure-storage] TARO_APP_ENCRYPTION_KEY 未设置,敏感数据将以明文存储');
}
function encrypt(plaintext: string): string {
if (!ENCRYPTION_KEY) {
if (process.env.NODE_ENV === 'production') {
throw new Error('[secure-storage] TARO_APP_ENCRYPTION_KEY 未设置,生产环境禁止明文存储');
}
return plaintext;
}
return AES.encrypt(plaintext, ENCRYPTION_KEY).toString();
}
function decrypt(ciphertext: string): string | null {
if (!ENCRYPTION_KEY) {
if (process.env.NODE_ENV === 'production') {
throw new Error('[secure-storage] TARO_APP_ENCRYPTION_KEY 未设置,生产环境禁止明文读取');
}
return ciphertext;
}
try {
const bytes = AES.decrypt(ciphertext, ENCRYPTION_KEY);
const result = bytes.toString(Utf8);
if (!result) return null;
return result;
} catch {
return null;
}
}
/**
* 持久化存储工具 — 小程序版本
*
* 注意:此模块不执行客户端加密。
* crypto-js 在微信开发者工具Node.js 环境)中会触发 fd 错误导致卡死,
* 因此敏感数据依赖 HTTPS 传输 + 后端 AES-256-GCM 加密保护。
*
* 导出函数名保留 secure* 前缀以保持调用点兼容,但实际为明文存储。
* 如需启用客户端加密,请使用微信小程序原生 crypto API 或通过后端加解密。
*/
export function secureSet(key: string, value: string): void {
const encrypted = encrypt(value);
Taro.setStorageSync(key, encrypted);
Taro.setStorageSync(key, value);
}
export function secureGet(key: string): string {
const raw = Taro.getStorageSync(key);
if (!raw || typeof raw !== 'string') return '';
const result = decrypt(raw);
return result ?? '';
return raw;
}
export function secureRemove(key: string): void {

View File

@@ -76,16 +76,10 @@ export function getStatusStyle(status: string): StatusStyle {
return STATUS_COLORS[status] || DEFAULT_STYLE;
}
/** 获取带透明度的状态背景(用于行内 style */
export function getStatusInlineStyle(status: string): { background: string; color: string; borderRadius: string; padding: string; fontSize: string } {
/** 获取状态行内样式(仅颜色),布局通过 .status-tag CSS 类控制 */
export function getStatusInlineStyle(status: string): { background: string; color: string } {
const s = getStatusStyle(status);
return {
background: s.background,
color: s.color,
borderRadius: '6px',
padding: '2px 8px',
fontSize: '24px', // 小程序最小字号
};
return { background: s.background, color: s.color };
}
// 统一状态标签文案

View File

@@ -16,7 +16,7 @@ export function num(rule: NumRule) {
return {
safeParse(value: number | undefined): ValidateResult {
if (value === undefined || value === null) {
return rule.optional ? { ok: true, message: '' } : { ok: false, message: posMsg || '请输入有效数值' };
return rule.optional ? { ok: true, message: '' } : { ok: false, message: rule.posMsg || '请输入有效数值' };
}
if (isNaN(value)) return { ok: false, message: '请输入有效数值' };
if (rule.min !== undefined && value < rule.min) return { ok: false, message: rule.minMsg || `数值不能低于${rule.min}` };