Lyyyi 4 dagar sedan
förälder
incheckning
67ff2f1d70

+ 87 - 95
OASystem/OASystem.Domain/Dtos/Groups/ApprovalJourneyDto.cs

@@ -1,99 +1,91 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+namespace OASystem.Domain.Dtos.Groups;
 
-namespace OASystem.Domain.Dtos.Groups
+public class ApprovalJourneyDto
 {
-    public class ApprovalJourneyDto
-    {
-        public int Diid { get; set; }
-        public int UserId { get; set; }
-    }
-
-    public class CreateApprovalJourneyDto
-    {
-        public int Diid { get; set; }
-
-        public int Userid { get; set; }
-
-        public int BlackCodeId { get; set; }
-    }
-
-    public class ExportApprovalJourneyWord
-    {
-        public int Diid { get; set; }
-
-        /// <summary>
-        ///  1 默认模板  2 省外办  3 市外办
-        /// </summary>
-
-        public int FileIndex { get; set; } = 1;
-    }
-
-    public class ApprovalJourneyAiWriteDto
-    {
-        public string ClientName { get; set; }
-
-        public string ClientPurpose { get; set; }
-    }
-
-
-    public class SaveApprovalJourney
-    {
-        public List<ApprovalJourneyItem> Arr { get; set; }
-
-        public int UserId { get; set; }
-    }
-
-    public class DeleteApprovalJourney
-    {
-        public int Diid { get; set; }
-        public int uesrId { get; set; }
-    }
-
-    public class ChiListItem
-    {
-        /// <summary>
-        /// 时间区间
-        /// </summary>
-        public List<string> timeInterval { get; set; }
-
-        /// <summary>
-        /// 详细信息
-        /// </summary>
-        public string details { get; set; }
-
-        /// <summary>
-        /// 父级Id
-        /// </summary>
-
-        public int parentId { get; set; }
-        /// <summary>
-        /// id
-        /// </summary>
-        public int id { get; set; }
-    }
-
-    public class ApprovalJourneyItem
-    {
-        /// <summary>
-        /// id
-        /// </summary>
-        public int id { get; set; }
-        /// <summary>
-        /// 日期显示
-        /// </summary>
-        public string date { get; set; }
-        /// <summary>
-        /// 团组id
-        /// </summary>
-        public int diid { get; set; }
-        /// <summary>
-        ///项列表 
-        /// </summary>
-        public List<ChiListItem> chiList { get; set; }
-    }
+    public int Diid { get; set; }
+    public int UserId { get; set; }
+}
+
+public class CreateApprovalJourneyDto
+{
+    public int Diid { get; set; }
+
+    public int Userid { get; set; }
+
+    public int BlackCodeId { get; set; }
+}
+
+public class ExportApprovalJourneyWord
+{
+    public int Diid { get; set; }
+
+    /// <summary>
+    ///  1 默认模板  2 省外办  3 市外办
+    /// </summary>
+
+    public int FileIndex { get; set; } = 1;
+}
 
+public class ApprovalJourneyAiWriteDto
+{
+    public string ClientName { get; set; }
+
+    public string ClientPurpose { get; set; }
+}
+
+
+public class SaveApprovalJourney
+{
+    public List<ApprovalJourneyItem> Arr { get; set; }
+
+    public int UserId { get; set; }
+}
+
+public class DeleteApprovalJourney
+{
+    public int Diid { get; set; }
+    public int uesrId { get; set; }
+}
+
+public class ChiListItem
+{
+    /// <summary>
+    /// 时间区间
+    /// </summary>
+    public List<string> timeInterval { get; set; }
+
+    /// <summary>
+    /// 详细信息
+    /// </summary>
+    public string details { get; set; }
+
+    /// <summary>
+    /// 父级Id
+    /// </summary>
+
+    public int parentId { get; set; }
+    /// <summary>
+    /// id
+    /// </summary>
+    public int id { get; set; }
+}
+
+public class ApprovalJourneyItem
+{
+    /// <summary>
+    /// id
+    /// </summary>
+    public int id { get; set; }
+    /// <summary>
+    /// 日期显示
+    /// </summary>
+    public string date { get; set; }
+    /// <summary>
+    /// 团组id
+    /// </summary>
+    public int diid { get; set; }
+    /// <summary>
+    ///项列表 
+    /// </summary>
+    public List<ChiListItem> chiList { get; set; }
 }

+ 211 - 174
OASystem/OASystem.Domain/Entities/Groups/Grp_AirTicketReservations.cs

@@ -281,7 +281,7 @@ public class CustTicketInfo
     /// <summary>
     /// 退票记录
     /// </summary>
-    public RefundRecord? RefundRecord { get; set; }
+    public RefundRecord RefundRecord { get; set; } = new RefundRecord();
 
 }
 
@@ -334,110 +334,32 @@ public class AdditionalService
 
 }
 
-
-
 /// <summary>
