将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-enums(枚举常量包) - 已完成allin-company-module(依赖platform) - 已完成allin-salary-module(独立)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完成情况:
Company实体,配置复合唯一索引idx_company_name_platformCompanyService继承GenericCrudService,覆盖create、update方法实现业务逻辑as any)经过对dict_management模块的深入分析,发现其存储的数据主要分为两类:
固定业务枚举数据(适合用TypeScript枚举):
disability_type(残疾类型):7种固定类型disability_level(残疾等级):4个固定等级order_status(订单状态):5种固定状态work_status(工作状态):4种固定状态动态配置数据(但已有替代方案):
@d8d/geo-areas包管理放弃移植dict_management模块,改为创建枚举常量包,原因如下:
@d8d/geo-areas包目标:创建专门供Allin系统使用的枚举常量包,替换原有的数据库字典管理
验收标准:
allin-packages/enums目录结构DisabilityType(vision, hearing, speech, physical, intellectual, mental, multiple)DisabilityLevel(1, 2, 3, 4)OrderStatus(draft, confirmed, in_progress, completed, cancelled)WorkStatus(not_working, pre_working, working, resigned)@d8d/allin-enums,workspace依赖完成情况:
allin-packages/enums目录结构sys_dict表完全一致:
DisabilityType:7种残疾类型(vision, hearing, speech, physical, intellectual, mental, multiple)DisabilityLevel:4个残疾等级(1, 2, 3, 4)OrderStatus:5种订单状态(draft, confirmed, in_progress, completed, cancelled)WorkStatus:4种工作状态(not_working, pre_working, working, resigned)DISABILITY_TYPES:残疾类型数组DISABILITY_LEVELS:残疾等级数组(过滤掉反向映射值)ORDER_STATUSES:订单状态数组WORK_STATUSES:工作状态数组DisabilityTypeLabelMap:残疾类型中文标签映射DisabilityLevelLabelMap:残疾等级中文标签映射OrderStatusLabelMap:订单状态中文标签映射WorkStatusLabelMap:工作状态中文标签映射package.json:包名@d8d/allin-enums,"type": "module",workspace依赖Object.values()返回键和值)@vitest/coverage-v8依赖,测试覆盖率100%技术实现要求:
allin-packages/enums/@d8d/allin-enumssrc/index.ts统一导出所有枚举enum或const enum定义disability_type='vision' → DisabilityType.VISION = 'vision')对后续模块的影响:
DisabilityType和DisabilityLevel枚举OrderStatus和WorkStatus枚举优势:
经过对Allin系统模块的分析,发现以下模块使用行政区划数据:
残疾人管理模块 (disability_person):
province、city、district字段(字符串类型)薪资管理模块 (salary):
province、city、district字段(字符串类型)(province, city)当前实现存在以下问题:
@d8d/geo-areas包功能重叠采用方案A:完全依赖区域包,将所有区域数据改为引用@d8d/geo-areas包中的AreaEntity。
残疾人管理模块改造:
province、city、district字符串字段provinceId、cityId、districtId字段(外键引用AreaEntity)AreaService验证区域数据的有效性和层级关系薪资管理模块改造:
province、city、district字符串字段provinceId、cityId、districtId字段(外键引用AreaEntity)(provinceId, cityId)API兼容性处理:
目标:移植最复杂的模块,处理5个实体和跨模块依赖,完成文件实体集成,使用枚举常量
验收标准:
allin-packages/disability-module目录结构@d8d/allin-enums包中的DisabilityType和DisabilityLevel枚举DisabledPhoto实体,添加fileId字段引用File实体DisabledBankCard实体,添加fileId字段引用File实体fileId参数@d8d/file-module、@d8d/allin-enums的依赖完成情况:
DisabledPerson、DisabledBankCard、DisabledPhoto、DisabledRemark、DisabledVisitfileId字段引用File实体DisabledPersonService继承GenericCrudService,AggregatedService处理聚合业务逻辑API集成测试要求:
tests/integration/disability.integration.test.tsfileId参数接收和处理目标:移植订单模块,处理与disability-module的循环依赖,完成文件实体集成
验收标准:
allin-packages/order-module目录结构@d8d/allin-enums包中的OrderStatus和WorkStatus枚举OrderPersonAsset实体,添加fileId字段引用File实体fileId参数@d8d/file-module和@d8d/allin-enums的依赖完成情况:
EmploymentOrder、OrderPerson、OrderPersonAssetOrderPersonAsset实体使用fileId字段引用File实体OrderService继承GenericCrudService,处理订单业务逻辑和文件关联personId: number代替DisabledPerson直接引用OrderTestDataFactory,减少测试代码重复API集成测试要求:
tests/integration/order.integration.test.ts目标:移植基础模块,作为其他模块的依赖
验收标准:
allin-packages/platform-module目录结构Platform实体转换API集成测试要求:
tests/integration/platform.integration.test.ts完成情况:
Platform实体从下划线命名转换为驼峰命名,添加详细TypeORM配置GenericCrudService<Platform>,覆盖create、update、delete方法,添加平台名称唯一性检查目标:移植独立模块,完成所有模块移植
验收标准:
allin-packages/salary-module目录结构SalaryLevel实体转换,添加区域ID字段@d8d/geo-areas包管理区域数据@d8d/geo-areas的依赖API集成测试要求:
tests/integration/salary.integration.test.ts(provinceId, cityId)完成情况:
SalaryLevel实体,配置复合唯一索引UQ_51cd5d2613663c210df246f183c(provinceId, cityId)SalaryService继承GenericCrudService,覆盖create、update方法实现业务逻辑,包含区域验证和唯一性检查calculateTotalSalary方法中添加字符串转换逻辑,正确处理数据库返回的decimal字符串格式basicSalary、totalSalary、allowance、insurance、housingFund字段改为z.coerce.number(),正确处理字符串到数字的转换districtId字段改为.nullable().optional(),district字段改为.nullable().optional(),处理null值目标:将所有移植的Allin模块集成到主server中,配置路由和依赖
验收标准:
API集成测试要求:
packages/server/tests/integration/allin-modules.integration.test.ts完成情况:
packages/server/package.json中添加了所有7个Allin模块的workspace依赖✅ 路由注册:在packages/server/src/index.ts中导入并注册了所有Allin模块的路由:
export const channelApiRoutes = api.route('/api/v1/channel', channelRoutes)
export const companyApiRoutes = api.route('/api/v1/company', companyRoutes)
export const disabilityApiRoutes = api.route('/api/v1/disability', disabledPersonRoutes)
export const orderApiRoutes = api.route('/api/v1/order', orderRoutes)
export const platformApiRoutes = api.route('/api/v1/platform', platformRoutes)
export const salaryApiRoutes = api.route('/api/v1/salary', salaryRoutes)
✅ 实体注册:在数据库初始化中添加了所有Allin模块的实体
✅ 集成测试:创建了allin-modules.integration.test.ts文件,包含6个模块的路由连通性测试
✅ 测试通过:所有6个集成测试全部通过,验证了API端点连通性
✅ 类型检查:集成测试文件无类型错误(项目整体依赖问题不影响Allin模块集成)
✅ 兼容性:与现有模块兼容,不影响现有功能
修复的问题:
disabilityRoutes不存在,修复为disabledPersonRoutesCompany被当作类型使用,修复为从/entities路径导入$get()需要query: {}参数,修复所有模块的测试调用getAllOrders和getAllSalaryLevels端点不存在,修复为list端点测试结果:
File(对应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参数实施要点:
新增故事8说明:
/api/v1/{module}路径前缀史诗应在保持系统完整性的同时实现将有实体模块从NestJS架构移植到Hono架构的标准化独立包,每个模块都要有完整的API集成测试验证,并完成与现有file-module的文件集成,最后将所有模块集成到packages/server中。"