188 lines
12 KiB
Markdown
188 lines
12 KiB
Markdown
# 关于我们模块 + 数据库重置 + COS 清理 实施计划
|
||
|
||
## Context(背景与目标)
|
||
|
||
用户提出三项需求:
|
||
1. **数据库重置**:清空所有业务数据,仅保留 admin 账户,重置后保留种子数据(分类/平台/标签/示例项目/轮播图/合作伙伴由 DbSeeder 自动补回)。
|
||
2. **COS 资源全删**:清空腾讯云 COS 存储桶内所有应用上传的文件(保留 `backups/` 下的数据库备份)。
|
||
3. **关于我们模块**:
|
||
- 前台 About 页去掉"联系我们"模块
|
||
- 后台新增"关于我们"管理模块:控制前台是否显示该页 + 全部内容可编辑(内联所见即所得,编辑样式与前台一致)
|
||
|
||
当前 About 页 100% 硬编码([About.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/frontend/About.vue)),无任何后端存储。本计划从零构建内容存储 + 后台内联编辑 + 前台动态渲染。
|
||
|
||
## 关键设计决策(经用户确认 + 架构验证)
|
||
|
||
1. **存储方式**:全部 About 内容作为单个 JSON 字符串存入 SystemSettings 表(Key=`AboutContent`),与现有 PlatformName/ShowPartners 等配置一致。避免新建表 + 改 seeder 补丁的复杂度。
|
||
2. **编辑交互**:使用无边框 `el-input`/`el-textarea`(autosize),通过 hover/focus 样式提示可编辑性。不使用 `contenteditable`(Vue 响应性陷阱:光标跳动)。
|
||
3. **数组排序**:上下箭头按钮,不引入拖拽库。
|
||
4. **ShowAbout 控制**:隐藏导航链接 + 路由守卫拦截直接访问(重定向到首页)。
|
||
5. **重置安全**:要求输入 "RESET" 确认 + 重置前自动创建一次数据库备份 + 操作日志记录。
|
||
6. **COS 清理范围**:仅删除已知上传前缀(avatars/banners/logos/projects/webgl/packages/common/icons/videos/categories/platforms/partners),保留 `backups/`。
|
||
7. **AboutContent 不进 PublicSettingsDto**:public-settings 仅加 `showAbout`(小布尔值,供导航/路由用);AboutContent 由独立端点加载,仅 About 页消费。
|
||
|
||
## 实施步骤
|
||
|
||
### 阶段 A:后端基础设施
|
||
|
||
#### A1. FileStorageService 增加批量删除能力
|
||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Services\Infrastructure\FileStorageService.cs`
|
||
|
||
- 接口 `IFileStorageService` 新增:
|
||
- `Task<List<string>> ListAllObjectsAsync()` — 分页列出桶内所有对象 key(每页 1000,循环至结束)
|
||
- `Task<int> DeleteObjectsAsync(IEnumerable<string> keys)` — 批量删除(`DeleteMultiObjectRequest`,每批最多 1000)
|
||
- 实现:复用私有 `GetStorageConfigAsync()` + `GetCosXml(config)`;Local 模式则遍历 `wwwroot/uploads` 目录删除文件
|
||
- 不暴露到控制器单独使用,仅供 factory-reset 调用
|
||
|
||
#### A2. AboutContent DTO + 端点
|
||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Common\Dtos\Dtos.cs`
|
||
|
||
新增 `AboutContentDto`:
|
||
```csharp
|
||
public class AboutContentDto
|
||
{
|
||
public string HeroTag { get; set; } = "关于我们";
|
||
public string HeroTitle { get; set; } = "";
|
||
public string HeroDesc { get; set; } = "";
|
||
public string IntroTitle { get; set; } = "公司简介";
|
||
public List<string> IntroParagraphs { get; set; } = new();
|
||
public List<AboutStatDto> Stats { get; set; } = new();
|
||
public string MissionTitle { get; set; } = "使命与愿景";
|
||
public List<AboutMissionCardDto> MissionCards { get; set; } = new();
|
||
public string TimelineTitle { get; set; } = "发展历程";
|
||
public List<AboutTimelineItemDto> Timeline { get; set; } = new();
|
||
}
|
||
public class AboutStatDto { public string Number { get; set; } = ""; public string Label { get; set; } = ""; }
|
||
public class AboutMissionCardDto { public string Title { get; set; } = ""; public string Icon { get; set; } = "rocket"; public string Desc { get; set; } = ""; }
|
||
public class AboutTimelineItemDto { public string Year { get; set; } = ""; public string Title { get; set; } = ""; public string Desc { get; set; } = ""; }
|
||
|
||
public class AboutSettingsDto
|
||
{
|
||
public bool ShowAbout { get; set; } = true;
|
||
public AboutContentDto Content { get; set; } = new();
|
||
}
|
||
```
|
||
|
||
`PublicSettingsDto` 仅新增 `public bool ShowAbout { get; set; } = true;`(不加 AboutContent)。
|
||
|
||
#### A3. SystemService 关于我们读写
|
||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Services\SystemService.cs`
|
||
|
||
- `GetAboutSettingsAsync()` → 读 SystemSettings 中 `ShowAbout` + `AboutContent`(JSON 反序列化),返回 `AboutSettingsDto`
|
||
- `UpdateAboutSettingsAsync(AboutSettingsDto dto)` → 序列化 Content 为 JSON 存入 `AboutContent` key,`ShowAbout` 存 bool 字符串
|
||
- `GetPublicSettingsAsync()` 中加入 `ShowAbout` 读取
|
||
|
||
#### A4. SystemController 端点
|
||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Controllers\SystemController.cs`
|
||
|
||
- `GET /api/system/about` → `[AllowAnonymous]` 返回 `AboutSettingsDto`(前台 About 页消费)
|
||
- `PUT /api/system/about` → `[Authorize(Policy="Admin")]` 保存
|
||
|
||
#### A5. 工厂重置端点
|
||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Services\SystemService.cs` + `SystemController.cs`
|
||
|
||
- `POST /api/system/factory-reset` → `[Authorize(Policy="Admin")]`,body 含 `ConfirmText`(必须等于 "RESET")
|
||
- 逻辑:
|
||
1. 校验 `ConfirmText == "RESET"`,否则返回 400
|
||
2. 先调 `CreateBackupAsync()` 创建备份(保留安全网)
|
||
3. `SET FOREIGN_KEY_CHECKS=0`
|
||
4. 按依赖顺序 DELETE FROM 所有 21 张表(Users 仅删 `Username != 'admin'`,并清空 admin 的 Avatar/UpdatedAt 字段)
|
||
5. `SET FOREIGN_KEY_CHECKS=1`
|
||
6. 调 `DbSeeder.SeedAsync(db)` 重新补种子(admin 已存在跳过;分类/平台/标签/项目/轮播图/合作伙伴/设置按"为空才补"逻辑重新填入;AboutContent/ShowAbout 默认值也会被补入)
|
||
7. 调 `_fileStorage.ListAllObjectsAsync()` 列出所有对象,过滤掉 `backups/` 前缀,调 `DeleteObjectsAsync()` 批量删除
|
||
8. 记操作日志
|
||
9. 返回 `{ TablesCleared, ObjectsDeleted, BackupId }`
|
||
|
||
#### A6. DbSeeder 补种子
|
||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Data\DbSeeder.cs`
|
||
|
||
- 在 `SeedAsync` 的 SystemSettings 补种子段加入:
|
||
- `ShowAbout` = "true"
|
||
- `AboutContent` = 默认 JSON(当前 About.vue 的硬编码内容序列化)
|
||
- 仅在 key 不存在时插入(与现有逻辑一致)
|
||
|
||
### 阶段 B:前端 — 关于我们动态化 + 去掉联系模块
|
||
|
||
#### B1. 类型 + API + Store
|
||
**文件**:
|
||
- `d:\TraeProject\MilliSim4\frontend\src\types\index.ts`:加 `AboutContent` / `AboutStat` / `AboutMissionCard` / `AboutTimelineItem` 接口;`PublicSettings` 加 `showAbout: boolean`
|
||
- `d:\TraeProject\MilliSim4\frontend\src\api\index.ts`:`systemApi.getAbout()` GET `/system/about`;`systemApi.updateAbout(payload)` PUT;`systemApi.factoryReset(confirmText)` POST
|
||
- `d:\TraeProject\MilliSim4\frontend\src\stores\system.ts`:`settings` 加 `showAbout`(默认 true);加 `aboutContent` ref + `loadAbout()` 方法(About 页 onMounted 调用)
|
||
|
||
#### B2. About.vue 动态化
|
||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\views\frontend\About.vue`
|
||
|
||
- 去掉"联系我们"section(原 L101-130)+ 相关 CTA
|
||
- script:`onMounted` 调 `systemStore.loadAbout()` 获取内容;用 `computed` 映射 store 中的 aboutContent
|
||
- template:所有硬编码文本替换为 `{{ about.heroTag }}` 等绑定;timeline 用 `v-for="item in about.timeline"`
|
||
- 无数据时显示骨架屏/loading
|
||
|
||
#### B3. 路由 + 导航守卫
|
||
**文件**:
|
||
- `d:\TraeProject\MilliSim4\frontend\src\router\index.ts`:`/about` 路由 `meta.requiresAbout: true`;`beforeEach` 守卫中 `if (to.meta.requiresAbout && !systemStore.settings.showAbout) return '/'`,并先 `await systemStore.ensureLoaded()`
|
||
- `d:\TraeProject\MilliSim4\frontend\src\layouts\FrontendLayout.vue`:nav 菜单"关于我们"项加 `v-if="systemStore.settings.showAbout"`;footer 链接同理
|
||
|
||
### 阶段 C:后台 — 关于我们内联编辑页
|
||
|
||
#### C1. 新建 AboutSettings.vue
|
||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\views\admin\AboutSettings.vue`(新建)
|
||
|
||
布局完全复刻前台 About.vue 结构,但所有文字改为可编辑:
|
||
- 顶部工具栏:`ShowAbout` el-switch + "保存" el-button(loading 状态)
|
||
- Hero 区:3 个无边框 el-input(tag/title/desc),inherit 前台字体样式
|
||
- 公司简介:`el-textarea :autosize` × N 段(可增删),4 个统计卡片(number + label 各一输入),每卡右上角 hover 显示删除按钮 + 上下移按钮
|
||
- 使命与愿景:3 张卡片(title + icon 选择 + desc textarea),增删/排序
|
||
- 发展历程:时间线项(year + title + desc),增删/排序
|
||
- 编辑态样式:`.editable:hover { outline: 1px dashed var(--color-primary); }` `.editable:focus { outline: 2px solid var(--color-primary); background: var(--color-primary-light-9); }`
|
||
- 数组操作按钮:hover 时浮现"上移/下移/删除","添加"按钮在列表末尾
|
||
- `onMounted` 调 `systemApi.getAbout()` 加载;保存调 `systemApi.updateAbout()`;保存后 `systemStore.load()` 刷新前台状态
|
||
- 图标选择用 el-select 下拉(预设 SvgIcon 列表:rocket/globe/heart/target/users/star 等)
|
||
|
||
#### C2. 路由 + 菜单
|
||
**文件**:
|
||
- `d:\TraeProject\MilliSim4\frontend\src\router\index.ts`:加 `/admin/about-settings` 路由(admin auth)
|
||
- `d:\TraeProject\MilliSim4\frontend\src\layouts\AdminLayout.vue`:侧栏菜单加入"关于我们"项(icon: `info` 或 `building`)
|
||
|
||
### 阶段 D:后台 — 工厂重置入口
|
||
|
||
#### D1. Settings.vue 危险操作卡片
|
||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\views\admin\Settings.vue`
|
||
|
||
- 在页面末尾加"危险操作"卡片:
|
||
- 标题 + 警告说明("将清空所有业务数据并删除 COS 资源,仅保留 admin 账户,操作前会自动创建备份")
|
||
- el-input 要求输入 "RESET" 才能激活按钮
|
||
- el-button danger "恢复出厂设置"(disabled 直到输入正确)
|
||
- 二次确认弹窗 `showActionConfirm`
|
||
- 调 `systemApi.factoryReset(confirmText)`;成功后 `ElMessage.success` + 提示重新登录(admin 密码不变)
|
||
|
||
### 阶段 E:编译验证 + 重启
|
||
|
||
1. 杀掉后端进程(PID 13788/22680)→ `dotnet build` → 重启后端
|
||
2. 前端 Vite HMR 自动加载,确认无编译错误
|
||
3. 浏览器测试场景
|
||
|
||
## 验证步骤
|
||
|
||
### 后端验证
|
||
1. `dotnet build` 0 错误 0 警告
|
||
2. 启动后端,确认 `PendingUploads` + 新增设置 key(AboutContent/ShowAbout)建表/补种子成功(查日志)
|
||
3. Swagger 确认新端点存在:GET/PUT `/api/system/about`、POST `/api/system/factory-reset`
|
||
|
||
### 前端验证
|
||
1. Vite HMR 无编译错误
|
||
2. 访问 `/about` 确认内容从后端加载(非硬编码)
|
||
3. 关闭 ShowAbout 后导航不显示"关于我们",直接访问 `/about` 重定向到首页
|
||
4. 后台 `/admin/about-settings` 编辑任意文字 → 保存 → 刷新前台确认生效
|
||
5. 数组增删/排序正常
|
||
6. 后台"恢复出厂设置"输入 RESET → 执行 → 确认数据库清空 + COS 清空 + admin 可登录 + 种子数据恢复
|
||
|
||
## 假设与决策
|
||
|
||
1. **不删 admin 账户**:DbSeeder 对 admin 用户按 username 幂等,重置后保留原密码 `admin123`。
|
||
2. **保留 backups/ 前缀**:工厂重置不删 `backups/` 下的数据库备份文件,作为最后安全网。
|
||
3. **重置前自动备份**:调用现有 `CreateBackupAsync`,即使重置失败也有回滚依据。
|
||
4. **AboutContent 默认值**:取当前 About.vue 硬编码内容作为默认 JSON,保证未配置时前台显示不变。
|
||
5. **不引入拖拽库**:数组排序用上下箭头按钮,与现有 SortOrder 字段约定一致。
|
||
6. **PublicSettingsDto 不含 AboutContent**:避免每次应用启动加载大 JSON;AboutContent 由 About 页独立加载。
|
||
7. **factory-reset 同步删 COS**:DB 清空后立即删 COS,保证一致性;COS 删除失败不回滚 DB(DB 已是干净状态,COS 残留可手动清理,记录日志)。
|