-/// 航班信息实体(通用)
+/// 航班信息
 /// </summary>
 public class FlightInfo
 {
-    /// <summary>
-    /// 序号
-    /// </summary>
-    public int SequenceNo { get; set; }
-
-    /// <summary>
-    /// 航班号
-    /// </summary>
-    public string FlightNumber { get; set; }
-
-    /// <summary>
-    /// 舱位等级/舱位代码
-    /// </summary>
-    public string CabinClass { get; set; }
-
-    /// <summary>
-    /// 星期
-    /// </summary>
-    public string WeekDay { get; set; }
-
-    /// <summary>
-    /// 出发日期(原始格式)
-    /// </summary>
-    public string DepartureDateRaw { get; set; }
-
-    /// <summary>
-    /// 出发日期(标准格式 yyyy-MM-dd)
-    /// </summary>
-    public string DepartureDate { get; set; }
-
-    /// <summary>
-    /// 出发机场代码
-    /// </summary>
-    public string DepartureAirport { get; set; }
-
-    /// <summary>
-    /// 到达机场代码
-    /// </summary>
-    public string ArrivalAirport { get; set; }
-
-    /// <summary>
-    /// 出发时间(原始格式)
-    /// </summary>
-    public string DepartureTimeRaw { get; set; }
-
-    /// <summary>
-    /// 出发时间(格式化 HH:mm)
-    /// </summary>
-    public string DepartureTime { get; set; }
-
-    /// <summary>
-    /// 到达时间(原始格式)
-    /// </summary>
-    public string ArrivalTimeRaw { get; set; }
-
-    /// <summary>
-    /// 到达时间(格式化 HH:mm)
-    /// </summary>
-    public string ArrivalTime { get; set; }
-
-    /// <summary>
-    /// 是否次日到达
-    /// </summary>
-    public bool IsNextDayArrival { get; set; }
-
-    /// <summary>
-    /// 航站楼信息
-    /// </summary>
-    public string Terminal { get; set; }
-
-    /// <summary>
-    /// 机型
-    /// </summary>
-    public string AircraftType { get; set; }
-
-    /// <summary>
-    /// 飞行时长
-    /// </summary>
-    public string Duration { get; set; }
-
-    /// <summary>
-    /// 状态(HK/KK等)
-    /// </summary>
-    public string Status { get; set; }
-
-    /// <summary>
-    /// 原始文本
-    /// </summary>
-    public string RawText { get; set; }
-
-    /// <summary>
-    /// 显示信息
-    /// </summary>
-    public string DisplayInfo => $"{SequenceNo}.{FlightNumber} {DepartureAirport}→{ArrivalAirport} {DepartureTime}→{ArrivalTime}{(IsNextDayArrival ? "+1" : "")}";
+    public int SequenceNo { get; set; }           // 序号
+    public string FlightNumber { get; set; }      // 航班号
+    public string CabinClass { get; set; }        // 舱位等级
+    public string WeekDay { get; set; }           // 星期(MO,TU,WE,TH,FR,SA,SU)
+    public string DepartureDateRaw { get; set; }  // 原始日期字符串
+    public DateTime DepartureDate { get; set; }   // 起飞日期
+    public string DepartureTimeRaw { get; set; }  // 原始起飞时间
+    public string DepartureTime { get; set; }     // 格式化起飞时间
+    public string ArrivalTimeRaw { get; set; }    // 原始到达时间
+    public string ArrivalTime { get; set; }       // 格式化到达时间
+    public string DepartureAirport { get; set; }  // 起飞机场
+    public string ArrivalAirport { get; set; }    // 到达机场
+    public bool IsNextDayArrival { get; set; }    // 是否次日到达
+    public int NextDayCount { get; set; }         // 跨天天数(+1表示第二天)
+    public string Terminal { get; set; }          // 航站楼
+    public string AircraftType { get; set; }      // 机型
+    public string Duration { get; set; }          // 飞行时长
+    public string Status { get; set; }            // 状态(HK1,KK1,TK1等)
+    public string RawText { get; set; }           // 原始文本
 }
 
-
 /// <summary>
 /// 航班解析器
 /// </summary>
@@ -476,7 +398,7 @@ public static class FlightParser
         if (string.IsNullOrWhiteSpace(rawText))
             return new List<FlightInfo>();
 
-        var lines = rawText.Trim().Split(new[] { '\n', '\r' ,','}, StringSplitOptions.RemoveEmptyEntries);
+        var lines = rawText.Trim().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
         var result = new List<FlightInfo>();
 
         foreach (var line in lines)
@@ -507,6 +429,7 @@ public static class FlightParser
         }
 
         // 格式2:1.CA431 TU27MAY TFUFRA 0135 0645 T1 1 359 11H10M
