coding-standards.md 14 KB

编码标准

版本信息

版本 日期 描述 作者
3.0 2025-12-26 拆分测试策略到独立文档,保留编码标准 James (Claude Code)
2.5 2025-12-26 添加Mini UI包开发规范章节 Bob (Scrum Master)
2.4 2025-09-20 与主架构文档版本一致 Winston

现有标准合规性

代码风格

  • TypeScript严格模式: 所有项目必须启用严格类型检查
  • 一致的缩进: 使用2个空格缩进
  • 命名约定:
    • 文件名: kebab-case (如: user.service.ts)
    • 类名: PascalCase (如: UserService)
    • 函数/变量: camelCase (如: getUserById)
    • 常量: UPPER_SNAKE_CASE (如: API_BASE_URL)
    • 接口: PascalCase,无I前缀 (如: User)

Linting规则

  • ESLint: 已配置ESLint,支持TypeScript和React
  • Prettier: 统一代码格式化
  • 提交前检查: 使用husky进行pre-commit钩子检查

文档风格

  • 代码注释: 关键逻辑必须添加注释说明
  • JSDoc: 公共API必须包含JSDoc注释
  • README: 每个包必须有独立的README说明用途和使用方法

开发规范引用

UI包开发

开发Web UI包时,必须参考并遵循UI包开发规范,该规范基于史诗008(AllIn UI模块移植)的经验总结。

关键检查点

  1. API路径映射验证: 开发前必须验证故事中的API路径映射与实际后端路由定义的一致性
  2. 类型推断最佳实践: 必须使用RPC推断类型,而不是直接导入schema类型
  3. 测试选择器优化: 必须为关键交互元素添加data-testid属性
  4. 表单组件模式: 必须使用条件渲染两个独立的Form组件
  5. API调用一致性: 必须根据实际路由名称修正API调用

常见错误避免

  • ❌ 不要直接导入schema类型(可能导致Date/string类型不匹配)
  • ❌ 不要使用getByText()查找可能重复的文本元素
  • ❌ 不要在单个Form组件上动态切换props
  • ❌ 不要使用故事中描述但实际不存在的路由名称

参考实现

  • 广告管理UI包:packages/advertisement-management-ui
  • 平台管理UI包:allin-packages/platform-management-ui
  • 渠道管理UI包:allin-packages/channel-management-ui(史诗008.002)

Mini UI包开发

开发Mini UI包(Taro小程序UI包)时,必须参考并遵循Mini UI包开发规范,该规范基于史诗011(用人方小程序)和史诗017(人才小程序)的实施经验总结。

关键检查点

1. Taro小程序布局规范

  • View组件默认横向布局: View容器内的子元素默认是横向布局(flex-row),必须显式添加 flex flex-col 类才能实现垂直布局
  • Text组件默认内联显示: Text组件默认是内联显示(类似span),需要使用flex flex-col强制垂直排列

正确示例

// ✅ 正确: 使用 flex flex-col 实现垂直布局
<View className="flex flex-col">
  <Text>姓名: 张三</Text>
  <Text>性别: 男</Text>
  <Text>年龄: 35</Text>
</View>

// ❌ 错误: 缺少 flex flex-col,子元素会横向排列
<View>
  <Text>姓名: 张三</Text>
  <Text>性别: 男</Text>
  <Text>年龄: 35</Text>
</View>

2. 图标使用规范

  • 必须使用Heroicons图标类: 不要使用emoji或文本符号
  • 图标类命名格式: i-heroicons-{icon-name}-{size}-{style}
  • 必须添加尺寸类: 如 w-5 h-5text-lg

正确示例

// ✅ 正确: 使用Heroicons图标类
<View className="i-heroicons-chevron-left-20-solid w-5 h-5 text-gray-600" />
<View className="i-heroicons-user-20-solid w-6 h-6 text-blue-500" />
<View className="i-heroicons-bell-20-solid w-4 h-4 text-gray-400" />

// ❌ 错误: 使用emoji
<Text>🔔</Text>
<Text>👤</Text>
<View>←</View>

常用图标

  • chevron-left-20-solid - 左箭头(返回按钮)
  • user-20-solid - 用户
  • bell-20-solid - 通知铃
  • document-text-20-solid - 文档
  • chart-bar-20-solid - 图表
  • calendar-20-solid - 日历
  • phone-20-solid - 电话
  • lock-closed-20-solid - 锁
  • qr-code-20-solid - 二维码

