MilliSim/.trae/documents/authorization-system-refactor.md

448 lines
19 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.

# 授权系统全面重构计划
## 摘要
本次重构解决用户反馈的 5 个授权相关问题:
1. **续期 bug** — 一直提示"续期天数必须大于 0"
2. **授权卡片重新设计** — 要有设计感,状态标签中文表示
3. **批量授权窗口重新设计**
4. **客户管理窗口重新设计**
5. **授权逻辑重大问题** — 一个客户只能有一个授权记录(不论有效/已过期/已取消),且授权卡片可操作,客户卡片显示当前授权状态
## 当前状态分析
### Bug 根因(续期失败)
**后端** [`backend/MilliSim.Api/Controllers/AuthorizationsController.cs#L67-L73`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Controllers/AuthorizationsController.cs#L67-L73)
```csharp
[HttpPost("{id:long}/renew")]
public async Task<IActionResult> Renew(long id, [FromQuery] int days)
```
- 控制器使用 `[FromQuery] int days` 期望 query string
**前端** `frontend/src/api/index.ts`
```typescript
renew: (id: number, days: number) => http.post(`/authorizations/${id}/renew`, { days })
```
- 前端发送 POST body `{ days: 30 }`
→ 后端 days 绑定为 0触发 `if (days <= 0) return Fail("续期天数必须大于 0")`
### 授权逻辑缺陷
**后端** [`backend/MilliSim.Api/Services/AuthorizationService.cs#L155-L157`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/AuthorizationService.cs#L155-L157)
```csharp
var existing = await _db.Authorizations
.FirstOrDefaultAsync(a => a.UserId == request.UserId
&& a.Status == AuthorizationStatus.Active); // 只查 Active
```
- 现有授权检查只查 Active 状态,对 Cancelled/Expired 状态会创建新记录
- 这导致一个客户可能有多条授权记录,违反"一个客户一个授权"的约束
### 关键代码位置
- `AuthorizationStatus` 枚举:`ApiResponse.cs` — Cancelled=0, Active=1, Expired=2数字序列化
- `UserDto``Dtos.cs#L32-L46` — 不含授权信息字段
- `UserService.GetUsersAsync``UserService.cs#L29-L90` — 不联表 Authorizations
- 前端 `Authorizations.vue`418 行):卡片含头像+名称+时间+授权人+状态徽章+续期/取消按钮
- 前端 `Customers.vue`539 行):卡片含头像+用户状态徽章+用户名/手机/邮箱/注册时间+操作按钮,**不显示授权状态**
## 提议的变更
### 后端修改
#### 1. 修复续期 bug — 改用 [FromBody] + DTO
**文件**: [`backend/MilliSim.Api/Controllers/AuthorizationsController.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Controllers/AuthorizationsController.cs)
修改续期端点L67-L73
```csharp
[HttpPost("{id:long}/renew")]
[Authorize(Policy = "Staff")]
public async Task<IActionResult> Renew(long id, [FromBody] RenewAuthorizationRequest request)
{
var result = await _authorizationService.RenewAuthorizationAsync(id, request.Days);
return Ok(result);
}
```
**文件**: [`backend/MilliSim.Api/Common/Dtos/Dtos.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs)
`BatchAuthorizationRequest` 之后(约 L276 位置)添加新 DTO
```csharp
public class RenewAuthorizationRequest
{
public int Days { get; set; } = 30;
}
```
#### 2. 重构授权逻辑 — 一个客户一个授权记录
**文件**: [`backend/MilliSim.Api/Services/AuthorizationService.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/AuthorizationService.cs)
**关键变更**:将"检查 Active 才续期"改为"任意状态则复用原记录"。
`CreateAuthorizationAsync`L132-L225重构逻辑
```csharp
// 查询客户任意状态的现有授权(一个客户只能有一个授权记录)
var existing = await _db.Authorizations
.FirstOrDefaultAsync(a => a.UserId == request.UserId);
if (existing != null)
{
// 复用原记录:重置 StartTime/EndTime/Status/AuthorizedBy
var now = DateTime.Now;
existing.StartTime = now;
existing.EndTime = now.AddDays(request.Days);
existing.Status = AuthorizationStatus.Active;
existing.AuthorizedBy = _currentUser.UserId;
existing.ProjectId = null; // 客户级授权
existing.UpdatedAt = now;
await _db.SaveChangesAsync();
// 操作日志:标记"重新授权"或"开通"(根据原状态)
var actionLabel = existing.Status == AuthorizationStatus.Cancelled ? "重新激活" : "重新授权";
// ... 通知逻辑(区分续期/重新授权)
return ApiResponse<AuthorizationDto>.Success(MapToDto(...), "授权成功");
}
// 否则新建(同原逻辑)
```
`BatchAuthorizeAsync`L230-L333同样改造对每个 UserId 调用相同复用/新建逻辑。
`CancelAuthorizationAsync`L400-L441添加状态检查
```csharp
if (entity.Status == AuthorizationStatus.Cancelled)
{
return ApiResponse.Fail("该授权已取消,无需重复操作");
}
if (entity.Status == AuthorizationStatus.Expired)
{
return ApiResponse.Fail("该授权已过期,请使用重新授权功能");
}
// 仅 Active 可取消
```
`RenewAuthorizationAsync`L338-L395添加状态检查仅 Active 可续期):
```csharp
if (entity.Status != AuthorizationStatus.Active)
{
return ApiResponse<AuthorizationDto>.Fail(
entity.Status == AuthorizationStatus.Cancelled ? "已取消的授权请使用重新授权功能"
: "已过期的授权请使用重新授权功能");
}
// 续期EndTime += days
```
**关键决策**:取消 `CreateAuthorizationAsync` 内部"已有 Active 就续期"的特殊分支 — 改为统一逻辑:客户有任何状态的授权都复用原记录(重置 StartTime + EndTime + Status=Active。这样从 Customers.vue 点击"为客户授权"或"重新授权"都调用同一接口,后端自动处理复用。
#### 3. 客户列表返回授权信息
**文件**: [`backend/MilliSim.Api/Common/Dtos/Dtos.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs)
修改 `UserDto`L32-L46添加两个字段
```csharp
public AuthorizationStatus? AuthorizationStatus { get; set; }
public DateTime? AuthorizationEndTime { get; set; }
```
**文件**: [`backend/MilliSim.Api/Services/UserService.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/UserService.cs)
修改 `GetUsersAsync`L29-L90在取出 `users` 列表后联表查询 Authorizations
```csharp
// 一个客户只有一个授权记录,按 UserId 一次性取出
var userIds = users.Select(u => u.Id).ToList();
var authDict = await _db.Authorizations
.Where(a => userIds.Contains(a.UserId))
.ToDictionaryAsync(a => a.UserId, a => a); // 一个客户一条记录
var items = users.Select(u =>
{
var dto = AuthService.MapToDto(u);
if (u.CreatedBy.HasValue && creators.TryGetValue(u.CreatedBy.Value, out var name))
{
dto.CreatedByName = name;
}
if (authDict.TryGetValue(u.Id, out var auth))
{
dto.AuthorizationStatus = auth.Status;
dto.AuthorizationEndTime = auth.EndTime;
}
return dto;
}).ToList();
```
> 注意:`MarkExpiredAsync` 不在 UserService 调用,授权状态可能有"过期未刷新"的窗口期。前端展示时若 `AuthorizationEndTime < now` 且 `AuthorizationStatus == Active`UI 显示为"已过期"(前端容错)。
#### 4. 后端重启注意事项
后端运行在 `http://localhost:5041`background job: `job-a1f30a33998f4065abe22b34018dcc7a`)。
- 修改代码后需 `Stop-Process -Name "MilliSim.Api" -Force`,再 `dotnet build` 重新构建
- 启动新进程
### 前端修改
#### 5. 类型定义
**文件**: [`frontend/src/types/index.ts`](file:///d:/TraeProject/MilliSim4/frontend/src/types/index.ts)
修改 `User` 接口L61-L74添加授权字段
```typescript
export interface User {
// ... 原有字段
authorizationStatus?: AuthorizationStatus | null
authorizationEndTime?: string | null
}
```
#### 6. 授权卡片重新设计 + 批量授权窗口重新设计
**文件**: [`frontend/src/views/admin/Authorizations.vue`](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Authorizations.vue) + `Authorizations.css`
参考 `Categories.vue` / `Platforms.vue` 的设计语言(蓝紫渐变 + 卡片渐变背景层 + 状态胶囊)。
##### 授权卡片新结构
```
┌────────────────────────────────────┐
│ [渐变背景层] │
│ [头像 56x56] [状态胶囊 中文] │
│ │
│ 客户昵称 (15px 600) │
│ @用户名 │
│ │
│ 有效期2025-01-01 至 2025-12-31 │
│ 剩余天数180 天 │
│ 授权人:销售员 │
│ 创建时间2025-01-01 │
│ ───────────────────────────────── │
│ [按钮组 - 按状态显示] │
└────────────────────────────────────┘
```
##### 状态徽章中文映射
新增工具函数(在 Authorizations.vue 的 script 中):
```typescript
const statusConfig: Record<AuthorizationStatus, { label: string; class: string }> = {
[AuthorizationStatus.Active]: { label: '有效', class: 'active' },
[AuthorizationStatus.Expired]: { label: '已过期', class: 'expired' },
[AuthorizationStatus.Cancelled]: { label: '已取消', class: 'cancelled' },
}
```
CSS 新增 `.status-pill.expired`(橙色)和 `.status-pill.cancelled`(红色)样式:
```css
.status-pill.expired { color: #FA8C16; background: rgba(250, 140, 22, 0.12); }
.status-pill.expired .status-dot { background: #FA8C16; }
.status-pill.cancelled { color: #F5222D; background: rgba(245, 34, 45, 0.12); }
.status-pill.cancelled .status-dot { background: #F5222D; }
```
##### 按状态显示操作按钮
```typescript
function getCardActions(status: AuthorizationStatus) {
if (status === AuthorizationStatus.Active) {
return [
{ key: 'renew', label: '续期', icon: 'time', onClick: openRenewDialog },
{ key: 'cancel', label: '取消授权', icon: 'close', onClick: handleCancel, danger: true },
{ key: 'detail', label: '查看详情', icon: 'eye', onClick: openDetailDialog },
]
}
// Cancelled / Expired
return [
{ key: 'reactivate', label: '重新授权', icon: 'refresh', onClick: openReactivateDialog },
{ key: 'detail', label: '查看详情', icon: 'eye', onClick: openDetailDialog },
]
}
```
##### 新增对话框
- **续期对话框**(已有,复用):天数输入,调用 `authorizationApi.renew(id, days)`
- **重新授权对话框**(新增):天数输入(默认 365调用 `authorizationApi.create({ userId, days })` — 后端自动复用原记录
- **查看详情对话框**(新增):展示完整授权信息(客户、授权人、起止时间、状态、剩余天数等)
##### 批量授权窗口重新设计
参考设计:
```
┌── 批量授权 ──────────────────────────┐
│ [图标] 批量授权 │
│ 为多个客户一次性开通授权 │
│ │
│ ┌─ 选择客户 ─────────┐ ┌─ 授权配置 ─┐│
│ │ [搜索框] │ │ 授权天数: ││
│ │ ☐ 用户A 未授权 │ │ [输入框] ││
│ │ ☐ 用户B 有效 │ │ 默认 365 ││
│ │ ☐ 用户C 已过期 │ │ ││
│ │ ... │ │ ││
│ └────────────────────┘ └────────────┘│
│ │
│ 已选 3 个客户用户A、用户B、用户C │
│ [取消] [确认授权] │
└────────────────────────────────────┘
```
实现要点:
- 左侧客户列表(显示客户名 + 当前授权状态徽章),支持多选
- 右侧授权配置(仅天数输入,客户级授权不关联项目)
- 底部显示已选客户数量 + 确认按钮
- 调用 `authorizationApi.batch({ userIds, days })` — 后端批量处理复用/新建
#### 7. 客户管理卡片重新设计
**文件**: [`frontend/src/views/admin/Customers.vue`](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Customers.vue) + `Customers.css`
##### 客户卡片新结构
```
┌────────────────────────────────────┐
│ [渐变背景层] │
│ [头像 56x56] [用户状态胶囊] │
│ │
│ 客户昵称 (15px 600) │
│ @用户名 │
│ │
│ 授权状态:[徽章 - 中文] │
│ 到期时间2025-12-31 (剩余 180天) │
│ │
│ 手机138****0000 │
│ 邮箱user@example.com │
│ 注册时间2025-01-01 │
│ ───────────────────────────────── │
│ [按钮组] │
└────────────────────────────────────┘
```
##### 授权状态徽章(中文)
```typescript
const authStatusConfig: Record<number, { label: string; class: string; showExpiry: boolean }> = {
[AuthorizationStatus.Active]: { label: '授权有效', class: 'active', showExpiry: true },
[AuthorizationStatus.Expired]: { label: '已过期', class: 'expired', showExpiry: true },
[AuthorizationStatus.Cancelled]: { label: '已取消', class: 'cancelled', showExpiry: false },
}
// 无授权
const noAuthConfig = { label: '未授权', class: 'none', showExpiry: false }
```
##### 授权按钮文案动态化
```typescript
function getAuthButton(status?: AuthorizationStatus | null) {
if (status === undefined || status === null) return { label: '为客户授权', icon: 'plus' }
if (status === AuthorizationStatus.Active) return { label: '续期授权', icon: 'time' }
return { label: '重新授权', icon: 'refresh' } // Cancelled / Expired
}
```
##### 操作按钮(卡片底部)
- 编辑(编辑客户信息)
- 授权按钮(动态文案)
- 重置密码
- 删除(红色 danger
授权按钮点击行为:
- 无授权 / Cancelled / Expired → 打开授权对话框(天数输入),调用 `authorizationApi.create({ userId, days })`
- Active → 打开续期对话框,调用 `authorizationApi.renew(authId, days)`
> 注:需要先调用 `authorizationApi.check(userId)` 或在卡片数据中带 `authorizationId`。考虑在 UserDto 额外返回 `authorizationId?: long?`,这样前端续期可直接用。**决策**UserDto 也增加 `AuthorizationId?: long?` 字段,简化前端续期调用。
## 假设与决策
### 决策记录
1. **重新授权策略**:复用原记录(更新 Status=Active + 重置 StartTime/EndTime/AuthorizedBy。原因用户确认 + 历史可追溯 + 符合"一个客户一个授权"约束。
2. **客户卡片授权状态来源**后端联表查询UserDto 增加 AuthorizationStatus/AuthorizationEndTime/AuthorizationId 字段)。原因:用户确认 + 一次查询返回所有数据,避免 N+1 请求。
3. **续期 vs 重新授权 分工**
- **续期**(仅 ActiveEndTime += days状态不变
- **重新授权**Cancelled/Expired调用 create API后端自动复用原记录重置 StartTime=now, EndTime=now+days, Status=Active
- 两个独立操作,前端通过不同对话框触发
4. **批量授权窗口的"复用"语义**:批量授权调用同一 API对已有 Cancelled/Expired 状态的客户自动复用原记录,对已有 Active 状态的客户自动续期EndTime += days
### 假设
- AuthorizationStatus 数字序列化已配置(项目记忆显示 ApiResponse.cs 已移除 [JsonConverter]
- Authorizations.ProjectId 已通过 DbSeeder 改为可空(已完成)
- 前端 `authorizationApi.renew(id, days)` 已发送 `{ days }` body无需修改前端 API 调用,仅需后端 controller 接收方式调整)
- 前端 Vite dev server 热更新中,修改 .vue 文件后自动生效
## 验证步骤
### 后端验证
1. **构建检查**
```powershell
Stop-Process -Name "MilliSim.Api" -Force
cd d:\TraeProject\MilliSim4\backend\MilliSim.Api
dotnet build
```
确认无编译错误。
2. **启动后端**:通过 background 命令启动 `dotnet run --project backend/MilliSim.Api`
### 前端验证(浏览器手动测试)
1. **续期 bug 修复**
- 进入授权管理页面
- 对一个 Active 状态的授权点击"续期"
- 输入 30 天,确认提交
- 预期:续期成功,不再提示"续期天数必须大于 0"
2. **授权逻辑 — 一个客户一个授权**
- 在客户管理页面,为客户 A 授权(首次创建)
- 取消该授权
- 再次点击"重新授权"为客户 A 授权
- 预期:后端复用原记录(更新状态为 Active不创建新记录
- 数据库验证:`SELECT * FROM Authorizations WHERE UserId = ?` 只有一条记录
3. **客户卡片授权状态显示**
- 客户管理页面,每个客户卡片应显示授权状态徽章(有效/已过期/已取消/未授权)
- 不同状态显示不同颜色和文案
4. **授权卡片操作按钮**
- Active 卡片:显示"续期/取消授权/查看详情"3 个按钮
- Cancelled/Expired 卡片:显示"重新授权/查看详情"2 个按钮
5. **批量授权**
- 选择多个客户(含不同状态:未授权、有效、已过期、已取消)
- 设置 30 天
- 预期:所有客户都成功,原有记录被复用
6. **状态徽章中文**
- Active → "有效"(绿色)
- Expired → "已过期"(橙色)
- Cancelled → "已取消"(红色)
## 文件变更清单
### 后端
1. `backend/MilliSim.Api/Controllers/AuthorizationsController.cs` — Renew 端点改 [FromBody]
2. `backend/MilliSim.Api/Common/Dtos/Dtos.cs` — 添加 RenewAuthorizationRequestUserDto 增加 3 个授权字段
3. `backend/MilliSim.Api/Services/AuthorizationService.cs` — 重构 CreateAuthorization/BatchAuthorize复用原记录Cancel/Renew 增加状态检查
4. `backend/MilliSim.Api/Services/UserService.cs` — GetUsersAsync 联表查询 Authorizations
### 前端
5. `frontend/src/types/index.ts` — User 接口增加 authorizationStatus/authorizationEndTime/authorizationId 字段
6. `frontend/src/views/admin/Authorizations.vue` + `Authorizations.css` — 卡片重新设计 + 新对话框 + 中文状态徽章
7. `frontend/src/views/admin/Customers.vue` + `Customers.css` — 卡片重新设计 + 授权状态徽章 + 动态授权按钮
## 实施顺序
1. 后端 DTO + Controller 修复(续期 bug
2. 后端 AuthorizationService 重构(授权逻辑)
3. 后端 UserService + UserDto 修改(联表查询)
4. 后端构建 + 重启
5. 前端 types/index.ts 修改
6. 前端 Authorizations.vue + CSS 重新设计
7. 前端 Customers.vue + CSS 重新设计
8. 浏览器手动验证全部场景