From daca126ee21f38c8d03941b1a64b58a508e12493 Mon Sep 17 00:00:00 2001 From: wren Date: Mon, 23 Mar 2026 20:11:53 +0800 Subject: [PATCH] debug: add pointCode fallback and console log --- app/components/reviews/ReviewPointsList.tsx | 9 +- docs/attribute_type_frontend_integration.md | 332 + docs/开发规范手册.md | 1442 + opencode.json | 23 + package-lock.json | 27928 ++++++++++++++++++ 5 files changed, 29733 insertions(+), 1 deletion(-) create mode 100644 docs/attribute_type_frontend_integration.md create mode 100644 docs/开发规范手册.md create mode 100644 opencode.json create mode 100644 package-lock.json diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 1c392ce..6dd9276 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -781,6 +781,11 @@ export function ReviewPointsList({ * 过滤评查点 * 根据搜索文本和状态过滤条件筛选评查点 */ + // DEBUG: check pointCode + if (reviewPoints.length > 0) { + console.log('[DEBUG pointCode]', reviewPoints[0].pointName, 'pointCode:', reviewPoints[0].pointCode, 'keys:', Object.keys(reviewPoints[0]).filter(k => k.includes('ode') || k.includes('Code'))); + } + const filteredReviewPoints = reviewPoints.filter(point => { // 匹配搜索文本 const matchesSearch = searchText === '' || @@ -2745,8 +2750,10 @@ export function ReviewPointsList({
{/*
*/}
- {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 提交信息格式 + +``` +(): + + + +