+        // 格式2变体:2. LH098 TU28MAY FRAMUC 0915 1010 1 2 321 00H55M
         if (Regex.IsMatch(line, @"^\d+\.[A-Z0-9]+\s+[A-Z]{2}\d{2}[A-Z]{3}"))
         {
             return ParseFormat2(line, year);
@@ -517,11 +440,10 @@ public static class FlightParser
 
     #endregion
 
-    #region 格式1解析(HU7086 C   MO18MAY  TFUHAK HK1   1605 1820          E T2T2)
+    #region 格式1解析
 
     /// <summary>
     /// 解析格式1
-    /// 示例:1.  HU7086 C   MO18MAY  TFUHAK HK1   1605 1820          E T2T2
     /// </summary>
     private static FlightInfo ParseFormat1(string line, int year)
     {
@@ -532,23 +454,22 @@ public static class FlightParser
         var sequenceNo = int.Parse(seqMatch.Groups[1].Value);
         var content = line.Substring(seqMatch.Length).Trim();
 
-        // 正则匹配:航班号 + 舱位 + 日期 + 机场 + 状态 + 时间 + 其他
+        // 正则匹配
         var pattern = @"^([A-Z0-9]+)\s+([A-Z])\s+([A-Z]{2})(\d{2}[A-Z]{3})\s+([A-Z]{3})([A-Z]{3})\s+([A-Z]{2,3}\d?)\s+(\d{3,4})\s+(\d{3,4})(?:\+(\d+))?\s+([A-Z])\s+([A-Z0-9]+)";
         var match = Regex.Match(content, pattern);
 
         if (!match.Success) return null;
 
-        var weekDay = match.Groups[3].Value;      // MO
-        var dateRaw = match.Groups[4].Value;      // 18MAY
-        var departureAirport = match.Groups[5].Value; // TFU
-        var arrivalAirport = match.Groups[6].Value;   // HAK
-        var status = match.Groups[7].Value;       // HK1
-        var departureTime = match.Groups[8].Value; // 1605
-        var arrivalTime = match.Groups[9].Value;   // 1820
+        var weekDay = match.Groups[3].Value;
+        var dateRaw = match.Groups[4].Value;
+        var departureAirport = match.Groups[5].Value;
+        var arrivalAirport = match.Groups[6].Value;
+        var departureTime = match.Groups[8].Value;
+        var arrivalTime = match.Groups[9].Value;
         var isNextDay = match.Groups[10].Success && match.Groups[10].Value == "1";
-        var terminalFlag = match.Groups[12].Value; // T2T2
+        var terminalFlag = match.Groups[12].Value;
 
-        var flight = new FlightInfo
+        return new FlightInfo
         {
             SequenceNo = sequenceNo,
             FlightNumber = match.Groups[1].Value,
@@ -560,24 +481,18 @@ public static class FlightParser
             DepartureTimeRaw = departureTime,
             ArrivalTimeRaw = arrivalTime,
             IsNextDayArrival = isNextDay,
-            Status = status,
+            DepartureTime = FormatTime(departureTime),
+            ArrivalTime = FormatTime(arrivalTime),
+            DepartureDate = ParseDate(dateRaw, year),
+            Status = match.Groups[7].Value,
             Terminal = terminalFlag,
             RawText = line
         };
-
-        // 格式化时间
-        flight.DepartureTime = FormatTime(departureTime);
-        flight.ArrivalTime = FormatTime(arrivalTime);
-
-        // 解析日期
-        flight.DepartureDate = ParseDate(dateRaw, year);
-
-        return flight;
     }
 
     #endregion
 
-    #region 格式2解析(1.CA431 TU27MAY TFUFRA 0135 0645 T1 1 359 11H10M
+    #region 格式2解析(修复版)
 
     /// <summary>
     /// 解析格式2
@@ -585,49 +500,109 @@ public static class FlightParser
     /// </summary>
     private static FlightInfo ParseFormat2(string line, int year)
     {
-        // 提取序号和航班号
-        var pattern = @"^(\d+)\.([A-Z0-9]+)\s+([A-Z]{2})(\d{2}[A-Z]{3})\s+([A-Z]{3})([A-Z]{3})\s+(\d{3,4})\s+(\d{3,4})(?:\+(\d+))?\s+([A-Z0-9]+)\s+(\d+)\s+([A-Z0-9]+)\s+([\dH]+)";
-        var match = Regex.Match(line.Trim(), pattern);
+        // 1. 提取序号(支持 "1." 和 "1. ")
+        var seqMatch = Regex.Match(line, @"^(\d+)\.\s*");
+        if (!seqMatch.Success) return null;
 
-        if (!match.Success) return null;
+        var sequenceNo = int.Parse(seqMatch.Groups[1].Value);
+        var content = line.Substring(seqMatch.Length).Trim();
+
+        // 2. 分割空格
+        var parts = content.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+        if (parts.Length < 8) return null;
 
-        var weekDay = match.Groups[3].Value;      // TU
-        var dateRaw = match.Groups[4].Value;      // 27MAY
-        var departureAirport = match.Groups[5].Value; // TFU
-        var arrivalAirport = match.Groups[6].Value;   // FRA
-        var departureTime = match.Groups[7].Value;    // 0135
-        var arrivalTime = match.Groups[8].Value;      // 0645
-        var isNextDay = match.Groups[9].Success && match.Groups[9].Value == "1";
-        var terminal = match.Groups[10].Value;    // T1
-        var stopCount = match.Groups[11].Value;   // 1
-        var aircraftType = match.Groups[12].Value; // 359
-        var duration = match.Groups[13].Value;     // 11H10M
-
-        var flight = new FlightInfo
+        try
         {
-            SequenceNo = int.Parse(match.Groups[1].Value),
-            FlightNumber = match.Groups[2].Value,
-            WeekDay = weekDay,
-            DepartureDateRaw = $"{weekDay}{dateRaw}",
-            DepartureAirport = departureAirport,
-            ArrivalAirport = arrivalAirport,
-            DepartureTimeRaw = departureTime,
-            ArrivalTimeRaw = arrivalTime,
-            IsNextDayArrival = isNextDay,
-            Terminal = terminal,
-            AircraftType = aircraftType,
-            Duration = duration,
-            RawText = line
-        };
+            var flight = new FlightInfo
+            {
+                SequenceNo = sequenceNo,
+                RawText = line
+            };
+
+            int idx = 0;
+
+            // 航班号
+            flight.FlightNumber = parts[idx++];
+
+            // 日期(如 TU27MAY)
+            var dateRaw = parts[idx++];
+            if (dateRaw.Length >= 7)
+            {
+                flight.WeekDay = dateRaw.Substring(0, 2);
+                flight.DepartureDateRaw = dateRaw;
+            }
+
+            // 机场(如 TFUFRA 或分开的格式)
+            var airportStr = parts[idx++];
+            if (airportStr.Length >= 6)
+            {
+                // 连写格式:TFUFRA -> TFU + FRA
+                flight.DepartureAirport = airportStr.Substring(0, 3);
+                flight.ArrivalAirport = airportStr.Substring(3, 3);
+            }
+            else
+            {
+                // 分开格式
+                flight.DepartureAirport = airportStr;
+                flight.ArrivalAirport = parts[idx++];
+            }
 
-        // 格式化时间
-        flight.DepartureTime = FormatTime(departureTime);
-        flight.ArrivalTime = FormatTime(arrivalTime);
+            // 起飞时间
+            flight.DepartureTimeRaw = parts[idx++];
+            flight.DepartureTime = FormatTime(flight.DepartureTimeRaw);
+
+            // 到达时间(可能带 +1)
+            var arrivalRaw = parts[idx++];
+            if (arrivalRaw.Contains('+'))
+            {
+                var arrParts = arrivalRaw.Split('+');
+                flight.ArrivalTimeRaw = arrParts[0];
+                flight.IsNextDayArrival = true;
+                if (arrParts.Length > 1 && int.TryParse(arrParts[1], out var days))
+                {
+                    flight.NextDayCount = days;
+                }
+            }
+            else
+            {
+                flight.ArrivalTimeRaw = arrivalRaw;
+            }
+            flight.ArrivalTime = FormatTime(flight.ArrivalTimeRaw);
+
+            // 航站楼(可选,以T开头)
+            if (idx < parts.Length && parts[idx].StartsWith("T"))
+            {
+                flight.Terminal = parts[idx++];
+            }
+
+            // 序号(可选)
+            if (idx < parts.Length && int.TryParse(parts[idx], out _))
+            {
+                // 这个序号可能是经停次数或行程序号
+                idx++;
+            }
+
+            // 机型(可选,数字或数字+字母)
+            if (idx < parts.Length && Regex.IsMatch(parts[idx], @"^\d{2,3}[A-Z]?$"))
+            {
+                flight.AircraftType = parts[idx++];
+            }
+
+            // 飞行时长(可选,包含H)
+            if (idx < parts.Length && parts[idx].Contains('H'))
+            {
+                flight.Duration = parts[idx++];
+            }
 
-        // 解析日期
-        flight.DepartureDate = ParseDate(dateRaw, year);
+            // 解析日期
+            flight.DepartureDate = ParseDate(dateRaw, year);
 
-        return flight;
+            return flight;
+        }
+        catch
+        {
+            return null;
+        }
     }
 
     #endregion
