route.service.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { DataSource, In, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
  2. import { GenericCrudService } from '@/server/utils/generic-crud.service';
  3. import { RouteEntity } from './route.entity';
  4. import { VehicleType } from './route.schema';
  5. import { ActivityEntity } from '@/server/modules/activities/activity.entity';
  6. import { LocationEntity } from '@/server/modules/locations/location.entity';
  7. export interface RouteSearchParams {
  8. startLocationId?: number;
  9. endLocationId?: number;
  10. startAreaIds?: number[]; // 省市区ID数组 [provinceId, cityId, districtId]
  11. endAreaIds?: number[]; // 省市区ID数组 [provinceId, cityId, districtId]
  12. date?: string;
  13. routeType?: 'departure' | 'return';
  14. minPrice?: number;
  15. maxPrice?: number;
  16. vehicleType?: VehicleType;
  17. sortBy?: 'price' | 'departureTime';
  18. sortOrder?: 'ASC' | 'DESC';
  19. page?: number;
  20. pageSize?: number;
  21. }
  22. export interface RouteSearchResult {
  23. routes: RouteEntity[];
  24. activities: ActivityEntity[];
  25. pagination: {
  26. page: number;
  27. pageSize: number;
  28. total: number;
  29. totalPages: number;
  30. };
  31. }
  32. export class RouteService extends GenericCrudService<RouteEntity> {
  33. constructor(dataSource: DataSource) {
  34. super(dataSource, RouteEntity, {
  35. userTracking: {
  36. createdByField: 'createdBy',
  37. updatedByField: 'updatedBy'
  38. }
  39. });
  40. }
  41. /**
  42. * 搜索路线
  43. */
  44. async searchRoutes(params: RouteSearchParams): Promise<RouteSearchResult> {
  45. const {
  46. startLocationId,
  47. endLocationId,
  48. startAreaIds,
  49. endAreaIds,
  50. date,
  51. routeType,
  52. minPrice,
  53. maxPrice,
  54. vehicleType,
  55. sortBy = 'departureTime',
  56. sortOrder = 'ASC',
  57. page = 1,
  58. pageSize = 20
  59. } = params;
  60. // 构建查询条件
  61. const where: any = {
  62. isDeleted: 0,
  63. isDisabled: 0
  64. };
  65. // 精确地点查询(优先)
  66. if (startLocationId) {
  67. where.startLocationId = startLocationId;
  68. }
  69. if (endLocationId) {
  70. where.endLocationId = endLocationId;
  71. }
  72. // 日期筛选
  73. if (date) {
  74. const targetDate = new Date(date);
  75. const nextDate = new Date(targetDate);
  76. nextDate.setDate(targetDate.getDate() + 1);
  77. where.departureTime = MoreThanOrEqual(targetDate);
  78. where.departureTime = LessThanOrEqual(nextDate);
  79. }
  80. // 价格筛选
  81. if (minPrice !== undefined) {
  82. where.price = MoreThanOrEqual(minPrice);
  83. }
  84. if (maxPrice !== undefined) {
  85. where.price = LessThanOrEqual(maxPrice);
  86. }
  87. // 车型筛选
  88. if (vehicleType) {
  89. where.vehicleType = vehicleType;
  90. }
  91. // 构建排序
  92. const order: any = {};
  93. if (sortBy === 'price') {
  94. order.price = sortOrder;
  95. } else {
  96. order.departureTime = sortOrder;
  97. }
  98. // 获取路线列表 - 直接使用查询构建器避免关联关系冲突
  99. const query = this.repository.createQueryBuilder('route')
  100. .leftJoinAndSelect('route.startLocation', 'startLocation')
  101. .leftJoinAndSelect('startLocation.province', 'startLocation_province')
  102. .leftJoinAndSelect('startLocation.city', 'startLocation_city')
  103. .leftJoinAndSelect('startLocation.district', 'startLocation_district')
  104. .leftJoinAndSelect('route.endLocation', 'endLocation')
  105. .leftJoinAndSelect('endLocation.province', 'endLocation_province')
  106. .leftJoinAndSelect('endLocation.city', 'endLocation_city')
  107. .leftJoinAndSelect('endLocation.district', 'endLocation_district')
  108. .leftJoinAndSelect('route.activity', 'activity')
  109. .where(where)
  110. .skip((page - 1) * pageSize)
  111. .take(pageSize);
  112. // 添加排序
  113. if (sortBy === 'price') {
  114. query.orderBy('route.price', sortOrder);
  115. } else {
  116. query.orderBy('route.departureTime', sortOrder);
  117. }
  118. const [routes, total] = await query.getManyAndCount();
  119. // 根据省市区筛选(如果提供了省市区ID)
  120. let filteredRoutes = routes;
  121. if (startAreaIds && startAreaIds.length > 0) {
  122. filteredRoutes = filteredRoutes.filter(route => {
  123. const location = route.startLocation;
  124. return this.matchAreaIds(location, startAreaIds);
  125. });
  126. }
  127. if (endAreaIds && endAreaIds.length > 0) {
  128. filteredRoutes = filteredRoutes.filter(route => {
  129. const location = route.endLocation;
  130. return this.matchAreaIds(location, endAreaIds);
  131. });
  132. }
  133. // 根据路线类型筛选
  134. if (routeType) {
  135. filteredRoutes = filteredRoutes.filter(route => route.routeType === routeType);
  136. }
  137. // 获取去重后的活动列表
  138. const activityIds = Array.from(new Set(filteredRoutes.map(route => route.activityId)));
  139. const activities = activityIds.length > 0
  140. ? await this.dataSource.getRepository(ActivityEntity).find({
  141. where: { id: In(activityIds) },
  142. relations: ['venueLocation', 'venueLocation.province', 'venueLocation.city', 'venueLocation.district']
  143. })
  144. : [];
  145. return {
  146. routes: filteredRoutes,
  147. activities,
  148. pagination: {
  149. page,
  150. pageSize,
  151. total: filteredRoutes.length,
  152. totalPages: Math.ceil(filteredRoutes.length / pageSize)
  153. }
  154. };
  155. }
  156. /**
  157. * 检查地点是否匹配省市区ID
  158. */
  159. private matchAreaIds(location: any, areaIds: number[]): boolean {
  160. // 这里需要根据实际的Location实体结构来匹配
  161. // 假设Location实体有provinceId, cityId, districtId字段
  162. if (!location) return false;
  163. const [provinceId, cityId, districtId] = areaIds;
  164. // 匹配省份
  165. if (provinceId && location.provinceId !== provinceId) {
  166. return false;
  167. }
  168. // 匹配城市
  169. if (cityId && location.cityId !== cityId) {
  170. return false;
  171. }
  172. // 匹配区县
  173. if (districtId && location.districtId !== districtId) {
  174. return false;
  175. }
  176. return true;
  177. }
  178. /**
  179. * 根据ID获取路线详情
  180. */
  181. async getRouteById(id: number): Promise<RouteEntity | null> {
  182. return this.getById(id, ['startLocation', 'endLocation', 'activity', 'activity.venueLocation']);
  183. }
  184. /**
  185. * 获取热门路线(按预订次数排序)
  186. */
  187. async getPopularRoutes(limit: number = 10): Promise<RouteEntity[]> {
  188. // 这里可以添加更复杂的逻辑,比如按预订次数排序
  189. // 目前先返回最新的路线
  190. const [routes] = await this.getList(
  191. 1,
  192. limit,
  193. undefined,
  194. undefined,
  195. { isDeleted: 0, isDisabled: 0 },
  196. ['startLocation', 'endLocation', 'activity'],
  197. { createdAt: 'DESC' }
  198. );
  199. return routes;
  200. }
  201. /**
  202. * 获取可用座位数
  203. */
  204. async getAvailableSeats(routeId: number): Promise<number> {
  205. const route = await this.getById(routeId);
  206. return route?.availableSeats || 0;
  207. }
  208. /**
  209. * 更新座位数
  210. */
  211. async updateSeats(routeId: number, bookedSeats: number): Promise<boolean> {
  212. const route = await this.getById(routeId);
  213. if (!route) {
  214. return false;
  215. }
  216. const newAvailableSeats = route.availableSeats - bookedSeats;
  217. if (newAvailableSeats < 0) {
  218. return false;
  219. }
  220. await this.repository.update(routeId, { availableSeats: newAvailableSeats });
  221. return true;
  222. }
  223. }