Files

34 KiB

中国烟草AI合同及卷宗审核系统 - 开发规范手册

目录

  1. 项目概述
  2. 技术栈规范
  3. 项目结构规范
  4. TypeScript代码规范
  5. React组件规范
  6. 样式规范
  7. API调用规范
  8. 路由开发规范
  9. 安全规范
  10. Git提交规范
  11. 环境变量规范
  12. 注释规范
  13. 错误处理规范
  14. 命名规范速查表

1. 项目概述

1.1 项目简介

本项目是中国烟草AI合同及卷宗审核系统,采用 Remix (React) + TypeScript 构建,提供智能文档审查、风险评估和合规检查功能。

1.2 核心命令

# 开发
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 路径别名

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": ["./app/*"]
    }
  }
}

导入示例:

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 类型定义规范

// ✅ 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<T> = {
  data: T;
  status: number;
  message?: string;
};

type ErrorResponse = {
  error: string;
  status: number;
};

// ✅ 5. 枚举定义 (仅在需要组合值时使用)
enum FileType {
  CONTRACT = 'contract',
  LICENSE = 'license',
  OTHER = 'other'
}

4.2 类型使用原则

// ✅ 优先使用 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<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

4.3 导入类型

// ✅ 使用 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 组件定义

// ✅ 正确 - 使用函数声明
export function Card({ 
  children, 
  title, 
  icon,
  extra,
  className = ''
}: CardProps) {
  return (
    <div className={`card ${className}`}>
      {title && (
        <div className="card-header">
          <div className="card-title">
            {icon && <i className={`${icon} mr-2`}></i>}
            <span>{title}</span>
          </div>
          {extra && <div className="card-extra">{extra}</div>}
        </div>
      )}
      <div className="card-body">{children}</div>
    </div>
  );
}

// ❌ 错误 - 使用箭头函数
export const Card = ({ children, title }: CardProps) => { ... };

5.2 Hooks 使用顺序

export function DocumentList({ documents }: DocumentListProps) {
  // 1. State hooks (按依赖关系排序)
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [filter, setFilter] = useState<FilterType>('all');
  const [isLoading, setIsLoading] = useState(false);

  // 2. Ref hooks
  const containerRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(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 (
    <div ref={containerRef} className="document-list">
      {/* ... */}
    </div>
  );
}

5.3 组件 Props 规范

// ✅ Props 接口定义
interface ButtonProps {
  children: React.ReactNode;
  type?: ButtonType;
  size?: ButtonSize;
  disabled?: boolean;
  loading?: boolean;
  icon?: string;
  className?: string;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

// ✅ 带默认值的 props
interface CardProps {
  title?: React.ReactNode;
  icon?: string;
  className?: string;
  bodyClassName?: string;
  children: React.ReactNode;
}

// ✅ 提取子组件 Props
interface TableColumn<T> {
  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        # 导出
// Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

6. 样式规范

6.1 样式架构

项目采用 Tailwind CSS + 自定义 CSS 混合模式:

/* 1. Tailwind 工具类 (优先使用) */
<div className="flex items-center justify-between p-4 mb-6">

/* 2. 设计系统变量 */
<div className="text-[--color-primary]">

/* 3. 自定义 BEM 类 */
<div className="card">
  <div className="card-header"></div>
  <div className="card-body"></div>
</div>

6.2 设计系统变量

/* 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 命名规范

/* 组件块 */
.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 常用配置

/* 间距 */
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 图标使用

// 基本使用
<i className="ri-home-line"></i>
<i className="ri-file-list-3-line"></i>
<i className="ri-user-line"></i>

// 尺寸控制
<i className="ri-home-line ri-lg"></i>
<i className="ri-home-line ri-xl"></i>

// 结合样式
<i className="ri-error-warning-line text-red-500"></i>
<i className="ri-check-line text-green-600"></i>

// 在按钮中使用
<Button icon="ri-add-line">添加</Button>

⚠️ 重要 - 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 客户端使用

// app/api/axios-client.ts - 已配置拦截器

import { get, post, put, del } from '~/api/axios-client';

// GET 请求
const data = await get<UserInfo>('/admin/users/1');

// POST 请求
const result = await post<ApiResponse>('/admin/documents', {
  name: '合同.pdf',
  type: 'contract'
});

// PUT 请求
const updated = await put<Document>('/admin/documents/123', {
  name: '新名称.pdf'
});

// DELETE 请求
await del('/admin/documents/123');

7.3 PostgREST 客户端使用

// app/api/postgrest-client.ts

import { 
  postgrestGet, 
  postgrestPost, 
  postgrestPatch,
  postgrestDelete 
} from '~/api/postgrest-client';

// 查询参数
const result = await postgrestGet<Document[]>('/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 封装模式

// 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<Document[]> {
  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<Document[]>('/documents', params);
  
  if ('error' in result) {
    throw new Error(result.error);
  }
  
  return result.data;
}

/**
 * 创建文档
 */
export async function createDocument(
  data: Omit<Document, 'id' | 'created_at'>
): Promise<Document> {
  const result = await postgrestPost<Document>('/documents', data);
  
  if ('error' in result) {
    throw new Error(result.error);
  }
  
  return result.data;
}

7.5 Loader 中使用 API

// 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<typeof loader>();
  
