# 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 ``` ### 参数说明 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `typeIds` | `number[] \| number \| null` | 否 | 文档类型 ID 筛选:
- 单个 ID: `1` (筛选 type_id = 1)
- ID 数组: `[2, 3]` (筛选 type_id = 2 或 3)
- `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 ( apiCall: Promise<{ data?: unknown; headers?: Record; error?: string; status?: number }>, errorMessage: string, defaultValue: T ): Promise => { try { const response = await apiCall; if (response.error) { console.error(`${errorMessage}: ${response.error}`); return defaultValue; } const data = extractApiData(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(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 ; } ``` --- ## 使用示例 ### 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(); if (!homeData) { return
加载失败
; } // 根据 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 (

{getTitle()} - 数据统计

); } ``` --- ## 注意事项 ### 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 数组或不筛选) **维护者**: 系统开发团队