3. Navbar导航栏集成规范

  • TabBar页面(一级): 使用Navbar无返回按钮(leftIcon=""leftText=""
  • 非TabBar页面(二级): 使用Navbar带返回按钮(leftIcon="i-heroicons-chevron-left-20-solid"
  • Navbar组件来源: @d8d/mini-shared-ui-components/components/navbar

4. API客户端模式

  • 每个UI包独立管理: 每个Mini UI包包含自己的API客户端和RPC类型
  • 使用相对路径导入: UI包内部必须使用相对路径,不要使用别名
  • RPC推断类型: 必须使用RPC推断类型,而不是直接导入schema类型

常见错误避免

  • ❌ 不要忘记添加 flex flex-col 实现垂直布局
  • ❌ 不要使用emoji代替Heroicons图标
  • ❌ 不要忘记为图标添加尺寸类(w-5 h-5text-lg等)
  • ❌ 不要在Mini UI包内部导入中使用别名(@/~/等)
  • ❌ 不要使用Vitest作为Mini项目的测试框架(应使用Jest)

参考实现

  • 用人方小程序UI包:mini-ui-packages/yongren-dashboard-ui
  • 人才小程序UI包:mini-ui-packages/rencai-dashboard-ui
  • 共享组件包:mini-ui-packages/mini-shared-ui-components

后端模块包开发

开发后端模块包时,必须参考并遵循后端模块包开发规范,该规范基于史诗007系列(渠道、平台、公司、残疾管理等模块)的实际实施经验总结。

关键检查点

1. Entity定义规范

  • 完整的列定义: 必须包含 type, length, nullable, comment 等属性
  • 使用索引装饰器: 使用 @Index 定义唯一索引和普通索引
  • 时间戳字段: 使用 timestamp 类型,设置 default: () => 'CURRENT_TIMESTAMP'
  • 主键定义: 使用 @PrimaryGeneratedColumn,包含 unsigned: truecomment

正确示例

@Entity('channel_info')
export class Channel {
  @PrimaryGeneratedColumn({
    name: 'channel_id',
    type: 'int',
    unsigned: true,
    comment: '渠道ID'
  })
  id!: number;

  @Column({
    name: 'channel_name',
    type: 'varchar',
    length: 100,
    nullable: false,
    comment: '渠道名称'
  })
  @Index('idx_channel_name', { unique: true })
  channelName!: string;

  @Column({
    name: 'create_time',
    type: 'timestamp',
    default: () => 'CURRENT_TIMESTAMP',
    comment: '创建时间'
  })
  createTime!: Date;
}

2. Service层规范

  • 继承 GenericCrudService: 使用基类提供的CRUD能力
  • 使用 override 关键字: 明确标识覆盖父类方法
  • 软删除实现: 使用 status 字段而非物理删除
  • 业务逻辑检查: 在调用父类方法前进行验证

正确示例

export class ChannelService extends GenericCrudService<Channel> {
  constructor(dataSource: DataSource) {
    super(dataSource, Channel);
  }

  override async create(data: Partial<Channel>, userId?: string | number): Promise<Channel> {
    // 业务逻辑检查
    const existingChannel = await this.repository.findOne({
      where: { channelName: data.channelName, status: 1 }
    });
    if (existingChannel) {
      throw new Error('渠道名称已存在');
    }

    const channelData = {
      ...data,
      status: 1,
      createTime: new Date(),
      updateTime: new Date()
    };

    return super.create(channelData, userId);
  }
}

3. 路由层规范

  • 使用 OpenAPIHono: 而非普通的 Hono
  • 使用 AuthContext 泛型: 提供类型安全的认证上下文
  • 自定义路由必须使用 parseWithAwait: 验证响应数据符合Schema定义
  • 使用 createZodErrorResponse: 处理Zod验证错误

正确示例

import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';

channelCustomRoutes.get('/statistics/:id', async (c) => {
  try {
    const result = await channelService.getStatistics(id);

    // ✅ 必须:使用 parseWithAwait 验证和转换响应数据
    const validatedResult = await parseWithAwait(ChannelSchema, result);
    return c.json(validatedResult, 200);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return c.json(createZodErrorResponse(error), 400);
    }
    return c.json({ code: 500, message: error.message }, 500);
  }
});

4. Schema规范

  • 使用 .openapi() 装饰器: 添加描述和示例
  • 使用 z.coerce.date<Date>()z.coerce.number<number>(): Zod 4.0需要添加泛型参数
  • 不导出推断类型: 类型由RPC自动推断,不需要手动导出 z.infer<typeof Schema>

正确示例

export const ChannelSchema = z.object({
  id: z.number().int().positive().openapi({
    description: '渠道ID',
    example: 1
  }),
  channelName: z.string().max(100).openapi({
    description: '渠道名称',
    example: '微信小程序'
  }),
  createTime: z.coerce.date<Date>().openapi({
    description: '创建时间',
    example: '2024-01-01T00:00:00Z'
  })
});

常见错误避免

  • ❌ Entity列定义不要省略 type, comment, nullable 等属性
  • ❌ Service覆盖方法不要忘记使用 override 关键字
  • ❌ 自定义路由不要省略 parseWithAwait 验证
  • ❌ Schema中不要使用 z.coerce.date()z.coerce.number()(必须添加泛型)
  • ❌ Schema不要导出 z.infer 推断的类型(类型由RPC自动推断)
  • ❌ 不要使用物理删除(应使用 status 字段实现软删除)

参考实现

  • 渠道模块:allin-packages/channel-module
  • 平台模块:allin-packages/platform-module
  • 公司模块:allin-packages/company-module
  • 残疾管理模块:allin-packages/disability-module
  • 认证模块:packages/core-module/auth-module
  • 用户模块:packages/core-module/user-module

类型安全

TypeScript配置

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

类型定义

// ✅ 推荐: 使用interface定义对象形状
interface User {
  id: number;
  username: string;
  email: string;
  createdAt: Date;
}

// ✅ 推荐: 使用type定义联合类型
type Status = 'pending' | 'active' | 'inactive';

// ✅ 推荐: 使用泛型增强类型复用
interface ApiResponse<T> {
  status: number;
  data: T;
  message?: string;
}

错误处理

统一错误处理

// 定义自定义错误类
class ValidationError extends Error {
  constructor(public errors: Record<string, string[]>) {
    super('验证失败');
    this.name = 'ValidationError';
  }
}

// 使用自定义错误
function validateUser(data: unknown): User {
  const result = userSchema.safeParse(data);
  if (!result.success) {
    throw new ValidationError(result.error.flatten().fieldErrors);
  }
  return result.data;
}

错误日志

import { logger } from '@d8d/shared-utils/logger';

try {
  await userService.createUser(userData);
} catch (error) {
  logger.error('创建用户失败', {
    error: error.message,
    stack: error.stack,
    userData: JSON.stringify(userData)
  });
  throw error;
}

安全最佳实践

输入验证

// ✅ 使用Schema验证
import { userSchema } from './schemas/user.schema';

const validatedData = await userSchema.parseAsync(inputData);

// ❌ 不要直接信任用户输入
const user = { username: req.body.username }; // 不安全

敏感数据处理

// ✅ 从响应中排除敏感字段
function sanitizeUser(user: User): Partial<User> {
  const { password, ...sanitized } = user;
  return sanitized;
}

// ✅ 日志中不记录敏感信息
logger.info('用户登录', { userId: user.id }); // 正确
logger.info('用户登录', { user }); // 错误 - 会记录密码

SQL注入防护

// ✅ 使用TypeORM参数化查询
const users = await userRepo.find({
  where: { username: username }
});

// ❌ 不要拼接SQL字符串
const query = `SELECT * FROM users WHERE username = '${username}'`; // 危险

性能优化

数据库查询优化

// ✅ 只查询需要的字段
const users = await userRepo.find({
  select: ['id', 'username', 'email']
});

// ✅ 使用索引字段查询
const user = await userRepo.findOne({
  where: { email: userEmail } // email字段应有索引
});

// ❌ 避免N+1查询
const orders = await orderRepo.find({
  relations: ['user', 'products'] // 使用join而不是循环查询
});

缓存策略

// ✅ 使用Redis缓存
import { cacheGet, cacheSet } from '@d8d/shared-utils/redis.util';

async function getUserById(id: number) {
  const cached = await cacheGet(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await userRepo.findOne({ where: { id } });
  await cacheSet(`user:${id}`, JSON.stringify(user), 3600);
  return user;
}

代码组织

文件结构

packages/user-module/
├── src/
│   ├── entities/        # 数据实体
│   ├── services/        # 业务逻辑
│   ├── schemas/         # 验证Schema
│   ├── routes/          # API路由
│   ├── middleware/      # 中间件
│   ├── utils/           # 工具函数
│   └── index.ts         # 包入口
├── tests/               # 测试文件
├── README.md            # 包说明
└── package.json

导出规范

// index.ts - 统一导出
export * from './entities';
export * from './services';
export { userRoutes } from './routes';
export { userSchema } from './schemas';

相关文档

测试规范

开发规范


文档状态: 正式版 下次评审: 2026-01-26