Grp_AirTicketReservations.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. using System.Text.RegularExpressions;
  2. namespace OASystem.Domain.Entities.Groups;
  3. /// <summary>
  4. /// 机票费用录入
  5. /// </summary>
  6. [SugarTable("Grp_AirTicketReservations")]
  7. public class Grp_AirTicketReservations : EntityBase
  8. {
  9. /************************* 2026版数据结构 *************************/
  10. /// <summary>
  11. /// 团组外键编号
  12. /// </summary>
  13. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  14. public int DIId { get; set; }
  15. /// <summary>
  16. /// 记录类型:0-正常机票 1-退票记录
  17. /// </summary>
  18. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  19. public int RecordType { get; set; } = 0;
  20. /// <summary>
  21. /// 关联的原始机票记录ID(退票记录指向原机票记录)
  22. /// </summary>
  23. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  24. public int OriginalReservationId { get; set; }
  25. /// <summary>
  26. /// 航段代码描述
  27. /// </summary>
  28. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(500)")]
  29. public string FlightsDescription { get; set; }
  30. /// <summary>
  31. /// 航班基础信息(去程、联程、返程)
  32. /// </summary>
  33. [SugarColumn(IsNullable = true, IsJson = true, ColumnDataType = "varchar(500)")]
  34. public List<AirTicketBasicInfo> AirTicketBasicInfos { get; set; } = new List<AirTicketBasicInfo>();
  35. /// <summary>
  36. /// 客人名称
  37. /// RecordType = 0 时,存储正常机票的客人名称;RecordType = 1 时,存储退票记录的客人名称(可能与原机票记录不同)
  38. /// </summary>
  39. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(200)")]
  40. public string ClientName { get; set; }
  41. /// <summary>
  42. /// 客户人数
  43. /// </summary>
  44. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  45. public int ClientNum { get; set; }
  46. /// <summary>
  47. /// 舱类型(数据类型外键)
  48. /// </summary>
  49. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  50. public int CType { get; set; }
  51. /// <summary>
  52. /// 机票全价
  53. /// </summary>
  54. [SugarColumn(IsNullable = true, ColumnDataType = "decimal(10,2)")]
  55. public decimal Price { get; set; }
  56. /// <summary>
  57. /// 币种
  58. /// </summary>
  59. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  60. public int Currency { get; set; }
  61. /// <summary>
  62. /// 费用信息(含退票信息)
  63. /// </summary>
  64. [SugarColumn(IsNullable = true, IsJson = true, ColumnDataType = "varchar(max)")]
  65. public List<CustTicketInfo> CustTicketInfos { get; set; } = new List<CustTicketInfo>();
  66. /// <summary>
  67. /// 报价说明
  68. /// </summary>
  69. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(500)")]
  70. public string PriceDescription { get; set; }
  71. /************************* 2026版数据结构 *************************/
  72. #region 旧版兼容以前的数据结构,2026版不使用
  73. /// <summary>
  74. /// 航班号
  75. /// </summary>
  76. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(100)")]
  77. public string FlightsCode { get; set; }
  78. /// <summary>
  79. /// 城市A-B
  80. /// </summary>
  81. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(100)")]
  82. public string FlightsCity { get; set; }
  83. /// <summary>
  84. /// 航班日期
  85. /// </summary>
  86. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(30)")]
  87. public string FlightsDate { get; set; }
  88. /// <summary>
  89. /// 航班时间
  90. /// </summary>
  91. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(30)")]
  92. public string FlightsTime { get; set; }
  93. /// <summary>
  94. /// 抵达时间
  95. /// </summary>
  96. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(30)")]
  97. public string ArrivedTime { get; set; }
  98. /// <summary>
  99. /// 是否值机
  100. /// 0 否 1 是
  101. /// </summary>
  102. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  103. public int IsCheckIn { get; set; }
  104. /// <summary>
  105. /// 是否选座
  106. /// 0 否 1 是
  107. /// </summary>
  108. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  109. public int IsSetSeat { get; set; }
  110. /// <summary>
  111. /// 是否购买行李服务
  112. /// 0 否 1 是
  113. /// </summary>
  114. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  115. public int IsPackage { get; set; }
  116. /// <summary>
  117. /// 是否行李直挂
  118. /// 0 否 1 是
  119. /// </summary>
  120. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  121. public int IsBagHandle { get; set; }
  122. /// <summary>
  123. /// 是否火车票出票选座
  124. /// 0 否 1 是
  125. /// </summary>
  126. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  127. public int IsTrain { get; set; }
  128. /// <summary>
  129. /// 去程航班描述代码
  130. /// </summary>
  131. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(500)")]
  132. public string LeaveDescription { get; set; }
  133. /// <summary>
  134. /// 返程航班描述代码
  135. /// </summary>
  136. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(500)")]
  137. public string ReturnDescription { get; set; }
  138. /// <summary>
  139. /// 出票前报价
  140. /// </summary>
  141. [SugarColumn(IsNullable = true, ColumnDataType = "decimal(10,2)")]
  142. public decimal PrePrice { get; set; }
  143. /// <summary>
  144. /// 出票前报价币种
  145. /// </summary>
  146. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  147. public int PreCurrency { get; set; }
  148. /// <summary>
  149. /// 机票编号
  150. /// </summary>
  151. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(100)")]
  152. public string TicketNumber { get; set; }
  153. /// <summary>
  154. /// 机票票号
  155. /// </summary>
  156. [SugarColumn(IsNullable = true, ColumnDataType = "varchar(100)")]
  157. public string TicketCode { get; set; }
  158. /// <summary>
  159. /// 客人类型(数据类型外键)
  160. /// </summary>
  161. [SugarColumn(IsNullable = true, ColumnDataType = "int")]
  162. public int PassengerType { get; set; }
  163. #endregion
  164. }
  165. /// <summary>
  166. /// 机票基础信息
  167. /// </summary>
  168. public class AirTicketBasicInfo
  169. {
  170. /// <summary>
  171. /// 序号
  172. /// </summary>
  173. public int No { get; set; }
  174. /// <summary>
  175. /// 航班号
  176. /// </summary>
  177. public string FlightsCode { get; set; }
  178. /// <summary>
  179. /// 城市A-B
  180. /// </summary>
  181. public string FlightsCity { get; set; }
  182. /// <summary>
  183. /// 航班日期
  184. /// </summary>
  185. public string FlightsDate { get; set; }
  186. /// <summary>
  187. /// 航班时间
  188. /// </summary>
  189. public string FlightsTime { get; set; }
  190. /// <summary>
  191. /// 抵达时间
  192. /// </summary>
  193. public string ArrivedTime { get; set; }
  194. /// <summary>
  195. /// 出发航站楼
  196. /// </summary>
  197. public string DepartureTerminal { get; set; }
  198. /// <summary>
  199. /// 抵达航站楼
  200. /// </summary>
  201. public string ArrivalTerminal { get; set; }
  202. /// <summary>
  203. /// 机型(如:359、321、32A、73H)
  204. /// </summary>
  205. public string AircraftType { get; set; }
  206. /// <summary>
  207. /// 耗时(如:11H10M、00H55M、01H35M)
  208. /// </summary>
  209. public string Duration { get; set; }
  210. }
  211. /// <summary>
  212. /// 客户机票信息
  213. /// </summary>
  214. public class CustTicketInfo
  215. {
  216. /// <summary>
  217. /// 客户ID
  218. /// </summary>
  219. public int ClientId { get; set; }
  220. /// <summary>
  221. /// 舱类型(数据类型外键)
  222. /// </summary>
  223. public int CType { get; set; }
  224. /// <summary>
  225. /// 实际价格
  226. /// </summary>
  227. public decimal ActualPrice { get; set; }
  228. /// <summary>
  229. /// 机票票号
  230. /// </summary>
  231. public string TicketCode { get; set; }
  232. /// <summary>
  233. /// 机票编号
  234. /// </summary>
  235. public string TicketNumber { get; set; }
  236. /// <summary>
  237. /// 选中服务ID集合
  238. /// </summary>
  239. public List<int> SelectedServiceIds { get; set; } = new List<int>();
  240. /// <summary>
  241. /// 附加服务 json数组
  242. /// </summary>
  243. public List<AdditionalService> AdditionalServices { get; set; } = new List<AdditionalService>();
  244. /// <summary>
  245. /// 机票总费用
  246. /// 实际价格 + 附加服务费
  247. /// </summary>
  248. public decimal TotalTicketPrice { get; set; }
  249. /// <summary>
  250. /// 是否已退票
  251. /// </summary>
  252. public bool IsRefund { get; set; } = false;
  253. /// <summary>
  254. /// 退票记录
  255. /// </summary>
  256. public RefundRecord RefundRecord { get; set; } = new RefundRecord();
  257. }
  258. /// <summary>
  259. /// 退票记录
  260. /// </summary>
  261. public class RefundRecord
  262. {
  263. /// <summary>
  264. /// 退票金额
  265. /// </summary>
  266. public decimal RefundAmount { get; set; }
  267. /// <summary>
  268. /// 不可退税费
  269. /// </summary>
  270. public decimal NonRefundableTax { get; set; }
  271. /// <summary>
  272. /// 退票时间
  273. /// </summary>
  274. public string RefundTime { get; set; }
  275. /// <summary>
  276. /// 退款账户
  277. /// Setdata Id
  278. /// </summary>
  279. public int RefundAccount { get; set; }
  280. /// <summary>
  281. /// 退票原因
  282. /// </summary>
  283. public string RefundReason { get; set; }
  284. }
  285. /// <summary>
  286. /// 附加服务
  287. /// </summary>
  288. public class AdditionalService
  289. {
  290. /// <summary>
  291. /// 服务类型Id(Sys_SetData)
  292. /// </summary>
  293. public int ServiceTypeId { get; set; }
  294. /// <summary>
  295. /// 金额
  296. /// </summary>
  297. public decimal Amount { get; set; }
  298. }
  299. /// <summary>
  300. /// 航班信息
  301. /// </summary>
  302. public class FlightInfo
  303. {
  304. /// <summary>
  305. /// 航段序号
  306. /// </summary>
  307. public int SequenceNo { get; set; }
  308. /// <summary>
  309. /// 航班号
  310. /// 例如:CA431
  311. /// </summary>
  312. public string FlightNumber { get; set; }
  313. /// <summary>
  314. /// 星期
  315. /// 例如:TU
  316. /// </summary>
  317. public string WeekDay { get; set; }
  318. /// <summary>
  319. /// 起飞日期
  320. /// </summary>
  321. public DateTime DepartureDate { get; set; }
  322. /// <summary>
  323. /// 出发机场三字码
  324. /// 例如:TFU
  325. /// </summary>
  326. public string DepartureAirport { get; set; }
  327. /// <summary>
  328. /// 到达机场三字码
  329. /// 例如:FRA
  330. /// </summary>
  331. public string ArrivalAirport { get; set; }
  332. /// <summary>
  333. /// 起飞时间
  334. /// 格式:HH:mm
  335. /// </summary>
  336. public string DepartureTime { get; set; }
  337. /// <summary>
  338. /// 到达时间
  339. /// 格式:HH:mm
  340. /// </summary>
  341. public string ArrivalTime { get; set; }
  342. /// <summary>
  343. /// 是否跨天到达
  344. /// </summary>
  345. public bool IsNextDayArrival { get; set; }
  346. /// <summary>
  347. /// 跨天数量
  348. /// 例如:
  349. /// +1 表示次日到达
  350. /// +2 表示第三天到达
  351. /// </summary>
  352. public int NextDayCount { get; set; }
  353. /// <summary>
  354. /// 出发航站楼
  355. /// 例如:T1
  356. /// </summary>
  357. public string DepartureTerminal { get; set; }
  358. /// <summary>
  359. /// 到达航站楼
  360. /// 例如:T2
  361. /// </summary>
  362. public string ArrivalTerminal { get; set; }
  363. /// <summary>
  364. /// 机型代码
  365. /// 例如:
  366. /// 359=A350-900
  367. /// 321=A321
  368. /// 32A=A320neo
  369. /// </summary>
  370. public string AircraftType { get; set; }
  371. /// <summary>
  372. /// 飞行时长
  373. /// 例如:11H10M
  374. /// </summary>
  375. public string Duration { get; set; }
  376. /// <summary>
  377. /// 原始文本
  378. /// </summary>
  379. public string RawText { get; set; }
  380. }
  381. /// <summary>
  382. /// 航班解析器
  383. /// 支持格式:
  384. /// 1.CA431 TU27MAY TFUFRA 0135 0645 T1 1 359 11H10M
  385. /// 2.LH098 TU28MAY FRAMUC 0915 1010 1 2 321 00H55M
  386. /// 3.LH2440 TH29MAY MUCCPH 1025 1200 2 2 32A 01H35M
  387. /// 4.CA878 SA31MAY CPHPEK 1905 1000+1 3 T3 359 08H55M
  388. /// </summary>
  389. public static class FlightParser
  390. {
  391. /// <summary>
  392. /// 月份映射表
  393. /// </summary>
  394. private static readonly Dictionary<string, int> MonthMap = new()
  395. {
  396. ["JAN"] = 1,
  397. ["FEB"] = 2,
  398. ["MAR"] = 3,
  399. ["APR"] = 4,
  400. ["MAY"] = 5,
  401. ["JUN"] = 6,
  402. ["JUL"] = 7,
  403. ["AUG"] = 8,
  404. ["SEP"] = 9,
  405. ["OCT"] = 10,
  406. ["NOV"] = 11,
  407. ["DEC"] = 12
  408. };
  409. /// <summary>
  410. /// 解析航班信息
  411. /// 自动推断年份
  412. /// </summary>
  413. public static List<FlightInfo> ParseFlights(string text)
  414. {
  415. var result = new List<FlightInfo>();
  416. if (string.IsNullOrWhiteSpace(text))
  417. return result;
  418. // 支持:
  419. // 1.CA431 ... 2.LH098 ...
  420. // 或
  421. // 1.CA431 ...
  422. // 2.LH098 ...
  423. var matches = Regex.Matches(
  424. text,
  425. @"\d+\..*?(?=\d+\.|$)",
  426. RegexOptions.Singleline);
  427. foreach (Match match in matches)
  428. {
  429. var flight = ParseLine(match.Value.Trim());
  430. if (flight != null)
  431. {
  432. result.Add(flight);
  433. }
  434. }
  435. // 按序号排序
  436. result = result
  437. .OrderBy(x => x.SequenceNo)
  438. .ToList();
  439. // 自动修正跨年
  440. FixYear(result);
  441. return result;
  442. }
  443. /// <summary>
  444. /// 解析单条航班
  445. /// </summary>
  446. private static FlightInfo ParseLine(string line)
  447. {
  448. var seqMatch = Regex.Match(line, @"^(\d+)\.");
  449. if (!seqMatch.Success)
  450. return null;
  451. var flight = new FlightInfo
  452. {
  453. SequenceNo = int.Parse(seqMatch.Groups[1].Value),
  454. RawText = line
  455. };
  456. string content = line.Substring(seqMatch.Length).Trim();
  457. var parts = Regex.Split(content, @"\s+");
  458. if (parts.Length < 5)
  459. return null;
  460. int idx = 0;
  461. // 航班号
  462. flight.FlightNumber = parts[idx++];
  463. // 日期
  464. string dateText = parts[idx++];
  465. flight.WeekDay = dateText[..2];
  466. // 这里只解析月日
  467. flight.DepartureDate = ParseMonthDay(dateText);
  468. // 航线
  469. string route = parts[idx++];
  470. if (route.Length >= 6)
  471. {
  472. flight.DepartureAirport = route[..3];
  473. flight.ArrivalAirport = route.Substring(3, 3);
  474. }
  475. // 起飞时间
  476. flight.DepartureTime = FormatTime(parts[idx++]);
  477. // 到达时间
  478. string arrivalRaw = parts[idx++];
  479. if (arrivalRaw.Contains('+'))
  480. {
  481. var arr = arrivalRaw.Split('+');
  482. flight.ArrivalTime = FormatTime(arr[0]);
  483. flight.IsNextDayArrival = true;
  484. if (arr.Length > 1)
  485. {
  486. flight.NextDayCount = int.Parse(arr[1]);
  487. }
  488. }
  489. else
  490. {
  491. flight.ArrivalTime = FormatTime(arrivalRaw);
  492. }
  493. // 剩余字段
  494. var remain = parts.Skip(idx).ToList();
  495. ParseRemainFields(remain, flight);
  496. return flight;
  497. }
  498. ///// <summary>
  499. /////
  500. ///// </summary>
  501. ///// <param name="remain">
  502. ///// 例如:
  503. ///// T1 1 359 11H10M
  504. ///// 1 2 321 00H55M
  505. ///// 3 T3 359 08H55M
  506. ///// </param>
  507. /////
  508. /// <summary>
  509. /// 解析剩余字段
  510. /// </summary>
  511. /// <param name="remain">
  512. /// 例如:
  513. /// T1 1 359 11H10M
  514. /// 1 2 321 00H55M
  515. /// 3 T3 359 08H55M
  516. /// </param>
  517. /// <param name="flight"></param>
  518. private static void ParseRemainFields(
  519. List<string> remain,
  520. FlightInfo flight)
  521. {
  522. if (remain.Count < 2)
  523. return;
  524. // 最后一个字段一般是飞行时长
  525. if (Regex.IsMatch(remain[^1], @"^\d{2}H\d{2}M$"))
  526. {
  527. flight.Duration = remain[^1];
  528. remain.RemoveAt(remain.Count - 1);
  529. }
  530. // 倒数第二个字段一般是机型
  531. if (remain.Count > 0 &&
  532. Regex.IsMatch(remain[^1], @"^\d{2,3}[A-Z]?$"))
  533. {
  534. flight.AircraftType = remain[^1];
  535. remain.RemoveAt(remain.Count - 1);
  536. }
  537. // 剩余字段解析为航站楼
  538. if (remain.Count == 1)
  539. {
  540. flight.DepartureTerminal =
  541. NormalizeTerminal(remain[0]);
  542. }
  543. else if (remain.Count >= 2)
  544. {
  545. flight.DepartureTerminal =
  546. NormalizeTerminal(remain[0]);
  547. flight.ArrivalTerminal =
  548. NormalizeTerminal(remain[1]);
  549. }
  550. }
  551. /// <summary>
  552. /// 标准化航站楼
  553. /// </summary>
  554. /// <param name="terminal">
  555. /// 原始值:1、2、 T3
  556. /// </param>
  557. /// <returns>
  558. /// T1、T2、T3
  559. /// </returns>
  560. private static string NormalizeTerminal(string terminal)
  561. {
  562. if (string.IsNullOrWhiteSpace(terminal))
  563. return null;
  564. terminal = terminal.Trim().ToUpper();
  565. // 纯数字补T
  566. if (Regex.IsMatch(terminal, @"^\d+$"))
  567. {
  568. return $"T{terminal}";
  569. }
  570. return terminal;
  571. }
  572. /// <summary>
  573. /// 格式化时间
  574. /// 0135 -> 01:35
  575. /// 905 -> 09:05
  576. /// </summary>
  577. private static string FormatTime(string time)
  578. {
  579. if (string.IsNullOrWhiteSpace(time))
  580. return string.Empty;
  581. time = time.Split('+')[0];
  582. if (time.Length == 4)
  583. {
  584. return $"{time[..2]}:{time.Substring(2, 2)}";
  585. }
  586. if (time.Length == 3)
  587. {
  588. return $"0{time[0]}:{time.Substring(1, 2)}";
  589. }
  590. return time;
  591. }
  592. /// <summary>
  593. /// 自动修正跨年
  594. /// </summary>
  595. private static void FixYear(
  596. List<FlightInfo> flights)
  597. {
  598. if (flights.Count <= 1)
  599. return;
  600. int currentYear = DateTime.Now.Year;
  601. DateTime previousDate = DateTime.MinValue;
  602. foreach (var flight in flights)
  603. {
  604. if (flight.DepartureDate == DateTime.MinValue)
  605. continue;
  606. var date = new DateTime(
  607. currentYear,
  608. flight.DepartureDate.Month,
  609. flight.DepartureDate.Day);
  610. // 出现日期倒退
  611. if (previousDate != DateTime.MinValue &&
  612. date < previousDate)
  613. {
  614. currentYear++;
  615. date = new DateTime(
  616. currentYear,
  617. flight.DepartureDate.Month,
  618. flight.DepartureDate.Day);
  619. }
  620. flight.DepartureDate = date;
  621. previousDate = date;
  622. }
  623. }
  624. /// <summary>
  625. /// 解析月日
  626. /// TU27MAY -> 05-27
  627. /// </summary>
  628. private static DateTime ParseMonthDay(string value)
  629. {
  630. value = Regex.Replace(
  631. value,
  632. @"^[A-Z]{2}",
  633. "");
  634. var match = Regex.Match(
  635. value,
  636. @"(\d{2})([A-Z]{3})");
  637. if (!match.Success)
  638. return DateTime.MinValue;
  639. int day = int.Parse(match.Groups[1].Value);
  640. if (!MonthMap.TryGetValue(
  641. match.Groups[2].Value,
  642. out int month))
  643. {
  644. return DateTime.MinValue;
  645. }
  646. // 先使用当前年份
  647. return new DateTime(
  648. DateTime.Now.Year,
  649. month,
  650. day);
  651. }
  652. }