# 首页数据统计 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 ``` **返回数据结构:** ```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 ( apiCall: Promise, 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); 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`); ```