- {reviewPoint.pointCode && (
+ {reviewPoint.pointCode ? (
{reviewPoint.pointCode}
+ ) : (
+
#{reviewPoint.pointId || reviewPoint.id}
)}
{reviewPoint.pointName}
{ reviewPoint.pointName === '签署乙方详细信息校验' && (
diff --git a/docs/attribute_type_frontend_integration.md b/docs/attribute_type_frontend_integration.md
new file mode 100644
index 0000000..4725840
--- /dev/null
+++ b/docs/attribute_type_frontend_integration.md
@@ -0,0 +1,332 @@
+# 用户自定义合同类型 - 前后端对接文档
+
+## 概述
+
+为了优化 GraphRAG 抽取管道的规则加载,后端新增了 `attribute_type`(合同专属类型)参数支持。当指定该参数时,系统只加载 **通用 + 指定类型** 的评查点规则,大幅减少无关字段(816 → ~197),提升检索精度和抽取稳定性。
+
+## 后端修改内容
+
+### 1. 文档类型自动检测(`contract_server.py`)
+
+**文件**: `services/documents/v2/contract_server.py`
+
+**新增方法**: `_detect_attribute_type()`
+
+```python
+@staticmethod
+def _detect_attribute_type(filename: str) -> Optional[str]:
+ """
+ 从文件名关键词检测合同专属子类型。
+
+ 关键词表与 executor._CONTRACT_TYPE_KEYWORDS 保持一致。
+ 返回: "技术"/"租赁"/"买卖"/... 或 None
+ """
+ _KEYWORDS = [
+ ("买卖", ["采购", "买卖", "购销", "供货", "购买"]),
+ ("租赁", ["租赁", "租用", "出租"]),
+ ("服务", ["服务", "咨询", "顾问"]),
+ ("委托", ["委托", "代理"]),
+ ("建设工程", ["建设工程", "施工", "装修", "装饰"]),
+ ("培训", ["培训", "教育", "教学"]),
+ ("技术", ["技术", "开发", "软件", "系统集成", "信息化"]),
+ ("赠与", ["赠与", "捐赠"]),
+ ("运输", ["运输", "物流", "配送", "搬运"]),
+ ("仓储", ["仓储", "存储", "保管"]),
+ ("合作", ["合作", "联营", "合资"]),
+ ("承揽", ["承揽", "加工", "定作"]),
+ ]
+ if not filename:
+ return None
+ for type_code, keywords in _KEYWORDS:
+ for kw in keywords:
+ if kw in filename:
+ return type_code
+ return None
+```
+
+**调用位置**: `extract()` 方法(约 L455)
+
+```python
+# 确定合同专属子类型(用于精准加载规则)
+_attribute_type = self._detect_attribute_type(self.filename)
+if _attribute_type:
+ log.document.info(
+ f"[GraphRAG] doc={self.document_id} 合同子类型: {_attribute_type}"
+ )
+
+rag_result = await rag_service.extract(
+ ...
+ attribute_type=_attribute_type,
+)
+```
+
+### 2. GraphRAG 抽取服务支持(`rag_extraction_service.py`)
+
+**文件**: `services/graph_rag/rag_extraction_service.py`
+
+**修改点 1**: `extract()` 方法签名新增 `attribute_type` 参数(约 L175)
+
+```python
+async def extract(
+ self,
+ ...
+ attribute_type: Optional[str] = None, # 新增参数
+) -> Dict[str, Any]:
+ """
+ Args:
+ ...
+ attribute_type: 合同专属子类型(如"技术""租赁"),由上游指定,
+ 用于只加载 通用+该类型 的评查点规则,减少无关字段
+ """
+```
+
+**修改点 2**: `_load_extraction_rules()` 方法新增类型过滤逻辑(约 L901)
+
+```python
+# 指定了合同子类型时,只加载 通用+该类型(大幅减少无关字段)
+if attribute_type:
+ _ep_filters["document_attribute_type"] = f"in.(通用,{attribute_type})"
+```
+
+### 3. 支持的合同类型
+
+| 类型代码 | 类型名称 | 关键词(文件名检测) |
+|---------|---------|---------------------|
+| `通用` | 通用类型 | 默认类型 |
+| `买卖` | 买卖合同 | 采购、买卖、购销、供货、购买 |
+| `租赁` | 租赁合同 | 租赁、租用、出租 |
+| `服务` | 服务合同 | 服务、咨询、顾问 |
+| `委托` | 委托合同 | 委托、代理 |
+| `建设工程` | 建设工程合同 | 建设工程、施工、装修、装饰 |
+| `培训` | 培训合同 | 培训、教育、教学 |
+| `技术` | 技术合同 | 技术、开发、软件、系统集成、信息化 |
+| `赠与` | 赠与合同 | 赠与、捐赠 |
+| `运输` | 运输合同 | 运输、物流、配送、搬运 |
+| `仓储` | 仓储合同 | 仓储、存储、保管 |
+| `合作` | 合作合同 | 合作、联营、合资 |
+| `承揽` | 承揽合同 | 承揽、加工、定作 |
+
+## 前端对接指南
+
+### 当前自动检测机制
+
+**现状**: 后端已实现从**文件名**自动检测合同类型的逻辑。
+
+例如:
+- `技术合同(去空格).docx` → 自动识别为 `技术` 类型
+- `房屋租赁合同.pdf` → 自动识别为 `租赁` 类型
+- `货物采购合同.docx` → 自动识别为 `买卖` 类型
+
+### 前端手动指定合同类型(需后端额外支持)
+
+**注意**: 当前后端尚未支持从 `upload_info` 中读取 `attribute_type` 参数。如需前端手动指定,需要进行以下额外修改:
+
+#### 方案 A:修改上传接口传递 attribute_type
+
+**1. 前端上传时传递参数**
+
+```javascript
+// 示例:使用 FormData 上传
+const formData = new FormData();
+formData.append('file', fileBlob);
+
+// 在 upload_info 中指定合同类型
+const uploadInfo = {
+ type_id: 1, // 文档类型ID(1=合同)
+ document_number: "HT2024001", // 合同编号
+ attribute_type: "技术", // 👈 手动指定合同类型
+ evaluation_level: "普通",
+ remark: "测试合同"
+};
+formData.append('upload_info', JSON.stringify(uploadInfo));
+
+// 调用上传接口
+const response = await fetch('/api/v2/documents/upload', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData
+});
+```
+
+**2. 后端需要额外修改的位置**
+
+##### 2.1 修改 `contract_server.py` 的 `process_contract()` 方法
+
+在约 L455 处,优先使用用户指定的类型:
+
+```python
+# 确定合同专属子类型(用于精准加载规则)
+# 优先级:用户指定 > 文件名检测
+_attribute_type = None
+
+# 1. 尝试从 upload_info 获取用户指定类型(需要传入)
+# if hasattr(self, 'upload_info') and self.upload_info:
+# _attribute_type = self.upload_info.get('attribute_type')
+
+# 2. 文件名自动检测(当前已实现)
+if not _attribute_type:
+ _attribute_type = self._detect_attribute_type(self.filename)
+
+if _attribute_type:
+ log.document.info(
+ f"[GraphRAG] doc={self.document_id} 合同子类型: {_attribute_type} "
+ f"({'用户指定' if hasattr(self, 'upload_info') and self.upload_info and self.upload_info.get('attribute_type') == _attribute_type else '文件名检测'})"
+ )
+```
+
+##### 2.2 修改 `tasks.py` 的 `process_contract_markdown()` 任务
+
+在约 L1092 处,将 `attribute_type` 传递给 ContractMarkdownServer:
+
+```python
+# 获取用户指定的合同类型(如果有的话)
+attribute_type = upload_info.get("attribute_type")
+
+# 创建处理服务
+contract_server = ContractMarkdownServer(
+ document_id=document_id,
+ file_path=temp_path,
+ filename=filename,
+ template_id=template_id,
+ user_area=user_area,
+ user_role=user_role,
+ attribute_type=attribute_type, # 👈 新增参数
+)
+```
+
+##### 2.3 修改 `ContractMarkdownServer` 类
+
+**文件**: `services/documents/v2/contract_markdown_server.py`
+
+```python
+class ContractMarkdownServer:
+ def __init__(
+ self,
+ ...
+ attribute_type: Optional[str] = None, # 新增参数
+ ):
+ ...
+ self.attribute_type = attribute_type
+
+ async def process_contract(self):
+ ...
+ # 在调用 ContractServer.extract() 时传递 attribute_type
+ # (具体实现取决于 ContractMarkdownServer 如何调用 ContractServer)
+```
+
+### API 接口说明
+
+#### 上传文档接口
+
+**URL**: `POST /api/v2/documents/upload`
+
+**Content-Type**: `multipart/form-data`
+
+**参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|-------|------|------|------|
+| `file` | File | 是 | 上传的文件(PDF/Word) |
+| `upload_info` | String(JSON) | 否 | 上传信息 JSON 字符串 |
+| `attachments` | File[] | 否 | 附件文件列表 |
+
+**upload_info 结构**:
+
+```json
+{
+ "type_id": 1, // 文档类型ID:1=合同,2=案卷
+ "document_number": "HT2024001", // 合同编号(可选)
+ "attribute_type": "技术", // 合同专属类型(可选,待后端支持)
+ "evaluation_level": "普通", // 评查级别(可选)
+ "is_test_document": true, // 是否测试文档(可选)
+ "remark": "备注信息" // 备注(可选)
+}
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "result": {
+ "document_id": 2751,
+ "document_number": "HT2024001",
+ "file_name": "技术合同(去空格).docx",
+ "file_url": "documents/.../xxx.docx",
+ "status": "Queued",
+ "type_id": 1,
+ "doc_type": "HT",
+ "doc_type_description": "合同",
+ "api_version": "v2",
+ "is_test_document": true,
+ "remark": "",
+ "evaluation_level": "普通"
+ }
+}
+```
+
+### 可用的合同类型值(attribute_type)
+
+前端下拉选项建议:
+
+```javascript
+const CONTRACT_TYPES = [
+ { value: "通用", label: "通用合同" },
+ { value: "技术", label: "技术合同" },
+ { value: "租赁", label: "租赁合同" },
+ { value: "买卖", label: "买卖合同" },
+ { value: "服务", label: "服务合同" },
+ { value: "委托", label: "委托合同" },
+ { value: "建设工程", label: "建设工程合同" },
+ { value: "培训", label: "培训合同" },
+ { value: "赠与", label: "赠与合同" },
+ { value: "运输", label: "运输合同" },
+ { value: "仓储", label: "仓储合同" },
+ { value: "合作", label: "合作合同" },
+ { value: "承揽", label: "承揽合同" }
+];
+```
+
+## 验证方式
+
+### 1. 查看日志确认类型检测
+
+上传文档后,查看日志确认合同类型识别:
+
+```
+[DOCUMENT] [GraphRAG] doc=2751 合同子类型: 技术
+```
+
+### 2. 查看规则加载情况
+
+```
+[EXTRACTION] [RAG Rules] GROUP路径加载 197 个评查点 (过滤: 通用+技术), 子类型分布: {...}
+```
+
+- **未指定类型**: 加载 816 个评查点(所有类型)
+- **指定类型**: 加载 ~197 个评查点(通用+指定类型)
+
+### 3. 评查点过滤验证
+
+某些专属评查点会根据类型过滤:
+
+| 评查点 | 类型 | 行为 |
+|--------|------|------|
+| EP-089 技术内容-技术方案 | 技术 | 仅技术合同加载 |
+| EP-091 技术内容-技术指标 | 技术 | 仅技术合同加载 |
+| EP-020 违约责任条款完整性 | 通用 | 所有合同都加载 |
+
+## 待实现功能(需后端额外开发)
+
+| 功能 | 状态 | 说明 |
+|------|------|------|
+| 文件名自动检测 | ✅ 已实现 | 后端从文件名关键词自动识别 |
+| 前端手动指定 | ⚠️ 待实现 | 需修改 `contract_server.py` 和 `tasks.py` |
+| 用户历史记录 | ❌ 未规划 | 记录用户常用合同类型,自动填充 |
+| AI 智能推荐 | ❌ 未规划 | 根据文档内容推荐合同类型 |
+
+## 联系方式
+
+如有疑问,请联系后端开发团队。
diff --git a/docs/开发规范手册.md b/docs/开发规范手册.md
new file mode 100644
index 0000000..c3d0e66
--- /dev/null
+++ b/docs/开发规范手册.md
@@ -0,0 +1,1442 @@
+# 中国烟草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 提交信息格式
+
+```
+():
+
+
+
+