fix: 全面 QA 审计修复 — 安全加固/代码质量/跨平台一致性/测试覆盖
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

Phase 0 安全热修复 (CRITICAL):
- 外部化微信 appid/secret 到 ERP__WECHAT__APPID/SECRET 环境变量
- 正确连接 HealthCrypto 到 ERP__HEALTH__AES_KEY/HMAC_KEY 环境变量
- 外部化小程序加密密钥到 TARO_APP_ENCRYPTION_KEY 环境变量
- 移除小程序 auth store 中的敏感信息 console.log

Phase 1 安全加固:
- 微信自动注册 display_name 添加 sanitize 防止 XSS
- 测试数据库凭据改为从 TEST_DB_URL 环境变量读取

Phase 2 代码质量:
- 提取 useThemeMode hook 消除 22 处重复暗色模式检测
- 提取共享健康常量到 constants/health.ts
- 拆分 patient_service.rs 脱敏函数到 masking.rs
- 移除未使用的 i18next/react-i18next 依赖
- 移除未使用的 api/errors.ts 和 erp-auth/anyhow 依赖

Phase 3 测试覆盖:
- 新增 5 个患者模块集成测试 (CRUD/租户隔离/验证/软删除)

Phase 4 跨平台一致性:
- 统一小程序 Patient.birthday → birth_date 匹配后端
- 统一小程序 Appointment.time_slot → start_time/end_time 匹配后端

Phase 5 架构:
- 微信登录添加多租户 TODO 注释
- 更新 wiki/infrastructure.md 环境变量文档
This commit is contained in:
iven
2026-04-25 10:00:49 +08:00
parent 07f4ba41ba
commit 945ccd64ba
56 changed files with 634 additions and 273 deletions

View File

