fix: 全系统审计问题修复 — 安全/数据完整性/功能缺陷/UX (Phase 1-5)
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 1 安全热修复:
- P0-1: /uploads 文件服务添加 JWT 认证中间件(支持 header + query param)
- P0-2: analytics/batch 路由从 public 移到 protected_routes
- P0-3: plugin engine SQL 注入修复(format! → 参数化查询)
- P0-new: stats_service compute_avg_field 字段白名单 + FLOAT8 类型转换

Phase 2 数据完整性:
- P0-4: 组织删除级联检查(添加部门存在性校验)
- P0-5: 部门删除级联检查(添加岗位 + 用户存在性校验)
- P0-8: workflow on_tenant_deleted 实现 5 实体批量删除
- P0-7: 并行网关 race condition 修复(consumed → completed 原子转换)

Phase 3 P1 后端 Bug:
- P1-12: plugin host 表名消毒(使用 sanitize_identifier)
- P1-10: workflow deprecated 状态转换(published → deprecated)
- P1-11: workflow 更新验证条件(nodes/edges 任一变化即验证)
- P0-9: 小程序 .gitignore 添加 .env/.env.*/日志
- P1-19: 小程序加密密钥替换为 64 字符强密钥

Phase 4 消息模块:
- P1-5: 通知偏好 GET 路由 + handler
- P1-4: 消息模板 update/delete CRUD + version
- P2-8: mark_all_read SQL 添加 version + 1
- P2-7: markAsRead 改为乐观更新 + 失败回滚

Phase 5 前端修复:
- P2-9: 通知面板点击导航到 /messages
- P2-1: 随访任务患者名批量 ID 解析(替代 UUID 显示)
- P2-5: AppointmentList 分离 patient_id/doctor_id 分别调用 API
- P2-17: PluginMarket installed 字段修正(name → id)
- P3-3: 路由标题 fallback 改为模式匹配(支持 :id 动态路径)
- P2-15: workflow updateDefinition 添加 version 字段
- P3-9: Kanban 版本使用记录实际 version
- P2-21: secure-storage 生产环境无密钥时阻止存储
- P3-11: destroyOnHidden → destroyOnClose
- P3-13: PendingTasks 深色模式 Tag 颜色适配

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-26 19:16:23 +08:00
parent a19b097409
commit 83fe89cbcd
33 changed files with 1238 additions and 70 deletions

View File

@@ -99,26 +99,22 @@ export default function AppointmentList() {
date: dateFilter ? dateFilter.format('YYYY-MM-DD') : undefined,
});
const items = result.data;
// 批量解析患者和医生名称
const missingIds = new Set<string>();
// 批量解析患者和医生名称(分别调用对应 API
const missingPatientIds = new Set<string>();
const missingDoctorIds = new Set<string>();
items.forEach((a) => {
if (a.patient_id && !nameCache[a.patient_id]) missingIds.add(a.patient_id);
if (a.doctor_id && !nameCache[a.doctor_id]) missingIds.add(a.doctor_id);
if (a.patient_id && !nameCache[a.patient_id]) missingPatientIds.add(a.patient_id);
if (a.doctor_id && !nameCache[a.doctor_id]) missingDoctorIds.add(a.doctor_id);
});
const newCache: Record<string, string> = {};
await Promise.all(
Array.from(missingIds).map(async (id) => {
try {
const p = await patientApi.get(id);
newCache[id] = p.name;
} catch {
try {
const d = await doctorApi.get(id);
newCache[id] = d.name;
} catch { /* ignore */ }
}
await Promise.allSettled([
...Array.from(missingPatientIds).map(async (id) => {
try { const p = await patientApi.get(id); newCache[id] = p.name; } catch { /* skip */ }
}),
);
...Array.from(missingDoctorIds).map(async (id) => {
try { const d = await doctorApi.get(id); newCache[id] = d.name; } catch { /* skip */ }
}),
]);
if (Object.keys(newCache).length > 0) {
setNameCache((prev) => ({ ...prev, ...newCache }));
}

View File

@@ -63,7 +63,7 @@ export default function ArticleTagManage() {
const handleSubmit = async (values: { name: string; slug?: string; color?: string }) => {
try {
if (editing) {
await articleTagApi.update(editing.id, { name: values.name, version: editing.version });
await articleTagApi.update(editing.id, { name: values.name, version: editing.version ?? 0 });
message.success('标签更新成功');
} else {
const req: CreateTagReq = {
@@ -86,7 +86,7 @@ export default function ArticleTagManage() {
const handleDelete = async (record: ArticleTagItem) => {
try {
await articleTagApi.delete(record.id, record.version);
await articleTagApi.delete(record.id, record.version ?? 0);
message.success('标签已删除');
fetchTags();
} catch {

View File

@@ -15,6 +15,7 @@ import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-d
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import dayjs from 'dayjs';
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
import { patientApi } from '../../api/health/patients';
import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
@@ -104,6 +105,21 @@ export default function FollowUpTaskList() {
const result = await followUpApi.listTasks(params);
setTasks(result.data);
setTotal(result.total);
// Batch resolve patient names
const patientIds = [...new Set(result.data.map((t: FollowUpTask) => t.patient_id).filter(Boolean))];
const newLabels: Record<string, string> = {};
await Promise.allSettled(
patientIds.map(async (id: string) => {
try {
const p = await patientApi.get(id);
newLabels[id] = p.name;
} catch { /* skip */ }
}),
);
if (Object.keys(newLabels).length > 0) {
setPatientLabels((prev) => ({ ...prev, ...newLabels }));
}
} catch {
message.error('加载随访任务失败');
} finally {