generic-crud.service.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. import { DataSource, Repository, ObjectLiteral, DeepPartial, In } from 'typeorm';
  2. import { z } from '@hono/zod-openapi';
  3. import { DataPermissionOptions, validateDataPermissionOptions, PermissionError } from '../types/data-permission.types';
  4. export abstract class GenericCrudService<T extends ObjectLiteral> {
  5. protected repository: Repository<T>;
  6. private userTrackingOptions?: UserTrackingOptions;
  7. private dataPermissionOptions?: DataPermissionOptions;
  8. private tenantOptions?: TenantOptions;
  9. protected relationFields?: RelationFieldOptions;
  10. constructor(
  11. protected dataSource: DataSource,
  12. protected entity: new () => T,
  13. options?: {
  14. userTracking?: UserTrackingOptions;
  15. relationFields?: RelationFieldOptions;
  16. dataPermission?: DataPermissionOptions;
  17. tenantOptions?: TenantOptions;
  18. }
  19. ) {
  20. this.repository = this.dataSource.getRepository(entity);
  21. this.userTrackingOptions = options?.userTracking;
  22. this.relationFields = options?.relationFields;
  23. // 验证并设置数据权限配置
  24. if (options?.dataPermission) {
  25. validateDataPermissionOptions(options.dataPermission);
  26. this.dataPermissionOptions = options.dataPermission;
  27. }
  28. // 设置租户选项
  29. this.tenantOptions = options?.tenantOptions;
  30. }
  31. /**
  32. * 获取分页列表
  33. */
  34. async getList(
  35. page: number = 1,
  36. pageSize: number = 10,
  37. keyword?: string,
  38. searchFields?: string[],
  39. where?: Partial<T>,
  40. relations: string[] = [],
  41. order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
  42. filters?: {
  43. [key: string]: any;
  44. },
  45. userId?: string | number
  46. ): Promise<[T[], number]> {
  47. const skip = (page - 1) * pageSize;
  48. const query = this.repository.createQueryBuilder('entity');
  49. // 添加数据权限过滤
  50. if (this.dataPermissionOptions?.enabled && userId) {
  51. const userIdField = this.dataPermissionOptions.userIdField;
  52. query.andWhere(`entity.${userIdField} = :userId`, { userId });
  53. }
  54. // 添加租户隔离过滤
  55. if (this.tenantOptions?.enabled) {
  56. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  57. const tenantId = await this.extractTenantId(userId);
  58. if (tenantId !== undefined && tenantId !== null) {
  59. query.andWhere(`entity.${tenantIdField} = :tenantId`, { tenantId });
  60. }
  61. }
  62. // 添加关联关系(支持嵌套关联,如 ['contract.client'])
  63. // 使用一致的别名生成策略,确保搜索时能正确引用关联字段
  64. if (relations.length > 0) {
  65. relations.forEach((relation) => {
  66. const parts = relation.split('.');
  67. let currentAlias = 'entity';
  68. parts.forEach((part, index) => {
  69. // 生成一致的别名:对于嵌套关联,使用下划线连接路径
  70. const newAlias = index === 0 ? part : parts.slice(0, index + 1).join('_');
  71. query.leftJoinAndSelect(`${currentAlias}.${part}`, newAlias);
  72. currentAlias = newAlias;
  73. });
  74. });
  75. }
  76. // 关键词搜索 - 支持关联字段搜索(格式:relation.field 或 relation.nestedRelation.field)
  77. if (keyword && searchFields && searchFields.length > 0) {
  78. const searchConditions: string[] = [];
  79. const searchParams: Record<string, string> = { keyword: `%${keyword}%` };
  80. searchFields.forEach((field) => {
  81. // 检查是否为关联字段(包含点号)
  82. if (field.includes('.')) {
  83. const parts = field.split('.');
  84. const alias = parts.slice(0, -1).join('_'); // 使用下划线连接关系路径作为别名
  85. const fieldName = parts[parts.length - 1];
  86. searchConditions.push(`${alias}.${fieldName} LIKE :keyword`);
  87. } else {
  88. // 普通字段搜索
  89. searchConditions.push(`entity.${field} LIKE :keyword`);
  90. }
  91. });
  92. if (searchConditions.length > 0) {
  93. query.andWhere(`(${searchConditions.join(' OR ')})`, searchParams);
  94. }
  95. }
  96. // 条件查询
  97. if (where) {
  98. Object.entries(where).forEach(([key, value]) => {
  99. if (value !== undefined && value !== null) {
  100. query.andWhere(`entity.${key} = :${key}`, { [key]: value });
  101. }
  102. });
  103. }
  104. // 扩展筛选条件
  105. if (filters) {
  106. Object.entries(filters).forEach(([key, value]) => {
  107. if (value !== undefined && value !== null && value !== '') {
  108. const fieldName = key.startsWith('_') ? key.substring(1) : key;
  109. // 检查是否为关联字段(包含点号)
  110. let tableAlias: string = 'entity';
  111. let actualFieldName: string = fieldName;
  112. if (fieldName.includes('.')) {
  113. const parts = fieldName.split('.');
  114. tableAlias = parts.slice(0, -1).join('_') || 'entity'; // 使用下划线连接关系路径作为别名
  115. actualFieldName = parts[parts.length - 1] || fieldName;
  116. }
  117. // 支持不同类型的筛选
  118. if (Array.isArray(value)) {
  119. // 数组类型:IN查询
  120. if (value.length > 0) {
  121. query.andWhere(`${tableAlias}.${actualFieldName} IN (:...${key})`, { [key]: value });
  122. }
  123. } else if (typeof value === 'string' && value.includes('%')) {
  124. // 模糊匹配
  125. query.andWhere(`${tableAlias}.${actualFieldName} LIKE :${key}`, { [key]: value });
  126. } else if (typeof value === 'object' && value !== null) {
  127. // 范围查询
  128. if ('gte' in value) {
  129. query.andWhere(`${tableAlias}.${actualFieldName} >= :${key}_gte`, { [`${key}_gte`]: value.gte });
  130. }
  131. if ('gt' in value) {
  132. query.andWhere(`${tableAlias}.${actualFieldName} > :${key}_gt`, { [`${key}_gt`]: value.gt });
  133. }
  134. if ('lte' in value) {
  135. query.andWhere(`${tableAlias}.${actualFieldName} <= :${key}_lte`, { [`${key}_lte`]: value.lte });
  136. }
  137. if ('lt' in value) {
  138. query.andWhere(`${tableAlias}.${actualFieldName} < :${key}_lt`, { [`${key}_lt`]: value.lt });
  139. }
  140. if ('between' in value && Array.isArray(value.between) && value.between.length === 2) {
  141. query.andWhere(`${tableAlias}.${actualFieldName} BETWEEN :${key}_start AND :${key}_end`, {
  142. [`${key}_start`]: value.between[0],
  143. [`${key}_end`]: value.between[1]
  144. });
  145. }
  146. } else {
  147. // 精确匹配
  148. query.andWhere(`${tableAlias}.${actualFieldName} = :${key}`, { [key]: value });
  149. }
  150. }
  151. });
  152. }
  153. // 排序
  154. Object.entries(order).forEach(([key, direction]) => {
  155. query.orderBy(`entity.${key}`, direction);
  156. });
  157. const finalQuery = query.skip(skip).take(pageSize);
  158. // console.log(finalQuery.getSql())
  159. return finalQuery.getManyAndCount();
  160. }
  161. /**
  162. * 根据ID获取单个实体
  163. */
  164. async getById(id: number, relations: string[] = [], userId?: string | number): Promise<T | null> {
  165. const entity = await this.repository.findOne({
  166. where: { id } as any,
  167. relations
  168. });
  169. // 数据权限验证
  170. if (entity && this.dataPermissionOptions?.enabled && userId) {
  171. const hasPermission = await this.checkPermission(entity, userId);
  172. if (!hasPermission) {
  173. throw new PermissionError('没有权限访问该资源');
  174. }
  175. }
  176. // 租户隔离验证
  177. if (entity && this.tenantOptions?.enabled) {
  178. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  179. const tenantId = await this.extractTenantId(userId);
  180. if (tenantId !== undefined && tenantId !== null) {
  181. const entityTenantId = (entity as any)[tenantIdField];
  182. if (entityTenantId !== tenantId) {
  183. return null; // 不属于当前租户,返回null
  184. }
  185. }
  186. }
  187. return entity;
  188. }
  189. /**
  190. * 检查用户对实体的权限
  191. */
  192. private async checkPermission(entity: any, userId: string | number): Promise<boolean> {
  193. const options = this.dataPermissionOptions;
  194. if (!options?.enabled) return true;
  195. // 管理员权限覆盖检查
  196. if (options.adminOverride?.enabled && options.adminOverride.adminRole) {
  197. // 这里需要从认证系统获取用户角色信息
  198. // 暂时假设管理员可以访问所有数据
  199. // 实际实现中需要集成用户角色检查
  200. const isAdmin = await this.checkAdminRole(userId, options.adminOverride.adminRole);
  201. if (isAdmin) {
  202. return true;
  203. }
  204. }
  205. // 自定义权限验证器
  206. if (options.customValidator) {
  207. return await options.customValidator(userId, entity);
  208. }
  209. // 基础权限验证:用户ID字段匹配
  210. const userIdField = options.userIdField;
  211. const entityUserId = entity[userIdField];
  212. return entityUserId === userId;
  213. }
  214. /**
  215. * 检查用户是否为管理员
  216. * TODO: 需要集成实际的用户角色检查
  217. */
  218. private async checkAdminRole(userId: string | number, adminRole: string): Promise<boolean> {
  219. // 这里需要从认证系统获取用户角色信息
  220. // 暂时返回false,实际实现中需要集成用户角色检查
  221. return false;
  222. }
  223. /**
  224. * 提取租户ID
  225. * 从用户对象或认证上下文中提取租户ID
  226. */
  227. private async extractTenantId(userId?: string | number): Promise<string | number | undefined> {
  228. // 首先检查是否有存储的租户上下文
  229. if ((this as any)._tenantId !== undefined) {
  230. console.debug('从存储的租户上下文中获取租户ID:', (this as any)._tenantId);
  231. return (this as any)._tenantId;
  232. }
  233. // 如果租户选项启用了从上下文自动提取,则从上下文获取
  234. if (this.tenantOptions?.autoExtractFromContext) {
  235. // 这里需要从Hono上下文中获取租户ID
  236. // 在实际实现中,认证中间件应该设置租户上下文
  237. // 暂时返回undefined,实际实现中需要认证中间件设置tenantId
  238. console.debug('autoExtractFromContext为true,但未实现从Hono上下文获取租户ID');
  239. return undefined;
  240. }
  241. // 如果用户对象包含租户ID字段,则从用户对象中提取
  242. // 这里需要实际的用户对象,暂时返回undefined
  243. console.debug('没有找到租户ID,返回undefined');
  244. return undefined;
  245. }
  246. /**
  247. * 设置租户上下文
  248. * 用于从外部传递租户ID
  249. */
  250. setTenantContext(tenantId: string | number): void {
  251. // 存储租户上下文
  252. (this as any)._tenantId = tenantId;
  253. console.debug('设置租户上下文:', tenantId);
  254. }
  255. /**
  256. * 设置租户字段
  257. */
  258. private async setTenantFields(data: any, userId?: string | number): Promise<void> {
  259. if (!this.tenantOptions?.enabled) {
  260. return;
  261. }
  262. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  263. const tenantId = await this.extractTenantId(userId);
  264. // 只有在数据中不存在租户ID字段时才设置
  265. if (tenantId !== undefined && tenantId !== null && !data[tenantIdField]) {
  266. data[tenantIdField] = tenantId;
  267. }
  268. }
  269. /**
  270. * 设置用户跟踪字段
  271. */
  272. private setUserFields(data: any, userId?: string | number, isCreate: boolean = true): void {
  273. if (!this.userTrackingOptions || !userId) {
  274. return;
  275. }
  276. const {
  277. createdByField = 'createdBy',
  278. updatedByField = 'updatedBy',
  279. userIdField = 'userId'
  280. } = this.userTrackingOptions;
  281. // 设置创建人
  282. // 只有在数据中不存在该字段时才设置,避免覆盖管理员传入的用户ID
  283. if (isCreate && createdByField && !data[createdByField]) {
  284. data[createdByField] = userId;
  285. }
  286. // 设置更新人
  287. if (updatedByField) {
  288. data[updatedByField] = userId;
  289. }
  290. // 设置关联的用户ID(如userId字段)
  291. // 只有在数据中不存在该字段时才设置,避免覆盖管理员传入的用户ID
  292. if (isCreate && userIdField && !data[userIdField]) {
  293. data[userIdField] = userId;
  294. }
  295. }
  296. /**
  297. * 处理关联字段
  298. */
  299. private async handleRelationFields(data: any, entity: T, _isUpdate: boolean = false): Promise<void> {
  300. if (!this.relationFields) return;
  301. for (const [fieldName, config] of Object.entries(this.relationFields)) {
  302. if (data[fieldName] !== undefined) {
  303. const ids = data[fieldName];
  304. const relationRepository = this.dataSource.getRepository(config.targetEntity);
  305. if (ids && Array.isArray(ids) && ids.length > 0) {
  306. const relatedEntities = await relationRepository.findBy({ id: In(ids) });
  307. (entity as any)[config.relationName] = relatedEntities;
  308. } else {
  309. (entity as any)[config.relationName] = [];
  310. }
  311. // 清理原始数据中的关联字段
  312. delete data[fieldName];
  313. }
  314. }
  315. }
  316. /**
  317. * 创建实体
  318. */
  319. async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
  320. // 权限验证:防止用户创建不属于自己的数据
  321. if (this.dataPermissionOptions?.enabled && userId) {
  322. const userIdField = this.dataPermissionOptions.userIdField;
  323. // 如果数据中已经包含用户ID字段,验证是否与当前用户匹配
  324. const dataObj = data as any;
  325. if (dataObj[userIdField] && dataObj[userIdField] !== userId) {
  326. throw new Error('无权创建该资源');
  327. }
  328. }
  329. const entityData = { ...data };
  330. this.setUserFields(entityData, userId, true);
  331. await this.setTenantFields(entityData, userId);
  332. // 分离关联字段数据
  333. const relationData: any = {};
  334. if (this.relationFields) {
  335. for (const fieldName of Object.keys(this.relationFields)) {
  336. if (fieldName in entityData) {
  337. relationData[fieldName] = (entityData as any)[fieldName];
  338. delete (entityData as any)[fieldName];
  339. }
  340. }
  341. }
  342. const entity = this.repository.create(entityData as DeepPartial<T>);
  343. // 处理关联字段
  344. await this.handleRelationFields(relationData, entity);
  345. return this.repository.save(entity);
  346. }
  347. /**
  348. * 更新实体
  349. */
  350. async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
  351. // 权限验证
  352. if (this.dataPermissionOptions?.enabled && userId) {
  353. const entity = await this.getById(id);
  354. if (!entity) return null;
  355. const hasPermission = await this.checkPermission(entity, userId);
  356. if (!hasPermission) {
  357. throw new Error('无权更新该资源');
  358. }
  359. }
  360. // 租户隔离验证
  361. if (this.tenantOptions?.enabled) {
  362. const entity = await this.getById(id);
  363. if (!entity) return null;
  364. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  365. const tenantId = await this.extractTenantId(userId);
  366. if (tenantId !== undefined && tenantId !== null) {
  367. const entityTenantId = (entity as any)[tenantIdField];
  368. if (entityTenantId !== tenantId) {
  369. return null; // 不属于当前租户,返回null
  370. }
  371. }
  372. }
  373. const updateData = { ...data };
  374. this.setUserFields(updateData, userId, false);
  375. // 分离关联字段数据
  376. const relationData: any = {};
  377. if (this.relationFields) {
  378. for (const fieldName of Object.keys(this.relationFields)) {
  379. if (fieldName in updateData) {
  380. relationData[fieldName] = (updateData as any)[fieldName];
  381. delete (updateData as any)[fieldName];
  382. }
  383. }
  384. }
  385. // 先更新基础字段
  386. await this.repository.update(id, updateData);
  387. // 获取完整实体并处理关联字段
  388. const entity = await this.getById(id);
  389. if (!entity) return null;
  390. // 处理关联字段
  391. await this.handleRelationFields(relationData, entity, true);
  392. return this.repository.save(entity);
  393. }
  394. /**
  395. * 删除实体
  396. */
  397. async delete(id: number, userId?: string | number): Promise<boolean> {
  398. // 权限验证
  399. if (this.dataPermissionOptions?.enabled && userId) {
  400. const entity = await this.getById(id);
  401. if (!entity) return false;
  402. const hasPermission = await this.checkPermission(entity, userId);
  403. if (!hasPermission) {
  404. throw new Error('无权删除该资源');
  405. }
  406. }
  407. // 租户隔离验证
  408. if (this.tenantOptions?.enabled) {
  409. const entity = await this.getById(id);
  410. if (!entity) return false;
  411. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  412. const tenantId = await this.extractTenantId(userId);
  413. if (tenantId !== undefined && tenantId !== null) {
  414. const entityTenantId = (entity as any)[tenantIdField];
  415. if (entityTenantId !== tenantId) {
  416. return false; // 不属于当前租户,返回false
  417. }
  418. }
  419. }
  420. // 执行删除
  421. const result = await this.repository.delete(id);
  422. return result.affected === 1;
  423. }
  424. /**
  425. * 高级查询方法
  426. */
  427. createQueryBuilder(alias: string = 'entity') {
  428. return this.repository.createQueryBuilder(alias);
  429. }
  430. }
  431. export interface UserTrackingOptions {
  432. createdByField?: string;
  433. updatedByField?: string;
  434. userIdField?: string;
  435. }
  436. export interface RelationFieldOptions {
  437. [fieldName: string]: {
  438. relationName: string;
  439. targetEntity: new () => any;
  440. joinTableName?: string;
  441. };
  442. }
  443. export type CrudOptions<
  444. T extends ObjectLiteral,
  445. CreateSchema extends z.ZodSchema = z.ZodSchema,
  446. UpdateSchema extends z.ZodSchema = z.ZodSchema,
  447. GetSchema extends z.ZodSchema = z.ZodSchema,
  448. ListSchema extends z.ZodSchema = z.ZodSchema
  449. > = {
  450. entity: new () => T;
  451. createSchema: CreateSchema;
  452. updateSchema: UpdateSchema;
  453. getSchema: GetSchema;
  454. listSchema: ListSchema;
  455. searchFields?: string[];
  456. relations?: string[];
  457. middleware?: any[];
  458. userTracking?: UserTrackingOptions;
  459. relationFields?: RelationFieldOptions;
  460. readOnly?: boolean;
  461. /**
  462. * 数据权限控制配置
  463. */
  464. dataPermission?: DataPermissionOptions;
  465. /**
  466. * 默认过滤条件,会在所有查询中应用
  467. */
  468. defaultFilters?: Partial<T>;
  469. /**
  470. * 租户隔离配置
  471. */
  472. tenantOptions?: TenantOptions;
  473. };
  474. export interface TenantOptions {
  475. /**
  476. * 租户ID字段名,默认为 'tenantId'
  477. */
  478. tenantIdField?: string;
  479. /**
  480. * 是否启用租户隔离
  481. */
  482. enabled?: boolean;
  483. /**
  484. * 是否自动从认证上下文提取租户ID
  485. */
  486. autoExtractFromContext?: boolean;
  487. }