feat: 数据获取架构重构 + Mac指纹采集 + Token刷新容错 + 账单页面 (0.0.19)
## 架构重构
- useAsyncData 集成 AbortController:cleanup 时真正中断 HTTP 请求,
不再仅丢弃响应(解决 StrictMode 双重请求问题)
- API 模块统一 signal?: AbortSignal 参数,透传 axios config
- use-api.ts 成为数据 Hook 集中管理中心,新增 useAccountInfo、
useBillingBalance、useBillingLedger
- 页面组件改为纯渲染:const {data,loading,error,refetch} = useXxx()
## 新功能
- Mac 硬件指纹采集(electron/preload/device.ts):
ioreg(UUID) + sysctl(CPU) + system_profiler(HDD三级回退)
- 账单页面 BillingInfo(余额卡片 + 账单表格 + 分页)
- PageDispatcher 注册 /billing 页面(FloatingPanel 960×680)
- 共享工具函数 centToYuan / formatDate(src/utils/display.ts)
## Bug 修复
- Token 刷新容错:网络错误时保留 refresh_token,等网络恢复后重试;
仅 RefreshError(token 过期)时清除(auth-token.ts)
- AppProvider 自动登录时 nullable 字段默认值(email→''、admin_permissions→[])
- AccountInfo 重构消除 ~40 行手写状态管理代码
## 代码质量
- AccountInfo 消除 centToYuan/formatDate 重复定义
- BillingInfo 标题层级精简(移除冗余页面/Card标题)
- antd Table align 字面量类型修正('left' as const)
- billing.ts signal 参数位置修复(params→config)
- modelFirstFetchDone 模块级变量移除(StrictMode 不安全)
This commit is contained in:
130
ARCHITECTURE.md
130
ARCHITECTURE.md
@@ -82,6 +82,72 @@ src/
|
||||
|
||||
---
|
||||
|
||||
## 数据获取架构
|
||||
|
||||
### 三层模式:API 模块 → use-api Hook → 页面组件
|
||||
|
||||
```
|
||||
Step 1 — API 模块 Step 2 — use-api.ts Step 3 — 页面组件
|
||||
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ BillingAPI │ → │ useBillingBalance │ → │ BillingInfo.tsx │
|
||||
│ .getBalance(signal) │ │ useAsyncData( │ │ const {data,loading,│
|
||||
│ .getLedger(param,s) │ │ (signal)=> │ │ error,refetch}= │
|
||||
│ │ │ BillingAPI... │ │ useBillingBalance() │
|
||||
│ signal?: AbortSignal │ │ ,[deps], │ │ // 纯渲染,不调 API │
|
||||
│ 透传 axios config │ │ {enabled:isLoggedIn} │ │ │
|
||||
│ │ │ ) │ │ │
|
||||
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### 原则
|
||||
|
||||
- **API 模块**(`services/modules/*.ts`):`signal?: AbortSignal` 可选参数,透传给 axios config,向后兼容
|
||||
- **use-api.ts**:所有业务数据 Hook 集中于此,组件只 import Hook 不 import API 类
|
||||
- **页面组件**:纯渲染,通过 `{ data, loading, error, errorMessage, refetch }` 消费数据,不手写 `useState`/`useEffect`/`useRef` 状态管理
|
||||
- **`useAsyncData`**:自动处理竞态条件(raceId)、AbortController 取消、错误日志
|
||||
|
||||
### signal 参数规范
|
||||
|
||||
```
|
||||
// API 层 — signal 在 config 位置(第三个参数),不是 params
|
||||
static getBalance(signal?: AbortSignal): Promise<BalanceResponseBody> {
|
||||
return get<BalanceResponseBody>(url, undefined, { signal });
|
||||
}
|
||||
|
||||
// Hook 层 — signal 由 useAsyncData 自动注入
|
||||
export function useBillingBalance() {
|
||||
return useAsyncData<BalanceResponseBody>(
|
||||
(signal) => BillingAPI.getBalance(signal),
|
||||
[],
|
||||
{ enabled: isLoggedIn, label: 'billing-balance' },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### AbortController 生命周期
|
||||
|
||||
- 每次 effect 创建新 `AbortController`
|
||||
- cleanup 时调用 `abortController.abort()` → 真正中断飞行中的 HTTP 请求(不仅仅是丢弃响应)
|
||||
- `signal.aborted` 检查:取消的请求静默忽略,不设置 error 状态
|
||||
- 解决 React StrictMode 开发模式下组件双重挂载导致的重复请求问题
|
||||
|
||||
### 新增 Hook 规范
|
||||
|
||||
在 `use-api.ts` 中添加新 Hook 只需三行:
|
||||
|
||||
```typescript
|
||||
export function useXxx() {
|
||||
const { isLoggedIn } = useAppContext();
|
||||
return useAsyncData<XxxResponse>(
|
||||
(signal) => XxxAPI.getXxx(signal),
|
||||
[], // deps — 为空表示只请求一次,refetch 手动触发
|
||||
{ enabled: isLoggedIn, label: 'xxx' },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心流程
|
||||
|
||||
### 1. 登录
|
||||
@@ -134,6 +200,17 @@ App 启动 → AppProvider useEffect
|
||||
请求 A 收到 401 → 开始 refresh
|
||||
请求 B 同时收到 401 → 发现 isRefreshing=true → 加入队列等待
|
||||
refresh 完成 → 新 token 广播给所有排队请求 → 全部重试
|
||||
|
||||
### 3.1 Token 刷新错误分类(auth-token.ts)
|
||||
|
||||
| 错误类型 | 行为 | 原因 |
|
||||
|---------|------|------|
|
||||
| `RefreshError`(token 过期) | 清除 refresh_token | 无法再续期 |
|
||||
| `NETWORK` / `TIMEOUT` | **保留** refresh_token | 网络恢复后可重试 |
|
||||
| HTTP 5xx / Business 错误 | 清除 refresh_token | 保守处理 |
|
||||
|
||||
> 网络错误时不清除 refresh_token 是关键——否则网络短暂中断会导致用户被迫手动重新登录。
|
||||
> 只有 RefreshError(refresh 接口返回 401)才确认 token 真的过期。
|
||||
```
|
||||
|
||||
### 4. 退出登录
|
||||
@@ -187,6 +264,37 @@ NavBar / 设置页 → AppProvider.logout()
|
||||
- SettingsPage — Modal,事件总线 `OPEN_SETTINGS` 触发
|
||||
- AnnouncementDrawer — Drawer,NavBar 内部 state 控制
|
||||
|
||||
**PageDispatcher 页面注册**:
|
||||
|
||||
```typescript
|
||||
const PAGE_MAP: Record<string, PageConfig> = {
|
||||
'/account': {
|
||||
component: AccountInfo, // 页面组件(懒加载用 React.lazy)
|
||||
label: '账户', // 容器标题栏显示
|
||||
width: () => Math.min(window.outerHeight * 0.65, 720), // 动态宽度
|
||||
height: () => Math.min(window.outerHeight * 0.65, 680), // 动态高度
|
||||
},
|
||||
'/billing': {
|
||||
component: BillingInfo, // 账单页面(余额卡片 + 账单表格)
|
||||
label: '账单记录', // FloatingPanel 标题栏显示
|
||||
width: 960, // 固定宽度(适配账单表格)
|
||||
height: 680, // 固定高度
|
||||
},
|
||||
'/vip': { label: '开会员/激活' }, // 占位页:无 component → 自动渲染 "页面开发中"
|
||||
};
|
||||
```
|
||||
|
||||
**容器类型选择**:
|
||||
|
||||
| 类型 | 组件 | 场景 |
|
||||
|------|------|------|
|
||||
| `modal` | antd Modal | 居中模态,阻断交互(登录、VIP 开通) |
|
||||
| `drawer` | antd Drawer | 侧滑抽屉(设置、公告) |
|
||||
| `floating` | FloatingPanel | 非模态浮动面板,可拖拽(账户、账单) |
|
||||
|
||||
- **全局互斥**:同一时刻只有一个面板打开,重复点击不替换
|
||||
- **FloatingPanel** 自带 `padding: 16` 和标题栏,内部组件避免过度内边距叠加
|
||||
|
||||
**反模式警告**:叠加层绝不应通过路由跳转实现。`navigate('/xxx')` + `useLocation` 判断显隐会导致原有页面卸载,丢失所有组件状态。
|
||||
|
||||
### API 模块规范
|
||||
@@ -252,3 +360,25 @@ export class AuthAPI {
|
||||
|------------------|-------------------------|--------------------------|--------------------------|
|
||||
| `UPDATE_API_URL` | `http://127.0.0.1:8000` | `https://www.heixiu.com` | `https://www.heixiu.com` |
|
||||
| `EDITION` | `personal` | `personal` | `personal` |
|
||||
|
||||
---
|
||||
|
||||
## 平台兼容
|
||||
|
||||
### 设备指纹(electron/preload/device.ts)
|
||||
|
||||
| 平台 | 采集方式 |
|
||||
|------|---------|
|
||||
| Windows | WMIC / PowerShell — UUID、CPU、HDD 序列号 |
|
||||
| macOS | `ioreg -d2 -c IOPlatformExpertDevice`(UUID)、`sysctl -n hw.model`(CPU 型号)、`system_profiler`(HDD 三级回退) |
|
||||
|
||||
**Mac HDD 三级回退策略**:
|
||||
|
||||
1. `system_profiler SPNVMeDataType` → NVMe 磁盘序列号(Apple Silicon / 新款 Intel)
|
||||
2. `system_profiler SPSerialATADataType` → SATA 磁盘序列号(老款 Intel Mac)
|
||||
3. `system_profiler SPHardwareDataType` → 系统序列号(最终兜底)
|
||||
|
||||
- 通过 `platform() === 'darwin'` / `platform() === 'win32'` 区分平台
|
||||
- Mac 命令执行:`spawnSync` + 5s 超时,`ioreg`/`sysctl` 始终可用
|
||||
- 共享同一套 `SHA256(normalizeIdentifier(combined))` 管线
|
||||
- `getMachineCode()` 跨平台统一接口,自动选择最优采集方式
|
||||
|
||||
Reference in New Issue
Block a user