@@ -642,37 +617,43 @@ public static class FlightParser
         if (string.IsNullOrWhiteSpace(time))
             return string.Empty;
 
+        // 移除 +1 等后缀
+        var cleanTime = time.Split('+')[0];
+
         // 4位数字
-        if (time.Length == 4 && int.TryParse(time, out _))
+        if (cleanTime.Length == 4 && int.TryParse(cleanTime, out _))
         {
-            return $"{time.Substring(0, 2)}:{time.Substring(2, 2)}";
+            return $"{cleanTime.Substring(0, 2)}:{cleanTime.Substring(2, 2)}";
         }
 
         // 3位数字(905 -> 09:05)
-        if (time.Length == 3 && int.TryParse(time, out _))
+        if (cleanTime.Length == 3 && int.TryParse(cleanTime, out _))
         {
-            return $"0{time.Substring(0, 1)}:{time.Substring(1, 2)}";
+            return $"0{cleanTime.Substring(0, 1)}:{cleanTime.Substring(1, 2)}";
         }
 
         // 已经是 HH:mm 格式
-        if (Regex.IsMatch(time, @"^\d{1,2}:\d{2}$"))
+        if (Regex.IsMatch(cleanTime, @"^\d{1,2}:\d{2}$"))
         {
-            var parts = time.Split(':');
+            var parts = cleanTime.Split(':');
             return $"{parts[0].PadLeft(2, '0')}:{parts[1]}";
         }
 
