feat: 审计修复 Phase 6-7 — SSE 推送/工作流补全/消息群发/前端收尾
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 6 功能补全:
- P1-3: 消息 SSE 实时推送端点 + 前端 EventSource 连接
- P1-6: ServiceTask HTTP 调用能力 (reqwest GET/POST)
- P1-7: user.deleted 事件处理 — 终止相关流程实例
- P1-8: 任务认领 (claim) 端点 + handler
- P1-9: 超时检查器发布 task.timeout 事件
- P1-15: 组织/部门名称唯一性校验 (create + update)
- P1-18: 消息群发 fan-out (role/department/all 批量投递)

Phase 7 P3-P4 收尾:
- PluginAdmin purge 按钮状态修复
- ChangePassword 最小 8 字符 + 新旧密码不同验证
- AuditLogViewer 用户名缓存 + 扩展资源类型
- InstanceMonitor 通过 definition 缓存解析 node_name
- NotificationPreferences DND 时间范围校验
This commit is contained in:
iven
2026-04-26 19:44:04 +08:00
parent 83fe89cbcd
commit b05b7c27a0
28 changed files with 996 additions and 67 deletions

View File

@@ -1,20 +1,33 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { Table, Select, Input, Tag, message } from 'antd';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs';
import { listUsers } from '../../api/users';
import { useThemeMode } from '../../hooks/useThemeMode';
const RESOURCE_TYPE_OPTIONS = [
{ value: 'user', label: '用户' },
{ value: 'role', label: '角色' },
{ value: 'position', label: '岗位' },
{ value: 'organization', label: '组织' },
{ value: 'department', label: '部门' },
{ value: 'position', label: '岗位' },
{ value: 'process_instance', label: '流程实例' },
{ value: 'process_definition', label: '流程定义' },
{ value: 'task', label: '流程任务' },
{ value: 'dictionary', label: '字典' },
{ value: 'menu', label: '菜单' },
{ value: 'setting', label: '设置' },
{ value: 'numbering_rule', label: '编号规则' },
{ value: 'patient', label: '患者' },
{ value: 'patient_tag', label: '患者标签' },
{ value: 'patient_family_member', label: '家庭成员' },
{ value: 'patient_doctor_relation', label: '医患关系' },
{ value: 'points_transaction', label: '积分流水' },
{ value: 'points_product', label: '积分商品' },
{ value: 'points_order', label: '积分订单' },
{ value: 'points_rule', label: '积分规则' },
{ value: 'offline_event', label: '线下活动' },
{ value: 'offline_event_registration', label: '活动签到' },
];
const ACTION_STYLES: Record<string, { bg: string; color: string; text: string }> = {
@@ -40,6 +53,8 @@ export default function AuditLogViewer() {
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<AuditLogQuery>({ page: 1, page_size: 20 });
const isDark = useThemeMode();
const userNameCache = useRef<Record<string, string>>({});
const cacheLoaded = useRef(false);
const fetchLogs = useCallback(async (params: AuditLogQuery) => {
setLoading(true);
@@ -53,6 +68,34 @@ export default function AuditLogViewer() {
setLoading(false);
}, []);
// 加载用户名称缓存(分页遍历所有用户)
useEffect(() => {
if (cacheLoaded.current) return;
let cancelled = false;
const loadAllUsers = async () => {
try {
let currentPage = 1;
const pageSize = 100;
let hasMore = true;
while (hasMore && !cancelled) {
const result = await listUsers(currentPage, pageSize);
for (const user of result.data) {
userNameCache.current[user.id] = user.display_name || user.username;
}
hasMore = result.data.length >= pageSize;
currentPage += 1;
}
if (!cancelled) {
cacheLoaded.current = true;
}
} catch {
// 静默失败,将显示 UUID
}
};
loadAllUsers();
return () => { cancelled = true; };
}, []);
useEffect(() => {
fetchLogs(query);
}, [query, fetchLogs]);
@@ -126,11 +169,14 @@ export default function AuditLogViewer() {
key: 'user_id',
width: 200,
ellipsis: true,
render: (v: string) => (
<span style={{ fontFamily: 'monospace', fontSize: 12, color: isDark ? '#94a3b8' : '#475569' }}>
{v}
</span>
),
render: (v: string) => {
const name = userNameCache.current[v];
return (
<span title={v} style={{ fontSize: 13, color: isDark ? '#CBD5E1' : '#334155' }}>
{name || v}
</span>
);
},
},
{
title: '时间',

View File

@@ -56,12 +56,20 @@ export default function ChangePassword() {
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度不能少于6位' },
{ min: 8, message: '密码长度不能少于8位' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('current_password') !== value) {
return Promise.resolve();
}
return Promise.reject(new Error('新密码不能与当前密码相同'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入新密码(至少6位)"
placeholder="请输入新密码(至少8位)"
/>
</Form.Item>
<Form.Item