  return (
    <Layout userInfo={userInfo}>
      <div className="documents-page">
        {/* ... */}
      </div>
    </Layout>
  );
}

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 标准路由结构

// 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<typeof loader>();
  const navigation = useNavigation();
  const fetcher = useFetcher();
  
  const isSubmitting = navigation.state === "submitting";
  
  return (
    <Layout userInfo={userInfo}>
      <div className="page-container">
        <PageHeader 
          title="文档管理"
          actions={<Button icon="ri-add-line">新建</Button>}
        />
        
        <DocumentList documents={documents} />
      </div>
    </Layout>
  );
}

// ============ 7. ErrorBoundary ============
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-page">
        <h1>{error.status} - {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }
  
  return (
    <div className="error-page">
      <h1>发生了错误</h1>
      <p>{error instanceof Error ? error.message : "未知错误"}</p>
    </div>
  );
}

8.3 嵌套路由布局

routes/
├── documents.tsx              # 父路由 - 布局
├── documents.list.tsx         # 列表页面 (使用 documents.tsx 布局)
├── documents.create.tsx       # 创建页面 (使用 documents.tsx 布局)
└── documents_.detail.$id.tsx  # 详情页面 (不使用父布局,下划线跳过)
// routes/documents.tsx - 布局组件
export default function DocumentsLayout() {
  const { userInfo } = useLoaderData<typeof loader>();
  
  return (
    <Layout userInfo={userInfo}>
      <div className="documents-layout">
        <aside className="documents-sidebar">
          {/* 侧边栏导航 */}
        </aside>
        <main className="documents-main">
          <Outlet />  {/* 子路由内容 */}
        </main>
      </div>
    </Layout>
  );
}

8.4 API 路由

// 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 环境变量命名

# ✅ 客户端安全变量 (可被客户端代码访问)
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 认证白名单

// 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'
];
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 文件路径安全

// 防止路径遍历攻击
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 提交信息格式

<type>(<scope>): <subject>

<body>

<footer>

10.2 Type 类型

Type 说明
feat 新功能
fix Bug 修复
docs 文档更新
style 代码格式(不影响功能)
refactor 重构(不是新功能或修复)
perf 性能优化
test 测试相关
chore 构建/工具变更

10.3 Scope 范围

Scope 说明
api API 层
ui UI 组件
auth 认证相关
router 路由
config 配置
docs 文档
deps 依赖更新

10.4 提交示例

# 新功能
git commit -m "feat(api): 添加文档批量删除接口"

# Bug 修复
git commit -m "fix(ui): 修复 Button 组件在 Safari 的样式问题"

# 重构
git commit -m "refactor(auth): 重构 Token 刷新逻辑"

# 文档更新
git commit -m "docs: 更新 API 文档中的接口说明"

# 多项更改
git commit -m "feat(documents): 添加文档筛选和排序功能

- 添加状态筛选功能
- 添加日期范围筛选
- 优化排序逻辑
- 更新单元测试"