-        return time;
+        return cleanTime;
     }
 
     /// <summary>
     /// 解析日期(27MAY -> 2024-05-27)
     /// </summary>
-    private static string ParseDate(string dateStr, int year)
+    private static DateTime ParseDate(string dateStr, int year)
     {
         if (string.IsNullOrWhiteSpace(dateStr))
-            return string.Empty;
+            return DateTime.MinValue;
 
-        var match = Regex.Match(dateStr, @"(\d{2})([A-Z]{3})");
+        // 移除星期前缀
+        var cleanDate = Regex.Replace(dateStr, @"^[A-Z]{2}", "");
+
+        var match = Regex.Match(cleanDate, @"(\d{2})([A-Z]{3})");
         if (match.Success)
         {
             var day = int.Parse(match.Groups[1].Value);
@@ -682,17 +663,73 @@ public static class FlightParser
             {
                 try
                 {
-                    var date = new DateTime(year, month, day);
-                    return date.ToString("yyyy-MM-dd");
+                    return new DateTime(year, month, day);
                 }
                 catch
                 {
-                    return dateStr;
+                    return DateTime.MinValue;
                 }
             }
         }
 
-        return dateStr;
+        return DateTime.MinValue;
+    }
+
+    /// <summary>
+    /// 获取完整起飞时间(含时分)
+    /// </summary>
+    public static DateTime GetFullDepartureTime(FlightInfo flight, int year)
+    {
+        if (flight.DepartureDate == DateTime.MinValue || string.IsNullOrEmpty(flight.DepartureTimeRaw))
+            return DateTime.MinValue;
+
+        var time = ParseTimeToTimeSpan(flight.DepartureTimeRaw);
+        return flight.DepartureDate.Add(time);
+    }
+
+    /// <summary>
+    /// 获取完整到达时间(含时分和跨天)
+    /// </summary>
+    public static DateTime GetFullArrivalTime(FlightInfo flight, int year)
+    {
+        if (flight.DepartureDate == DateTime.MinValue || string.IsNullOrEmpty(flight.ArrivalTimeRaw))
+            return DateTime.MinValue;
+
+        var time = ParseTimeToTimeSpan(flight.ArrivalTimeRaw);
+        var arrivalDate = flight.DepartureDate.Add(time);
+
+        if (flight.IsNextDayArrival)
+        {
+            arrivalDate = arrivalDate.AddDays(flight.NextDayCount > 0 ? flight.NextDayCount : 1);
+        }
+
+        return arrivalDate;
+    }
+
+    /// <summary>
+    /// 解析时间字符串为 TimeSpan
+    /// </summary>
+    private static TimeSpan ParseTimeToTimeSpan(string time)
+    {
+        var cleanTime = time.Split('+')[0];
+
+        if (cleanTime.Length == 4)
+        {
+            return new TimeSpan(
+                int.Parse(cleanTime.Substring(0, 2)),
+                int.Parse(cleanTime.Substring(2, 2)),
+                0);
+        }
+
+        if (cleanTime.Length == 3)
+        {
+            return new TimeSpan(
+                int.Parse(cleanTime.Substring(0, 1)),
+                int.Parse(cleanTime.Substring(1, 2)),
+                0);
+        }
+
+        return TimeSpan.Zero;
     }
 
     /// <summary>

+ 192 - 49
OASystem/OASystem.Infrastructure/Repositories/Groups/AirTicketResRepository.cs

@@ -1192,7 +1192,6 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
                 item.CabinName += "(退票)"; 
             }
 
-
             // 7.1 处理航班描述
             if (!string.IsNullOrWhiteSpace(item.FlightDesc))
             {
@@ -1209,7 +1208,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
                         ? threeCodes[flightInfo.ArrivalAirport]?.ToString() ?? flightInfo.ArrivalAirport
                         : flightInfo.ArrivalAirport;
 
-                    string desc = $"{flightInfo.SequenceNo}.{departAirport} → {arrivalAirport}({flightInfo.DepartureDate})";
+                    string desc = $"{flightInfo.SequenceNo}.{departAirport} → {arrivalAirport}({flightInfo.DepartureDate:yyyy-MM-dd})";
                     flightDescBuilder.AppendLine(desc);
                 }
 
