docs: 三端审计修复实施计划 Phase 1 — 8 个 Task (#1-#6)

This commit is contained in:
iven
2026-05-01 17:17:19 +08:00
parent 988b405c5d
commit fc1d51e6f1

View File

@@ -0,0 +1,789 @@
# 三端联调审计问题修复实施计划
> **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` — 推送到远程