Files
leaudit-platform-frontend/docs/getHomeData_完整逻辑文档.md
2025-12-05 00:09:32 +08:00

1724 lines
44 KiB
Markdown
Raw Permalink 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.
# getHomeData 首页数据统计完整逻辑文档
## 文档信息
- **文件路径**: `app/api/home/home.ts`
- **主函数**: `getHomeData()`
- **创建日期**: 2025-01-17
- **文档版本**: 2.1
- **更新说明**: 改用 typeIds 参数直接指定文档类型 ID
---
## 目录
1. [函数概述](#函数概述)
2. [函数签名与返回值](#函数签名与返回值)
3. [业务逻辑流程](#业务逻辑流程)
4. [时间范围计算](#时间范围计算)
5. [数据查询详解](#数据查询详解)
6. [数据库例程详解](#数据库例程详解)
7. [增长率计算公式](#增长率计算公式)
8. [PostgREST 请求示例](#postgrest-请求示例)
9. [完整代码流程图](#完整代码流程图)
10. [错误处理机制](#错误处理机制)
11. [性能优化建议](#性能优化建议)
---
## 函数概述
`getHomeData()` 是首页数据统计的核心函数,用于获取以下 6 项关键业务指标:
1. **今日待审核文件数** - 统计今天创建的待审核文件
2. **本月已审核文件数** - 统计本月完成审核的文件总数
3. **审核文件同比增长** - 与上月对比的增长百分比
4. **本月审核通过率** - 本月通过文件数 / 本月已审核文件数
5. **通过率同比增长** - 与上月通过率对比的增长百分比
6. **检测到的问题总数** - 从评估结果中统计问题数量
7. **问题数量同比增长** - 与上月问题数量对比的增长百分比
---
## 函数签名与返回值
### 函数签名
```typescript
export async function getHomeData(
typeIds?: number[] | number | null, // 文档类型 ID 数组或单个 ID
userId?: string | number, // 用户 ID
token?: string // JWT Token
): Promise<HomeStatistics>
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `typeIds` | `number[] \| number \| null` | 否 | 文档类型 ID 筛选:<br>- 单个 ID: `1` (筛选 type_id = 1)<br>- ID 数组: `[2, 3]` (筛选 type_id = 2 或 3)<br>- `null``undefined`: 不筛选类型 |
| `userId` | `string \| number` | 是 | 当前用户的 ID,用于过滤用户数据 |
| `token` | `string` | 是 | JWT 认证令牌 |
### 返回值结构
```typescript
interface HomeStatistics {
todayPendingFiles: number; // 今日待审核文件数
monthlyReviewedFiles: number; // 本月已审核文件数
monthlyReviewGrowth: { // 本月审核文件同比增长
value: number; // 增长百分比(绝对值)
isUp: boolean; // 是否增长(true: 增长, false: 下降)
};
monthlyPassRate: number; // 本月审核通过率(百分比)
passRateGrowth: { // 通过率同比增长
value: number;
isUp: boolean;
};
issuesDetected: number; // 检测到的问题总数
issuesGrowth: { // 问题数量同比增长
value: number;
isUp: boolean;
};
}
```
### 返回示例
```json
{
"todayPendingFiles": 15,
"monthlyReviewedFiles": 120,
"monthlyReviewGrowth": {
"value": 25.5,
"isUp": true
},
"monthlyPassRate": 85.3,
"passRateGrowth": {
"value": 3.2,
"isUp": true
},
"issuesDetected": 42,
"issuesGrowth": {
"value": 12.8,
"isUp": false
}
}
```
---
## 业务逻辑流程
### 主流程图
```
开始
├─→ 1. 计算时间范围
│ ├─ 今天开始时间 (startOfToday)
│ ├─ 本月开始/结束 (startOfThisMonth, endOfThisMonth)
│ └─ 上月开始/结束 (startOfLastMonth, endOfLastMonth)
├─→ 2. 构建类型过滤条件
│ ├─ 单个 ID (如 1) → type_id.eq.1
│ ├─ ID 数组 (如 [2,3]) → (type_id.eq.2,type_id.eq.3)
│ └─ null/undefined → 不过滤
├─→ 3. 查询今日待审核文件数
│ └─ 条件: audit_status IN (0,2,NULL) AND created_at >= 今天开始
├─→ 4. 查询本月已审核文件数
│ └─ 条件: audit_status NOT IN (0,2) AND upload_time >= 本月开始
├─→ 5. 查询上月已审核文件数
│ └─ 条件: audit_status NOT IN (0,2) AND upload_time BETWEEN 上月开始 AND 上月结束
├─→ 6. 计算审核文件同比增长
│ └─ 增长率 = (本月数量 - 上月数量) / 上月数量 * 100
├─→ 7. 查询本月审核通过数量
│ └─ 条件: audit_status = 1 AND created_at >= 本月开始
├─→ 8. 计算本月审核通过率
│ └─ 通过率 = 本月通过数量 / 本月已审核数量 * 100
├─→ 9. 查询上月审核通过数量
│ └─ 条件: audit_status = 1 AND upload_time BETWEEN 上月开始 AND 上月结束
├─→ 10. 计算上月审核通过率
│ └─ 通过率 = 上月通过数量 / 上月已审核数量 * 100
├─→ 11. 计算通过率同比增长
│ └─ 增长率 = (本月通过率 - 上月通过率) / 上月通过率 * 100
├─→ 12. 调用数据库函数统计问题数量
│ ├─ 本月问题数量 (RPC: count_evaluation_results_by_type)
│ └─ 上月问题数量 (RPC: count_evaluation_results_by_type)
├─→ 13. 计算问题数量同比增长
│ └─ 增长率 = (本月问题数 - 上月问题数) / 上月问题数 * 100
└─→ 14. 返回统计结果
└─ HomeStatistics 对象
```
---
## 时间范围计算
使用 `dayjs` 库进行时间计算,确保时区一致性。
### 代码实现
```typescript
import dayjs from 'dayjs';
// 今天开始时间 (00:00:00)
const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
// 示例: '2025-01-17 00:00:00'
// 本月开始时间 (月初 00:00:00)
const startOfThisMonth = dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss');
// 示例: '2025-01-01 00:00:00'
// 本月结束时间 (月末 23:59:59)
const endOfThisMonth = dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss');
// 示例: '2025-01-31 23:59:59'
// 上月开始时间
const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss');
// 示例: '2024-12-01 00:00:00'
// 上月结束时间
const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss');
// 示例: '2024-12-31 23:59:59'
```
### 时间范围示例表
假设当前时间为 **2025-01-17 15:30:45**
| 时间变量 | 值 | 说明 |
|---------|-----|------|
| `startOfToday` | `2025-01-17 00:00:00` | 今天凌晨 |
| `startOfThisMonth` | `2025-01-01 00:00:00` | 本月第一天凌晨 |
| `endOfThisMonth` | `2025-01-31 23:59:59` | 本月最后一天 23:59:59 |
| `startOfLastMonth` | `2024-12-01 00:00:00` | 上月第一天凌晨 |
| `endOfLastMonth` | `2024-12-31 23:59:59` | 上月最后一天 23:59:59 |
---
## 数据查询详解
### 辅助函数:buildTypeFilter()
**功能**: 根据 `typeIds` 参数构建 PostgREST 类型过滤条件
```typescript
function buildTypeFilter(typeIds?: number[] | number | null): string {
if (!typeIds) {
return ''; // null 或 undefined 返回空字符串,表示不过滤
}
// 标准化为数组
const idsArray = Array.isArray(typeIds) ? typeIds : [typeIds];
if (idsArray.length === 0) {
return '';
}
if (idsArray.length === 1) {
// 单个 ID: type_id.eq.1
return `type_id.eq.${idsArray[0]}`;
} else {
// 多个 ID: (type_id.eq.2,type_id.eq.3)
return `(${idsArray.map(id => `type_id.eq.${id}`).join(',')})`;
}
}
```
**参数说明**:
- `typeIds`: 可以是单个数字、数字数组或 null/undefined
**返回值说明**:
- `1``'type_id.eq.1'`
- `[2, 3]``'(type_id.eq.2,type_id.eq.3)'`
- `null` / `undefined``''` (空字符串)
**使用示例**:
```typescript
buildTypeFilter(1); // 'type_id.eq.1'
buildTypeFilter([2, 3]); // '(type_id.eq.2,type_id.eq.3)'
buildTypeFilter([1, 2, 3]); // '(type_id.eq.1,type_id.eq.2,type_id.eq.3)'
buildTypeFilter(null); // ''
buildTypeFilter(undefined); // ''
```
---
### 查询 1: 今日待审核文件数
**业务逻辑**: 统计今天创建的、状态为待审核的文件数量
**审核状态说明**:
- `audit_status = 0`: 未审核
- `audit_status = 2`: 审核中
- `audit_status = NULL`: 未设置状态
**PostgREST 参数**:
```typescript
const todayPendingParams: PostgrestParams = {
select: 'count',
filter: {
or: `(audit_status.eq.0,audit_status.eq.2,audit_status.is.null)`,
created_at: `gte.${startOfToday}`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 如果有类型过滤
if (typeFilter) {
if (typeFilter.startsWith('(')) {
// OR 条件: (type_id.eq.2,type_id.eq.3)
todayPendingParams.filter.or = typeFilter + ',' + todayPendingParams.filter.or;
} else {
// 单一条件: type_id.eq.1
const [field, op, value] = typeFilter.split('.');
todayPendingParams.filter[field] = `${op}.${value}`;
}
}
```
**等价 SQL 查询**:
```sql
SELECT COUNT(*)
FROM documents
WHERE (audit_status = 0 OR audit_status = 2 OR audit_status IS NULL)
AND created_at >= '2025-01-17 00:00:00'
AND is_test_document = false
AND user_id = 123
AND type_id = 1; -- 如果 typeIds = 1
-- 或 (type_id = 2 OR type_id = 3); -- 如果 typeIds = [2, 3]
```
**实际 API 调用**:
```typescript
const todayPendingCount = await postgrestGet('documents', {
...todayPendingParams,
token
});
// 返回格式: [{ count: 15 }]
const todayPendingFiles = todayPendingCount[0]?.count || 0;
```
---
### 查询 2: 本月已审核文件数
**业务逻辑**: 统计本月上传的、已完成审核的文件数量(排除待审核和审核中)
**PostgREST 参数**:
```typescript
const thisMonthReviewedParams: PostgrestParams = {
select: 'count',
filter: {
and: `(audit_status.neq.0,audit_status.neq.2)`,
upload_time: `gte.${startOfThisMonth}`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
thisMonthReviewedParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
thisMonthReviewedParams.filter[field] = `${op}.${value}`;
}
}
```
**等价 SQL 查询**:
```sql
SELECT COUNT(*)
FROM documents
WHERE audit_status != 0
AND audit_status != 2
AND upload_time >= '2025-01-01 00:00:00'
AND is_test_document = false
AND user_id = 123
AND type_id = 1; -- 根据 typeIds 过滤
```
**实际 API 调用**:
```typescript
const thisMonthReviewedCount = await postgrestGet('documents', {
...thisMonthReviewedParams,
token
});
// 返回格式: [{ count: 120 }]
const monthlyReviewedFiles = thisMonthReviewedCount[0]?.count || 0;
```
---
### 查询 3: 上月已审核文件数
**业务逻辑**: 统计上月的已审核文件数量,用于计算同比增长
**PostgREST 参数**:
```typescript
const lastMonthReviewedParams: PostgrestParams = {
select: 'count',
filter: {
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth},audit_status.neq.0,audit_status.neq.2)`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
lastMonthReviewedParams.filter.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
lastMonthReviewedParams.filter[field] = `${op}.${value}`;
}
}
```
**等价 SQL 查询**:
```sql
SELECT COUNT(*)
FROM documents
WHERE upload_time >= '2024-12-01 00:00:00'
AND upload_time <= '2024-12-31 23:59:59'
AND audit_status != 0
AND audit_status != 2
AND is_test_document = false
AND user_id = 123
AND type_id = 1;
```
**实际 API 调用**:
```typescript
const lastMonthReviewedCount = await postgrestGet('documents', {
...lastMonthReviewedParams,
token
});
// 返回格式: [{ count: 96 }]
const lastMonthReviewed = lastMonthReviewedCount[0]?.count || 0;
```
---
### 查询 4: 本月审核通过数量
**业务逻辑**: 统计本月审核通过的文件数量(`audit_status = 1`
**PostgREST 参数**:
```typescript
const thisMonthTotalParams: PostgrestParams = {
select: 'count',
filter: {
audit_status: `eq.1`,
created_at: `gte.${startOfThisMonth}`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
thisMonthTotalParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
thisMonthTotalParams.filter[field] = `${op}.${value}`;
}
}
```
**等价 SQL 查询**:
```sql
SELECT COUNT(*)
FROM documents
WHERE audit_status = 1 -- 审核通过
AND created_at >= '2025-01-01 00:00:00'
AND is_test_document = false
AND user_id = 123
AND type_id = 1;
```
**实际 API 调用**:
```typescript
const thisMonthTotalCount = await postgrestGet('documents', {
...thisMonthTotalParams,
token
});
// 返回格式: [{ count: 102 }]
const thisMonthPassTotal = thisMonthTotalCount[0]?.count || 0;
```
---
### 查询 5: 上月审核通过数量
**业务逻辑**: 统计上月审核通过的文件数量,用于计算通过率同比增长
**PostgREST 参数**:
```typescript
const lastMonthTotalParams: PostgrestParams = {
select: 'count',
filter: {
audit_status: `eq.1`,
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth})`,
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
lastMonthTotalParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
lastMonthTotalParams.filter[field] = `${op}.${value}`;
}
}
```
**等价 SQL 查询**:
```sql
SELECT COUNT(*)
FROM documents
WHERE audit_status = 1
AND upload_time >= '2024-12-01 00:00:00'
AND upload_time <= '2024-12-31 23:59:59'
AND is_test_document = false
AND user_id = 123
AND type_id = 1;
```
**实际 API 调用**:
```typescript
const lastMonthTotalCount = await postgrestGet('documents', {
...lastMonthTotalParams,
token
});
// 返回格式: [{ count: 78 }]
const lastMonthTotal = lastMonthTotalCount[0]?.count || 0;
```
---
## 数据库例程详解
### count_evaluation_results_by_type 函数
这是一个 PostgreSQL 存储函数(PL/pgSQL),用于统计特定时间范围内、特定文档类型的评估问题数量。
#### 函数定义
```sql
CREATE OR REPLACE FUNCTION public.count_evaluation_results_by_type(
start_time TIMESTAMP WITHOUT TIME ZONE,
end_time TIMESTAMP WITHOUT TIME ZONE,
type_val INTEGER[] DEFAULT NULL,
userid INTEGER DEFAULT NULL
) RETURNS TABLE(count BIGINT)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT COUNT(*)
FROM evaluation_results er
JOIN documents d ON er.document_id = d.id
WHERE (type_val IS NULL OR d.type_id = ANY(type_val))
AND (er.evaluated_results ->> 'result')::text = 'false'
AND er.created_at >= start_time
AND er.created_at <= end_time
AND (userid IS NULL OR d.user_id = userid);
END;
$function$
```
#### 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `start_time` | `TIMESTAMP` | 必填 | 统计开始时间 |
| `end_time` | `TIMESTAMP` | 必填 | 统计结束时间 |
| `type_val` | `INTEGER[]` | `NULL` | 文档类型 ID 数组,`NULL` 表示不筛选类型 |
| `userid` | `INTEGER` | `NULL` | 用户 ID`NULL` 表示不筛选用户 |
#### 返回值
返回一个表,包含单列 `count`(BIGINT 类型),表示符合条件的问题数量。
#### 业务逻辑
1. **关联查询**: 从 `evaluation_results` 表关联 `documents`
2. **类型筛选**: 如果 `type_val` 不为 `NULL`,则筛选指定类型的文档
3. **问题筛选**: 只统计 `evaluated_results.result = 'false'` 的记录(表示有问题)
4. **时间范围**: 筛选 `er.created_at` 在指定时间范围内的记录
5. **用户筛选**: 如果 `userid` 不为 `NULL`,则筛选指定用户的文档
#### 关键逻辑
**判断评估结果是否为问题**:
```sql
(er.evaluated_results ->> 'result')::text = 'false'
```
- `evaluated_results` 是 JSONB 类型字段
- `->>` 操作符提取 JSON 字段的文本值
-`result` 字段为 `'false'` 时,表示该评估点检查未通过(有问题)
**类型筛选的灵活性**:
```sql
(type_val IS NULL OR d.type_id = ANY(type_val))
```
- 如果 `type_val``NULL`,则不筛选类型
- 否则使用 `ANY(type_val)` 匹配数组中的任意值
#### 使用示例
**示例 1: 查询合同类型的问题数量**
```typescript
const response = await postgrestPost<{ count: number }[]>(
'rpc/count_evaluation_results_by_type',
{
start_time: '2025-01-01 00:00:00',
end_time: '2025-01-31 23:59:59',
type_val: [1], // 合同类型 ID
userid: 123
},
token
);
// 返回: [{ count: 42 }]
const issuesCount = response[0]?.count || 0;
```
**示例 2: 查询记录类型(多个类型)的问题数量**
```typescript
const response = await postgrestPost<{ count: number }[]>(
'rpc/count_evaluation_results_by_type',
{
start_time: '2024-12-01 00:00:00',
end_time: '2024-12-31 23:59:59',
type_val: [2, 3], // 记录类型 ID(类型2和类型3)
userid: 123
},
token
);
// 返回: [{ count: 35 }]
const lastMonthIssues = response[0]?.count || 0;
```
**示例 3: 查询所有类型的问题数量(不筛选用户)**
```typescript
const response = await postgrestPost<{ count: number }[]>(
'rpc/count_evaluation_results_by_type',
{
start_time: '2025-01-01 00:00:00',
end_time: '2025-01-31 23:59:59',
type_val: null, // 所有类型
userid: null // 所有用户
},
token
);
// 返回: [{ count: 150 }]
```
#### 实际调用代码(在 getHomeData 中)
**示例 1: 查询单个类型(type_id = 1**:
```typescript
// typeIds = 1 或 [1]
const typeToQuery = Array.isArray(typeIds) ? typeIds : [typeIds];
// 本月问题数量
const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery, // [1]
userid: parseInt(userId as string)
}, token),
'获取本月问题数据失败',
[]
);
thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
// 上月问题数量
const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfLastMonth,
end_time: endOfLastMonth,
type_val: typeToQuery, // [1]
userid: parseInt(userId as string)
}, token),
'获取上月问题数据失败',
[]
);
lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
```
**示例 2: 查询多个类型(type_id = 2 或 3**:
```typescript
// typeIds = [2, 3]
const typeToQuery = typeIds; // [2, 3]
// 本月问题数量(类型2和3合并统计)
const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery, // [2, 3]
userid: parseInt(userId as string)
}, token),
'获取本月问题数据失败',
[]
);
thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
// 上月问题数量
const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfLastMonth,
end_time: endOfLastMonth,
type_val: typeToQuery, // [2, 3]
userid: parseInt(userId as string)
}, token),
'获取上月问题数据失败',
[]
);
lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
```
**示例 3: 查询所有类型(不筛选)**:
```typescript
// typeIds = null 或 undefined
const typeToQuery = null;
// 本月问题数量(所有类型)
const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery, // null
userid: parseInt(userId as string)
}, token),
'获取本月问题数据失败',
[]
);
thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
```
#### 性能优化建议
**索引优化**:
```sql
-- 评估结果表索引
CREATE INDEX idx_eval_results_created_at ON evaluation_results(created_at);
CREATE INDEX idx_eval_results_document_id ON evaluation_results(document_id);
CREATE INDEX idx_eval_results_result ON evaluation_results((evaluated_results->>'result'));
-- 文档表索引
CREATE INDEX idx_documents_type_user ON documents(type_id, user_id);
CREATE INDEX idx_documents_user_id ON documents(user_id);
-- 复合索引(覆盖查询)
CREATE INDEX idx_eval_doc_composite ON evaluation_results(document_id, created_at)
INCLUDE ((evaluated_results->>'result'));
```
**查询计划分析**:
```sql
EXPLAIN ANALYZE
SELECT COUNT(*)
FROM evaluation_results er
JOIN documents d ON er.document_id = d.id
WHERE d.type_id = ANY(ARRAY[1])
AND (er.evaluated_results ->> 'result')::text = 'false'
AND er.created_at >= '2025-01-01 00:00:00'
AND er.created_at <= '2025-01-31 23:59:59'
AND d.user_id = 123;
```
---
## 增长率计算公式
### 审核文件同比增长
**计算逻辑**:
```typescript
let reviewGrowthValue = 0;
let reviewGrowthIsUp = true;
if (lastMonthReviewed > 0) {
// 正常计算增长率
const growthRate = ((monthlyReviewedFiles - lastMonthReviewed) / lastMonthReviewed) * 100;
reviewGrowthValue = Math.abs(parseFloat(growthRate.toFixed(1)));
reviewGrowthIsUp = growthRate >= 0;
} else if (lastMonthReviewed == 0 && monthlyReviewedFiles > 0) {
// 特殊情况:上月为0,本月大于0
reviewGrowthValue = 100;
reviewGrowthIsUp = true;
}
```
**公式**:
```
增长率 = (本月数量 - 上月数量) / 上月数量 × 100%
```
**计算示例**:
| 本月数量 | 上月数量 | 增长率 | value | isUp |
|---------|---------|--------|-------|------|
| 120 | 96 | (120-96)/96 × 100 = 25% | 25.0 | true |
| 80 | 100 | (80-100)/100 × 100 = -20% | 20.0 | false |
| 100 | 100 | (100-100)/100 × 100 = 0% | 0.0 | true |
| 50 | 0 | 特殊处理 | 100.0 | true |
| 0 | 0 | 不计算 | 0.0 | true |
**注意事项**:
- `value` 始终为绝对值(非负数)
- `isUp` 表示增长方向(true: 增长或持平, false: 下降)
- 上月为 0 时,固定返回 100% 增长
---
### 审核通过率计算
**本月通过率**:
```typescript
const monthlyPassRate = (thisMonthPassTotal > 0 && monthlyReviewedFiles > 0)
? parseFloat(((thisMonthPassTotal / monthlyReviewedFiles) * 100).toFixed(1))
: 0;
```
**公式**:
```
通过率 = 本月审核通过数量 / 本月已审核数量 × 100%
```
**计算示例**:
| 审核通过数量 | 已审核数量 | 通过率 |
|------------|----------|-------|
| 102 | 120 | 102/120 × 100 = 85.0% |
| 78 | 96 | 78/96 × 100 = 81.3% |
| 0 | 100 | 0% |
| 50 | 0 | 0% (防止除0错误) |
---
### 通过率同比增长
**计算逻辑**:
```typescript
let passRateGrowthValue = 0;
let passRateGrowthIsUp = true;
if (lastMonthPassRate > 0) {
const passRateGrowth = ((monthlyPassRate - lastMonthPassRate) / lastMonthPassRate) * 100;
passRateGrowthValue = Math.abs(parseFloat(passRateGrowth.toFixed(1)));
passRateGrowthIsUp = passRateGrowth >= 0;
} else if (lastMonthPassRate == 0 && monthlyPassRate > 0) {
passRateGrowthValue = 100;
passRateGrowthIsUp = true;
}
```
**公式**:
```
通过率增长率 = (本月通过率 - 上月通过率) / 上月通过率 × 100%
```
**计算示例**:
| 本月通过率 | 上月通过率 | 增长率 | value | isUp |
|----------|----------|--------|-------|------|
| 85.0% | 81.3% | (85.0-81.3)/81.3 × 100 = 4.6% | 4.6 | true |
| 80.0% | 85.0% | (80.0-85.0)/85.0 × 100 = -5.9% | 5.9 | false |
| 85.0% | 0% | 特殊处理 | 100.0 | true |
---
### 问题数量同比增长
**计算逻辑**:
```typescript
let issuesGrowthValue = 0;
let issuesGrowthIsUp = true;
if (lastMonthIssuesCount > 0) {
const issuesGrowth = ((thisMonthIssuesCount - lastMonthIssuesCount) / lastMonthIssuesCount) * 100;
issuesGrowthValue = Math.abs(parseFloat(issuesGrowth.toFixed(1)));
issuesGrowthIsUp = issuesGrowth >= 0;
} else if (lastMonthIssuesCount == 0 && thisMonthIssuesCount > 0) {
issuesGrowthValue = 100;
issuesGrowthIsUp = true;
}
```
**公式**:
```
问题增长率 = (本月问题数 - 上月问题数) / 上月问题数 × 100%
```
**计算示例**:
| 本月问题数 | 上月问题数 | 增长率 | value | isUp |
|----------|----------|--------|-------|------|
| 42 | 35 | (42-35)/35 × 100 = 20.0% | 20.0 | true |
| 30 | 42 | (30-42)/42 × 100 = -28.6% | 28.6 | false |
| 50 | 0 | 特殊处理 | 100.0 | true |
**注意**: 对于问题数量,`isUp = true` 表示问题增多(通常是负面指标)
---
## PostgREST 请求示例
### 完整请求流程示例
假设:
- 用户 ID: `123`
- 文档类型 ID: `1` (合同类型)
- 当前时间: `2025-01-17`
#### 请求 1: 今日待审核文件数
**HTTP 请求**:
```http
GET /documents?select=count&or=(audit_status.eq.0,audit_status.eq.2,audit_status.is.null,type_id.eq.1)&created_at=gte.2025-01-17%2000:00:00&is_test_document=eq.false&user_id=eq.123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**响应**:
```json
[
{ "count": 15 }
]
```
---
#### 请求 2: 本月已审核文件数
**HTTP 请求**:
```http
GET /documents?select=count&and=(audit_status.neq.0,audit_status.neq.2)&upload_time=gte.2025-01-01%2000:00:00&is_test_document=eq.false&user_id=eq.123&type_id=eq.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**响应**:
```json
[
{ "count": 120 }
]
```
---
#### 请求 3: 上月已审核文件数
**HTTP 请求**:
```http
GET /documents?select=count&and=(upload_time.gte.2024-12-01%2000:00:00,upload_time.lte.2024-12-31%2023:59:59,audit_status.neq.0,audit_status.neq.2)&is_test_document=eq.false&user_id=eq.123&type_id=eq.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**响应**:
```json
[
{ "count": 96 }
]
```
---
#### 请求 4: 本月审核通过数量
**HTTP 请求**:
```http
GET /documents?select=count&audit_status=eq.1&created_at=gte.2025-01-01%2000:00:00&is_test_document=eq.false&user_id=eq.123&type_id=eq.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**响应**:
```json
[
{ "count": 102 }
]
```
---
#### 请求 5: 本月问题数量(RPC 调用)
**HTTP 请求**:
```http
POST /rpc/count_evaluation_results_by_type
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"start_time": "2025-01-01 00:00:00",
"end_time": "2025-01-31 23:59:59",
"type_val": [1],
"userid": 123
}
```
**响应**:
```json
[
{ "count": 42 }
]
```
---
## 完整代码流程图
```
getHomeData(typeIds=1, userId=123, token='...')
├─→ [1] 时间计算
│ ├─ startOfToday = '2025-01-17 00:00:00'
│ ├─ startOfThisMonth = '2025-01-01 00:00:00'
│ ├─ endOfThisMonth = '2025-01-31 23:59:59'
│ ├─ startOfLastMonth = '2024-12-01 00:00:00'
│ └─ endOfLastMonth = '2024-12-31 23:59:59'
├─→ [2] 类型过滤
│ └─ typeFilter = 'type_id.eq.1' (单个 ID: 1)
├─→ [3] 今日待审核文件数
│ ├─ API: GET /documents?select=count&...
│ ├─ 条件: audit_status IN (0,2,NULL), created_at >= '2025-01-17'
│ └─ 结果: todayPendingFiles = 15
├─→ [4] 本月已审核文件数
│ ├─ API: GET /documents?select=count&...
│ ├─ 条件: audit_status NOT IN (0,2), upload_time >= '2025-01-01'
│ └─ 结果: monthlyReviewedFiles = 120
├─→ [5] 上月已审核文件数
│ ├─ API: GET /documents?select=count&...
│ ├─ 条件: audit_status NOT IN (0,2), upload_time BETWEEN '2024-12-01' AND '2024-12-31'
│ └─ 结果: lastMonthReviewed = 96
├─→ [6] 计算审核文件增长率
│ ├─ growthRate = (120 - 96) / 96 * 100 = 25%
│ └─ 结果: { value: 25.0, isUp: true }
├─→ [7] 本月审核通过数量
│ ├─ API: GET /documents?select=count&...
│ ├─ 条件: audit_status = 1, created_at >= '2025-01-01'
│ └─ 结果: thisMonthPassTotal = 102
├─→ [8] 计算本月通过率
│ ├─ monthlyPassRate = 102 / 120 * 100 = 85.0%
│ └─ 结果: 85.0
├─→ [9] 上月审核通过数量
│ ├─ API: GET /documents?select=count&...
│ ├─ 条件: audit_status = 1, upload_time BETWEEN '2024-12-01' AND '2024-12-31'
│ └─ 结果: lastMonthTotal = 78
├─→ [10] 计算上月通过率
│ ├─ lastMonthPassRate = 78 / 96 * 100 = 81.3%
│ └─ 结果: 81.3
├─→ [11] 计算通过率增长
│ ├─ passRateGrowth = (85.0 - 81.3) / 81.3 * 100 = 4.6%
│ └─ 结果: { value: 4.6, isUp: true }
├─→ [12] 本月问题数量
│ ├─ API: POST /rpc/count_evaluation_results_by_type
│ ├─ 参数: start_time='2025-01-01', end_time='2025-01-31', type_val=[1], userid=123
│ └─ 结果: thisMonthIssuesCount = 42
├─→ [13] 上月问题数量
│ ├─ API: POST /rpc/count_evaluation_results_by_type
│ ├─ 参数: start_time='2024-12-01', end_time='2024-12-31', type_val=[1], userid=123
│ └─ 结果: lastMonthIssuesCount = 35
├─→ [14] 计算问题数量增长
│ ├─ issuesGrowth = (42 - 35) / 35 * 100 = 20.0%
│ └─ 结果: { value: 20.0, isUp: true }
└─→ [15] 返回结果
└─ {
todayPendingFiles: 15,
monthlyReviewedFiles: 120,
monthlyReviewGrowth: { value: 25.0, isUp: true },
monthlyPassRate: 85.0,
passRateGrowth: { value: 4.6, isUp: true },
issuesDetected: 42,
issuesGrowth: { value: 20.0, isUp: true }
}
```
---
## 错误处理机制
### 辅助函数:handleApiResponse()
**功能**: 统一处理 API 调用的错误和默认值
```typescript
const handleApiResponse = async <T>(
apiCall: Promise<{
data?: unknown;
headers?: Record<string, string>;
error?: string;
status?: number
}>,
errorMessage: string,
defaultValue: T
): Promise<T> => {
try {
const response = await apiCall;
if (response.error) {
console.error(`${errorMessage}: ${response.error}`);
return defaultValue;
}
const data = extractApiData<T>(response.data);
if (!data) {
console.warn(`${errorMessage}: 无法提取有效数据`);
return defaultValue;
}
return data;
} catch (error) {
console.error(`${errorMessage}: ${error instanceof Error ? error.message : '未知错误'}`);
return defaultValue;
}
};
```
**使用示例**:
```typescript
const todayPendingCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', { ...todayPendingParams, token }),
'获取今日待审核文件数量失败',
[] // 默认返回空数组
);
```
---
### 全局错误处理
**外层 try-catch**:
```typescript
try {
// 所有业务逻辑
...
} catch (error) {
console.error('获取首页数据失败:', error instanceof Error ? error.message : String(error));
// 返回默认值以防止页面崩溃
return {
todayPendingFiles: 0,
monthlyReviewedFiles: 0,
monthlyReviewGrowth: { value: 0, isUp: true },
monthlyPassRate: 0,
passRateGrowth: { value: 0, isUp: true },
issuesDetected: 0,
issuesGrowth: { value: 0, isUp: true }
};
}
```
**错误处理策略**:
1. **单个查询失败**: 返回默认值(0 或空数组),记录错误日志,不中断整体流程
2. **整体失败**: 返回完整的默认数据结构,确保页面可以正常渲染
---
### 数据提取函数:extractApiData()
**功能**: 从不同格式的 API 响应中提取数据
```typescript
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) {
console.warn('API响应数据为空');
return null;
}
try {
// 检查错误码
if (typeof responseData === 'object' && responseData !== null) {
if ('code' in responseData) {
const code = (responseData as { code: number }).code;
if (code !== 0 && code !== 200) {
const errorMsg = 'msg' in responseData
? (responseData as { msg: string }).msg
: '未知错误';
console.error(`API响应错误: [${code}] ${errorMsg}`);
return null;
}
}
// 检查错误消息
if ('error' in responseData && (responseData as { error: unknown }).error) {
const error = (responseData as { error: unknown }).error;
console.error(`API响应包含错误: ${typeof error === 'string' ? error : JSON.stringify(error)}`);
return null;
}
// 格式1: { code: number, msg: string, data: T }
if ('data' in responseData) {
const data = (responseData as { data: unknown }).data;
if (!data) {
console.warn('API响应中的data字段为空');
return null;
}
return data as T;
}
}
// 格式2: 直接是数据对象
return responseData as T;
} catch (error) {
console.error('处理API响应数据时出错:', error);
return null;
}
}
```
**支持的响应格式**:
1. **格式1 (9000端口)**:
```json
{
"code": 0,
"msg": "success",
"data": [{ "count": 10 }]
}
```
2. **格式2 (8000端口)**:
```json
[{ "count": 10 }]
```
3. **错误响应**:
```json
{
"code": 500,
"msg": "Internal Server Error"
}
```
---
## 性能优化建议
### 1. 并行查询
**当前实现**: 串行查询(一个接一个)
**优化方案**: 使用 `Promise.all` 并行执行所有查询
```typescript
// ✅ 推荐:并行查询
const [
todayPendingCount,
thisMonthReviewedCount,
lastMonthReviewedCount,
thisMonthPassCount,
lastMonthPassCount,
thisMonthIssuesResponse,
lastMonthIssuesResponse
] = await Promise.all([
postgrestGet('documents', { ...todayPendingParams, token }),
postgrestGet('documents', { ...thisMonthReviewedParams, token }),
postgrestGet('documents', { ...lastMonthReviewedParams, token }),
postgrestGet('documents', { ...thisMonthTotalParams, token }),
postgrestGet('documents', { ...lastMonthTotalParams, token }),
postgrestPost('rpc/count_evaluation_results_by_type', thisMonthIssuesParams, token),
postgrestPost('rpc/count_evaluation_results_by_type', lastMonthIssuesParams, token)
]);
// 性能提升:从 ~700ms (7 × 100ms) 降低到 ~100ms
```
---
### 2. Redis 缓存
**缓存策略**: 缓存 5 分钟(统计数据不需要实时更新)
```typescript
import Redis from 'ioredis';
const redis = new Redis();
export async function getHomeData(typeIds, userId, token) {
// 生成缓存键
const date = dayjs().format('YYYY-MM-DD');
// 将 typeIds 转换为字符串作为键的一部分
const typeKey = typeIds
? Array.isArray(typeIds)
? typeIds.join(',')
: String(typeIds)
: 'all';
const cacheKey = `home:stats:${userId}:${typeKey}:${date}`;
// 尝试从缓存获取
const cached = await redis.get(cacheKey);
if (cached) {
console.log('✅ 从缓存返回首页数据');
return JSON.parse(cached);
}
// 查询数据库
const result = await fetchHomeDataFromDB(typeIds, userId, token);
// 缓存 5 分钟
await redis.setex(cacheKey, 300, JSON.stringify(result));
return result;
}
```
**缓存失效策略**:
- 每天零点自动失效(键包含日期)
- 用户上传新文档后手动清除缓存
- TTL 设置为 5 分钟
---
### 3. 数据库索引优化
**documents 表索引**:
```sql
-- 复合索引:覆盖常用查询
CREATE INDEX idx_documents_user_status_time ON documents(user_id, audit_status, created_at);
CREATE INDEX idx_documents_user_type_upload ON documents(user_id, type_id, upload_time);
CREATE INDEX idx_documents_user_created ON documents(user_id, created_at) WHERE is_test_document = false;
-- 条件索引:仅索引非测试文档
CREATE INDEX idx_documents_non_test ON documents(user_id, audit_status) WHERE is_test_document = false;
```
**evaluation_results 表索引**:
```sql
-- 支持 count_evaluation_results_by_type 函数
CREATE INDEX idx_eval_results_time_result ON evaluation_results(created_at, (evaluated_results->>'result'));
CREATE INDEX idx_eval_results_doc_time ON evaluation_results(document_id, created_at);
```
---
### 4. 物化视图(高级优化)
**创建物化视图**: 预计算每日统计数据
```sql
CREATE MATERIALIZED VIEW mv_daily_document_stats AS
SELECT
user_id,
type_id,
DATE(created_at) as stat_date,
COUNT(*) FILTER (WHERE audit_status IN (0, 2)) as pending_count,
COUNT(*) FILTER (WHERE audit_status NOT IN (0, 2)) as reviewed_count,
COUNT(*) FILTER (WHERE audit_status = 1) as passed_count
FROM documents
WHERE is_test_document = false
GROUP BY user_id, type_id, DATE(created_at);
-- 创建索引
CREATE INDEX ON mv_daily_document_stats(user_id, type_id, stat_date);
-- 定时刷新(每小时)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_document_stats;
```
**使用物化视图查询**:
```typescript
// 查询本月已审核文件数
const response = await postgrestGet('mv_daily_document_stats', {
select: 'sum(reviewed_count)',
filter: {
'user_id': `eq.${userId}`,
'type_id': `eq.1`,
'stat_date': `gte.${startOfThisMonth.split(' ')[0]}`
}
});
```
**性能提升**: 从全表扫描降低到索引查询,查询时间从 ~100ms 降低到 ~10ms
---
### 5. 数据库函数优化
**优化 count_evaluation_results_by_type 函数**:
```sql
-- 添加结果缓存(需要 PostgreSQL 12+
CREATE OR REPLACE FUNCTION count_evaluation_results_by_type(...)
RETURNS TABLE(count BIGINT)
LANGUAGE plpgsql
STABLE -- 标记为稳定函数,允许查询计划缓存
AS $function$
BEGIN
RETURN QUERY
SELECT COUNT(*)
FROM evaluation_results er
INNER JOIN documents d ON er.document_id = d.id -- 使用 INNER JOIN 代替 JOIN
WHERE (type_val IS NULL OR d.type_id = ANY(type_val))
AND (er.evaluated_results->>'result') = 'false' -- 去除不必要的类型转换
AND er.created_at BETWEEN start_time AND end_time -- 使用 BETWEEN
AND (userid IS NULL OR d.user_id = userid);
END;
$function$;
```
---
### 6. 前端优化
**使用 SWR 缓存**:
```typescript
import useSWR from 'swr';
function HomePage() {
const { data, error, isLoading } = useSWR(
'/api/home-data?typeId=1', // 单个 ID
// 或 '/api/home-data?typeId=2,3', // 多个 ID
// 或 '/api/home-data', // 不筛选类型
fetcher,
{
refreshInterval: 300000, // 5 分钟自动刷新
revalidateOnFocus: false, // 切换标签页不重新请求
dedupingInterval: 60000 // 1 分钟内去重
}
);
return <Dashboard data={data} />;
}
```
---
## 使用示例
### URL 参数传递方式
通过 URL 查询参数传递 `typeId`
```
# 查询单个类型
GET /home?typeId=1
# 查询多个类型(逗号分隔)
GET /home?typeId=2,3
# 不筛选类型
GET /home
```
### 函数调用方式
```typescript
// 方式 1: 单个 ID
await getHomeData(1, userId, token);
// 方式 2: ID 数组
await getHomeData([2, 3], userId, token);
// 方式 3: 不筛选
await getHomeData(null, userId, token);
// 或
await getHomeData(undefined, userId, token);
```
### 使用场景说明
| 场景 | typeIds 值 | 说明 | 适用情况 |
|------|-----------|------|---------|
| 查询合同类型 | `1` | 单个 type_id = 1 | 合同审核专用页面 |
| 查询记录类型 | `[2, 3]` | type_id = 2 或 3 | 许可卷宗审核页面 |
| 查询所有类型 | `null``undefined` | 不筛选类型 | 首页总览 |
| 查询多种类型 | `[1, 2, 3]` | type_id = 1 或 2 或 3 | 综合审核页面 |
---
### Remix Loader 中使用
```typescript
// app/routes/home.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getHomeData } from "~/api/home/home";
import { getUserSession } from "~/api/login/auth.server";
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 获取用户会话
const { frontendJWT, userId } = await getUserSession(request);
// 获取 URL 参数
const url = new URL(request.url);
const typeIdParam = url.searchParams.get('typeId');
// 解析 typeIds 参数
let typeIds: number[] | number | null = null;
if (typeIdParam) {
// 支持单个 ID: ?typeId=1
// 或多个 ID: ?typeId=2,3
const ids = typeIdParam.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
typeIds = ids.length === 1 ? ids[0] : ids.length > 1 ? ids : null;
}
// 调用 getHomeData
const homeData = await getHomeData(typeIds, userId, frontendJWT);
return json({ homeData, typeIds });
} catch (error) {
console.error('加载首页数据失败:', error);
return json(
{
error: '加载首页数据失败',
homeData: null
},
{ status: 500 }
);
}
}
export default function HomePage() {
const { homeData, typeIds } = useLoaderData<typeof loader>();
if (!homeData) {
return <div></div>;
}
// 根据 typeIds 显示标题
const getTitle = () => {
if (!typeIds) return '全部文档审核';
if (typeIds === 1 || (Array.isArray(typeIds) && typeIds.includes(1))) return '合同审核';
if (Array.isArray(typeIds) && typeIds.includes(2)) return '记录审核';
return '文档审核';
};
return (
<div className="home-page">
<h1>{getTitle()} - </h1>
<div className="stats-grid">
<StatCard
title="今日待审核"
value={homeData.todayPendingFiles}
icon="⏳"
/>
<StatCard
title="本月已审核"
value={homeData.monthlyReviewedFiles}
growth={homeData.monthlyReviewGrowth}
icon="📄"
/>
<StatCard
title="审核通过率"
value={`${homeData.monthlyPassRate}%`}
growth={homeData.passRateGrowth}
icon="✅"
/>
<StatCard
title="检测到的问题"
value={homeData.issuesDetected}
growth={homeData.issuesGrowth}
icon="⚠️"
/>
</div>
</div>
);
}
```
---
## 注意事项
### 1. 日期字段不一致问题
**当前实现中的不一致**:
| 查询 | 使用字段 | 说明 |
|------|---------|------|
| 今日待审核文件 | `created_at` | 创建时间 |
| 本月已审核文件 | `upload_time` | 上传时间 |
| 本月审核通过数 | `created_at` | 创建时间 |
| 上月审核通过数 | `upload_time` | 上传时间 |
**潜在问题**:
- 同一个文档的 `created_at``upload_time` 可能不同
- 导致统计口径不一致
**建议**: 统一使用 `created_at``upload_time`
---
### 2. 增长率计算的边界情况
**特殊情况处理**:
| 本月 | 上月 | 处理方式 | 结果 |
|-----|------|---------|------|
| > 0 | > 0 | 正常计算 | 实际增长率 |
| > 0 | = 0 | 固定返回 | 100% 增长 |
| = 0 | = 0 | 不计算 | 0% 增长 |
| = 0 | > 0 | 正常计算 | -100% 增长 |
**代码逻辑**:
```typescript
if (lastMonth > 0) {
// 正常计算
} else if (lastMonth == 0 && thisMonth > 0) {
// 特殊处理:固定 100%
} else {
// 默认 0%
}
```
---
### 3. Token 传递
**重要**: 所有 PostgREST 查询都必须传递 JWT Token
```typescript
// ✅ 正确
await postgrestGet('documents', { ...params, token });
// ❌ 错误(会被 401 拒绝)
await postgrestGet('documents', params);
```
---
### 4. 类型转换
**userId 参数**: 需要转换为整数(用于 RPC 调用)
```typescript
const response = await postgrestPost('rpc/count_evaluation_results_by_type', {
userid: parseInt(userId as string) // ✅ 转换为整数
}, token);
```
---
## 总结
### 核心功能
`getHomeData()` 函数实现了首页数据统计的完整业务逻辑,包括:
1.**7 项核心指标统计**
2.**3 种同比增长计算**
3.**灵活的类型筛选** (支持单个 ID、ID 数组或不筛选)
4.**完善的错误处理**
5.**统一的 API 响应格式处理**
### 数据库例程
`count_evaluation_results_by_type` 函数提供了:
1.**高效的问题统计查询**
2.**灵活的参数筛选** (类型、用户、时间范围)
3.**JSONB 字段查询优化**
### 优化建议
1. 🚀 **并行查询** - 性能提升 ~7 倍
2. 💾 **Redis 缓存** - 减少数据库负载
3. 📊 **物化视图** - 查询时间降低 ~10 倍
4. 🔍 **索引优化** - 提升查询效率
---
**文档版本**: 2.1
**最后更新**: 2025-01-17
**更新内容**: 将 reviewType 参数改为直接传递 typeIds (支持单个 ID、ID 数组或不筛选)
**维护者**: 系统开发团队