@@ -1481,7 +1480,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
                 var custTicketInfos = g.SelectMany(x => x.CustTicketInfos ?? new List<CustTicketInfo>()).ToList();
 
                 // 累计退票金额
-                decimal refundAmount = custTicketInfos.Where(x => x.IsRefund).Sum(x => x.RefundRecord?.RefundAmount ?? 0m + x.RefundRecord?.NonRefundableTax ?? 0m);
+                decimal refundAmount = custTicketInfos.Where(x => x.IsRefund).Sum(x => (x.RefundRecord?.RefundAmount ?? 0m) + (x.RefundRecord?.NonRefundableTax ?? 0m));
 
                 // 付款金额
                 decimal payMoney = g.Sum(x => x.PayMoney) + refundAmount;
@@ -1534,7 +1533,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
         var flightsDescAll = new StringBuilder();
         foreach (var item in groupedByFlights)
         {
-            flightsDescAll.AppendLine(item.FlightsDescription);
+            flightsDescAll.Append(item.FlightsDescription);
         }
         groupInfo.FlightsDescription = flightsDescAll.ToString();
 
@@ -1641,6 +1640,9 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
     /// 保存 2026版
     /// </summary>
     /// <returns></returns>
+    /// <summary>
+    /// 保存 2026版(无ID,通过业务字段定位)
+    /// </summary>
     public async Task<JsonView<AppPushBodyView>> SaveAsync(AirTicketFeeSaveDto dto)
     {
         // 1. 基础字段设置
@@ -1680,7 +1682,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
                 cardPaymentInfo.CreateUserId = currUserId;
                 cardPaymentInfo.IsDel = 0;
 
-                // 判断是否存在(通过唯一键)
+                // ========== 通过业务字段查找现有记录 ==========
                 var existingAir = await _sqlSugar.Queryable<Grp_AirTicketReservations>()
                     .FirstAsync(x => x.DIId == groupId
                         && x.FlightsDescription == airFeeInfo.FlightsDescription
@@ -1716,12 +1718,6 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
                     var result = await UpdateExistingRecord(airFeeInfo, cardPaymentInfo, existingAir, groupId, index);
                     if (!result.Success)
                     {
-                        //if (result.IsSkip)
-                        //{
-                        //    successMsg.AppendLine(result.ErrorMessage);
-                        //    index++;
-                        //    continue;
-                        //}
                         RollbackTran();
                         return JsonView<AppPushBodyView>.Fail(result.ErrorMessage);
                     }
@@ -1902,6 +1898,12 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
     /// <param name="groupId"></param>
     /// <param name="index"></param>
     /// <returns></returns>
+    /// <summary>
+    /// 更新记录(支持舱位变更:当前舱位无人员时删除,新舱位新增或合并)
+    /// </summary>
+    /// <summary>
+    /// 更新记录(在修改时处理舱位变更)
+    /// </summary>
     private async Task<(bool Success, bool IsSkip, string ErrorMessage, int AirId, int CardId)> UpdateExistingRecord(
         Grp_AirTicketReservations airFeeInfo,
         Grp_CreditCardPayment cardPaymentInfo,
@@ -1909,67 +1911,210 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
         int groupId,
         int index)
     {
-        var airId = existingAir.Id;
-        airFeeInfo.Id = airId;
+        var existingAirId = existingAir.Id;
+        var oldCabinType = existingAir.CType;
+        var newCabinType = airFeeInfo.CType;
 
-        // 如果是退票记录,需要关联正常机票ID
-        if (airFeeInfo.RecordType == 1)
+        // 1. 验证费用状态
+        var existingCard = await _sqlSugar.Queryable<Grp_CreditCardPayment>()
+            .FirstAsync(x => x.DIId == groupId && x.CTable == 85 && x.CId == existingAirId && x.IsDel == 0);
+
+        if (existingCard != null)
         {
-            // 如果还没有关联正常机票ID,或者需要更新关联
-            if (airFeeInfo.OriginalReservationId <= 0)
+            if (existingCard.IsAuditGM == 1 || existingCard.IsAuditGM == 3)
             {
-                var normalAir1 = await _sqlSugar.Queryable<Grp_AirTicketReservations>()
-                    .FirstAsync(x => x.DIId == groupId
-                        && x.FlightsDescription == airFeeInfo.FlightsDescription
-                        && x.CType == airFeeInfo.CType
-                        && x.RecordType == 0
-                        && x.IsDel == 0);
+                return (false, true, $"第{index}条,机票费用已审核,不可操作!", 0, 0);
+            }
+            if (existingCard.IsPay == 1)
+            {
+                return (false, true, $"第{index}条,机票费用已付款,不可操作!", 0, 0);
+            }
+        }
 
-                if (normalAir1 == null)
-                {
-                    return (false, true, $"第{index}条,退票记录关联的正常机票不存在,请先添加正常机票", 0, 0);
-                }
+        // 2. 如果是退票记录,处理关联
+        if (airFeeInfo.RecordType == 1 && airFeeInfo.OriginalReservationId <= 0)
+        {
+            var normalAir = await _sqlSugar.Queryable<Grp_AirTicketReservations>()
+                .FirstAsync(x => x.DIId == groupId
+                    && x.FlightsDescription == airFeeInfo.FlightsDescription
+                    && x.CType == airFeeInfo.CType
+                    && x.RecordType == 0
+                    && x.IsDel == 0);
 
-                airFeeInfo.OriginalReservationId = normalAir1.Id;
+            if (normalAir == null)
+            {
+                return (false, true, $"第{index}条,退票记录关联的正常机票不存在,请先添加正常机票", 0, 0);
             }
-        }
 
-        // 获取付款信息
-        var existingCard = await _sqlSugar.Queryable<Grp_CreditCardPayment>()
-            .FirstAsync(x => x.DIId == groupId && x.CTable == 85 && x.CId == airId &&  x.IsDel == 0);
+            airFeeInfo.OriginalReservationId = normalAir.Id;
+        }
 
-        // 审核验证
-        if (existingCard != null)
+        // ========== 3. 处理舱位变更 ==========
+        if (oldCabinType != newCabinType)
         {
-            if (existingCard.IsAuditGM == 1 || existingCard.IsAuditGM == 3)
+            // 3.1 检查原舱位是否有退票记录
+            var hasRefund = existingAir.CustTicketInfos?.Any(x => x.IsRefund) == true;
+            if (hasRefund)
             {
-                return (false, true, $"第{index}条,机票费用已审核,不可操作!", 0, 0);
+                return (false, true, $"第{index}条,原舱位下存在退票记录,不可变更舱位!请先处理退票", 0, 0);
             }
 
-            if (existingCard.IsPay == 1)
+            // 3.2 获取原舱位的人员列表
+            var movingClients = existingAir.CustTicketInfos ?? new List<CustTicketInfo>();
+
+            if (!movingClients.Any())
             {
-                return (false, true, $"第{index}条,机票费用已付款,不可操作!", 0, 0);
+                // 没有人员,直接更新舱位类型
+                airFeeInfo.Id = existingAirId;
+                var airUpdateCount = await _sqlSugar.Updateable(airFeeInfo)
+                    .IgnoreColumns(ignoreAllNullColumns: true)
+                    .Where(x => x.Id == existingAirId)
+                    .ExecuteCommandAsync();
+
+                if (airUpdateCount < 1)
+                {
+                    return (false, false, $"第{index}条,更新舱位类型失败", 0, 0);
+                }
+
+                return (true, false, string.Empty, existingAirId, existingCard?.Id ?? 0);
+            }
+
+            // 3.3 查找目标舱位是否已存在记录
+            var targetRecord = await _sqlSugar.Queryable<Grp_AirTicketReservations>()
+                .FirstAsync(x => x.DIId == groupId
+                    && x.FlightsDescription == airFeeInfo.FlightsDescription
+                    && x.CType == newCabinType
+                    && x.RecordType == airFeeInfo.RecordType
+                    && x.IsDel == 0);
+
+            if (targetRecord != null)
+            {
+                // ========== 目标舱位已存在:合并人员 ==========
+
+                // 3.3.1 获取目标舱位的人员列表
+                var targetClients = targetRecord.CustTicketInfos ?? new List<CustTicketInfo>();
+                var existingClientIds = targetClients.Select(x => x.ClientId).ToHashSet();
+
+                // 3.3.2 合并人员(去重,保留原舱位的人员信息)
+                foreach (var movingClient in movingClients)
+                {
+                    if (!existingClientIds.Contains(movingClient.ClientId))
+                    {
+                        targetClients.Add(movingClient);
+                    }
+                }
+
+                // 3.3.3 重新计算目标舱位的总金额
+                var totalTicketPrice = targetClients.Sum(x =>
+                    x.ActualPrice + (x.AdditionalServices?.Sum(x1 => x1.Amount) ?? 0.00m));
+
+                // 3.3.4 更新目标舱位记录
+                targetRecord.CustTicketInfos = targetClients;
+                targetRecord.ClientNum = targetClients.Count;
+                targetRecord.ClientName = string.Join(",", targetClients.Select(x => x.ClientId));
+                targetRecord.Price = totalTicketPrice;
+
+                var targetUpdateCount = await _sqlSugar.Updateable(targetRecord)
+                    .IgnoreColumns(ignoreAllNullColumns: true)
+                    .Where(x => x.Id == targetRecord.Id)
+                    .ExecuteCommandAsync();
+
+                if (targetUpdateCount < 1)
+                {
+                    return (false, false, $"第{index}条,合并到目标舱位失败", 0, 0);
+                }
+
+                // 3.3.5 更新目标舱位的费用记录
+                var targetCard = await _sqlSugar.Queryable<Grp_CreditCardPayment>()
+                    .FirstAsync(x => x.DIId == groupId && x.CTable == 85 && x.CId == targetRecord.Id && x.IsDel == 0);
+
+                if (targetCard != null)
+                {
+                    targetCard.PayMoney = totalTicketPrice;
+                    targetCard.RMBPrice = totalTicketPrice * (targetCard?.DayRate ?? 1);
+                    targetCard.UpdateDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+
+                    await _sqlSugar.Updateable(targetCard)
+                        .IgnoreColumns(ignoreAllNullColumns: true)
+                        .Where(x => x.Id == targetCard.Id)
+                        .ExecuteCommandAsync();
+                }
+
+                // 3.3.6 删除原舱位记录(软删除)
+                existingAir.IsDel = 1;
+                existingAir.DeleteUserId = airFeeInfo.CreateUserId;
+                existingAir.DeleteTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+
+                await _sqlSugar.Updateable(existingAir)
+                    .UpdateColumns(x => new { x.IsDel, x.DeleteUserId, x.DeleteTime })
+                    .Where(x => x.Id == existingAirId)
+                    .ExecuteCommandAsync();
+
+                // 3.3.7 删除原舱位费用记录
+                if (existingCard != null)
+                {
+                    existingCard.IsDel = 1;
+                    existingCard.DeleteUserId = airFeeInfo.CreateUserId;
+                    existingCard.DeleteTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+
+                    await _sqlSugar.Updateable(existingCard)
+                        .UpdateColumns(x => new { x.IsDel, x.DeleteUserId, x.DeleteTime })
+                        .Where(x => x.Id == existingCard.Id)
+                        .ExecuteCommandAsync();
+                }
+
+                return (true, false, string.Empty, targetRecord.Id, targetCard?.Id ?? 0);
+            }
+            else
+            {
+                // ========== 目标舱位不存在:直接修改原记录的舱位类型 ==========
+                airFeeInfo.Id = existingAirId;
+                airFeeInfo.CreateUserId = existingAir.CreateUserId;
+                airFeeInfo.CreateTime = existingAir.CreateTime;
+
+                var airUpdateCount = await _sqlSugar.Updateable(airFeeInfo)
+                    .IgnoreColumns(ignoreAllNullColumns: true)
+                    .Where(x => x.Id == existingAirId)
+                    .ExecuteCommandAsync();
+
+                if (airUpdateCount < 1)
+                {
+                    return (false, false, $"第{index}条,更新舱位类型失败", 0, 0);
+                }
+
+                return (true, false, string.Empty, existingAirId, existingCard?.Id ?? 0);
             }
         }
 
-        // 更新机票信息
-        var airUpdateCount = await _sqlSugar.Updateable(airFeeInfo)
+        // ========== 4. 舱位未变,正常更新 ==========
+
+        // 4.1 保留创建信息
+        airFeeInfo.Id = existingAirId;
+        airFeeInfo.CreateUserId = existingAir.CreateUserId;
+        airFeeInfo.CreateTime = existingAir.CreateTime;
+        airFeeInfo.IsDel = 0;
+
+        // 4.2 更新机票信息
+        var updateCount = await _sqlSugar.Updateable(airFeeInfo)
             .IgnoreColumns(ignoreAllNullColumns: true)
-            .Where(x => x.Id == airId)
+            .Where(x => x.Id == existingAirId)
             .ExecuteCommandAsync();
 
-        if (airUpdateCount < 1)
+        if (updateCount < 1)
         {
             return (false, false, $"第{index}条,机票信息保存失败", 0, 0);
         }
 
+        // 4.3 更新或新增付款信息
         int cardId = 0;
-        cardPaymentInfo.Id = existingCard?.Id ?? 0;
-        cardPaymentInfo.CId = airId;
+        cardPaymentInfo.CId = existingAirId;
 
-        // 更新或新增付款信息
         if (existingCard != null)
         {
+            cardPaymentInfo.Id = existingCard.Id;
+            cardPaymentInfo.CreateUserId = existingCard.CreateUserId;
+            cardPaymentInfo.CreateTime = existingCard.CreateTime;
+
             var cardUpdateCount = await _sqlSugar.Updateable(cardPaymentInfo)
                 .IgnoreColumns(ignoreAllNullColumns: true)
                 .Where(x => x.Id == existingCard.Id)
@@ -1979,7 +2124,6 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
             {
                 return (false, false, $"第{index}条,机票费用信息保存失败", 0, 0);
             }
-
             cardId = existingCard.Id;
         }
         else
@@ -1990,11 +2134,10 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
             {
                 return (false, false, $"第{index}条,机票费用信息保存失败", 0, 0);
             }
-
             cardId = (int)cardIdLong;
         }
 
-        return (true, false, string.Empty, airId, cardId);
+        return (true, false, string.Empty, existingAirId, cardId);
     }
 
     #endregion
@@ -2279,7 +2422,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
             if (targetClient != null)
             {
                 targetClient.IsRefund = false;
-                targetClient.RefundRecord = null;  
+                targetClient.RefundRecord = new RefundRecord();  
             }
 
             // 6. 保存正常机票记录