# 中国烟草AI合同及卷宗审核系统 - 开发规范手册 ## 目录 1. [项目概述](#1-项目概述) 2. [技术栈规范](#2-技术栈规范) 3. [项目结构规范](#3-项目结构规范) 4. [TypeScript代码规范](#4-typescript代码规范) 5. [React组件规范](#5-react组件规范) 6. [样式规范](#6-样式规范) 7. [API调用规范](#7-api调用规范) 8. [路由开发规范](#8-路由开发规范) 9. [安全规范](#9-安全规范) 10. [Git提交规范](#10-git提交规范) 11. [环境变量规范](#11-环境变量规范) 12. [注释规范](#12-注释规范) 13. [错误处理规范](#13-错误处理规范) 14. [命名规范速查表](#14-命名规范速查表) --- ## 1. 项目概述 ### 1.1 项目简介 本项目是中国烟草AI合同及卷宗审核系统,采用 Remix (React) + TypeScript 构建,提供智能文档审查、风险评估和合规检查功能。 ### 1.2 核心命令 ```bash # 开发 npm run dev # 启动开发服务器 (端口 5173) npm run typecheck # TypeScript 类型检查 npm run lint # ESLint 检查 # 构建 npm run build # 生产构建 npm run build:production:multi # 多实例生产构建 # 部署 npm start # 单实例生产启动 npm run start:pm2:multi # PM2 多实例启动 ``` --- ## 2. 技术栈规范 ### 2.1 核心技术 | 技术 | 版本 | 用途 | |------|------|------| | Remix | ^2.16.2 | React 全栈框架 | | React | ^18.2.0 | UI 库 | | TypeScript | ^5.x | 类型系统 | | Vite | ^5.x | 构建工具 | | Tailwind CSS | ^3.4 | 样式框架 | | Ant Design | ^6.0 | UI 组件库 | | Axios | ^1.9 | HTTP 客户端 | | Remixicon | 本地化 | 图标库 | ### 2.2 路径别名 ```json // tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "~/*": ["./app/*"] } } } ``` **导入示例**: ```typescript import { Button } from '~/components/ui/Button'; // ✅ 正确 import { Button } from '../components/ui/Button'; // ❌ 不推荐 ``` --- ## 3. 项目结构规范 ### 3.1 目录结构 ``` app/ ├── api/ # API 层 (服务端调用封装) │ ├── axios-client.ts # Axios 核心客户端 │ ├── postgrest-client.ts # PostgREST API 封装 │ ├── login/ # 登录认证 API │ │ ├── auth.server.ts # 会话管理 │ │ ├── oauth-client.ts # OAuth2.0 客户端 │ │ └── token-manager.server.ts # Token 管理 │ ├── contracts/ # 合同相关 API │ ├── cross-checking/ # 交叉评查 API │ └── [feature]/ # 其他功能 API │ ├── components/ # 组件目录 │ ├── ui/ # 通用 UI 组件 (自包含设计) │ │ ├── Button/ │ │ │ ├── Button.tsx │ │ │ ├── Button.css │ │ │ └── index.ts │ │ ├── Card/ │ │ └── index.ts # 统一导出 │ ├── layout/ # 布局组件 │ │ ├── Layout.tsx │ │ ├── Sidebar.tsx │ │ └── Header.tsx │ ├── reviews/ # 评查功能组件 │ └── [feature]/ # 功能特定组件 │ ├── routes/ # Remix 路由 (76个) │ ├── _index.tsx # 首页 │ ├── login.tsx # 登录页 │ ├── callback.tsx # OAuth 回调 │ ├── documents.tsx # 文档管理 │ ├── cross-checking.tsx # 交叉评查 │ └── api.*.tsx # API 路由 │ ├── config/ # 配置文件 │ └── api-config.ts # API 端口配置 │ ├── styles/ # 样式文件 │ ├── main.css # 主样式 │ └── components/ # 组件样式 │ ├── card.css │ ├── sidebar.css │ └── button.css │ ├── types/ # 类型定义 │ ├── document.ts # 文档相关类型 │ ├── user.ts # 用户相关类型 │ └── api.ts # API 相关类型 │ ├── hooks/ # 自定义 Hooks ├── contexts/ # React Context ├── utils/ # 工具函数 └── root.tsx # 应用根组件 ``` ### 3.2 文件组织原则 | 目录 | 组织方式 | 说明 | |------|----------|------| | `api/` | 按功能模块 | 每个功能模块独立目录 | | `components/` | 按组件类型 | UI 组件自包含 (tsx + css + index.ts) | | `routes/` | 按路由 | 一个文件一个路由 | | `types/` | 按领域 | 按业务领域分类 | | `styles/` | 按组件 | 与组件对应 | --- ## 4. TypeScript代码规范 ### 4.1 类型定义规范 ```typescript // ✅ 1. 接口命名 - PascalCase interface DocumentInfo { id: string; name: string; path: string; status: ProcessingStatus; createdAt: string; } // ✅ 2. Props 接口命名 - ComponentNameProps interface ButtonProps { children: React.ReactNode; type?: 'primary' | 'default' | 'danger'; size?: 'small' | 'medium' | 'large'; disabled?: boolean; className?: string; onClick?: (e: React.MouseEvent) => void; } // ✅ 3. 状态类型 - 字符串字面量联合 type ProcessingStatus = | 'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed'; type UserRole = 'common' | 'developer' | 'admin'; // ✅ 4. 函数类型 type ApiResponse = { data: T; status: number; message?: string; }; type ErrorResponse = { error: string; status: number; }; // ✅ 5. 枚举定义 (仅在需要组合值时使用) enum FileType { CONTRACT = 'contract', LICENSE = 'license', OTHER = 'other' } ``` ### 4.2 类型使用原则 ```typescript // ✅ 优先使用 interface interface UserInfo { name: string; email: string; } // ✅ 需要合并时使用 type type UserWithRole = UserInfo & { role: UserRole }; // ✅ 避免使用 any,使用 unknown function handleData(data: unknown): void { if (typeof data === 'string') { console.log(data.toUpperCase()); } } // ✅ 使用 as const 冻结对象 const STATUS_CONFIG = { WAITING: { label: '等待中', color: 'gray' }, SUCCESS: { label: '成功', color: 'green' }, } as const; // ✅ 泛型约束 function getProperty(obj: T, key: K): T[K] { return obj[key]; } ``` ### 4.3 导入类型 ```typescript // ✅ 使用 type 导入仅类型的依赖 import type { DocumentInfo } from '~/types/document'; import type { UserRole } from '~/types/user'; // ✅ 混合导入 import { Button } from '~/components/ui/Button'; // 值导入 import type { ButtonProps } from '~/components/ui/Button'; // 类型导入 // ✅ 从同一模块导入值和类型 import { useState, useEffect } from 'react'; // 运行时 import type { Dispatch, SetStateAction } from 'react'; // 仅类型 ``` --- ## 5. React组件规范 ### 5.1 组件定义 ```typescript // ✅ 正确 - 使用函数声明 export function Card({ children, title, icon, extra, className = '' }: CardProps) { return (
{title && (
{icon && } {title}
{extra &&
{extra}
}
)}
{children}
); } // ❌ 错误 - 使用箭头函数 export const Card = ({ children, title }: CardProps) => { ... }; ``` ### 5.2 Hooks 使用顺序 ```typescript export function DocumentList({ documents }: DocumentListProps) { // 1. State hooks (按依赖关系排序) const [selectedIds, setSelectedIds] = useState([]); const [filter, setFilter] = useState('all'); const [isLoading, setIsLoading] = useState(false); // 2. Ref hooks const containerRef = useRef(null); const inputRef = useRef(null); // 3. Effect hooks (按依赖关系分组) useEffect(() => { // 数据获取 fetchDocuments(); }, []); useEffect(() => { // 订阅/事件监听 const handler = () => { ... }; window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); // 4. 计算属性 (useMemo/useCallback) const filteredDocuments = useMemo(() => { return documents.filter(doc => { if (filter === 'all') return true; return doc.status === filter; }); }, [documents, filter]); const handleSelectAll = useCallback(() => { setSelectedIds(documents.map(d => d.id)); }, [documents]); // 5. 事件处理函数 const handleSelect = (id: string) => { setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] ); }; const handleDelete = async (id: string) => { setIsLoading(true); try { await deleteDocument(id); } finally { setIsLoading(false); } }; // 6. Render return (
{/* ... */}
); } ``` ### 5.3 组件 Props 规范 ```typescript // ✅ Props 接口定义 interface ButtonProps { children: React.ReactNode; type?: ButtonType; size?: ButtonSize; disabled?: boolean; loading?: boolean; icon?: string; className?: string; onClick?: (e: React.MouseEvent) => void; } // ✅ 带默认值的 props interface CardProps { title?: React.ReactNode; icon?: string; className?: string; bodyClassName?: string; children: React.ReactNode; } // ✅ 提取子组件 Props interface TableColumn { key: keyof T | string; title: string; render?: (value: T[keyof T], record: T) => React.ReactNode; width?: number | string; } ``` ### 5.4 自包含组件设计 每个 UI 组件应包含: ``` components/ └── Button/ ├── Button.tsx # 组件实现 ├── Button.css # 组件样式 └── index.ts # 导出 ``` ```typescript // Button/index.ts export { Button } from './Button'; export type { ButtonProps } from './Button'; ``` --- ## 6. 样式规范 ### 6.1 样式架构 项目采用 **Tailwind CSS + 自定义 CSS** 混合模式: ```css /* 1. Tailwind 工具类 (优先使用) */
/* 2. 设计系统变量 */
/* 3. 自定义 BEM 类 */
``` ### 6.2 设计系统变量 ```css /* app/root.tsx 或 main.css */ :root { /* 主色调 */ --color-primary: #00684a; --color-primary-hover: #005a3f; --color-primary-light: rgba(0, 104, 74, 0.1); /* 状态色 */ --color-success: #52c41a; --color-warning: #faad14; --color-error: #f5222d; /* 中性色 */ --color-gray-50: #f8f9fa; --color-gray-100: #f1f3f5; --color-gray-200: #e9ecef; --color-gray-300: #dee2e6; --color-gray-400: #ced4da; --color-gray-500: #adb5bd; --color-gray-600: #868e96; --color-gray-700: #495057; --color-gray-800: #343a40; --color-gray-900: #212529; } ``` ### 6.3 BEM 命名规范 ```css /* 组件块 */ .card { } .sidebar { } .modal { } /* 元素 */ .card-header { } .card-title { } .card-body { } .card-footer { } .sidebar-menu-item { } .sidebar-menu-item.active { } /* 修饰符 */ .card--compact { } .button--primary { } .button--disabled { } ``` ### 6.4 Tailwind 常用配置 ```css /* 间距 */ p-4 = 16px, p-5 = 20px, p-6 = 24px mb-4 = 16px, mb-6 = 24px /* 圆角 */ rounded = 4px, rounded-md = 6px, rounded-lg = 8px /* 阴影 */ shadow-sm, shadow-md, shadow-lg /* 过渡 */ transition-all duration-200 ease-in-out ``` ### 6.5 RemixIcon 图标使用 ```tsx // 基本使用 // 尺寸控制 // 结合样式 // 在按钮中使用 ``` **⚠️ 重要 - CSS 隔离时的图标兼容**: ```css /* 当使用 CSS 隔离时,必须添加图标例外规则 */ .my-isolated-container * { font-family: inherit !important; } .my-isolated-container [class^="ri-"], .my-isolated-container [class*=" ri-"], .my-isolated-container i[class^="ri-"], .my-isolated-container i[class*=" ri-"] { font-family: 'remixicon' !important; font-style: normal !important; font-weight: normal !important; line-height: 1 !important; } ``` --- ## 7. API调用规范 ### 7.1 API 分层架构 ``` ┌─────────────────────────────────────────────┐ │ 路由层 (routes/*.tsx) │ │ loader / action 函数 │ └────────────────────┬────────────────────────┘ │ ┌────────────────────▼────────────────────────┐ │ 业务 API 层 (app/api/[feature]/) │ │ 封装业务逻辑的 API 函数 │ └────────────────────┬────────────────────────┘ │ ┌────────────────────▼────────────────────────┐ │ PostgREST 客户端 (postgrest-client.ts) │ │ 处理 PostgREST 特定参数 │ └────────────────────┬────────────────────────┘ │ ┌────────────────────▼────────────────────────┐ │ Axios 核心 (axios-client.ts) │ │ 请求拦截、响应拦截、JWT 处理 │ └─────────────────────────────────────────────┘ ``` ### 7.2 Axios 客户端使用 ```typescript // app/api/axios-client.ts - 已配置拦截器 import { get, post, put, del } from '~/api/axios-client'; // GET 请求 const data = await get('/admin/users/1'); // POST 请求 const result = await post('/admin/documents', { name: '合同.pdf', type: 'contract' }); // PUT 请求 const updated = await put('/admin/documents/123', { name: '新名称.pdf' }); // DELETE 请求 await del('/admin/documents/123'); ``` ### 7.3 PostgREST 客户端使用 ```typescript // app/api/postgrest-client.ts import { postgrestGet, postgrestPost, postgrestPatch, postgrestDelete } from '~/api/postgrest-client'; // 查询参数 const result = await postgrestGet('/documents', { select: 'id,name,status', eq: { status: 'Processed' }, order: 'created_at.desc', limit: 10 }); // 插入数据 await postgrestPost('/documents', { name: '新文档', status: 'Waiting' }); // 更新数据 await postgrestPatch('/documents', { id: 123, name: '更新后的名称' }); // 删除数据 await postgrestDelete('/documents', 123); ``` ### 7.4 业务 API 封装模式 ```typescript // app/api/contracts/documents.ts import { postgrestGet, postgrestPost } from '~/api/postgrest-client'; import type { Document, DocumentFilters } from '~/types/document'; /** * 获取文档列表 */ export async function getDocuments( filters?: DocumentFilters ): Promise { const params = { select: 'id,name,path,status,created_at', order: 'created_at.desc', limit: filters?.limit ?? 20, offset: filters?.offset ?? 0, ...(filters?.status && { eq: { status: filters.status } }) }; const result = await postgrestGet('/documents', params); if ('error' in result) { throw new Error(result.error); } return result.data; } /** * 创建文档 */ export async function createDocument( data: Omit ): Promise { const result = await postgrestPost('/documents', data); if ('error' in result) { throw new Error(result.error); } return result.data; } ``` ### 7.5 Loader 中使用 API ```typescript // routes/documents.tsx import { type LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { getDocuments } from "~/api/contracts/documents"; import { getUserSession } from "~/api/login/auth.server"; export async function loader({ request }: LoaderFunctionArgs) { // 1. 获取用户会话 const { userInfo, frontendJWT } = await getUserSession(request); // 2. 解析查询参数 const url = new URL(request.url); const status = url.searchParams.get('status') ?? undefined; const page = parseInt(url.searchParams.get('page') ?? '1', 10); // 3. 获取数据 const documents = await getDocuments({ status, limit: 20, offset: (page - 1) * 20 }); // 4. 返回数据 return Response.json({ documents, userInfo, pagination: { page, total: documents.length } }); } export default function DocumentsPage() { const { documents, userInfo } = useLoaderData(); return (
{/* ... */}
); } ``` --- ## 8. 路由开发规范 ### 8.1 路由文件命名 | 类型 | 命名格式 | 示例 | |------|----------|------| | 页面路由 | kebab-case | `documents.tsx`, `user-profile.tsx` | | 嵌套路由 | `_` 前缀 | `documents_.list.tsx`, `documents_.detail.$id.tsx` | | 动态路由 | `$param` | `reviews.$id.tsx`, `contract-template.detail.$id.tsx` | | API 路由 | `api.` 前缀 | `api.documents.tsx`, `api.users.$id.tsx` | ### 8.2 标准路由结构 ```typescript // routes/documents.tsx import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; import { useLoaderData, useFetcher, useNavigation } from "@remix-run/react"; // ============ 1. Meta 配置 ============ export const meta: MetaFunction = () => [ { title: "文档管理 - 合同审核系统" }, { name: "description", content: "文档管理和评查功能" } ]; // ============ 2. Links 配置 ============ import styles from '~/styles/pages/documents.css?url'; export function links() { return [ { rel: "stylesheet", href: styles } ]; } // ============ 3. Handle 配置 ============ export const handle = { hideBreadcrumb: false, // 显示面包屑 title: "文档管理" }; // ============ 4. Loader 函数 ============ export async function loader({ request }: LoaderFunctionArgs) { // 获取用户会话 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); // 获取数据 const documents = await getDocuments(request); return Response.json({ documents, userInfo, frontendJWT }); } // ============ 5. Action 函数 ============ export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); switch (intent) { case "delete": const id = formData.get("id") as string; await deleteDocument(id); return Response.json({ success: true, action: "delete" }); case "batch-delete": const ids = formData.getAll("ids") as string[]; await batchDeleteDocuments(ids); return Response.json({ success: true, action: "batch-delete" }); default: return Response.json({ error: "未知操作" }, { status: 400 }); } } // ============ 6. 组件实现 ============ export default function DocumentsPage() { const { documents, userInfo } = useLoaderData(); const navigation = useNavigation(); const fetcher = useFetcher(); const isSubmitting = navigation.state === "submitting"; return (
新建} />
); } // ============ 7. ErrorBoundary ============ import { useRouteError, isRouteErrorResponse } from "@remix-run/react"; export function ErrorBoundary() { const error = useRouteError(); if (isRouteErrorResponse(error)) { return (

