Files
hms/docs/superpowers/plans/2026-05-01-tri-platform-audit-fix-plan.md

790 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 三端联调审计问题修复实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复三端联调审计发现的 15 项问题,恢复系统统一性和可用性。
**Architecture:** 按优先级分 3 个 Phase 执行。Phase 1 快速修复基础设施和统一性问题(#1-#6Phase 2 补全用户体验(#7-#11Phase 3 实现功能闭环(#12-#15)。每个 Task 独立可提交。
**Tech Stack:** Rust/Axum/SeaORM (后端) · React 19/Ant Design 6/Zustand (Web 前端) · Taro 4.2 (小程序) · PostgreSQL (数据库)
**Spec:** `docs/superpowers/specs/2026-05-01-tri-platform-audit-fix-design.md`
---
## Chunk 1: Phase 1 — 快速修复 (#1-#6)
### Task 1: erp-plugin 测试编译失败修复
**Files:**
- Modify: `crates/erp-plugin/src/plugin_validator.rs:227`
- [ ] **Step 1: 添加缺失的 import**
在第 227 行 `use super::*;` 之后添加一行:
```rust
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::parse_manifest;
```
- [ ] **Step 2: 验证编译通过**
Run: `cargo test -p erp-plugin --no-run`
Expected: 编译成功,无错误
- [ ] **Step 3: 运行测试**
Run: `cargo test -p erp-plugin`
Expected: 所有测试通过
- [ ] **Step 4: 提交**
```bash
git add crates/erp-plugin/src/plugin_validator.rs
git commit -m "fix(plugin): 修复测试编译失败 — 补充 parse_manifest 导入"
```
---
### Task 2: 后端 ThemeResp 增加品牌字段
**Files:**
- Modify: `crates/erp-config/src/dto.rs:220-227`
- Modify: `crates/erp-config/src/handler/theme_handler.rs:15-21`
- Modify: `crates/erp-config/src/handler/theme_handler.rs:110-135`(测试)
- [ ] **Step 1: 扩展 ThemeResp DTO**
`crates/erp-config/src/dto.rs` 第 227 行 `sidebar_style` 字段之后添加 4 个品牌字段:
```rust
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
pub struct ThemeResp {
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sidebar_style: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brand_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brand_slogan: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brand_features: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brand_copyright: Option<String>,
}
```
- [ ] **Step 2: 更新 default_theme 函数**
`crates/erp-config/src/handler/theme_handler.rs` 第 15-21 行,更新 `default_theme()` 函数:
```rust
fn default_theme() -> ThemeResp {
ThemeResp {
primary_color: None,
logo_url: None,
sidebar_style: None,
brand_name: Some("HMS 健康管理平台".into()),
brand_slogan: Some("新一代健康管理平台".into()),
brand_features: Some("患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()),
brand_copyright: Some("HMS 健康管理平台 · ©汕头市智界科技有限公司".into()),
}
}
```
- [ ] **Step 3: 更新测试**
`crates/erp-config/src/handler/theme_handler.rs` 的测试模块中,更新 `default_theme_all_fields_none` 测试(因为 default_theme 现在品牌字段有值了)和 `theme_resp_serde_roundtrip` 测试:
```rust
#[test]
fn default_theme_has_brand_defaults() {
let theme = default_theme();
assert!(theme.primary_color.is_none());
assert!(theme.logo_url.is_none());
assert!(theme.sidebar_style.is_none());
assert_eq!(theme.brand_name, Some("HMS 健康管理平台".to_string()));
assert_eq!(theme.brand_slogan, Some("新一代健康管理平台".to_string()));
assert!(theme.brand_features.is_some());
assert!(theme.brand_copyright.is_some());
}
#[test]
fn theme_resp_serde_roundtrip() {
let theme = ThemeResp {
primary_color: Some("#1890ff".to_string()),
logo_url: None,
sidebar_style: Some("dark".to_string()),
brand_name: Some("测试平台".to_string()),
brand_slogan: None,
brand_features: None,
brand_copyright: None,
};
let json = serde_json::to_string(&theme).unwrap();
let back: ThemeResp = serde_json::from_str(&json).unwrap();
assert_eq!(back.primary_color, Some("#1890ff".to_string()));
assert_eq!(back.brand_name, Some("测试平台".to_string()));
assert!(back.brand_slogan.is_none());
}
```
- [ ] **Step 4: 添加公开品牌信息端点**
`crates/erp-config/src/handler/theme_handler.rs` 中新增公开端点 handler无需认证
```rust
/// 品牌信息公开响应(不含内部配置)
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
pub struct PublicBrandResp {
pub brand_name: String,
pub brand_slogan: String,
pub brand_features: String,
pub brand_copyright: String,
}
/// GET /api/v1/public/brand — 公开品牌信息(无需认证)
pub async fn get_public_brand() -> JsonResponse<ApiResponse<PublicBrandResp>> {
let defaults = default_theme();
JsonResponse(ApiResponse::ok(PublicBrandResp {
brand_name: defaults.brand_name.unwrap_or_else(|| "HMS 健康管理平台".into()),
brand_slogan: defaults.brand_slogan.unwrap_or_else(|| "新一代健康管理平台".into()),
brand_features: defaults.brand_features.unwrap_or_else(|| "患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()),
brand_copyright: defaults.brand_copyright.unwrap_or_else(|| "HMS 健康管理平台 · ©汕头市智界科技有限公司".into()),
}))
}
```
> **注意**: 此端点需要在 `erp-server` 的公开路由中注册。先完成此 Task 的代码修改,路由注册在单独步骤中处理。公开端点暂时返回默认值,后续可通过 settings 读取租户自定义配置。
- [ ] **Step 5: 验证编译**
Run: `cargo check`
Expected: 编译通过
- [ ] **Step 6: 注册公开路由**
`crates/erp-server/src/main.rs`(或对应的路由注册文件)中,找到公开路由组(通常包含 `/login``/register` 等),添加:
```rust
// 在公开路由区域添加
.route("/api/v1/public/brand", get(config_handler::get_public_brand))
```
需要导入 handler`use erp_config::handler as config_handler;`(或按项目已有模式导入)。
> 在 `erp-config/src/module.rs` 的 `ErpModule::public_routes()` 中注册此路由。
- [ ] **Step 7: 验证端点可用**
Run: `cargo run`(启动后端)
Run: `curl http://localhost:3000/api/v1/public/brand`
Expected: `{"success":true,"data":{"brand_name":"HMS 健康管理平台",...}}`
- [ ] **Step 8: 提交**
```bash
git add crates/erp-config/src/dto.rs crates/erp-config/src/handler/theme_handler.rs
git commit -m "feat(config): ThemeResp 增加品牌字段 + 公开品牌信息端点"
```
---
### Task 3: 前端主题设置联动 — API + Store + 主题设置页面
**Files:**
- Modify: `apps/web/src/api/themes.ts`
- Modify: `apps/web/src/stores/app.ts`
- Modify: `apps/web/src/pages/settings/ThemeSettings.tsx`
- [ ] **Step 1: 扩展 ThemeConfig 接口**
修改 `apps/web/src/api/themes.ts`
```typescript
import client from './client';
export interface ThemeConfig {
primary_color?: string;
logo_url?: string;
sidebar_style?: 'light' | 'dark';
brand_name?: string;
brand_slogan?: string;
brand_features?: string;
brand_copyright?: string;
}
export async function getTheme() {
const { data } = await client.get<{ success: boolean; data: ThemeConfig }>(
'/config/themes',
);
return data.data;
}
export async function updateTheme(theme: ThemeConfig) {
const { data } = await client.put<{ success: boolean; data: ThemeConfig }>(
'/config/themes',
theme,
);
return data.data;
}
export interface BrandConfig {
brand_name: string;
brand_slogan: string;
brand_features: string;
brand_copyright: string;
}
const BRAND_DEFAULTS: BrandConfig = {
brand_name: 'HMS 健康管理平台',
brand_slogan: '新一代健康管理平台',
brand_features: '患者管理 · 健康监测 · 随访管理 · AI 智能分析',
brand_copyright: 'HMS 健康管理平台 · ©汕头市智界科技有限公司',
};
export async function getPublicBrand(): Promise<BrandConfig> {
try {
const res = await fetch('/api/v1/public/brand');
const json = await res.json();
if (json?.success && json?.data) return json.data;
} catch {}
return BRAND_DEFAULTS;
}
```
- [ ] **Step 2: 扩展 app store — 添加 themeConfig 缓存**
修改 `apps/web/src/stores/app.ts`,添加 themeConfig 和 loadThemeConfig
```typescript
import { create } from 'zustand';
import type { ThemeConfig } from '../api/themes';
import { getTheme } from '../api/themes';
export type ThemeName = 'blue' | 'warm' | 'dark' | 'emerald';
// ... (THEME_OPTIONS 保持不变)
const STORAGE_KEY = 'hms-theme';
const THEME_CONFIG_KEY = 'hms-theme-config';
function loadTheme(): ThemeName {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && THEME_OPTIONS.some((t) => t.key === saved)) return saved as ThemeName;
} catch {}
return 'blue';
}
interface AppState {
theme: ThemeName;
sidebarCollapsed: boolean;
themeConfig: ThemeConfig | null;
toggleSidebar: () => void;
setTheme: (theme: ThemeName) => void;
loadThemeConfig: () => Promise<void>;
}
export const useAppStore = create<AppState>((set) => ({
theme: loadTheme(),
sidebarCollapsed: false,
themeConfig: null,
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
setTheme: (theme) => {
try { localStorage.setItem(STORAGE_KEY, theme); } catch {}
set({ theme });
},
loadThemeConfig: async () => {
try {
const config = await getTheme();
try { localStorage.setItem(THEME_CONFIG_KEY, JSON.stringify(config)); } catch {}
set({ themeConfig: config });
} catch {}
},
}));
```
- [ ] **Step 3: 在 MainLayout mount 时加载 themeConfig**
修改 `apps/web/src/layouts/MainLayout.tsx`,在组件中添加 loadThemeConfig 调用:
在组件函数体内,现有 useEffect 附近添加:
```typescript
const loadThemeConfig = useAppStore((s) => s.loadThemeConfig);
useEffect(() => {
loadThemeConfig();
}, [loadThemeConfig]);
```
- [ ] **Step 4: 扩展 ThemeSettings 页面**
修改 `apps/web/src/pages/settings/ThemeSettings.tsx`,在现有表单的保存按钮之前,添加品牌信息区域:
```tsx
import { useEffect, useState, useCallback } from 'react';
import { Form, Input, Select, Button, ColorPicker, message, Typography, Divider } from 'antd';
import { getTheme, updateTheme } from '../../api/themes';
export default function ThemeSettings() {
const [form] = Form.useForm();
const [, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const fetchTheme = useCallback(async () => {
setLoading(true);
try {
const theme = await getTheme();
form.setFieldsValue({
primary_color: theme.primary_color || '#1677ff',
logo_url: theme.logo_url || '',
sidebar_style: theme.sidebar_style || 'light',
brand_name: theme.brand_name || '',
brand_slogan: theme.brand_slogan || '',
brand_features: theme.brand_features || '',
brand_copyright: theme.brand_copyright || '',
});
} catch {
form.setFieldsValue({
primary_color: '#1677ff',
logo_url: '',
sidebar_style: 'light',
});
}
setLoading(false);
}, [form]);
useEffect(() => { fetchTheme(); }, [fetchTheme]);
const handleSave = async (values: Record<string, unknown>) => {
setSaving(true);
try {
const primaryColor = typeof values.primary_color === 'string'
? values.primary_color
: (values.primary_color as { toHexString?: () => string })?.toHexString?.() ?? String(values.primary_color);
await updateTheme({
primary_color: primaryColor,
logo_url: values.logo_url as string,
sidebar_style: values.sidebar_style as 'light' | 'dark',
brand_name: (values.brand_name as string) || undefined,
brand_slogan: (values.brand_slogan as string) || undefined,
brand_features: (values.brand_features as string) || undefined,
brand_copyright: (values.brand_copyright as string) || undefined,
});
message.success('主题设置已保存');
} catch (err: unknown) {
const errorMsg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '保存失败';
message.error(errorMsg);
}
setSaving(false);
};
return (
<div>
<Typography.Title level={5} style={{ marginBottom: 16 }}></Typography.Title>
<Form form={form} onFinish={handleSave} layout="vertical" style={{ maxWidth: 480 }}>
<Form.Item name="primary_color" label="主色调">
<ColorPicker format="hex" />
</Form.Item>
<Form.Item name="logo_url" label="Logo URL">
<Input placeholder="https://example.com/logo.png" />
</Form.Item>
<Form.Item name="sidebar_style" label="侧边栏风格">
<Select options={[{ label: '亮色', value: 'light' }, { label: '暗色', value: 'dark' }]} />
</Form.Item>
<Divider></Divider>
<Form.Item name="brand_name" label="品牌名称">
<Input placeholder="HMS 健康管理平台" />
</Form.Item>
<Form.Item name="brand_slogan" label="品牌标语">
<Input placeholder="新一代健康管理平台" />
</Form.Item>
<Form.Item name="brand_features" label="功能亮点">
<Input placeholder="患者管理 · 健康监测 · 随访管理 · AI 智能分析" />
</Form.Item>
<Form.Item name="brand_copyright" label="版权信息">
<Input placeholder="HMS 健康管理平台 · ©汕头市智界科技有限公司" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
</div>
);
}
```
- [ ] **Step 5: 验证前端编译**
Run: `cd apps/web && pnpm build`
Expected: 构建成功
- [ ] **Step 6: 提交**
```bash
git add apps/web/src/api/themes.ts apps/web/src/stores/app.ts apps/web/src/pages/settings/ThemeSettings.tsx
git commit -m "feat(web): 主题设置增加品牌信息字段 + store 缓存 + 公开品牌 API"
```
---
### Task 4: Login.tsx 和 MainLayout.tsx 从主题配置读取品牌信息
**Files:**
- Modify: `apps/web/src/pages/Login.tsx:35-56,105-107`
- Modify: `apps/web/src/layouts/MainLayout.tsx:434,526-528`
- [ ] **Step 1: 修改 Login.tsx — 从公开端点读取品牌信息**
`apps/web/src/pages/Login.tsx` 中:
1. 添加 `useEffect, useState` 导入(如尚未导入)
2. 添加 brandConfig 状态和 useEffect 获取逻辑
3. 替换硬编码品牌文字
在组件函数体开头添加:
```typescript
const [brandConfig, setBrandConfig] = useState<{
brand_name?: string;
brand_slogan?: string;
brand_features?: string;
brand_copyright?: string;
} | null>(null);
useEffect(() => {
const cached = localStorage.getItem('hms-theme-config');
if (cached) {
try { setBrandConfig(JSON.parse(cached)); } catch {}
}
fetch('/api/v1/public/brand')
.then(res => res.json())
.then(data => {
if (data?.success && data?.data) {
setBrandConfig(data.data);
localStorage.setItem('hms-theme-config', JSON.stringify(data.data));
}
})
.catch(() => {});
}, []);
```
替换第 40-42 行硬编码文字:
```tsx
<h1 className="brand-title">{brandConfig?.brand_name || 'HMS 健康管理平台'}</h1>
<p className="brand-desc">{brandConfig?.brand_slogan || '新一代健康管理平台'}</p>
<p className="brand-sub-desc">{brandConfig?.brand_features || '患者管理 · 健康监测 · 随访管理 · AI 智能分析'}</p>
```
替换第 106 行版权文字:
```tsx
<div className="form-footer">
{brandConfig?.brand_copyright || 'HMS 健康管理平台 · ©汕头市智界科技有限公司'}
</div>
```
- [ ] **Step 2: 修改 MainLayout.tsx — Footer 和 Logo 从配置读取**
`apps/web/src/layouts/MainLayout.tsx` 中:
1. 获取 themeConfig在组件函数体内添加
```typescript
const themeConfig = useAppStore((s) => s.themeConfig);
```
2. 替换第 434 行侧边栏 Logo 文字:
```tsx
<span className="erp-sidebar-logo-text">{themeConfig?.brand_name || 'HMS 健康'}</span>
```
3. 替换第 527 行 Footer 文字:
```tsx
<Footer className="erp-footer">
{themeConfig?.brand_copyright || 'HMS 健康管理平台'}
</Footer>
```
- [ ] **Step 3: 验证前端编译**
Run: `cd apps/web && pnpm build`
Expected: 构建成功
- [ ] **Step 4: 浏览器验证**
1. 打开登录页,确认显示 "HMS 健康管理平台"(默认值)
2. 登录后进入系统设置 → 主题设置
3. 修改品牌名称为 "XX 医院健康管理"
4. 保存后刷新页面,确认侧边栏/页脚更新
5. 登出后确认登录页显示新品牌名称
- [ ] **Step 5: 提交**
```bash
git add apps/web/src/pages/Login.tsx apps/web/src/layouts/MainLayout.tsx
git commit -m "feat(web): Login/MainLayout 从主题配置读取品牌信息"
```
---
### Task 5: 告警和行动收件箱侧边栏菜单入口
**Files:**
- Modify: `apps/web/src/layouts/MainLayout.tsx:1-32,51-75`
- [ ] **Step 1: 在 MainLayout.tsx 补充图标导入**
`apps/web/src/layouts/MainLayout.tsx` 的 import 区域(第 31 行 `BarChartOutlined` 之后)添加:
```typescript
AlertOutlined,
BellOutlined,
ControlOutlined,
InboxOutlined,
ApiOutlined,
ReadOutlined,
ExperimentOutlined,
```
- [ ] **Step 2: 在 iconMap 中补充图标映射**
`iconMap` 对象(第 74 行 `BarChartOutlined` 之后)添加:
```typescript
AlertOutlined: <AlertOutlined />,
BellOutlined: <BellOutlined />,
ControlOutlined: <ControlOutlined />,
InboxOutlined: <InboxOutlined />,
ApiOutlined: <ApiOutlined />,
ReadOutlined: <ReadOutlined />,
ExperimentOutlined: <ExperimentOutlined />,
```
- [ ] **Step 3: 验证前端编译**
Run: `cd apps/web && pnpm build`
Expected: 构建成功
- [ ] **Step 4: 提交**
```bash
git add apps/web/src/layouts/MainLayout.tsx
git commit -m "fix(web): 补充告警/行动收件箱/设备等菜单图标映射"
```
---
### Task 6: 行动收件箱菜单种子迁移
**Files:**
- Create: `crates/erp-server/migration/src/m20260501_000100_seed_action_inbox_menu.rs`
- Modify: `crates/erp-server/migration/src/lib.rs:207`
- [ ] **Step 1: 创建迁移文件**
创建 `crates/erp-server/migration/src/m20260501_000100_seed_action_inbox_menu.rs`
```rust
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
// 插入行动收件箱菜单
db.execute_unprepared(
r#"
INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order,
created_at, updated_at, is_active)
SELECT
'b0000003-0000-7000-8000-000000000020'::uuid,
t.id,
(SELECT id FROM menus WHERE path = '/health' AND tenant_id = t.id LIMIT 1),
'行动收件箱',
'/health/action-inbox',
'InboxOutlined',
36,
NOW(), NOW(), true
FROM tenants t
WHERE NOT EXISTS (
SELECT 1 FROM menus
WHERE path = '/health/action-inbox' AND tenant_id = t.id
)
"#,
)
.await?;
// 关联权限码
db.execute_unprepared(
r#"
INSERT INTO role_permissions (role_id, permission_code, tenant_id)
SELECT r.id, 'health.action-inbox.list', t.id
FROM tenants t
JOIN roles r ON r.tenant_id = t.id AND r.code = 'admin'
WHERE NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.permission_code = 'health.action-inbox.list'
AND rp.role_id = r.id
)
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
"DELETE FROM menus WHERE path = '/health/action-inbox'",
)
.await?;
Ok(())
}
}
```
- [ ] **Step 2: 在 lib.rs 中注册迁移**
`crates/erp-server/migration/src/lib.rs` 中:
1. 添加模块声明(在文件末尾 `mod` 声明区域):
```rust
mod m20260501_000100_seed_action_inbox_menu;
```
2. 在 migration list 的最后一项 `m20260501_000099_create_ai_risk_threshold::Migration` 之后添加:
```rust
Box::new(m20260501_000100_seed_action_inbox_menu::Migration),
```
- [ ] **Step 3: 验证编译**
Run: `cargo check`
Expected: 编译通过
- [ ] **Step 4: 提交**
```bash
git add crates/erp-server/migration/src/m20260501_000100_seed_action_inbox_menu.rs crates/erp-server/migration/src/lib.rs
git commit -m "feat(migration): 行动收件箱菜单种子数据 + 权限关联"
```
---
### Task 7: 危急值告警端点 500 修复
**Files:**
- Modify: `crates/erp-health/src/service/critical_alert_service.rs`
- [ ] **Step 1: 添加 tracing 日志**
`crates/erp-health/src/service/critical_alert_service.rs``list_pending_alerts` 函数中,找到查询执行的 `.await`,在错误处理链中添加 tracing
```rust
// 找到类似以下代码,添加 tracing::error!
.all(&state.db)
.await
.map_err(|e| {
tracing::error!(error = %e, "查询危急值告警列表失败");
HealthError::DbError(e.to_string())
})?;
```
`get_alert` 函数同理添加。
- [ ] **Step 2: 验证数据库状态并测试**
启动后端 `cargo run`,用 curl 测试端点,查看 tracing 日志确认根因。
根据日志输出决定下一步:
- 如果是表不存在 → 确认迁移 m000090 是否执行
- 如果是 RLS 策略缺失 → 创建 Task 7b RLS 补齐迁移
- 如果是其他错误 → 根据日志修复
> **此步骤需要运行时验证,根据实际错误决定是否需要创建 RLS 补齐迁移。**
- [ ] **Step 3: 提交**
```bash
git add crates/erp-health/src/service/critical_alert_service.rs
git commit -m "fix(health): 危急值告警查询添加 tracing 日志"
```
---
### Task 8: domain_events 堆积清理
**Files:**
- Modify: `crates/erp-server/src/tasks.rs:26`
- [ ] **Step 1: 修改事件保留期**
`crates/erp-server/src/tasks.rs` 第 26 行,将 90 天改为 7 天:
```rust
// 修改前
"SELECT cleanup_old_published_events(90, 1000)"
// 修改后
"SELECT cleanup_old_published_events(7, 1000)"
```
同时更新第 6 行注释:
```rust
/// - 调用 `cleanup_old_published_events()` 归档 >7 天的已发布事件
```
- [ ] **Step 2: 手动清理当前堆积**
在数据库中执行 SQL 清理已发布事件:
```sql
INSERT INTO domain_events_archive (id, tenant_id, event_type, payload, correlation_id, status, attempts, last_error, created_at, published_at)
SELECT id, tenant_id, event_type, payload, correlation_id, status, attempts, last_error, created_at, published_at
FROM domain_events WHERE status = 'published';
DELETE FROM domain_events WHERE status = 'published'
AND id IN (SELECT id FROM domain_events_archive);
```
> **此步骤需要手动执行 SQL不在代码中自动化。**
- [ ] **Step 3: 验证编译**
Run: `cargo check`
Expected: 编译通过
- [ ] **Step 4: 提交**
```bash
git add crates/erp-server/src/tasks.rs
git commit -m "fix(server): 事件清理保留期从 90 天调整为 7 天"
```
---
## Phase 1 验证清单
完成所有 Task 后执行:
- [ ] `cargo check` — 全 workspace 编译通过
- [ ] `cargo test -p erp-plugin` — 测试通过
- [ ] `cd apps/web && pnpm build` — 前端构建通过
- [ ] 浏览器验证:登录页品牌信息、侧边栏菜单图标、主题设置品牌编辑
- [ ] `git push` — 推送到远程