Files
2025-12-05 00:09:32 +08:00

562 lines
14 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.
# 首页数据统计 API 分析文档
## 文件位置
`app/api/home/home.ts`
## 概述
该文件提供首页数据统计功能,包括文档审核状态、通过率、问题检测等核心业务指标的统计和同比分析。
## 核心功能
### 主函数:`getHomeData()`
**函数签名:**
```typescript
async function getHomeData(
reviewType?: string | null, // 文档类型: 'contract' | 'record' | null
userId?: string | number, // 用户ID
token?: string // JWT token
): Promise<HomeStatistics>
```
**返回数据结构:**
```typescript
interface HomeStatistics {
todayPendingFiles: number; // 今日待审核文件数
monthlyReviewedFiles: number; // 本月已审核文件数
monthlyReviewGrowth: { // 本月审核文件同比增长
value: number; // 增长百分比(绝对值)
isUp: boolean; // 是否增长
};
monthlyPassRate: number; // 本月审核通过率
passRateGrowth: { // 通过率同比增长
value: number;
isUp: boolean;
};
issuesDetected: number; // 检测到的问题总数
issuesGrowth: { // 问题数量同比增长
value: number;
isUp: boolean;
};
}
```
---
## 数据库查询详解
### 1. 今日待审核文件数
**业务逻辑:**
统计今天创建的、状态为待审核的文件数量(`audit_status = 0 或 2 或 NULL`
**PostgREST 参数:**
```typescript
{
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}`
}
}
```
**等价 SQL 查询:**
```sql
SELECT COUNT(*)
FROM documents
WHERE (audit_status = 0 OR audit_status = 2 OR audit_status IS NULL)
AND created_at >= '2025-11-17 00:00:00' -- 今天开始时间
AND is_test_document = false
AND user_id = ${userId}
AND (type_id = 1); -- 如果 reviewType = 'contract'
-- 或 (type_id = 2 OR type_id = 3); -- 如果 reviewType = 'record'
```
**状态说明:**
- `audit_status = 0`: 未审核
- `audit_status = 2`: 审核中
- `audit_status = NULL`: 未设置状态
---
### 2. 本月已审核文件数
**业务逻辑:**
统计本月上传的、已完成审核的文件数量(`audit_status ≠ 0 且 ≠ 2`
**PostgREST 参数:**
```typescript
{
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}`
}
}
```
**等价 SQL 查询:**
```sql
SELECT COUNT(*)
FROM documents
WHERE audit_status != 0
AND audit_status != 2
AND upload_time >= '2025-11-01 00:00:00' -- 本月开始
AND is_test_document = false
AND user_id = ${userId}
AND type_id = 1; -- 根据 reviewType 过滤
```
---
### 3. 上月已审核文件数
**业务逻辑:**
统计上月的已审核文件数量,用于计算同比增长
**PostgREST 参数:**
```typescript
{
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}`
}
}
```
**等价 SQL 查询:**
```sql
SELECT COUNT(*)
FROM documents
WHERE upload_time >= '2025-10-01 00:00:00' -- 上月开始
AND upload_time <= '2025-10-31 23:59:59' -- 上月结束
AND audit_status != 0
AND audit_status != 2
AND is_test_document = false
AND user_id = ${userId}
AND type_id = 1;
```
**增长率计算:**
```typescript
// 同比增长百分比
const growthRate = (( - ) / ) * 100;
reviewGrowthValue = Math.abs(parseFloat(growthRate.toFixed(1)));
reviewGrowthIsUp = growthRate >= 0;
// 特殊情况:上月为0,本月大于0
if (lastMonthReviewed == 0 && monthlyReviewedFiles > 0) {
reviewGrowthValue = 100;
reviewGrowthIsUp = true;
}
```
---
### 4. 本月审核通过数量
**业务逻辑:**
统计本月审核通过的文件数量(`audit_status = 1`
**PostgREST 参数:**
```typescript
{
select: 'count',
filter: {
audit_status: 'eq.1',
created_at: `gte.${startOfThisMonth}`,
is_test_document: 'eq.false',
user_id: `eq.${userId}`
}
}
```
**等价 SQL 查询:**
```sql
SELECT COUNT(*)
FROM documents
WHERE audit_status = 1 -- 审核通过
AND created_at >= '2025-11-01 00:00:00'
AND is_test_document = false
AND user_id = ${userId}
AND type_id = 1;
```
**通过率计算:**
```typescript
const monthlyPassRate = (thisMonthPassTotal > 0 && monthlyReviewedFiles > 0)
? parseFloat(((thisMonthPassTotal / monthlyReviewedFiles) * 100).toFixed(1))
: 0;
```
---
### 5. 上月审核通过数量与通过率
**PostgREST 参数:**
```typescript
{
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}`
}
}
```
**等价 SQL 查询:**
```sql
SELECT COUNT(*)
FROM documents
WHERE audit_status = 1
AND upload_time >= '2025-10-01 00:00:00'
AND upload_time <= '2025-10-31 23:59:59'
AND is_test_document = false
AND user_id = ${userId};
```
**通过率增长计算:**
```typescript
const lastMonthPassRate = (lastMonthTotal > 0 && lastMonthReviewed > 0)
? parseFloat(((lastMonthTotal / lastMonthReviewed) * 100).toFixed(1))
: 0;
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;
}
```
---
### 6. 问题检测统计(调用数据库函数)
**业务逻辑:**
通过 RPC 调用 PostgreSQL 存储函数 `count_evaluation_results_by_type` 统计评估结果中的问题数量
#### 6.1 合同类型(reviewType = 'contract'
**RPC 调用参数:**
```typescript
{
start_time: '2025-11-01 00:00:00',
end_time: '2025-11-30 23:59:59',
type_val: [1], // 合同类型ID
userid: parseInt(userId)
}
```
**等价 SQL(推测函数实现):**
```sql
-- 数据库函数 count_evaluation_results_by_type 的可能实现
CREATE OR REPLACE FUNCTION count_evaluation_results_by_type(
start_time TIMESTAMP,
end_time TIMESTAMP,
type_val INTEGER[],
userid INTEGER
) RETURNS TABLE(count BIGINT) AS $$
BEGIN
RETURN QUERY
SELECT COUNT(*)
FROM evaluation_results er
JOIN documents d ON er.document_id = d.id
WHERE d.type_id = ANY(type_val)
AND d.user_id = userid
AND er.created_at >= start_time
AND er.created_at <= end_time
AND d.is_test_document = false;
END;
$$ LANGUAGE plpgsql;
```
#### 6.2 记录类型(reviewType = 'record'
**RPC 调用参数:**
```typescript
{
start_time: '2025-11-01 00:00:00',
end_time: '2025-11-30 23:59:59',
type_val: [2, 3], // 许可卷宗类型ID
userid: parseInt(userId)
}
```
**问题增长率计算:**
```typescript
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;
}
```
---
## 辅助函数
### 1. `buildTypeFilter()` - 类型过滤构建
**功能:** 根据 `reviewType` 参数构建 PostgREST 过滤条件
```typescript
function buildTypeFilter(reviewType: string | null): string {
let typeFilter = '';
if (reviewType === 'contract') {
typeFilter = 'type_id.eq.1'; // 合同类型
} else if (reviewType === 'record') {
typeFilter = '(type_id.eq.2,type_id.eq.3)'; // 记录类型(OR条件)
}
return typeFilter;
}
```
### 2. `extractApiData()` - API 响应数据提取
**功能:** 统一处理不同格式的 API 响应,提取真实数据
**支持的响应格式:**
```typescript
// 格式1: { code: number, msg: string, data: T }
{ code: 0, msg: 'success', data: [...] }
// 格式2: 直接返回数据
[{ count: 10 }]
// 错误格式
{ code: 500, msg: 'error' }
{ error: 'something went wrong' }
```
### 3. `handleApiResponse()` - API 调用错误处理
**功能:** 封装 API 调用、错误处理和默认值返回
```typescript
const handleApiResponse = async <T>(
apiCall: Promise<ApiResponse>,
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);
return data || defaultValue;
} catch (error) {
console.error(`${errorMessage}: ${error.message}`);
return defaultValue;
}
};
```
---
## 时间范围计算
使用 `dayjs` 库进行时间计算:
```typescript
// 今天开始时间
const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
// 输出: '2025-11-17 00:00:00'
// 本月开始/结束
const startOfThisMonth = dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss');
const endOfThisMonth = dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss');
// 输出: '2025-11-01 00:00:00' ~ '2025-11-30 23:59:59'
// 上月开始/结束
const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss');
const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss');
// 输出: '2025-10-01 00:00:00' ~ '2025-10-31 23:59:59'
```
---
## 数据库表结构(推测)
### `documents` 表
```sql
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
type_id INTEGER NOT NULL, -- 1: 合同, 2/3: 记录
audit_status INTEGER, -- 0: 未审核, 1: 通过, 2: 审核中, -1: 不通过
created_at TIMESTAMP,
upload_time TIMESTAMP,
is_test_document BOOLEAN DEFAULT false,
-- 其他字段...
);
-- 索引优化建议
CREATE INDEX idx_documents_user_status ON documents(user_id, audit_status);
CREATE INDEX idx_documents_created_at ON documents(created_at);
CREATE INDEX idx_documents_upload_time ON documents(upload_time);
CREATE INDEX idx_documents_type_id ON documents(type_id);
```
### `evaluation_results` 表(推测)
```sql
CREATE TABLE evaluation_results (
id SERIAL PRIMARY KEY,
document_id INTEGER REFERENCES documents(id),
created_at TIMESTAMP,
-- 评估结果字段...
);
CREATE INDEX idx_eval_results_doc_created ON evaluation_results(document_id, created_at);
```
---
## 性能优化建议
### 1. 数据库优化
- ✅ 使用复合索引:`(user_id, audit_status, created_at)`
- ✅ 分离时间范围查询:避免在 `AND` 条件中混用多个时间字段
- ✅ 使用物化视图:定期统计常用指标
### 2. 缓存策略
```typescript
// 建议添加 Redis 缓存
const cacheKey = `home:stats:${userId}:${reviewType}:${date}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 查询后缓存 5 分钟
await redis.setex(cacheKey, 300, JSON.stringify(result));
```
### 3. 并行查询优化
当前实现是串行查询,可以改为并行:
```typescript
const [
todayPending,
thisMonthReviewed,
lastMonthReviewed,
thisMonthPassed,
lastMonthPassed,
thisMonthIssues,
lastMonthIssues
] = await Promise.all([
// 所有查询并行执行
]);
```
---
## 错误处理机制
### 1. 单个查询失败
- 返回默认值(如 `[]``0`
- 记录错误日志但不中断整体流程
### 2. 整体失败
```typescript
catch (error) {
console.error('获取首页数据失败:', error.message);
return {
todayPendingFiles: 0,
monthlyReviewedFiles: 0,
monthlyReviewGrowth: { value: 0, isUp: true },
monthlyPassRate: 0,
passRateGrowth: { value: 0, isUp: true },
issuesDetected: 0,
issuesGrowth: { value: 0, isUp: true }
};
}
```
---
## 使用示例
```typescript
// 在 Remix loader 中使用
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getUserSession(request);
const userId = session.get('user_id');
const token = session.get('jwt_token');
const homeData = await getHomeData('contract', userId, token);
return json({ homeData });
}
```
---
## 注意事项
1. **类型过滤逻辑:**
- `contract``type_id = 1`
- `record``type_id = 2 OR type_id = 3`
- `null` → 不过滤类型
2. **日期字段不一致:**
- 待审核文件使用 `created_at`
- 已审核文件使用 `upload_time`
- 通过率统计本月用 `created_at`,上月用 `upload_time`
3. **增长率计算边界情况:**
- 上月为0时,增长率固定为 100%
- 本月和上月都为0时,增长率为 0%
4. **Token 传递:**
- JWT token 需要在每个 API 调用中传递
- 通过 `postgrestGet/postgrestPost``token` 参数传递
---
## 改进建议
### 1. 统一时间字段
建议数据库统一使用 `created_at``upload_time`,避免混用造成统计不一致。
### 2. 添加 TypeScript 类型守卫
```typescript
function isValidAuditStatus(status: unknown): status is number {
return typeof status === 'number' && [0, 1, 2, -1].includes(status);
}
```
### 3. 提取常量
```typescript
const AUDIT_STATUS = {
PENDING: 0,
PASSED: 1,
IN_PROGRESS: 2,
REJECTED: -1
} as const;
const DOCUMENT_TYPES = {
CONTRACT: 1,
RECORD_TYPE_2: 2,
RECORD_TYPE_3: 3
} as const;
```
### 4. 添加请求日志
```typescript
console.log(`[HomeStats] User: ${userId}, Type: ${reviewType}, Duration: ${Date.now() - startTime}ms`);
```