{error.status} - {error.statusText}

{error.data}

); } return (

发生了错误

{error instanceof Error ? error.message : "未知错误"}

); } ``` ### 8.3 嵌套路由布局 ``` routes/ ├── documents.tsx # 父路由 - 布局 ├── documents.list.tsx # 列表页面 (使用 documents.tsx 布局) ├── documents.create.tsx # 创建页面 (使用 documents.tsx 布局) └── documents_.detail.$id.tsx # 详情页面 (不使用父布局,下划线跳过) ``` ```typescript // routes/documents.tsx - 布局组件 export default function DocumentsLayout() { const { userInfo } = useLoaderData(); return (
{/* 子路由内容 */}
); } ``` ### 8.4 API 路由 ```typescript // routes/api.documents.tsx import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { getDocuments, createDocument } from "~/api/contracts/documents"; // GET - 获取列表 export async function loader({ request }: LoaderFunctionArgs) { const documents = await getDocuments(); return Response.json({ data: documents }); } // POST - 创建 export async function action({ request }: ActionFunctionArgs) { if (request.method !== "POST") { return Response.json({ error: "方法不允许" }, { status: 405 }); } const data = await request.json(); const document = await createDocument(data); return Response.json({ data: document }, { status: 201 }); } ``` --- ## 9. 安全规范 ### 9.1 敏感信息处理 | 信息类型 | 处理方式 | 错误做法 | |----------|----------|----------| | `JWT_SECRET` | 环境变量,绝对不提交 | 硬编码在代码中 | | `OAUTH_CLIENT_SECRET` | 服务端环境变量 | 使用 `NEXT_PUBLIC_` 前缀 | | API 密钥 | 服务端配置 | 暴露到客户端 | | 用户密码 | 不存储,MD5 传输 | 明文传输或存储 | ### 9.2 环境变量命名 ```bash # ✅ 客户端安全变量 (可被客户端代码访问) NEXT_PUBLIC_API_BASE_URL=http://10.79.97.17:8000 NEXT_PUBLIC_DOCUMENT_URL=http://10.76.244.156:9000/docauditai/ # ❌ 服务端专用变量 (不可被客户端访问) JWT_SECRET=your-secret-key-here # ❌ NEXT_PUBLIC_JWT_SECRET OAUTH_CLIENT_SECRET=your-client-secret # ❌ NEXT_PUBLIC_OAUTH_CLIENT_SECRET ``` ### 9.3 认证白名单 ```typescript // app/api/login/auth.server.ts // 不需要认证的路径 const PUBLIC_PATHS = [ '/login', '/callback', '/oauth/authorize' ]; // 401 错误容忍的路径 (不触发登出) const ERROR_TOLERANT_PATHS = [ '/admin/statistics/top-error-points', '/admin/statistics/top-risk-users' ]; ``` ### 9.4 Cookie 安全配置 ```typescript export const sessionStorage = createCookieSessionStorage({ cookie: { name: "__lgsession", httpOnly: true, // 防止 XSS 攻击 path: "/", sameSite: "lax", // CSRF 保护 secrets: [process.env.JWT_SECRET ?? "default-secret"], maxAge: 60 * 60 * 8, // 8 小时 secure: process.env.NODE_ENV === "production" } }); ``` ### 9.5 文件路径安全 ```typescript // 防止路径遍历攻击 import path from "path"; function safeFilePath(userPath: string): string { const normalized = path.normalize(userPath); const baseDir = "/safe/uploads/directory"; // 确保路径在安全目录内 if (!normalized.startsWith(baseDir)) { throw new Error("非法文件路径"); } return normalized; } ``` --- ## 10. Git提交规范 ### 10.1 提交信息格式 ``` ():