@@ -1,10 +1,11 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Button, Input, Spin, Popconfirm, message, theme, Typography } from 'antd';
import { Button, Input, Spin, Popconfirm, message, Typography } from 'antd';
import { SendOutlined, CloseCircleOutlined, ArrowUpOutlined } from '@ant-design/icons';
import { useParams } from 'react-router-dom';
import { consultationApi, type Session, type Message } from '../../api/health/consultations';
import { StatusTag } from './components/StatusTag';
import { ImagePreview } from './components/ImagePreview';
import { useThemeMode } from '../../hooks/useThemeMode';
const PAGE_SIZE = 30;
@@ -53,10 +54,7 @@ export default function ConsultationDetail() {
const chatEndRef = useRef<HTMLDivElement>(null);
const shouldScrollRef = useRef(true);
const { token: themeToken } = theme.useToken();
const isDark =
themeToken.colorBgContainer === '#111827' ||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
// --- Fetch session info ---
const fetchSession = useCallback(async () => {

View File

@@ -8,7 +8,6 @@ import {
Space,
Popconfirm,
message,
theme,
} from 'antd';
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
@@ -18,6 +17,7 @@ import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
import { ExportButton } from './components/ExportButton';
import { useThemeMode } from '../../hooks/useThemeMode';
const STATUS_OPTIONS = [
{ value: 'waiting', label: '等待中' },
@@ -70,10 +70,7 @@ export default function ConsultationList() {
const [patientLabels, setPatientLabels] = useState<Record<string, string>>({});
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
const { token: themeToken } = theme.useToken();
const isDark =
themeToken.colorBgContainer === '#111827' ||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
// --- Data fetching ---
const fetchSessions = useCallback(async (params: { page: number; page_size: number; status?: string }) => {
@@ -84,8 +81,9 @@ export default function ConsultationList() {
setTotal(result.total);
} catch {
message.error('加载咨询列表失败');
} finally {
setLoading(false);
}
setLoading(false);
}, []);
useEffect(() => {

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useCallback } from 'react';
import { Table, DatePicker, message, theme } from 'antd';
import { Table, DatePicker, message } from 'antd';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import dayjs from 'dayjs';
import { followUpApi, type FollowUpRecord } from '../../api/health/followUp';
import { PatientSelect } from './components/PatientSelect';
import { useThemeMode } from '../../hooks/useThemeMode';
const RESULT_MAP: Record<string, string> = {
normal: '正常',
@@ -28,10 +29,7 @@ export default function FollowUpRecordList() {
const [query, setQuery] = useState<QueryParams>({ page: 1, page_size: 20 });
const [selectedPatient, setSelectedPatient] = useState<string | undefined>();
const { token: themeToken } = theme.useToken();
const isDark =
themeToken.colorBgContainer === '#111827' ||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
// --- Data fetching ---
const fetchRecords = useCallback(async (params: QueryParams) => {

View File

@@ -10,7 +10,6 @@ import {
Space,
Popconfirm,
message,
theme,
} from 'antd';
import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-design/icons';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
@@ -19,6 +18,7 @@ import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type Update
import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
import { useThemeMode } from '../../hooks/useThemeMode';
const STATUS_OPTIONS = [
{ value: 'pending', label: '待处理' },
@@ -94,10 +94,7 @@ export default function FollowUpTaskList() {
const [patientLabels, setPatientLabels] = useState<Record<string, string>>({});
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
const { token: themeToken } = theme.useToken();
const isDark =
themeToken.colorBgContainer === '#111827' ||
themeToken.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
// --- Data fetching ---
const fetchTasks = useCallback(async (params: { page: number; page_size: number; status?: string }) => {

View File

@@ -15,7 +15,6 @@ import {
Tag,
message,
Spin,
theme,
} from 'antd';
import {
ArrowLeftOutlined,
@@ -36,19 +35,8 @@ import { followUpApi } from '../../api/health/followUp';
import type { FollowUpRecord } from '../../api/health/followUp';
import { StatusTag } from './components/StatusTag';
import { VitalSignsChart } from './components/VitalSignsChart';
const GENDER_OPTIONS = [
{ value: 'male', label: '男' },
{ value: 'female', label: '女' },
{ value: 'other', label: '其他' },
];
const BLOOD_TYPE_OPTIONS = [
{ value: 'A', label: 'A 型' },
{ value: 'B', label: 'B 型' },
{ value: 'AB', label: 'AB 型' },
{ value: 'O', label: 'O 型' },
];
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS } from '../../constants/health';
import { useThemeMode } from '../../hooks/useThemeMode';
const GENDER_LABEL: Record<string, string> = {
male: '男',
@@ -63,10 +51,7 @@ export default function PatientDetail() {
const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [form] = Form.useForm();
const { token } = theme.useToken();
const isDark =
token.colorBgContainer === '#111827' ||
token.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
// 健康数据子 tab 的状态
const [vitalSigns, setVitalSigns] = useState<VitalSigns[]>([]);

View File

@@ -11,7 +11,6 @@ import {
Popconfirm,
DatePicker,
message,
theme,
} from 'antd';
import {
PlusOutlined,
@@ -26,26 +25,8 @@ import type {
UpdatePatientReq,
} from '../../api/health/patients';
import { StatusTag } from './components/StatusTag';
const GENDER_OPTIONS = [
{ value: 'male', label: '男' },
{ value: 'female', label: '女' },
{ value: 'other', label: '其他' },
];
const BLOOD_TYPE_OPTIONS = [
{ value: 'A', label: 'A 型' },
{ value: 'B', label: 'B 型' },
{ value: 'AB', label: 'AB 型' },
{ value: 'O', label: 'O 型' },
];
const STATUS_OPTIONS = [
{ value: '', label: '全部状态' },
{ value: 'active', label: '活跃' },
{ value: 'inactive', label: '停用' },
{ value: 'deceased', label: '已故' },
];
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS, STATUS_OPTIONS } from '../../constants/health';
import { useThemeMode } from '../../hooks/useThemeMode';
export default function PatientList() {
const [patients, setPatients] = useState<PatientListItem[]>([]);
@@ -57,10 +38,7 @@ export default function PatientList() {
const [modalOpen, setModalOpen] = useState(false);
const [editingPatient, setEditingPatient] = useState<PatientListItem | null>(null);
const [form] = Form.useForm();
const { token } = theme.useToken();
const isDark =
token.colorBgContainer === '#111827' ||
token.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
const navigate = useNavigate();
const fetchPatients = useCallback(
@@ -77,8 +55,9 @@ export default function PatientList() {
setTotal(result.total);
} catch {
message.error('加载患者列表失败');
} finally {
setLoading(false);
}
setLoading(false);
},
[page, searchText, statusFilter],
);

View File

@@ -8,12 +8,12 @@ import {
Tag,
Card,
message,
theme,
Typography,
} from 'antd';
import { TagsOutlined, AppstoreOutlined } from '@ant-design/icons';
import { patientApi } from '../../api/health/patients';
import type { PatientListItem } from '../../api/health/patients';
import { useThemeMode } from '../../hooks/useThemeMode';
export default function PatientTagManage() {
const [patients, setPatients] = useState<PatientListItem[]>([]);
@@ -24,10 +24,7 @@ export default function PatientTagManage() {
const [selectedPatient, setSelectedPatient] = useState<PatientListItem | null>(null);
const [tagInput, setTagInput] = useState('');
const [saving, setSaving] = useState(false);
const { token } = theme.useToken();
const isDark =
token.colorBgContainer === '#111827' ||
token.colorBgContainer === 'rgb(17, 24, 39)';
const isDark = useThemeMode();
const fetchPatients = useCallback(
async (p = page) => {
@@ -38,8 +35,9 @@ export default function PatientTagManage() {
setTotal(result.total);
} catch {
message.error('加载患者列表失败');
} finally {
setLoading(false);
}
setLoading(false);
},
[page],
);
@@ -69,8 +67,9 @@ export default function PatientTagManage() {
fetchPatients();
} catch {
message.error('标签更新失败');
} finally {
setSaving(false);
}
setSaving(false);
};
const columns = [