将allin_system-master/server/src目录下有对应数据库实体的模块移植到packages目录下,作为非多租户独立包,遵循项目现有的独立包结构和命名规范,实现模块的现代化重构和标准化管理。
当前相关功能:allin_system-master是一个基于NestJS的后端系统,根据allin_2025-11-25.sql分析,有以下有实体的模块:
无实体模块(本次不移植):admin、auth、users模块
技术栈:TypeScript、NestJS、PostgreSQL、Redis、MinIO
集成点:模块间通过NestJS模块系统集成,共享数据库连接和配置
根据对每个模块module.ts文件的分析,依赖关系如下:
channel_info模块 (channel.module.ts)
@nestjs/common, @nestjs/typeormChannel (对应channel_info表)company模块 (company.module.ts)
@nestjs/common, @nestjs/typeorm, PlatformModuleCompany (对应employer_company表)platformdict_management模块 (dict.module.ts)
@nestjs/common, @nestjs/typeormDict (对应sys_dict表)disability_person模块 (disability_person.module.ts)
@nestjs/common, @nestjs/typeormDisabledPerson, DisabledBankCard, DisabledPhoto, DisabledRemark, DisabledVisitEmploymentOrder, OrderPerson (order模块), Platform (platform模块), Company (company模块), Channel (channel_info模块)order模块 (order.module.ts)
@nestjs/common, @nestjs/typeormEmploymentOrder, OrderPerson, OrderPersonAssetDisabledPerson (disability_person模块), Platform (platform模块), Company (company模块), Channel (channel_info模块)platform模块 (platform.module.ts)
@nestjs/common, @nestjs/typeormPlatform (对应employer_platform表)company模块依赖此模块salary模块 (salary.module.ts)
@nestjs/common, @nestjs/typeormSalaryLevel (对应salary_level表)platform ───┐
↓
company ────┐
↓
channel_info ─┐
↓ ↓
order ───────┘
↑
disability_person
↑
dict_management (独立)
salary (独立)
关键发现:
platform模块是基础依赖,被company模块直接依赖company、channel_info模块被order和disability_person模块引用order和disability_person模块相互引用实体,形成循环依赖dict_management和salary模块完全独立基于现有项目命名规范,并考虑这是Allin系统专属模块,制定以下映射方案:
| 原模块名 | 独立包名 | 目录名 | 说明 |
|---|---|---|---|
| channel_info | @d8d/allin-channel-module |
channel-module |
渠道管理模块 |
| company | @d8d/allin-company-module |
company-module |
公司管理模块,依赖platform-module |
| dict_management | @d8d/allin-dict-module |
dict-module |
字典管理模块 |
| disability_person | @d8d/allin-disability-module |
disability-module |
残疾人管理模块(简化名称) |
| order | @d8d/allin-order-module |
order-module |
订单管理模块 |
| platform | @d8d/allin-platform-module |
platform-module |
平台管理模块 |
| salary | @d8d/allin-salary-module |
salary-module |
薪资管理模块 |
命名原则:
@d8d/allin-前缀,明确表明是Allin系统专属包{模块名}-module格式,便于在allin-packages目录中管理-module后缀-mt后缀(本次移植为非多租户版本)由于这些模块是allin_system-master项目独有的业务模块(非通用模块),将在项目根目录创建独立目录:
项目根目录/
├── allin-packages/ # Allin系统专属包目录
│ ├── channel-module/ # 渠道管理模块
│ ├── company-module/ # 公司管理模块
│ ├── dict-module/ # 字典管理模块
│ ├── disability-module/ # 残疾人管理模块
│ ├── order-module/ # 订单管理模块
│ ├── platform-module/ # 平台管理模块
│ └── salary-module/ # 薪资管理模块
├── packages/ # 现有通用包目录(保持不变)
└── allin_system-master/ # 原始代码目录(移植源)
目录命名说明:allin-packages清晰表明这是Allin系统专属的包目录,与allin_system-master源目录对应,区别于通用的packages目录。
参考现有auth-module结构,每个独立包应包含:
allin-packages/{module-name}/
├── package.json # 包配置,workspace依赖管理
├── tsconfig.json # TypeScript配置
├── vitest.config.ts # 测试配置(如需要)
├── src/
│ ├── index.ts # 主导出文件
│ ├── schemas/ # Zod模式定义(如需要)
│ │ └── index.ts
│ ├── entities/ # 数据库实体
│ ├── services/ # 业务服务
│ ├── controllers/ # API控制器
│ ├── dtos/ # 数据传输对象
│ └── types/ # TypeScript类型定义
└── dist/ # 构建输出
关键配置要求:
package.json中设置"type": "module"src/index.ts"@d8d/core-module": "workspace:*"@d8d/allin-前缀,如@d8d/allin-channel-module| 方面 | 当前项目(目标) | allin_system-master(源) | 差异程度 |
|---|---|---|---|
| Web框架 | Hono(轻量级) | NestJS(重量级) | 重大 |
| 路由系统 | Hono路由 | NestJS装饰器路由 | 重大 |
| 验证库 | Zod + @hono/zod-openapi | class-validator + class-transformer | 中等 |
| 服务架构 | GenericCrudService继承 | 自定义Service类 | 中等 |
| 实体配置 | 详细TypeORM装饰器 | 简单TypeORM装饰器 | 轻微 |
| 命名规范 | 驼峰命名(channelId) | 下划线命名(channel_id) | 轻微 |
| 模块系统 | npm包 + workspace | NestJS模块系统 | 重大 |
| API风格 | RESTful + OpenAPI | 自定义端点 | 中等 |
@d8d/core-module、@d8d/shared-crud、@d8d/shared-utils、TypeORM、Hono、Zod@nestjs/*、TypeORM、class-validator、Passport/JWT// 原allin实体(下划线命名)
@Entity('channel_info')
export class Channel {
@PrimaryGeneratedColumn('increment')
channel_id: number; // 下划线
@Column()
channel_name: string;
}
// 转换后实体(驼峰命名,详细配置)
@Entity('channel_info')
export class Channel {
@PrimaryGeneratedColumn({ name: 'channel_id', type: 'int' })
channelId!: number; // 驼峰
@Column({ name: 'channel_name', type: 'varchar', length: 100 })
channelName!: string;
}
// 原allin服务(NestJS风格)
@Injectable()
export class ChannelService {
constructor(@InjectRepository(Channel) private repo: Repository<Channel>) {}
async createChannel(data: CreateChannelDto): Promise<boolean> {
// 自定义逻辑
}
}
// 转换后服务(Hono + GenericCrudService)
export class ChannelService extends GenericCrudService<Channel> {
constructor(dataSource: DataSource) {
super(dataSource, Channel, {
searchFields: ['channelName', 'contactPerson'],
});
}
// 保留特殊业务方法
async createChannel(data: CreateChannelDto): Promise<boolean> {
// 复用或重写逻辑
}
}
// 原NestJS控制器
@Controller('channel')
@UseGuards(JwtAuthGuard)
export class ChannelController {
@Post('createChannel')
createChannel(@Body() req: CreateChannelDto) {
return this.service.createChannel(req);
}
}
// 转换后Hono路由
import { OpenAPIHono } from '@hono/zod-openapi';
const channelRoutes = new OpenAPIHono()
.post('/createChannel', async (c) => {
const data = await c.req.json();
const result = await channelService.createChannel(data);
return c.json({ success: result });
});
// 原class-validator DTO
export class CreateChannelDto {
@IsString()
channel_name: string;
}
// 转换后Zod Schema
export const CreateChannelSchema = z.object({
channelName: z.string().min(1).max(100),
});
export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
针对发现的循环依赖问题(order ↔ disability_person),解决方案:
建议移植顺序:
allin-platform-module(基础)allin-channel-module、allin-dict-module、allin-salary-module(独立)allin-company-module(依赖platform)allin-order-module和allin-disability-module(最后处理,解决循环依赖)注:每个故事对应一个模块的完整移植,包括技术栈转换和API集成测试。
目标:将channel_info模块移植为独立包,完成技术栈转换并验证功能完整性
验收标准:
allin-packages/channel-module目录结构Channel实体从下划线命名转换为驼峰命名@d8d/allin-channel-module包名,workspace依赖API集成测试要求:
tests/integration/channel.integration.test.ts目标:将company模块移植为独立包,处理对platform-module的依赖
验收标准:
allin-packages/company-module目录结构Company实体转换@d8d/allin-platform-module的依赖API集成测试要求:
tests/integration/company.integration.test.ts目标:将dict_management模块移植为独立包,验证独立模块移植模式
验收标准:
allin-packages/dict-module目录结构Dict实体转换API集成测试要求:
tests/integration/dict.integration.test.ts目标:移植最复杂的模块,处理5个实体和跨模块依赖,完成文件实体集成
验收标准:
allin-packages/disability-module目录结构DisabledPhoto实体,添加fileId字段引用File实体fileId参数@d8d/file-module的依赖API集成测试要求:
tests/integration/disability.integration.test.tsfileId参数接收和处理目标:移植订单模块,处理与disability-module的循环依赖,完成文件实体集成
验收标准:
allin-packages/order-module目录结构OrderPersonAsset实体,添加fileId字段引用File实体fileId参数@d8d/file-module的依赖API集成测试要求:
tests/integration/order.integration.test.ts目标:移植基础模块,作为其他模块的依赖
验收标准:
allin-packages/platform-module目录结构Platform实体转换API集成测试要求:
tests/integration/platform.integration.test.ts目标:移植独立模块,完成所有模块移植
验收标准:
allin-packages/salary-module目录结构SalaryLevel实体转换API集成测试要求:
tests/integration/salary.integration.test.tsFile(对应files表)fullUrl属性)id, name, type, size, path, description, upload_user_id, upload_time等DisabledPhoto(disabled_photo表):残疾人照片管理
photo_id, person_id, photo_type, photo_url, upload_time, can_downloadphoto_url字段OrderPersonAsset(order_person_asset表):订单人员资产管理
op_id, order_id, person_id, asset_type, asset_file_type, asset_url, related_timeasset_url字段核心思想:文件上传在UI层通过文件选择器组件统一处理,后端只负责文件ID的关联管理
架构分层:
file_id引用File实体数据库迁移:
-- 1. 添加file_id字段
ALTER TABLE disabled_photo ADD COLUMN file_id INT;
ALTER TABLE order_person_asset ADD COLUMN file_id INT;
-- 2. 建立外键关系
ALTER TABLE disabled_photo ADD FOREIGN KEY (file_id) REFERENCES files(id);
ALTER TABLE order_person_asset ADD FOREIGN KEY (file_id) REFERENCES files(id);
-- 3. 可选:迁移现有URL数据到files表
-- 根据现有photo_url/asset_url创建files记录,更新file_id
实体重构:
// 重构后的DisabledPhoto实体
@Entity('disabled_photo')
export class DisabledPhoto {
@PrimaryGeneratedColumn({ name: 'photo_id' })
photoId!: number;
@Column({ name: 'person_id' })
personId!: number;
@Column({ name: 'file_id' })
fileId!: number; // 引用File实体的ID
@ManyToOne(() => File)
@JoinColumn({ name: 'file_id' })
file!: File; // 关联File实体
// 业务字段保持不变
@Column({ name: 'photo_type' })
photoType!: string;
@Column({ name: 'can_download' })
canDownload!: number;
@CreateDateColumn({ name: 'upload_time' })
uploadTime!: Date;
// 关联残疾人实体
@ManyToOne(() => DisabledPerson, person => person.photos)
@JoinColumn({ name: 'person_id' })
person!: DisabledPerson;
}
API接口设计:
// 创建残疾人照片的API接口
// UI层已通过文件选择器上传文件,这里只接收文件ID
export const CreateDisabledPhotoSchema = z.object({
personId: z.number().int().positive(),
fileId: z.number().int().positive(), // 文件ID,由UI层提供
photoType: z.string().min(1).max(50),
canDownload: z.number().int().min(0).max(1).default(1)
});
// 路由实现
channelRoutes.post('/photos', async (c) => {
const data = await c.req.valid('json'); // 包含fileId
const photo = await disabilityService.createPhoto(data);
return c.json(photo, 201);
});
// 获取照片详情(包含文件URL)
channelRoutes.get('/photos/:id', async (c) => {
const { id } = c.req.param();
const photo = await disabilityService.getPhotoWithFile(Number(id));
// 构建响应数据,包含文件URL
const response = {
...photo,
photoUrl: await photo.file.fullUrl, // 通过关联实体获取文件URL
fileName: photo.file.name,
fileSize: photo.file.size,
fileType: photo.file.type
};
return c.json(response);
});
// 获取残疾人的所有照片(包含文件信息)
channelRoutes.get('/persons/:personId/photos', async (c) => {
const { personId } = c.req.param();
const photos = await disabilityService.getPersonPhotosWithFiles(Number(personId));
// 为每张照片添加文件URL
const photosWithUrls = await Promise.all(
photos.map(async (photo) => ({
...photo,
photoUrl: await photo.file.fullUrl,
fileName: photo.file.name,
fileSize: photo.file.size
}))
);
return c.json(photosWithUrls);
});
服务层实现:
export class DisabilityService {
async createPhoto(data: CreateDisabledPhotoDto): Promise<DisabledPhoto> {
// 验证文件是否存在
const file = await this.fileRepository.findOne({
where: { id: data.fileId }
});
if (!file) {
throw new Error('文件不存在');
}
// 创建照片记录(只关联文件ID)
const photo = this.photoRepository.create({
personId: data.personId,
fileId: data.fileId, // 使用UI层提供的文件ID
photoType: data.photoType,
canDownload: data.canDownload
});
return this.photoRepository.save(photo);
}
async getPhotoWithFile(photoId: number): Promise<DisabledPhoto> {
// 查询照片时加载关联的File实体
const photo = await this.photoRepository.findOne({
where: { photoId },
relations: ['file'] // 加载关联的File实体
});
if (!photo) {
throw new Error('照片不存在');
}
return photo;
}
async getPhotoUrl(photoId: number): Promise<string> {
const photo = await this.getPhotoWithFile(photoId);
// 通过关联的file实体访问fullUrl属性
// file.fullUrl是File实体中的Promise属性
return await photo.file.fullUrl;
}
async getPersonPhotosWithFiles(personId: number): Promise<DisabledPhoto[]> {
// 查询某个残疾人的所有照片,并加载关联的File实体
return this.photoRepository.find({
where: { personId },
relations: ['file'], // 加载关联的File实体
order: { uploadTime: 'DESC' }
});
}
}
核心思想:保持表结构不变,在服务层集成FileService
实施步骤:
disabled_photo和order_person_asset表服务层集成:
export class DisabilityService {
async getPhotoUrl(photoId: number): Promise<string> {
const photo = await this.getPhoto(photoId);
// 如果photo_url是MinIO路径,使用FileService生成预签名URL
if (this.isMinioPath(photo.photoUrl)) {
const path = this.extractMinioPath(photo.photoUrl);
return this.fileService.getPresignedUrl(path);
}
// 否则返回原始URL
return photo.photoUrl;
}
}
UI层(管理后台):
1. 用户点击"上传照片"按钮
2. 打开文件选择器组件(复用现有组件)
3. 选择文件 → 组件调用file-module上传接口
4. 获取文件ID → 填充到表单的fileId字段
5. 提交表单(包含fileId和其他业务数据)
API层(disability-module):
1. 接收包含fileId的请求
2. 验证fileId对应的文件是否存在
3. 创建DisabledPhoto记录,关联fileId
4. 返回创建结果
服务层:
1. 验证业务规则(如照片类型限制)
2. 处理文件与业务的关联逻辑
3. 返回包含文件URL的完整数据
需要调整以下故事的文件实体处理:
故事4(disability-module):
DisabledPhoto实体,添加fileId字段引用File实体fileId参数(而非文件内容)故事5(order-module):
OrderPersonAsset实体,添加fileId字段引用File实体fileId参数relations: ['file']加载关联实体,避免N+1查询file实体访问文件属性(file.fullUrl、file.name等)relations选项加载关联数据fileId对应的文件是否存在"模块分析和技术栈分析已完成,关键发现:
@d8d/allin-前缀,-module后缀,非多租户版本allin-packages/目录存放专属包新的故事拆分方案:
每个故事的关键要求:
tests/integration/{module}.integration.test.ts文件执行顺序建议:
技术栈转换关键点:
API集成测试模板参考:
参考/packages/advertisements-module/tests/integration/advertisements.integration.test.ts
testClient创建测试客户端setupIntegrationDatabaseHooksWithEntities设置测试数据库文件实体集成方案: 发现allin_system-master中有文件相关实体(DisabledPhoto、OrderPersonAsset),需要与现有file-module集成。
采用方案:实体重构 + UI层统一上传
DisabledPhoto实体,添加fileId字段引用File实体fileId参数(而非文件内容)OrderPersonAsset实体,添加fileId字段引用File实体fileId参数实施要点:
史诗应在保持系统完整性的同时实现将有实体模块从NestJS架构移植到Hono架构的标准化独立包,每个模块都要有完整的API集成测试验证,并完成与现有file-module的文件集成。"