generic-crud.service.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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. public 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. if (!entity) {
  170. return null;
  171. }
  172. // 租户隔离验证 - 先于数据权限验证
  173. if (this.tenantOptions?.enabled) {
  174. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  175. const tenantId = await this.extractTenantId(userId);
  176. if (tenantId !== undefined && tenantId !== null) {
  177. const entityTenantId = (entity as any)[tenantIdField];
  178. if (entityTenantId !== tenantId) {
  179. return null; // 不属于当前租户,返回null
  180. }
  181. }
  182. }
  183. // 数据权限验证
  184. if (this.dataPermissionOptions?.enabled && userId) {
  185. const hasPermission = await this.checkPermission(entity, userId);
  186. if (!hasPermission) {
  187. throw new PermissionError('没有权限访问该资源');
  188. }
  189. }
  190. return entity;
  191. }
  192. /**
  193. * 检查用户对实体的权限
  194. */
  195. private async checkPermission(entity: any, userId: string | number): Promise<boolean> {
  196. const options = this.dataPermissionOptions;
  197. if (!options?.enabled) return true;
  198. // 管理员权限覆盖检查
  199. if (options.adminOverride?.enabled && options.adminOverride.adminRole) {
  200. // 这里需要从认证系统获取用户角色信息
  201. // 暂时假设管理员可以访问所有数据
  202. // 实际实现中需要集成用户角色检查
  203. const isAdmin = await this.checkAdminRole(userId, options.adminOverride.adminRole);
  204. if (isAdmin) {
  205. return true;
  206. }
  207. }
  208. // 自定义权限验证器
  209. if (options.customValidator) {
  210. return await options.customValidator(userId, entity);
  211. }
  212. // 基础权限验证:用户ID字段匹配
  213. const userIdField = options.userIdField;
  214. const entityUserId = entity[userIdField];
  215. return entityUserId === userId;
  216. }
  217. /**
  218. * 检查用户是否为管理员
  219. * TODO: 需要集成实际的用户角色检查
  220. */
  221. private async checkAdminRole(userId: string | number, adminRole: string): Promise<boolean> {
  222. // 这里需要从认证系统获取用户角色信息
  223. // 暂时返回false,实际实现中需要集成用户角色检查
  224. return false;
  225. }
  226. /**
  227. * 提取租户ID
  228. * 从用户对象或认证上下文中提取租户ID
  229. */
  230. private async extractTenantId(userId?: string | number): Promise<string | number | undefined> {
  231. // 首先检查是否有存储的租户上下文
  232. if ((this as any)._tenantId !== undefined) {
  233. console.debug('从存储的租户上下文中获取租户ID:', (this as any)._tenantId);
  234. return (this as any)._tenantId;
  235. }
  236. // 如果租户选项启用了从上下文自动提取,则从上下文获取
  237. if (this.tenantOptions?.autoExtractFromContext) {
  238. // 这里需要从Hono上下文中获取租户ID
  239. // 在实际实现中,认证中间件应该设置租户上下文
  240. // 暂时返回undefined,实际实现中需要认证中间件设置tenantId
  241. console.debug('autoExtractFromContext为true,但未实现从Hono上下文获取租户ID');
  242. return undefined;
  243. }
  244. // 如果用户对象包含租户ID字段,则从用户对象中提取
  245. // 这里需要实际的用户对象,暂时返回undefined
  246. console.debug('没有找到租户ID,返回undefined');
  247. return undefined;
  248. }
  249. /**
  250. * 设置租户上下文
  251. * 用于从外部传递租户ID
  252. */
  253. setTenantContext(tenantId: string | number): void {
  254. // 存储租户上下文
  255. (this as any)._tenantId = tenantId;
  256. console.debug('设置租户上下文:', tenantId);
  257. }
  258. /**
  259. * 设置租户字段
  260. */
  261. private async setTenantFields(data: any, userId?: string | number): Promise<void> {
  262. if (!this.tenantOptions?.enabled) {
  263. return;
  264. }
  265. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  266. const tenantId = await this.extractTenantId(userId);
  267. // 只有在数据中不存在租户ID字段时才设置
  268. if (tenantId !== undefined && tenantId !== null && !data[tenantIdField]) {
  269. data[tenantIdField] = tenantId;
  270. }
  271. }
  272. /**
  273. * 设置用户跟踪字段
  274. */
  275. private setUserFields(data: any, userId?: string | number, isCreate: boolean = true): void {
  276. if (!this.userTrackingOptions || !userId) {
  277. return;
  278. }
  279. const {
  280. createdByField = 'createdBy',
  281. updatedByField = 'updatedBy',
  282. userIdField = 'userId'
  283. } = this.userTrackingOptions;
  284. // 设置创建人
  285. // 只有在数据中不存在该字段时才设置,避免覆盖管理员传入的用户ID
  286. if (isCreate && createdByField && !data[createdByField]) {
  287. data[createdByField] = userId;
  288. }
  289. // 设置更新人
  290. if (updatedByField) {
  291. data[updatedByField] = userId;
  292. }
  293. // 设置关联的用户ID(如userId字段)
  294. // 只有在数据中不存在该字段时才设置,避免覆盖管理员传入的用户ID
  295. if (isCreate && userIdField && !data[userIdField]) {
  296. data[userIdField] = userId;
  297. }
  298. }
  299. /**
  300. * 处理关联字段
  301. */
  302. private async handleRelationFields(data: any, entity: T, _isUpdate: boolean = false): Promise<void> {
  303. if (!this.relationFields) return;
  304. for (const [fieldName, config] of Object.entries(this.relationFields)) {
  305. if (data[fieldName] !== undefined) {
  306. const ids = data[fieldName];
  307. const relationRepository = this.dataSource.getRepository(config.targetEntity);
  308. if (ids && Array.isArray(ids) && ids.length > 0) {
  309. const relatedEntities = await relationRepository.findBy({ id: In(ids) });
  310. (entity as any)[config.relationName] = relatedEntities;
  311. } else {
  312. (entity as any)[config.relationName] = [];
  313. }
  314. // 清理原始数据中的关联字段
  315. delete data[fieldName];
  316. }
  317. }
  318. }
  319. /**
  320. * 创建实体
  321. */
  322. async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
  323. // 权限验证:防止用户创建不属于自己的数据
  324. if (this.dataPermissionOptions?.enabled && userId) {
  325. const userIdField = this.dataPermissionOptions.userIdField;
  326. // 如果数据中已经包含用户ID字段,验证是否与当前用户匹配
  327. const dataObj = data as any;
  328. if (dataObj[userIdField] && dataObj[userIdField] !== userId) {
  329. throw new Error('无权创建该资源');
  330. }
  331. }
  332. const entityData = { ...data };
  333. this.setUserFields(entityData, userId, true);
  334. await this.setTenantFields(entityData, userId);
  335. // 分离关联字段数据
  336. const relationData: any = {};
  337. if (this.relationFields) {
  338. for (const fieldName of Object.keys(this.relationFields)) {
  339. if (fieldName in entityData) {
  340. relationData[fieldName] = (entityData as any)[fieldName];
  341. delete (entityData as any)[fieldName];
  342. }
  343. }
  344. }
  345. const entity = this.repository.create(entityData as DeepPartial<T>);
  346. // 处理关联字段
  347. await this.handleRelationFields(relationData, entity);
  348. return this.repository.save(entity);
  349. }
  350. /**
  351. * 更新实体
  352. */
  353. async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
  354. // 权限验证
  355. if (this.dataPermissionOptions?.enabled && userId) {
  356. const entity = await this.getById(id);
  357. if (!entity) return null;
  358. const hasPermission = await this.checkPermission(entity, userId);
  359. if (!hasPermission) {
  360. throw new Error('无权更新该资源');
  361. }
  362. }
  363. // 租户隔离验证
  364. if (this.tenantOptions?.enabled) {
  365. const entity = await this.getById(id);
  366. if (!entity) return null;
  367. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  368. const tenantId = await this.extractTenantId(userId);
  369. if (tenantId !== undefined && tenantId !== null) {
  370. const entityTenantId = (entity as any)[tenantIdField];
  371. if (entityTenantId !== tenantId) {
  372. return null; // 不属于当前租户,返回null
  373. }
  374. }
  375. }
  376. const updateData = { ...data };
  377. this.setUserFields(updateData, userId, false);
  378. // 分离关联字段数据
  379. const relationData: any = {};
  380. if (this.relationFields) {
  381. for (const fieldName of Object.keys(this.relationFields)) {
  382. if (fieldName in updateData) {
  383. relationData[fieldName] = (updateData as any)[fieldName];
  384. delete (updateData as any)[fieldName];
  385. }
  386. }
  387. }
  388. // 先更新基础字段
  389. await this.repository.update(id, updateData);
  390. // 获取完整实体并处理关联字段
  391. const entity = await this.getById(id);
  392. if (!entity) return null;
  393. // 处理关联字段
  394. await this.handleRelationFields(relationData, entity, true);
  395. return this.repository.save(entity);
  396. }
  397. /**
  398. * 删除实体
  399. */
  400. async delete(id: number, userId?: string | number): Promise<boolean> {
  401. // 权限验证
  402. if (this.dataPermissionOptions?.enabled && userId) {
  403. const entity = await this.getById(id);
  404. if (!entity) return false;
  405. const hasPermission = await this.checkPermission(entity, userId);
  406. if (!hasPermission) {
  407. throw new Error('无权删除该资源');
  408. }
  409. }
  410. // 租户隔离验证
  411. if (this.tenantOptions?.enabled) {
  412. const entity = await this.getById(id);
  413. if (!entity) return false;
  414. const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
  415. const tenantId = await this.extractTenantId(userId);
  416. if (tenantId !== undefined && tenantId !== null) {
  417. const entityTenantId = (entity as any)[tenantIdField];
  418. if (entityTenantId !== tenantId) {
  419. return false; // 不属于当前租户,返回false
  420. }
  421. }
  422. }
  423. // 执行删除
  424. const result = await this.repository.delete(id);
  425. return result.affected === 1;
  426. }
  427. /**
  428. * 高级查询方法
  429. */
  430. createQueryBuilder(alias: string = 'entity') {
  431. return this.repository.createQueryBuilder(alias);
  432. }
  433. }
  434. export interface UserTrackingOptions {
  435. createdByField?: string;
  436. updatedByField?: string;
  437. userIdField?: string;
  438. }
  439. export interface RelationFieldOptions {
  440. [fieldName: string]: {
  441. relationName: string;
  442. targetEntity: new () => any;
  443. joinTableName?: string;
  444. };
  445. }
  446. export type CrudOptions<
  447. T extends ObjectLiteral,
  448. CreateSchema extends z.ZodSchema = z.ZodSchema,
  449. UpdateSchema extends z.ZodSchema = z.ZodSchema,
  450. GetSchema extends z.ZodSchema = z.ZodSchema,
  451. ListSchema extends z.ZodSchema = z.ZodSchema
  452. > = {
  453. entity: new () => T;
  454. createSchema: CreateSchema;
  455. updateSchema: UpdateSchema;
  456. getSchema: GetSchema;
  457. listSchema: ListSchema;
  458. searchFields?: string[];
  459. relations?: string[];
  460. middleware?: any[];
  461. userTracking?: UserTrackingOptions;
  462. relationFields?: RelationFieldOptions;
  463. readOnly?: boolean;
  464. /**
  465. * 数据权限控制配置
  466. */
  467. dataPermission?: DataPermissionOptions;
  468. /**
  469. * 默认过滤条件,会在所有查询中应用
  470. */
  471. defaultFilters?: Partial<T>;
  472. /**
  473. * 租户隔离配置
  474. */
  475. tenantOptions?: TenantOptions;
  476. };
  477. export interface TenantOptions {
  478. /**
  479. * 租户ID字段名,默认为 'tenantId'
  480. */
  481. tenantIdField?: string;
  482. /**
  483. * 是否启用租户隔离
  484. */
  485. enabled?: boolean;
  486. /**
  487. * 是否自动从认证上下文提取租户ID
  488. */
  489. autoExtractFromContext?: boolean;
  490. }