# 关闭 Issue
git commit -m "fix(auth): 修复 Token 过期后无法自动登出的问题

Closes #123"

10.5 分支命名

main                    # 主分支
develop                 # 开发分支
feature/doc-viewer      # 功能分支
fix/token-refresh       # 修复分支
hotfix/critical-bug     # 紧急修复分支
release/v1.2.0         # 发布分支

11. 环境变量规范

11.1 必需的环境变量

# JWT 配置
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production

# OAuth2.0 配置
OAUTH_CLIENT_SECRET=your-oauth-client-secret

# API 配置 (可选,优先使用端口配置)
NEXT_PUBLIC_API_BASE_URL=http://10.79.97.17:8000
NEXT_PUBLIC_DOCUMENT_URL=http://10.76.244.156:9000/docauditai/
NEXT_PUBLIC_UPLOAD_URL=http://10.79.97.17:8000/admin/documents

11.2 PM2 多实例配置

// ecosystem.config.cjs

module.exports = {
  apps: [
    {
      name: 'docreview-meizhou',
      script: 'npm',
      args: 'start',
      env: {
        NODE_ENV: 'production',
        PORT: 51703,
        CLIENT_ID: 'meizhou'
      }
    },
    {
      name: 'docreview-yunfu',
      script: 'npm',
      args: 'start',
      env: {
        NODE_ENV: 'production',
        PORT: 51704,
        CLIENT_ID: 'yunfu'
      }
    }
  ]
};

11.3 端口配置映射

// app/config/api-config.ts

const PORT_CONFIGS = {
  '51703': { // 梅州
    apiBaseUrl: 'http://10.79.97.17:8000',
    documentUrl: 'http://10.76.244.156:9000/docauditai/'
  },
  '51704': { // 云浮
    apiBaseUrl: 'http://10.79.97.18:8000',
    documentUrl: 'http://10.76.244.157:9000/docauditai/'
  }
  // ...
};

12. 注释规范

12.1 JSDoc 注释

/**
 * 文件描述注释
 * @description 该模块提供文档相关的 API 接口封装
 * @author 开发团队
 */

/**
 * 获取文档详情
 * @param id - 文档 ID
 * @param request - 请求对象
 * @returns 文档详情对象
 * @throws {ApiError} 当文档不存在时抛出错误
 */
async function getDocumentById(id: string, request: Request): Promise<Document> {
  // ...
}

/**
 * 文档状态类型
 * @typedef {'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed'} ProcessingStatus
 */

/**
 * 用户角色类型
 * @enum {string}
 */
enum UserRole {
  COMMON = 'common',
  DEVELOPER = 'developer',
  ADMIN = 'admin'
}

12.2 代码内注释

// ✅ 使用单行注释说明 WHY,不说明 WHAT

// 因为 API 需要 1-based 页码
const page = parseInt(url.searchParams.get('page') ?? '1', 10);

// 等待 DOM 完全加载后再初始化编辑器
useEffect(() => {
  initEditor();
}, []);

// 防止重复提交
if (isSubmitting) return;

// ❌ 避免无意义的注释
// 设置状态为 true
setIsLoading(true);

// 获取用户信息
const user = await getUser();

12.3 TODO 注释

// TODO: 优化性能 - 考虑使用虚拟滚动
// TODO(FEATURE): 添加批量导出功能
// TODO(BUG): 修复 Safari 上的日期选择器问题 (#123)
// FIXME: 临时解决方案,需要重构
// HACK: 处理旧版 API 兼容性

13. 错误处理规范

13.1 API 错误响应格式

// 成功响应
{
  data: T,
  status: 200
}

// 错误响应
{
  error: string,
  status: number
}

13.2 错误处理模式

// 1. Loader 中的错误处理
export async function loader({ request }: LoaderFunctionArgs) {
  try {
    const data = await fetchData();
    return Response.json({ data });
  } catch (error) {
    console.error('数据获取失败:', error);
    return Response.json(
      { error: '获取数据失败,请稍后重试' },
      { status: 500 }
    );
  }
}

