|
@@ -3,334 +3,534 @@
|
|
|
## 版本信息
|
|
## 版本信息
|
|
|
| 版本 | 日期 | 描述 | 作者 |
|
|
| 版本 | 日期 | 描述 | 作者 |
|
|
|
|------|------|------|------|
|
|
|------|------|------|------|
|
|
|
|
|
+| 2.0 | 2025-12-26 | 基于实际实现重写,修正不准确的描述 | James (Claude Code) |
|
|
|
| 1.0 | 2025-12-02 | 基于史诗007系列移植经验创建 | Claude Code |
|
|
| 1.0 | 2025-12-02 | 基于史诗007系列移植经验创建 | Claude Code |
|
|
|
|
|
|
|
|
## 概述
|
|
## 概述
|
|
|
-本文档定义了后端模块包的设计、开发和集成规范,基于史诗007系列(Allin系统模块移植)的实际经验总结。这些规范旨在确保模块包的一致性、可维护性和可集成性。
|
|
|
|
|
|
|
+
|
|
|
|
|
+本文档定义了后端模块包的设计、开发和集成规范,基于项目实际的模块包实现经验总结。
|
|
|
|
|
+
|
|
|
|
|
+### 包组织方式
|
|
|
|
|
+
|
|
|
|
|
+项目采用两种包组织方式:
|
|
|
|
|
+
|
|
|
|
|
+1. **allin-packages模式**: 每个业务模块独立成包
|
|
|
|
|
+ - 目录: `allin-packages/{module-name}-module/`
|
|
|
|
|
+ - 示例: `@d8d/allin-channel-module`, `@d8d/allin-platform-module`
|
|
|
|
|
+
|
|
|
|
|
+2. **core-module聚合模式**: 将多个相关模块打包在一起
|
|
|
|
|
+ - 目录: `packages/core-module/{module-name}/`
|
|
|
|
|
+ - 示例: `@d8d/core-module/auth-module`, `@d8d/core-module/user-module`
|
|
|
|
|
|
|
|
## 1. 包结构规范
|
|
## 1. 包结构规范
|
|
|
|
|
|
|
|
### 1.1 目录结构
|
|
### 1.1 目录结构
|
|
|
|
|
+
|
|
|
|
|
+#### allin-packages模式
|
|
|
```
|
|
```
|
|
|
allin-packages/{module-name}-module/
|
|
allin-packages/{module-name}-module/
|
|
|
-├── package.json # 包配置
|
|
|
|
|
-├── tsconfig.json # TypeScript配置
|
|
|
|
|
-├── vitest.config.ts # 测试配置
|
|
|
|
|
|
|
+├── package.json
|
|
|
|
|
+├── tsconfig.json
|
|
|
|
|
+├── vitest.config.ts
|
|
|
├── src/
|
|
├── src/
|
|
|
-│ ├── entities/ # 实体定义
|
|
|
|
|
|
|
+│ ├── entities/
|
|
|
│ │ └── {entity-name}.entity.ts
|
|
│ │ └── {entity-name}.entity.ts
|
|
|
-│ ├── services/ # 服务层
|
|
|
|
|
|
|
+│ ├── services/
|
|
|
│ │ └── {service-name}.service.ts
|
|
│ │ └── {service-name}.service.ts
|
|
|
-│ ├── routes/ # 路由层
|
|
|
|
|
-│ │ ├── {module}-custom.routes.ts # 自定义路由
|
|
|
|
|
-│ │ ├── {module}-crud.routes.ts # CRUD路由
|
|
|
|
|
-│ │ └── {module}.routes.ts # 主路由
|
|
|
|
|
-│ ├── schemas/ # 验证Schema
|
|
|
|
|
|
|
+│ ├── routes/
|
|
|
|
|
+│ │ ├── {module}-custom.routes.ts
|
|
|
|
|
+│ │ ├── {module}-crud.routes.ts
|
|
|
|
|
+│ │ ├── {module}.routes.ts
|
|
|
|
|
+│ │ └── index.ts
|
|
|
|
|
+│ ├── schemas/
|
|
|
│ │ └── {schema-name}.schema.ts
|
|
│ │ └── {schema-name}.schema.ts
|
|
|
-│ ├── types/ # 类型定义
|
|
|
|
|
|
|
+│ ├── types/
|
|
|
│ │ └── index.ts
|
|
│ │ └── index.ts
|
|
|
-│ └── index.ts # 包入口
|
|
|
|
|
|
|
+│ └── index.ts
|
|
|
└── tests/
|
|
└── tests/
|
|
|
- └── integration/ # 集成测试
|
|
|
|
|
- └── {module}.integration.test.ts
|
|
|
|
|
|
|
+ ├── integration/
|
|
|
|
|
+ │ └── {module}.integration.test.ts
|
|
|
|
|
+ └── utils/
|
|
|
|
|
+ └── test-data-factory.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### core-module聚合模式
|
|
|
|
|
+```
|
|
|
|
|
+packages/core-module/
|
|
|
|
|
+├── auth-module/
|
|
|
|
|
+│ ├── src/
|
|
|
|
|
+│ │ ├── entities/
|
|
|
|
|
+│ │ ├── services/
|
|
|
|
|
+│ │ ├── routes/
|
|
|
|
|
+│ │ └── schemas/
|
|
|
|
|
+│ └── tests/
|
|
|
|
|
+├── user-module/
|
|
|
|
|
+│ └── ...
|
|
|
|
|
+└── file-module/
|
|
|
|
|
+ └── ...
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### 1.2 包命名规范
|
|
### 1.2 包命名规范
|
|
|
-- **前缀**: `@d8d/allin-`
|
|
|
|
|
-- **后缀**: `-module`
|
|
|
|
|
-- **示例**: `@d8d/allin-channel-module`, `@d8d/allin-platform-module`
|
|
|
|
|
|
|
+
|
|
|
|
|
+| 包类型 | 命名模式 | 示例 |
|
|
|
|
|
+|--------|----------|------|
|
|
|
|
|
+| allin业务包 | `@d8d/allin-{name}-module` | `@d8d/allin-channel-module` |
|
|
|
|
|
+| core子模块 | `@d8d/core-module` | 内部按路径区分 |
|
|
|
|
|
|
|
|
### 1.3 workspace配置
|
|
### 1.3 workspace配置
|
|
|
```yaml
|
|
```yaml
|
|
|
# pnpm-workspace.yaml
|
|
# pnpm-workspace.yaml
|
|
|
packages:
|
|
packages:
|
|
|
- - 'allin-packages/*' # 自动包含所有allin包
|
|
|
|
|
- - 'allin-packages/{module-name}-module' # 或显式指定
|
|
|
|
|
|
|
+ - 'allin-packages/*'
|
|
|
|
|
+ - 'packages/*'
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
## 2. 实体设计规范
|
|
## 2. 实体设计规范
|
|
|
|
|
|
|
|
-### 2.1 主键命名
|
|
|
|
|
-```typescript
|
|
|
|
|
-// ✅ 正确:使用id作为主键名
|
|
|
|
|
-@PrimaryGeneratedColumn({ name: 'channel_id' })
|
|
|
|
|
-id!: number;
|
|
|
|
|
-
|
|
|
|
|
-// ❌ 错误:使用特定名称
|
|
|
|
|
-@PrimaryGeneratedColumn({ name: 'channel_id' })
|
|
|
|
|
-channelId!: number;
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### 2.1 Entity定义
|
|
|
|
|
|
|
|
-### 2.2 字段命名转换
|
|
|
|
|
|
|
+**实际实现模式**:
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 数据库下划线命名 → TypeScript驼峰命名
|
|
|
|
|
-@Column({ name: 'channel_name' })
|
|
|
|
|
-channelName!: string;
|
|
|
|
|
-
|
|
|
|
|
-@Column({ name: 'contact_person' })
|
|
|
|
|
-contactPerson!: string;
|
|
|
|
|
-
|
|
|
|
|
-@Column({ name: 'create_time' })
|
|
|
|
|
-createTime!: Date;
|
|
|
|
|
|
|
+import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
|
|
|
|
|
+
|
|
|
|
|
+@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: 'status',
|
|
|
|
|
+ type: 'int',
|
|
|
|
|
+ default: 1,
|
|
|
|
|
+ comment: '状态:1-正常,0-禁用'
|
|
|
|
|
+ })
|
|
|
|
|
+ status!: number;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({
|
|
|
|
|
+ name: 'create_time',
|
|
|
|
|
+ type: 'timestamp',
|
|
|
|
|
+ default: () => 'CURRENT_TIMESTAMP',
|
|
|
|
|
+ comment: '创建时间'
|
|
|
|
|
+ })
|
|
|
|
|
+ createTime!: Date;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({
|
|
|
|
|
+ name: 'update_time',
|
|
|
|
|
+ type: 'timestamp',
|
|
|
|
|
+ default: () => 'CURRENT_TIMESTAMP',
|
|
|
|
|
+ onUpdate: 'CURRENT_TIMESTAMP',
|
|
|
|
|
+ comment: '更新时间'
|
|
|
|
|
+ })
|
|
|
|
|
+ updateTime!: Date;
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 2.3 唯一性约束
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 单字段唯一性
|
|
|
|
|
-@Unique(['channelName'])
|
|
|
|
|
|
|
+### 2.2 关键要点
|
|
|
|
|
|
|
|
-// 复合字段唯一性(如公司名称在同一平台下唯一)
|
|
|
|
|
-@Unique(['companyName', 'platformId'])
|
|
|
|
|
-```
|
|
|
|
|
|
|
+- **完整的列定义**: 包含 `type`, `length`, `nullable`, `comment` 等属性
|
|
|
|
|
+- **索引装饰器**: 使用 `@Index` 定义唯一索引
|
|
|
|
|
+- **时间戳字段**: 使用 `timestamp` 类型而非 `datetime`
|
|
|
|
|
+- **默认值**: 使用 `default: () => 'CURRENT_TIMESTAMP'`
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 关联关系
|
|
|
|
|
|
|
|
-### 2.4 关联关系配置
|
|
|
|
|
|
|
+**多对一关系**:
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 多对一关系(如公司关联平台)
|
|
|
|
|
-@ManyToOne(() => Platform, { eager: true })
|
|
|
|
|
-@JoinColumn({ name: 'platform_id' })
|
|
|
|
|
|
|
+@ManyToOne(() => Platform, { eager: false })
|
|
|
|
|
+@JoinColumn({ name: 'platform_id', referencedColumnName: 'id' })
|
|
|
platform!: Platform;
|
|
platform!: Platform;
|
|
|
-
|
|
|
|
|
-// 一对多关系
|
|
|
|
|
-@OneToMany(() => Company, company => company.platform)
|
|
|
|
|
-companies!: Company[];
|
|
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 2.5 软删除实现
|
|
|
|
|
|
|
+**避免循环依赖**:
|
|
|
```typescript
|
|
```typescript
|
|
|
-@Column({
|
|
|
|
|
- name: 'status',
|
|
|
|
|
- type: 'tinyint',
|
|
|
|
|
- default: 1,
|
|
|
|
|
- comment: '状态:0-删除,1-正常'
|
|
|
|
|
-})
|
|
|
|
|
-status!: number;
|
|
|
|
|
|
|
+// 使用字符串引用避免循环依赖
|
|
|
|
|
+@ManyToOne('DisabledPerson', { nullable: true })
|
|
|
|
|
+@JoinColumn({ name: 'person_id', referencedColumnName: 'id' })
|
|
|
|
|
+person!: import('@d8d/allin-disability-module/entities').DisabledPerson | null;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 3. 数据库类型规范
|
|
|
|
|
|
|
+### 2.4 文件关联模式
|
|
|
|
|
|
|
|
-### 3.1 PostgreSQL类型兼容
|
|
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 源类型 → 目标类型
|
|
|
|
|
-@Column({ name: 'some_flag', type: 'tinyint' }) // tinyint → smallint
|
|
|
|
|
-someFlag!: number;
|
|
|
|
|
|
|
+@Column({ name: 'avatar_file_id', type: 'int', unsigned: true, nullable: true })
|
|
|
|
|
+avatarFileId!: number | null;
|
|
|
|
|
|
|
|
-@Column({ name: 'create_time', type: 'datetime' }) // datetime → timestamp
|
|
|
|
|
-createTime!: Date;
|
|
|
|
|
|
|
+@ManyToOne(() => File)
|
|
|
|
|
+@JoinColumn({ name: 'avatar_file_id', referencedColumnName: 'id' })
|
|
|
|
|
+avatarFile!: File | null;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 3.2 Decimal字段处理
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 实体定义
|
|
|
|
|
-@Column({
|
|
|
|
|
- name: 'total_amount',
|
|
|
|
|
- type: 'decimal',
|
|
|
|
|
- precision: 10,
|
|
|
|
|
- scale: 2
|
|
|
|
|
-})
|
|
|
|
|
-totalAmount!: number;
|
|
|
|
|
|
|
+## 3. 服务层规范
|
|
|
|
|
|
|
|
-// Schema验证(使用z.coerce.number()处理字符串)
|
|
|
|
|
-const CreateSchema = z.object({
|
|
|
|
|
- totalAmount: z.coerce.number().min(0),
|
|
|
|
|
-});
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### 3.1 GenericCrudService继承
|
|
|
|
|
|
|
|
-### 3.3 枚举值一致性
|
|
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 保持与数据库值一致(小写字符串,下划线分隔)
|
|
|
|
|
-enum OrderStatus {
|
|
|
|
|
- DRAFT = 'draft',
|
|
|
|
|
- CONFIRMED = 'confirmed',
|
|
|
|
|
- IN_PROGRESS = 'in_progress',
|
|
|
|
|
- COMPLETED = 'completed',
|
|
|
|
|
- CANCELLED = 'cancelled'
|
|
|
|
|
-}
|
|
|
|
|
|
|
+import { GenericCrudService } from '@d8d/shared-crud';
|
|
|
|
|
+import { DataSource } from 'typeorm';
|
|
|
|
|
+import { Channel } from '../entities/channel.entity';
|
|
|
|
|
|
|
|
-// 数字枚举
|
|
|
|
|
-enum DisabilityLevel {
|
|
|
|
|
- ONE = 1,
|
|
|
|
|
- TWO = 2,
|
|
|
|
|
- THREE = 3,
|
|
|
|
|
- FOUR = 4
|
|
|
|
|
|
|
+export class ChannelService extends GenericCrudService<Channel> {
|
|
|
|
|
+ constructor(dataSource: DataSource) {
|
|
|
|
|
+ super(dataSource, Channel);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 4. 服务层规范
|
|
|
|
|
|
|
+### 3.2 方法覆盖模式(使用override)
|
|
|
|
|
|
|
|
-### 4.1 GenericCrudService继承
|
|
|
|
|
|
|
+**实际实现**:
|
|
|
```typescript
|
|
```typescript
|
|
|
export class ChannelService extends GenericCrudService<Channel> {
|
|
export class ChannelService extends GenericCrudService<Channel> {
|
|
|
constructor(dataSource: DataSource) {
|
|
constructor(dataSource: DataSource) {
|
|
|
super(dataSource, Channel);
|
|
super(dataSource, Channel);
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
|
|
|
-### 4.2 方法覆盖模式
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 覆盖create方法:添加唯一性检查
|
|
|
|
|
-async create(data: CreateChannelDto): Promise<Channel> {
|
|
|
|
|
- // 业务逻辑:检查名称唯一性
|
|
|
|
|
- const existing = await this.repository.findOne({
|
|
|
|
|
- where: { channelName: data.channelName }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (existing) {
|
|
|
|
|
- throw new Error('渠道名称已存在');
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建渠道 - 覆盖父类方法,添加名称唯一性检查
|
|
|
|
|
+ */
|
|
|
|
|
+ override async create(data: Partial<Channel>, userId?: string | number): Promise<Channel> {
|
|
|
|
|
+ // 检查渠道名称是否已存在(只检查正常状态的渠道)
|
|
|
|
|
+ if (data.channelName) {
|
|
|
|
|
+ const existingChannel = await this.repository.findOne({
|
|
|
|
|
+ where: { channelName: data.channelName, status: 1 }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (existingChannel) {
|
|
|
|
|
+ throw new Error('渠道名称已存在');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置默认值
|
|
|
|
|
+ const channelData = {
|
|
|
|
|
+ contactPerson: '',
|
|
|
|
|
+ contactPhone: '',
|
|
|
|
|
+ channelType: '',
|
|
|
|
|
+ description: '',
|
|
|
|
|
+ ...data,
|
|
|
|
|
+ status: 1,
|
|
|
|
|
+ createTime: new Date(),
|
|
|
|
|
+ updateTime: new Date()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return super.create(channelData, userId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return super.create(data);
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新渠道 - 覆盖父类方法,添加存在性和名称重复检查
|
|
|
|
|
+ */
|
|
|
|
|
+ override async update(id: number, data: Partial<Channel>, userId?: string | number): Promise<Channel | null> {
|
|
|
|
|
+ // 检查渠道是否存在
|
|
|
|
|
+ const channel = await this.repository.findOne({ where: { id, status: 1 } });
|
|
|
|
|
+ if (!channel) {
|
|
|
|
|
+ throw new Error('渠道不存在');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查名称是否与其他渠道重复
|
|
|
|
|
+ if (data.channelName && data.channelName !== channel.channelName) {
|
|
|
|
|
+ const existingChannel = await this.repository.findOne({
|
|
|
|
|
+ where: { channelName: data.channelName, id: Not(id), status: 1 }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (existingChannel) {
|
|
|
|
|
+ throw new Error('渠道名称已存在');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const updateData = {
|
|
|
|
|
+ ...data,
|
|
|
|
|
+ updateTime: new Date()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return super.update(id, updateData, userId);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-// 覆盖findAll方法:返回标准格式
|
|
|
|
|
-async findAll(options?: FindManyOptions<Channel>): Promise<{ data: Channel[], total: number }> {
|
|
|
|
|
- const [data, total] = await this.repository.findAndCount(options);
|
|
|
|
|
- return { data, total };
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除渠道 - 覆盖父类方法,改为软删除
|
|
|
|
|
+ */
|
|
|
|
|
+ override async delete(id: number, userId?: string | number): Promise<boolean> {
|
|
|
|
|
+ // 软删除:设置status为0
|
|
|
|
|
+ const result = await this.repository.update({ id }, { status: 0 });
|
|
|
|
|
+ return result.affected === 1;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 4.3 自定义业务方法
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 按名称搜索
|
|
|
|
|
-async searchByName(name: string): Promise<Channel[]> {
|
|
|
|
|
- return this.repository.find({
|
|
|
|
|
- where: { channelName: Like(`%${name}%`) }
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### 3.3 关键要点
|
|
|
|
|
|
|
|
-## 5. 路由层规范
|
|
|
|
|
|
|
+- **使用 `override` 关键字**: 明确标识覆盖父类方法
|
|
|
|
|
+- **软删除逻辑**: 使用 `status` 字段而非物理删除
|
|
|
|
|
+- **业务逻辑检查**: 在调用父类方法前进行验证
|
|
|
|
|
+- **设置默认值**: 为可选字段设置合理的默认值
|
|
|
|
|
+- **时间戳管理**: 自动设置 `createTime` 和 `updateTime`
|
|
|
|
|
|
|
|
-### 5.1 路由聚合模式
|
|
|
|
|
|
|
+## 4. 路由层规范
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 使用OpenAPIHono
|
|
|
|
|
+
|
|
|
|
|
+**实际实现**:
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 主路由文件:聚合自定义路由和CRUD路由
|
|
|
|
|
-import customRoutes from './channel-custom.routes';
|
|
|
|
|
-import crudRoutes from './channel-crud.routes';
|
|
|
|
|
|
|
+import { OpenAPIHono } from '@hono/zod-openapi';
|
|
|
|
|
+import { AuthContext } from '@d8d/shared-types';
|
|
|
|
|
+import channelCustomRoutes from './channel-custom.routes';
|
|
|
|
|
+import { channelCrudRoutes } from './channel-crud.routes';
|
|
|
|
|
|
|
|
-const channelRoutes = new Hono()
|
|
|
|
|
- .basePath('/channels')
|
|
|
|
|
- .route('/', customRoutes) // 自定义业务逻辑路由
|
|
|
|
|
- .route('/', crudRoutes); // 标准CRUD路由
|
|
|
|
|
|
|
+// 创建路由实例 - 聚合自定义路由和CRUD路由
|
|
|
|
|
+const channelRoutes = new OpenAPIHono<AuthContext>()
|
|
|
|
|
+ .route('/', channelCustomRoutes)
|
|
|
|
|
+ .route('/', channelCrudRoutes);
|
|
|
|
|
|
|
|
|
|
+export { channelRoutes };
|
|
|
export default channelRoutes;
|
|
export default channelRoutes;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 5.2 API兼容性
|
|
|
|
|
|
|
+### 4.2 导出模式
|
|
|
|
|
+
|
|
|
|
|
+**routes/index.ts**:
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 保持与原始NestJS API相同的端点路径和功能
|
|
|
|
|
-// 原始:POST /channel/createChannel
|
|
|
|
|
-// 新:POST /channels/createChannel
|
|
|
|
|
-app.post('/createChannel', async (c) => {
|
|
|
|
|
- // 实现逻辑
|
|
|
|
|
-});
|
|
|
|
|
|
|
+export * from './channel.routes';
|
|
|
|
|
+export * from './channel-custom.routes';
|
|
|
|
|
+export * from './channel-crud.routes';
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 5.3 布尔返回值处理
|
|
|
|
|
|
|
+### 4.3 关键要点
|
|
|
|
|
+
|
|
|
|
|
+- **使用 `OpenAPIHono`**: 而非普通的 `Hono`
|
|
|
|
|
+- **使用 `AuthContext` 泛型**: 提供类型安全的认证上下文
|
|
|
|
|
+- **路由聚合**: 分别定义自定义路由和CRUD路由,然后聚合
|
|
|
|
|
+- **不设置 `basePath`**: 在聚合路由时处理路径
|
|
|
|
|
+
|
|
|
|
|
+## 5. Schema规范
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 使用Zod + OpenAPI装饰器
|
|
|
|
|
+
|
|
|
|
|
+**实际实现**:
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 正确处理布尔返回值
|
|
|
|
|
-app.post('/create', async (c) => {
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await service.create(data);
|
|
|
|
|
- return c.json({ success: true }, 200);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- return c.json({
|
|
|
|
|
- success: false,
|
|
|
|
|
- message: error.message || '创建失败'
|
|
|
|
|
- }, 400);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+import { z } from '@hono/zod-openapi';
|
|
|
|
|
+
|
|
|
|
|
+// 渠道实体Schema
|
|
|
|
|
+export const ChannelSchema = z.object({
|
|
|
|
|
+ id: z.number().int().positive().openapi({
|
|
|
|
|
+ description: '渠道ID',
|
|
|
|
|
+ example: 1
|
|
|
|
|
+ }),
|
|
|
|
|
+ channelName: z.string().max(100).openapi({
|
|
|
|
|
+ description: '渠道名称',
|
|
|
|
|
+ example: '微信小程序'
|
|
|
|
|
+ }),
|
|
|
|
|
+ channelType: z.string().max(50).openapi({
|
|
|
|
|
+ description: '渠道类型',
|
|
|
|
|
+ example: '小程序'
|
|
|
|
|
+ }),
|
|
|
|
|
+ contactPerson: z.string().max(50).openapi({
|
|
|
|
|
+ description: '联系人',
|
|
|
|
|
+ example: '张三'
|
|
|
|
|
+ }),
|
|
|
|
|
+ contactPhone: z.string().max(20).openapi({
|
|
|
|
|
+ description: '联系电话',
|
|
|
|
|
+ example: '13800138000'
|
|
|
|
|
+ }),
|
|
|
|
|
+ description: z.string().nullable().optional().openapi({
|
|
|
|
|
+ description: '描述',
|
|
|
|
|
+ example: '微信小程序渠道'
|
|
|
|
|
+ }),
|
|
|
|
|
+ status: z.number().int().min(0).max(1).default(1).openapi({
|
|
|
|
|
+ description: '状态:1-正常,0-禁用',
|
|
|
|
|
+ example: 1
|
|
|
|
|
+ }),
|
|
|
|
|
+ createTime: z.coerce.date().openapi({
|
|
|
|
|
+ description: '创建时间',
|
|
|
|
|
+ example: '2024-01-01T00:00:00Z'
|
|
|
|
|
+ }),
|
|
|
|
|
+ updateTime: z.coerce.date().openapi({
|
|
|
|
|
+ description: '更新时间',
|
|
|
|
|
+ example: '2024-01-01T00:00:00Z'
|
|
|
|
|
+ })
|
|
|
});
|
|
});
|
|
|
-```
|
|
|
|
|
|
|
|
|
|
-### 5.4 错误信息明确
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 提供明确的错误信息
|
|
|
|
|
-app.put('/update/:id', async (c) => {
|
|
|
|
|
- const id = parseInt(c.req.param('id'));
|
|
|
|
|
- const data = await c.req.json();
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await service.update(id, data);
|
|
|
|
|
- return c.json({ success: true });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- return c.json({
|
|
|
|
|
- success: false,
|
|
|
|
|
- message: error.message || '平台不存在或名称重复'
|
|
|
|
|
- }, 400);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 创建渠道DTO
|
|
|
|
|
+export const CreateChannelSchema = z.object({
|
|
|
|
|
+ channelName: z.string().min(1).max(100).openapi({
|
|
|
|
|
+ description: '渠道名称',
|
|
|
|
|
+ example: '微信小程序'
|
|
|
|
|
+ }),
|
|
|
|
|
+ channelType: z.string().max(50).optional().openapi({
|
|
|
|
|
+ description: '渠道类型',
|
|
|
|
|
+ example: '小程序'
|
|
|
|
|
+ }),
|
|
|
|
|
+ contactPerson: z.string().max(50).optional().openapi({
|
|
|
|
|
+ description: '联系人',
|
|
|
|
|
+ example: '张三'
|
|
|
|
|
+ }),
|
|
|
|
|
+ contactPhone: z.string().max(20).optional().openapi({
|
|
|
|
|
+ description: '联系电话',
|
|
|
|
|
+ example: '13800138000'
|
|
|
|
|
+ }),
|
|
|
|
|
+ description: z.string().optional().openapi({
|
|
|
|
|
+ description: '描述',
|
|
|
|
|
+ example: '微信小程序渠道'
|
|
|
|
|
+ })
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+// 更新渠道DTO(所有字段可选)
|
|
|
|
|
+export const UpdateChannelSchema = CreateChannelSchema.partial();
|
|
|
|
|
+
|
|
|
|
|
+// 类型推断
|
|
|
|
|
+export type Channel = z.infer<typeof ChannelSchema>;
|
|
|
|
|
+export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
|
|
|
|
|
+export type UpdateChannelDto = z.infer<typeof UpdateChannelSchema>;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 6. 验证系统规范
|
|
|
|
|
|
|
+### 5.2 关键要点
|
|
|
|
|
|
|
|
-### 6.1 Zod Schema定义
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 创建Schema
|
|
|
|
|
-const CreateChannelSchema = z.object({
|
|
|
|
|
- channelName: z.string().min(1).max(100),
|
|
|
|
|
- contactPerson: z.string().max(50).optional(),
|
|
|
|
|
- contactPhone: z.string().max(20).optional(),
|
|
|
|
|
- status: z.number().int().min(0).max(1).default(1),
|
|
|
|
|
-});
|
|
|
|
|
|
|
+- **使用 `.openapi()` 装饰器**: 添加描述和示例
|
|
|
|
|
+- **使用 `z.coerce.date()`**: 处理日期字符串自动转换
|
|
|
|
|
+- **使用 `.nullable().optional()`**: 处理可空字段
|
|
|
|
|
+- **类型推断导出**: 导出推断的TypeScript类型
|
|
|
|
|
|
|
|
-// 更新Schema(id通过路径参数传递,不在body中)
|
|
|
|
|
-const UpdateChannelSchema = CreateChannelSchema.partial();
|
|
|
|
|
|
|
+## 6. 软删除规范
|
|
|
|
|
|
|
|
-// 查询Schema
|
|
|
|
|
-const QueryChannelSchema = z.object({
|
|
|
|
|
- channelName: z.string().optional(),
|
|
|
|
|
- skip: z.coerce.number().int().min(0).default(0),
|
|
|
|
|
- take: z.coerce.number().int().min(1).max(100).default(10),
|
|
|
|
|
-});
|
|
|
|
|
|
|
+### 6.1 字段定义
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+@Column({
|
|
|
|
|
+ name: 'status',
|
|
|
|
|
+ type: 'int',
|
|
|
|
|
+ default: 1,
|
|
|
|
|
+ comment: '状态:1-正常,0-禁用'
|
|
|
|
|
+})
|
|
|
|
|
+status!: number;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 6.2 枚举验证
|
|
|
|
|
|
|
+### 6.2 Service层实现
|
|
|
|
|
+
|
|
|
```typescript
|
|
```typescript
|
|
|
-import { OrderStatus, WorkStatus } from '@d8d/allin-enums';
|
|
|
|
|
|
|
+// 覆盖delete方法实现软删除
|
|
|
|
|
+override async delete(id: number, userId?: string | number): Promise<boolean> {
|
|
|
|
|
+ const result = await this.repository.update({ id }, { status: 0 });
|
|
|
|
|
+ return result.affected === 1;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 6.3 查询时过滤
|
|
|
|
|
|
|
|
-const CreateOrderSchema = z.object({
|
|
|
|
|
- orderStatus: z.nativeEnum(OrderStatus).default(OrderStatus.DRAFT),
|
|
|
|
|
- workStatus: z.nativeEnum(WorkStatus).default(WorkStatus.NOT_WORKING),
|
|
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 查询时只查询正常状态的记录
|
|
|
|
|
+const channel = await this.repository.findOne({
|
|
|
|
|
+ where: { id, status: 1 }
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 7. 模块集成规范
|
|
|
|
|
|
|
+## 7. 数据库类型规范
|
|
|
|
|
+
|
|
|
|
|
+### 7.1 PostgreSQL类型映射
|
|
|
|
|
+
|
|
|
|
|
+| 数据库类型 | TypeORM类型 | 备注 |
|
|
|
|
|
+|------------|-------------|------|
|
|
|
|
|
+| `int unsigned` | `int` + `unsigned: true` | 主键常用 |
|
|
|
|
|
+| `varchar(n)` | `varchar` + `length: n` | 字符串 |
|
|
|
|
|
+| `text` | `text` | 长文本 |
|
|
|
|
|
+| `timestamp` | `timestamp` | 时间戳 |
|
|
|
|
|
+| `decimal(p,s)` | `decimal` + `precision/scale` | 金额 |
|
|
|
|
|
+| `int` (状态) | `int` | 状态枚举 |
|
|
|
|
|
+
|
|
|
|
|
+### 7.2 Decimal字段处理
|
|
|
|
|
|
|
|
-### 7.1 文件模块集成
|
|
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 使用fileId字段而非URL字段
|
|
|
|
|
|
|
+// 实体定义
|
|
|
@Column({
|
|
@Column({
|
|
|
- name: 'file_id',
|
|
|
|
|
- type: 'int',
|
|
|
|
|
- nullable: false,
|
|
|
|
|
- comment: '文件ID,引用files表'
|
|
|
|
|
|
|
+ name: 'total_amount',
|
|
|
|
|
+ type: 'decimal',
|
|
|
|
|
+ precision: 10,
|
|
|
|
|
+ scale: 2
|
|
|
})
|
|
})
|
|
|
-fileId!: number;
|
|
|
|
|
|
|
+totalAmount!: number;
|
|
|
|
|
|
|
|
-@ManyToOne(() => File)
|
|
|
|
|
-@JoinColumn({ name: 'file_id' })
|
|
|
|
|
-file!: File;
|
|
|
|
|
|
|
+// Schema验证(使用z.coerce.number()处理字符串)
|
|
|
|
|
+const CreateSchema = z.object({
|
|
|
|
|
+ totalAmount: z.coerce.number().min(0),
|
|
|
|
|
+});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 7.2 循环依赖处理
|
|
|
|
|
|
|
+## 8. 错误处理规范
|
|
|
|
|
+
|
|
|
|
|
+### 8.1 标准错误响应格式
|
|
|
|
|
+
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 原代码(有循环依赖):
|
|
|
|
|
-// @ManyToOne(() => DisabledPerson)
|
|
|
|
|
-// person!: DisabledPerson;
|
|
|
|
|
|
|
+// 正确的错误响应格式
|
|
|
|
|
+return c.json({
|
|
|
|
|
+ code: 400,
|
|
|
|
|
+ message: '渠道名称已存在'
|
|
|
|
|
+}, 400);
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-// 新代码(解耦):
|
|
|
|
|
-@Column({
|
|
|
|
|
- name: 'person_id',
|
|
|
|
|
- type: 'int',
|
|
|
|
|
- nullable: false,
|
|
|
|
|
- comment: '人员ID'
|
|
|
|
|
-})
|
|
|
|
|
-personId!: number;
|
|
|
|
|
|
|
+### 8.2 HTTP状态码使用
|
|
|
|
|
+
|
|
|
|
|
+| 状态码 | 使用场景 |
|
|
|
|
|
+|--------|----------|
|
|
|
|
|
+| 200 | 操作成功(包括删除) |
|
|
|
|
|
+| 201 | 创建成功 |
|
|
|
|
|
+| 400 | 请求参数错误 |
|
|
|
|
|
+| 401 | 未授权 |
|
|
|
|
|
+| 404 | 资源不存在 |
|
|
|
|
|
+| 500 | 服务器内部错误 |
|
|
|
|
|
+
|
|
|
|
|
+### 8.3 DELETE操作响应
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// DELETE成功应返回200,而不是404
|
|
|
|
|
+app.delete('/:id', async (c) => {
|
|
|
|
|
+ const result = await service.delete(id);
|
|
|
|
|
+ return c.json({ success: true }, 200); // ✅ 正确
|
|
|
|
|
+ // return c.json({ success: true }, 404); // ❌ 错误
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 9. 包配置规范
|
|
|
|
|
+
|
|
|
|
|
+### 9.1 package.json
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "name": "@d8d/allin-channel-module",
|
|
|
|
|
+ "version": "1.0.0",
|
|
|
|
|
+ "type": "module",
|
|
|
|
|
+ "main": "src/index.ts",
|
|
|
|
|
+ "types": "src/index.ts",
|
|
|
|
|
+ "scripts": {
|
|
|
|
|
+ "test": "vitest run",
|
|
|
|
|
+ "test:coverage": "vitest run --coverage",
|
|
|
|
|
+ "typecheck": "tsc --noEmit"
|
|
|
|
|
+ },
|
|
|
|
|
+ "dependencies": {
|
|
|
|
|
+ "@d8d/shared-crud": "workspace:*",
|
|
|
|
|
+ "@d8d/shared-types": "workspace:*",
|
|
|
|
|
+ "@d8d/shared-utils": "workspace:*",
|
|
|
|
|
+ "typeorm": "^0.3.20"
|
|
|
|
|
+ },
|
|
|
|
|
+ "devDependencies": {
|
|
|
|
|
+ "@hono/zod-openapi": "latest",
|
|
|
|
|
+ "vitest": "latest"
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 7.3 依赖配置
|
|
|
|
|
|
|
+### 9.2 依赖配置
|
|
|
|
|
+
|
|
|
```json
|
|
```json
|
|
|
{
|
|
{
|
|
|
- "name": "@d8d/allin-company-module",
|
|
|
|
|
"dependencies": {
|
|
"dependencies": {
|
|
|
"@d8d/allin-platform-module": "workspace:*",
|
|
"@d8d/allin-platform-module": "workspace:*",
|
|
|
"@d8d/file-module": "workspace:*",
|
|
"@d8d/file-module": "workspace:*",
|
|
@@ -339,62 +539,56 @@ personId!: number;
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 7.4 基础包设计
|
|
|
|
|
-- **基础包**(如platform-module):不需要依赖其他allin模块
|
|
|
|
|
-- **业务包**(如company-module):可以依赖基础包和其他业务包
|
|
|
|
|
-- **工具包**(如enums包):提供共享常量,被其他模块依赖
|
|
|
|
|
|
|
+## 10. 测试规范
|
|
|
|
|
|
|
|
-## 8. 测试规范
|
|
|
|
|
|
|
+详细的测试规范请参考 [后端模块包测试规范](./backend-module-testing-standards.md)。
|
|
|
|
|
|
|
|
-### 8.1 测试数据完整性
|
|
|
|
|
-```typescript
|
|
|
|
|
-// 确保测试数据包含所有必填字段
|
|
|
|
|
-const testData = {
|
|
|
|
|
- channelName: '测试渠道',
|
|
|
|
|
- contactPerson: '测试联系人',
|
|
|
|
|
- contactPhone: '13800138000',
|
|
|
|
|
- status: 1,
|
|
|
|
|
- // 不要遗漏任何必填字段
|
|
|
|
|
-};
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### 10.1 测试数据工厂
|
|
|
|
|
|
|
|
-### 8.2 唯一性约束测试
|
|
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 避免测试数据违反唯一性约束
|
|
|
|
|
-test('创建重复名称的渠道应失败', async () => {
|
|
|
|
|
- // 使用不同的测试数据避免冲突
|
|
|
|
|
- const data1 = { channelName: '渠道A', ...otherFields };
|
|
|
|
|
- const data2 = { channelName: '渠道B', ...otherFields }; // 使用不同的名称
|
|
|
|
|
-
|
|
|
|
|
- await service.create(data1);
|
|
|
|
|
- await expect(service.create(data1)).rejects.toThrow(); // 第二次创建应失败
|
|
|
|
|
-});
|
|
|
|
|
|
|
+export class TestDataFactory {
|
|
|
|
|
+ static createChannelData(overrides: Partial<Channel> = {}): Partial<Channel> {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ return {
|
|
|
|
|
+ channelName: `测试渠道_${timestamp}`,
|
|
|
|
|
+ channelType: '测试类型',
|
|
|
|
|
+ contactPerson: '测试联系人',
|
|
|
|
|
+ contactPhone: '13800138000',
|
|
|
|
|
+ status: 1,
|
|
|
|
|
+ ...overrides
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static async createTestChannel(
|
|
|
|
|
+ dataSource: DataSource,
|
|
|
|
|
+ overrides: Partial<Channel> = {}
|
|
|
|
|
+ ): Promise<Channel> {
|
|
|
|
|
+ const channelData = this.createChannelData(overrides);
|
|
|
|
|
+ const channelRepo = dataSource.getRepository(Channel);
|
|
|
|
|
+ const channel = channelRepo.create(channelData);
|
|
|
|
|
+ return await channelRepo.save(channel);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 8.3 调试信息
|
|
|
|
|
|
|
+### 10.2 测试配置
|
|
|
|
|
+
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 在关键路由中添加调试信息
|
|
|
|
|
-app.post('/create', async (c) => {
|
|
|
|
|
- const body = await c.req.json();
|
|
|
|
|
- console.debug('创建请求:', body);
|
|
|
|
|
- // ...处理逻辑
|
|
|
|
|
|
|
+// vitest.config.ts
|
|
|
|
|
+export default defineConfig({
|
|
|
|
|
+ test: {
|
|
|
|
|
+ globals: true,
|
|
|
|
|
+ environment: 'node',
|
|
|
|
|
+ include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
|
|
|
+ fileParallelism: false // 避免数据库连接冲突
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 8.4 测试覆盖率标准
|
|
|
|
|
-- **单元测试**:核心业务逻辑 > 80%
|
|
|
|
|
-- **集成测试**:API端点覆盖 ≥ 60%
|
|
|
|
|
-- **错误场景**:覆盖各种错误场景和边界条件
|
|
|
|
|
-
|
|
|
|
|
-## 9. 开发流程规范
|
|
|
|
|
|
|
+## 11. 开发流程
|
|
|
|
|
|
|
|
-### 9.1 workspace配置
|
|
|
|
|
-```bash
|
|
|
|
|
-# 创建新包后,确保在pnpm-workspace.yaml中添加
|
|
|
|
|
-# 或在根目录配置'allin-packages/*'自动包含
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### 11.1 类型检查
|
|
|
|
|
|
|
|
-### 9.2 类型检查
|
|
|
|
|
```bash
|
|
```bash
|
|
|
# 开发过程中运行类型检查
|
|
# 开发过程中运行类型检查
|
|
|
pnpm typecheck
|
|
pnpm typecheck
|
|
@@ -403,100 +597,86 @@ pnpm typecheck
|
|
|
cd allin-packages/channel-module && pnpm typecheck
|
|
cd allin-packages/channel-module && pnpm typecheck
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 9.3 包配置优化
|
|
|
|
|
-```json
|
|
|
|
|
-{
|
|
|
|
|
- "name": "@d8d/allin-enums",
|
|
|
|
|
- "type": "module",
|
|
|
|
|
- "main": "src/index.ts", # workspace中直接引用源码
|
|
|
|
|
- "types": "src/index.ts", # 类型定义直接使用源码
|
|
|
|
|
- "scripts": {
|
|
|
|
|
- "test": "vitest run",
|
|
|
|
|
- "typecheck": "tsc --noEmit"
|
|
|
|
|
- # 不需要"build"脚本(workspace中直接引用源码)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+### 11.2 运行测试
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 进入模块目录
|
|
|
|
|
+cd allin-packages/channel-module
|
|
|
|
|
+
|
|
|
|
|
+# 运行测试
|
|
|
|
|
+pnpm test
|
|
|
|
|
+
|
|
|
|
|
+# 运行集成测试
|
|
|
|
|
+pnpm test:integration
|
|
|
|
|
+
|
|
|
|
|
+# 生成覆盖率报告
|
|
|
|
|
+pnpm test:coverage
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 9.4 注释规范
|
|
|
|
|
|
|
+### 11.3 注释规范
|
|
|
|
|
+
|
|
|
```typescript
|
|
```typescript
|
|
|
/**
|
|
/**
|
|
|
- * 订单状态枚举
|
|
|
|
|
- * - draft: 草稿状态,可编辑
|
|
|
|
|
- * - confirmed: 已确认,不可编辑
|
|
|
|
|
- * - in_progress: 进行中,有工作人员处理
|
|
|
|
|
- * - completed: 已完成,可归档
|
|
|
|
|
- * - cancelled: 已取消,用户主动取消
|
|
|
|
|
|
|
+ * 创建渠道 - 覆盖父类方法,添加名称唯一性检查
|
|
|
|
|
+ * @param data 渠道数据
|
|
|
|
|
+ * @param userId 操作用户ID
|
|
|
|
|
+ * @returns 创建的渠道
|
|
|
|
|
+ * @throws Error 当渠道名称已存在时
|
|
|
*/
|
|
*/
|
|
|
-enum OrderStatus {
|
|
|
|
|
- DRAFT = 'draft',
|
|
|
|
|
- CONFIRMED = 'confirmed',
|
|
|
|
|
- IN_PROGRESS = 'in_progress',
|
|
|
|
|
- COMPLETED = 'completed',
|
|
|
|
|
- CANCELLED = 'cancelled'
|
|
|
|
|
|
|
+override async create(data: Partial<Channel>, userId?: string | number): Promise<Channel> {
|
|
|
|
|
+ // ...
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 10. 错误处理规范
|
|
|
|
|
|
|
+## 12. 参考实现
|
|
|
|
|
|
|
|
-### 10.1 HTTP状态码
|
|
|
|
|
-- `200 OK`:操作成功
|
|
|
|
|
-- `201 Created`:创建成功
|
|
|
|
|
-- `400 Bad Request`:请求参数错误
|
|
|
|
|
-- `404 Not Found`:资源不存在
|
|
|
|
|
-- `409 Conflict`:资源冲突(如唯一性冲突)
|
|
|
|
|
-- `500 Internal Server Error`:服务器内部错误
|
|
|
|
|
|
|
+### 12.1 已完成模块
|
|
|
|
|
|
|
|
-### 10.2 错误响应格式
|
|
|
|
|
-```json
|
|
|
|
|
-{
|
|
|
|
|
- "success": false,
|
|
|
|
|
- "message": "渠道名称已存在",
|
|
|
|
|
- "code": "CHANNEL_NAME_EXISTS",
|
|
|
|
|
- "timestamp": "2025-12-02T10:30:00Z"
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+**allin-packages**:
|
|
|
|
|
+- `channel-module`: 基础CRUD模块
|
|
|
|
|
+- `platform-module`: 基础依赖包
|
|
|
|
|
+- `company-module`: 模块间依赖
|
|
|
|
|
+- `disability-module`: 文件模块集成
|
|
|
|
|
|
|
|
-### 10.3 DELETE操作响应
|
|
|
|
|
-```typescript
|
|
|
|
|
-// DELETE成功应返回200,而不是404
|
|
|
|
|
-app.delete('/:id', async (c) => {
|
|
|
|
|
- const result = await service.delete(id);
|
|
|
|
|
- return c.json({ success: true }, 200); // ✅ 正确
|
|
|
|
|
- // return c.json({ success: true }, 404); // ❌ 错误
|
|
|
|
|
-});
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-## 11. 参考实现
|
|
|
|
|
|
|
+**core-module**:
|
|
|
|
|
+- `auth-module`: 认证模块
|
|
|
|
|
+- `user-module`: 用户模块
|
|
|
|
|
+- `file-module`: 文件模块
|
|
|
|
|
|
|
|
-### 11.1 已完成模块参考
|
|
|
|
|
-- **channel-module** (007.001):基础CRUD模块实现
|
|
|
|
|
-- **company-module** (007.002):模块间依赖处理
|
|
|
|
|
-- **platform-module** (007.006):基础依赖包设计
|
|
|
|
|
-- **disability-module** (007.004):文件模块集成
|
|
|
|
|
-- **salary-module** (007.007):外部包集成(geo-areas)
|
|
|
|
|
|
|
+### 12.2 最佳实践
|
|
|
|
|
|
|
|
-### 11.2 最佳实践总结
|
|
|
|
|
-1. **保持API兼容性**:移植时保持原始API功能
|
|
|
|
|
-2. **统一错误处理**:提供明确的错误信息
|
|
|
|
|
-3. **避免循环依赖**:使用ID引用解耦模块
|
|
|
|
|
-4. **完整测试覆盖**:覆盖成功和失败场景
|
|
|
|
|
-5. **及时类型检查**:开发过程中持续运行类型检查
|
|
|
|
|
|
|
+1. **使用 `override` 关键字**: 明确标识覆盖父类方法
|
|
|
|
|
+2. **完整的列定义**: 包含 `type`, `comment`, `nullable` 等属性
|
|
|
|
|
+3. **OpenAPI文档**: 使用 `.openapi()` 装饰器添加文档
|
|
|
|
|
+4. **类型推断导出**: 导出 `z.infer` 推断的类型
|
|
|
|
|
+5. **软删除实现**: 使用 `status` 字段而非物理删除
|
|
|
|
|
+6. **时间戳管理**: 自动设置 `createTime` 和 `updateTime`
|
|
|
|
|
+7. **错误处理**: 提供明确的错误消息
|
|
|
|
|
|
|
|
## 附录:检查清单
|
|
## 附录:检查清单
|
|
|
|
|
|
|
|
### 新模块包创建检查清单
|
|
### 新模块包创建检查清单
|
|
|
-- [ ] 包名符合规范:`@d8d/allin-{name}-module`
|
|
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 包名符合规范:`@d8d/allin-{name}-module` 或 `@d8d/core-module`
|
|
|
- [ ] 目录结构完整:entities, services, routes, schemas, tests
|
|
- [ ] 目录结构完整:entities, services, routes, schemas, tests
|
|
|
-- [ ] workspace配置:在pnpm-workspace.yaml中添加或配置通配符
|
|
|
|
|
-- [ ] 实体主键命名为`id`
|
|
|
|
|
-- [ ] 字段命名正确转换(下划线→驼峰)
|
|
|
|
|
-- [ ] 唯一性约束正确配置
|
|
|
|
|
-- [ ] 服务层继承GenericCrudService
|
|
|
|
|
-- [ ] 路由聚合模式正确
|
|
|
|
|
-- [ ] Schema验证完整
|
|
|
|
|
-- [ ] 测试数据完整且不违反约束
|
|
|
|
|
|
|
+- [ ] Entity包含完整列定义:type, comment, nullable等
|
|
|
|
|
+- [ ] Service使用 `override` 关键字
|
|
|
|
|
+- [ ] 软删除实现:使用 `status` 字段
|
|
|
|
|
+- [ ] Schema使用 `.openapi()` 装饰器
|
|
|
|
|
+- [ ] 导出类型推断:`export type * = z.infer<typeof *Schema>`
|
|
|
|
|
+- [ ] 路由使用 `OpenAPIHono` 和 `AuthContext`
|
|
|
|
|
+- [ ] 测试数据工厂使用时间戳保证唯一性
|
|
|
|
|
+- [ ] vitest.config.ts 设置 `fileParallelism: false`
|
|
|
- [ ] 类型检查通过
|
|
- [ ] 类型检查通过
|
|
|
- [ ] 所有测试通过
|
|
- [ ] 所有测试通过
|
|
|
-- [ ] 错误处理规范
|
|
|
|
|
-- [ ] 注释完整清晰
|
|
|
|
|
|
|
+
|
|
|
|
|
+## 相关文档
|
|
|
|
|
+
|
|
|
|
|
+- [后端模块包测试规范](./backend-module-testing-standards.md)
|
|
|
|
|
+- [测试策略概述](./testing-strategy.md)
|
|
|
|
|
+- [编码标准](./coding-standards.md)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**文档状态**: 正式版
|
|
|
|
|
+**基于实际实现**: 2025-12-26
|