// 2. Action 中的错误处理
export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  
  try {
    const result = await submitData(formData);
    return Response.json({ success: true, data: result });
  } catch (error) {
    if (error instanceof ValidationError) {
      return Response.json(
        { error: error.message, fields: error.fields },
        { status: 400 }
      );
    }
    
    return Response.json(
      { error: '提交失败,请稍后重试' },
      { status: 500 }
    );
  }
}

// 3. 前端错误处理
const fetcher = useFetcher();

useEffect(() => {
  if (fetcher.data?.error) {
    toast.error(fetcher.data.error);
  }
}, [fetcher.data]);

13.3 错误边界组件

// routes/reviews.tsx

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-container">
        <i className="ri-error-warning-line text-4xl text-red-500"></i>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
        <Button onClick={() => window.location.href = '/'}>
          返回首页
        </Button>
      </div>
    );
  }
  
  return (
    <div className="error-container">
      <h1>发生错误</h1>
      <p>{error instanceof Error ? error.message : '未知错误'}</p>
      <Button onClick={() => window.location.href = '/'}>
        返回首页
      </Button>
    </div>
  );
}

13.4 自定义错误类

// app/utils/errors.ts

export class ApiError extends Error {
  status: number;
  code?: string;
  
  constructor(message: string, status: number = 500, code?: string) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.code = code;
  }
}

export class ValidationError extends ApiError {
  fields: Record<string, string>;
  
  constructor(message: string, fields: Record<string, string> = {}) {
    super(message, 400, 'VALIDATION_ERROR');
    this.name = 'ValidationError';
    this.fields = fields;
  }
}

export class NotFoundError extends ApiError {
  constructor(resource: string) {
    super(`${resource} 不存在`, 404, 'NOT_FOUND');
    this.name = 'NotFoundError';
  }
}

14. 命名规范速查表

14.1 文件命名

类型 规范 示例
组件文件 PascalCase.tsx Button.tsx, Card.tsx
样式文件 kebab-case.css button.css, card-header.css
路由文件 kebab-case.tsx documents.tsx, user-profile.tsx
工具文件 camelCase.ts utils.ts, dateHelper.ts
类型文件 kebab-case.ts document-types.ts
测试文件 ComponentName.test.tsx Button.test.tsx

14.2 变量命名

类型 规范 示例
普通变量 camelCase userName, isLoading
常量 UPPER_SNAKE_CASE MAX_RETRIES, API_BASE_URL
枚举值 UPPER_SNAKE_CASE FileType.CONTRACT
组件状态 camelCase + 前缀 isLoading, hasError, isVisible
数组 复数名词或加 List/Suffix users, documentList, items

14.3 React 命名

类型 规范 示例
组件名 PascalCase function Button()
Props 接口 ComponentNameProps ButtonProps
事件处理 handle + 动作 handleClick, handleSubmit
布尔属性 is/has/should + 描述 isDisabled, hasChildren

14.4 CSS 类命名

类型 规范 示例
组件块 BEM .card, .sidebar
元素 BEM .card-header, .card-title
状态 BEM 修饰符 .card--compact, .button--disabled
工具类 Tailwind .flex, .text-center
图标类 RemixIcon .ri-home-line

附录

A. 开发检查清单

新组件开发:

  • TypeScript 接口定义完整
  • Props 定义包含必要的可选属性
  • 使用函数声明而非箭头函数
  • Hooks 顺序正确
  • 样式符合设计系统
  • 组件自包含 (tsx + css + index.ts)
  • 导出类型定义

新页面开发:

  • Loader 函数完整
  • Action 函数处理所有意图
  • Meta 配置完整
  • Links 加载必要样式
  • ErrorBoundary 处理错误
  • 响应式设计适配
  • 权限检查

API 调用:

  • 使用 axios-client 或 postgrest-client
  • 错误处理完整
  • Loading 状态处理
  • 类型定义正确

代码提交前:

  • ESLint 检查通过
  • TypeScript 编译无错误
  • 代码格式规范
  • 注释完整
  • 无敏感信息泄露
  • Git 提交信息规范

B. 参考资源


本文档最后更新: 2026-03-18