Przeglądaj źródła

机票费用录入异步优化及公务导出增强

- 机票费用录入后通知、日志、汇率校验等操作异步执行,提升接口性能
- 公务信息导出接口支持团组ID筛选,导出逻辑与文件管理优化
- AI企业核验规则文案细化,强化多源公证与官网刚性约束
- 机票费用数据整理逻辑重构,退票按退款卡类型分组
- 信用卡支付视图模型增加默认值,避免空引用
- 控制器构造参数命名规范化
- AI图片识别种子消息格式与规则优化
- 统一事务处理方法,提升代码健壮性
- 其他接口校验与提示信息细节完善
Lyyyi 1 miesiąc temu
rodzic
commit
097625a5d2

Plik diff jest za duży
+ 72 - 63
OASystem/OASystem.Api/Controllers/GroupsController.cs


+ 45 - 29
OASystem/OASystem.Api/Controllers/ResourceController.cs

@@ -6321,6 +6321,11 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             return numStr;
         }
 
+        /// <summary>
+        /// 公务信息 - 导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
         [HttpPost]
         public IActionResult ExportOfficialActivitiesInfo(ExportOfficialActivitiesInfoDto dto)
         {
@@ -6344,29 +6349,31 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             if (startTime_bool && endTime_bool)
             {
                 //处理时间模板
-                var Query_DB = _sqlSugar.Queryable<Grp_DelegationInfo>()
-                                .LeftJoin<Res_OfficialActivities>((a, b) => a.Id == b.DiId && b.IsDel == 0)
-                                .LeftJoin<Sys_Users>((a, b, c) => b.CreateUserId == c.Id && c.IsDel == 0)
-                                .Where((a, b, c) => a.IsDel == 0 && a.VisitDate >= startTime_parse && a.VisitDate <= endTime_parse)
-                                .WhereIF(dto.State != -1, (a, b, c) => b.ConfirmTheInvitation == dto.State)
-                                .Select((a, b, c) => new
-                                {
-                                    a.TeamName,
-                                    b.Client,
-                                    b.ConfirmTheInvitation,
-                                    b.Contact,
-                                    ConfirmTheInvitationStr = b.ConfirmTheInvitation == 1 ? "是" : "否",
-                                    b.Date,
-                                    b.Nature,
-                                    c.CnName,
-                                    b.Address,
-                                    b.CreateTime
-                                })
-                                .ToList()
-                                .Where(x => !string.IsNullOrWhiteSpace(x.Client))
-                                .ToList();
-
-                if (Query_DB.Count > 0)
+                var query_DB = _sqlSugar.Queryable<Res_OfficialActivities>()
+                    .LeftJoin<Grp_DelegationInfo>((a, b) => a.DiId == b.Id)
+                    .LeftJoin<Sys_Users>((a, b,c) => a.CreateUserId == c.Id)
+                    .Where((a, b, c) => a.DiId == dto.GroupId && 
+                                        a.IsDel == 0 && 
+                                        !string.IsNullOrEmpty(a.Client) &&
+                                        Convert.ToDateTime(a.Date) >= startTime_parse && Convert.ToDateTime(a.Date) <= endTime_parse)
+                    .WhereIF(dto.State != -1, (a, b, c) => a.ConfirmTheInvitation == dto.State)
+                    .Select((a, b, c) => new
+                    {
+                        b.TeamName,
+                        a.Client,
+                        a.ConfirmTheInvitation,
+                        a.Contact,
+                        ConfirmTheInvitationStr = a.ConfirmTheInvitation == 1 ? "是" : "否",
+                        a.Date,
+                        a.Nature,
+                        c.CnName,
+                        a.Address,
+                        CreateTime = a.CreateTime.ToString("yyyy-MM-dd HH:mm:ss")
+                    })
+                    .OrderBy(a => a.Date)
+                    .ToList();
+
+                if (query_DB.Count > 0)
                 {
                     //打开excel
                     var ftpPath = AppSettingsHelper.Get("ExcelFtpPath");
@@ -6377,14 +6384,14 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     DataTable dtSource = new DataTable();
                     dtSource.TableName = "TB";
 
-                    var firstElem = Query_DB.First();
+                    var firstElem = query_DB.First();
                     //遍历firstElem所有属性
                     foreach (var item in firstElem.GetType().GetProperties())
                     {
                         dtSource.Columns.Add(item.Name, item.PropertyType);
                     }
 
-                    foreach (var item in Query_DB)
+                    foreach (var item in query_DB)
                     {
                         DataRow dr = dtSource.NewRow();
                         //遍历dtSource所有列头
@@ -6398,13 +6405,22 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     WorkbookDesigner designer = new WorkbookDesigner();
                     designer.Workbook = new Workbook(fileBasePath + ("Template/公务出访信息.xlsx"));
 
-                    var title = startTime_parse.ToString("yyyy年MM月dd日") + " - " + endTime_parse.ToString("yyyy年MM月dd日") + " 公务出访信息";
+                    var title = query_DB.FirstOrDefault()?.TeamName ?? "";
+                    title += "-公务出访信息(" + startTime_parse.ToString("yyyy年MM月dd日") + "-" + endTime_parse.ToString("yyyy年MM月dd日") + ")";
                     designer.SetDataSource("title", title);
                     designer.SetDataSource(dtSource);
                     designer.Process();
 
-                    var exportSerevePath = fileBasePath + "ExportOfficialActivitiesInfo/" + title + ".xlsx";
-                    var exportFtpPaht = ftpPath + "ExportOfficialActivitiesInfo/" + title + ".xlsx";
+                    var dirName = "ExportOfficialActivitiesInfo/";
+                    var serverPath = $"{fileBasePath}{dirName}";
+                    var exportSerevePath = serverPath + title + ".xlsx";
+                    var exportFtpPaht = ftpPath + dirName + title + ".xlsx";
+
+                    // 验证文件夹是否存在
+                    if (!System.IO.Directory.Exists(serverPath))
+                    {
+                        Directory.CreateDirectory(serverPath);
+                    }
 
                     //保存文件
                     designer.Workbook.Save(exportSerevePath);
@@ -6414,7 +6430,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 }
                 else
                 {
-                    jw.Data = "无公务信息!";
+                    jw.Msg = "无公务信息!";
                 }
 
             }

+ 42 - 23
OASystem/OASystem.Api/OAMethodLib/GeneralMethod.cs

@@ -1743,13 +1743,22 @@ CountryTasks: {JsonConvert.SerializeObject(countryTasks)}
 Tasks: {JsonConvert.SerializeObject(tasks)}
 
 # [REALITY_CHECK_RULES]
-
-## 1. 企业存在性三位一体核验
-必须同时满足:
-- 官网可访问且业务匹配
-- 管道A:目标国工商/证券/经商处备案
-- 管道B:LinkedIn(认证页)/Bloomberg/Crunchbase 近3年动态
-→ 任一缺失 → 整条作废
+## 1. 企业存在性三位一体核验(多源公证)
+必须同时满足,且每项至少通过 **两种独立、可公证的来源** 交叉验证:
+- 官网可访问且业务匹配(来源:企业官网 + 工商/协会/权威媒体)
+- 管道A:目标国工商/证券/经商处备案(来源:政府公示 + 权威平台)
+- 管道B:LinkedIn(官方认证页)/Bloomberg/Crunchbase 近3年动态(来源:平台 + 媒体)
+→ 任一核心字段无法通过多源公证 → 整条作废
+
+### 可接受的公证件来源(任选 ≥ 2):
+- 政府/监管:工商注册、证券交易所、驻华经商处
+- 国际平台:LinkedIn(认证页)、Bloomberg、Crunchbase
+- 行业协会/展会:国际农业展、种业协会名录
+- 主流媒体:Reuters、Bloomberg、当地财经媒体
+- 企业披露:官网 About / Contact / Imprint
+
+### 不可作为公证来源:
+- Wikipedia、论坛、博客、聚合导航站、模型推断
 
 ## 2. 实时可达性检测
 SiteUrl / PostUrl 必须通过:
@@ -1757,13 +1766,13 @@ SiteUrl / PostUrl 必须通过:
 - HTTP 200,响应 < 5s
 - HTML ≥ 500 字符
 - 不含:404/not found/page not found/under maintenance/coming soon/site is temporarily down
-→ SiteUrl 不通过 → 整条作废
+→ SiteUrl 不通过 → SiteUrl 返回 """"
 → PostUrl 不通过 → PostUrl 返回 []
 
 ## 3. 软 404 熔断
 URL 含以下关键词 → 死链:
 404 Not Found, Page Not Found, Oops, Seite nicht gefunden, foundpage, 链接不存在
-→ SiteUrl 命中 → 整条作废
+→ SiteUrl 命中 → SiteUrl 返回 """"
 → PostUrl 命中 → PostUrl 返回 []
 
 ## 4. 属地物理地址强校验
@@ -1783,7 +1792,14 @@ LTD, LLC, JSC, AO, TOO, IP, PLC, INC, CORP, GMBH, AG, BV, NV 等
 DNS(20)+HTTP200(30)+HTML≥500(20)+无软死链(20)+归属企业一致(10)
 → <90 → 对应 URL 直接丢弃
 
-## 7. 零容忍熔断
+## 7. 官网地址刚性约束(Zero-Guess Rule)
+- SiteUrl 必须是企业在互联网上真实存在、可直接访问的官网地址;
+- AI 严禁对网址进行任何形式的拼接、补全、改写或推断;
+- 仅允许原样返回企业在官网、工商备案或 AB 管道中公开披露的地址;
+- 严禁添加国别编码、语言路径、查询参数、目录层级或任何模型生成的路径片段;
+- 若模型无法确认某个网址的真实存在性,则该字段必须返回空字符串。
+
+## 8. 零容忍熔断
 触发任一即终止条目:
 - 官网不可达
 - 地址无法地图验证
@@ -1791,9 +1807,9 @@ DNS(20)+HTTP200(30)+HTML≥500(20)+无软死链(20)+归属企业一致(10)
 - 任何字段推测性补全
 → 严禁为填满配额降低标准
 
-## 8. 时效约束
-- 所有数据必须是 {DateTime.Now:yyyy-MM-dd} 当天检测结果
-- 禁止缓存 / 历史快照 / 旧索引
+## 9. 时效约束
+• 所有数据必须是 {DateTime.Now.Year} 当天检测结果
+ 禁止缓存 / 历史快照 / 旧索引
 
 # [BUSINESS_CONSTRAINTS]
 - 零虚构:禁止编造企业/人名/链接/动态
@@ -1806,15 +1822,18 @@ DNS(20)+HTTP200(30)+HTML≥500(20)+无软死链(20)+归属企业一致(10)
 - 禁止凑数,禁止降级标准
 
 # [EXECUTION_PIPELINE]
-严格顺序执行,禁止跳步、禁止提前生成企业名称或 JSON:
+严格顺序执行,禁止跳步、禁止提前生成企业名称或 JSON:
 1. 解析 CountryTasks + BusinessConstraints
 2. 检索候选企业(仅目标国)
-3. 三位一体核验(官网 + 管道A + 管道B)
+3. 多源公证核验(官网 + 管道A + 管道B + 交叉验证)
+   - 企业名称、官网、地址、联系方式必须通过 ≥2 个独立来源验证
 4. 官网实时可达性检测(DNS/HTTP/HTML)
 5. 软 404 与链接健康评分 ≥ 90
 6. 属地物理地址强校验(地图实体验证)
 7. 英文命名清洗
 8. 提取字段(官网优先,AB 管道补位)
+   - SiteUrl 必须原样取自企业公开信息,禁止任何形式的拼接、改写或补全;
+   - 拿不准就留空,严禁生成“看起来合理”的地址。
 9. 最终零容忍熔断检查
 10. 输出纯净 JSON 或失败审计
 
@@ -1823,9 +1842,10 @@ DNS(20)+HTTP200(30)+HTML≥500(20)+无软死链(20)+归属企业一致(10)
 AuditReason::StepX::{{失败原因}}
 
 示例:
-AuditReason::Step3::三位一体核验失败,无工商备案
+AuditReason::Step3::多源公证失败,企业名称仅出现在单一来源
+AuditReason::Step3::多源公证失败,官网地址无法通过工商与平台交叉验证
 AuditReason::Step4::官网不可达,DNS解析失败
-AuditReason::Step6::地址无法在Google Maps验证为真实建筑
+AuditReason::Step7::官网地址为模型拼接,已熔断
 
 # [OUTPUT_SCHEMA]
 PascalCase,禁止新增字段:
@@ -1849,12 +1869,11 @@ PascalCase,禁止新增字段:
 ]
 
 # [OUTPUT_RULES]
-- 非空结果:仅输出 JSON
-- 空结果:仅输出 AuditReason 字符串
-- 不输出 Markdown / 解释 / 思考过程
-- JSON 首字符 [,末字符 ]
-- 空结果输出 []
-- 禁止在同一响应中同时包含 JSON 与 AuditReason 文本
+• 非空结果:仅输出 JSON
+• 空结果:仅输出 AuditReason 字符串
+• 不输出 Markdown / 解释 / 思考过程
+• JSON 首字符 [,末字符 ]
+• 空结果输出 []
 
 # [EXECUTE]
 立即执行:以 {DateTime.Now.Year} 为锚点,严格按 EXECUTION_PIPELINE 执行,任何步骤失败即终止并输出 AuditReason。

+ 2 - 0
OASystem/OASystem.Domain/Dtos/Resource/OfficialActivitiesDto.cs

@@ -222,6 +222,8 @@ namespace OASystem.Domain.Dtos.Resource
 
     public class ExportOfficialActivitiesInfoDto
     {
+        [Required(ErrorMessage = "请输入有效的团组Id")]
+        public int GroupId { get; set; }
         [Required(ErrorMessage = "请输入开始时间")]
         public string StartTime { get; set; }
         [Required(ErrorMessage = "请输入结束时间")]

+ 1 - 1
OASystem/OASystem.Domain/ViewModels/Groups/Grp_CreditCardPaymentView.cs

@@ -102,7 +102,7 @@ namespace OASystem.Domain.ViewModels.Groups
         public string TotalStr3 { get; set; }
         public string TotalStr4 { get; set; }
 
-        public ExpenseAuditOverview Overview { get; set; }
+        public ExpenseAuditOverview Overview { get; set; } = new ExpenseAuditOverview();
         //public string TotalStr5 { get; set; }
     }
 

+ 56 - 29
OASystem/OASystem.Infrastructure/Repositories/Groups/AirTicketResRepository.cs

@@ -1665,7 +1665,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
         }
 
         // 3. 开启事务
-        _sqlSugar.Ado.BeginTran();
+        BeginTran();
 
         try
         {
@@ -1748,7 +1748,7 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
                 index++;
             }
 
-            _sqlSugar.Ado.CommitTran();
+            CommitTran();
 
             var view = new AppPushBodyView()
             {
@@ -1773,6 +1773,9 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
     /// <summary>
     /// 整理数据(按舱位类型分组,分离退票和正常记录)
     /// </summary>
+    /// <param name="airTicketFeeInfos"></param>
+    /// <param name="groupId"></param>
+    /// <returns></returns>
     private Task<List<AirTicketFeeInfo>> OrganizeAirTicketDataAsync(List<AirTicketFeeInfo> airTicketFeeInfos, int groupId)
     {
         var result = new List<AirTicketFeeInfo>();
@@ -1782,8 +1785,6 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
             if (airInfo.CustTicketInfos == null || !airInfo.CustTicketInfos.Any())
                 continue;
 
-            var custRecord = _mapper.Map<AirTicketFeeOpInfo>(airInfo);
-
             // 按舱位类型分组
             var groupByCabinType = airInfo.CustTicketInfos
                 .GroupBy(x => x.CType)
@@ -1793,38 +1794,53 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
             {
                 var customers = cabinGroup.ToList();
 
-                // 有退票的客户
-                var refundCustomers = customers.Where(x => x.IsRefund).ToList();
-
-                // 1. 同类型舱位(有退票信息的客户正常存储)- 合并成一条数据
-                if (customers.Any())
+                // 1. 正常记录(非退票客户)- 按舱位类型合并成一条数据
+                var normalCustomers = customers.Where(x => !x.IsRefund).ToList();
+                if (normalCustomers.Any())
                 {
-                    custRecord.ClientNum = customers.Count;
-                    custRecord.ClientName = string.Join(",", customers.Select(x => x.ClientId));
-                    custRecord.CustTicketInfos = customers;
-                    custRecord.Price = customers.Sum(x => x.ActualPrice);
+                    var custRecord = _mapper.Map<AirTicketFeeOpInfo>(airInfo);
+                    custRecord.ClientNum = normalCustomers.Count;
+                    custRecord.ClientName = string.Join(",", normalCustomers.Select(x => x.ClientId));
+                    custRecord.CustTicketInfos = normalCustomers;
+                    custRecord.Price = normalCustomers.Sum(x => x.ActualPrice);
                     custRecord.Currency = airInfo.PaymentCurrency;
-                    custRecord.PayMoney = customers.Sum(x => x.TotalTicketPrice);
+                    custRecord.PayMoney = normalCustomers.Sum(x => x.TotalTicketPrice);
+                    custRecord.RecordType = 0; // 正常记录
 
                     result.Add(custRecord);
                 }
 
-                // 2. 有退票记录的客户信息 - 按舱位类型合并成一条数据
+                // 2. 退票记录 - 按舱位类型 + 退款卡类型 分组
+                var refundCustomers = customers.Where(x => x.IsRefund).ToList();
                 if (refundCustomers.Any())
                 {
-                    custRecord.RecordType = 1; // 退票记录
-                    custRecord.OriginalReservationId = custRecord.Id; // 关联原始记录
-                    custRecord.ClientNum = refundCustomers.Count;
-                    custRecord.ClientName = string.Join(",", refundCustomers.Select(x => x.ClientId));
-                    custRecord.CustTicketInfos = refundCustomers;
-
-                    // 计算退款金额, 当前金额为负数
-                    decimal totalRefundAmount = refundCustomers.Sum(x => (x.RefundRecord?.RefundAmount ?? 0) + (x.RefundRecord?.NonRefundableTax ?? 0));
-                    custRecord.Price = totalRefundAmount;
-                    custRecord.Currency = airInfo.PaymentCurrency;
-                    custRecord.PayMoney = totalRefundAmount;
-                    custRecord.CTDId = refundCustomers.FirstOrDefault()?.RefundRecord?.RefundAccount ?? 0;
-                    result.Add(custRecord);
+                    // 按退款卡类型分组
+                    var groupByRefundAccount = refundCustomers
+                        .GroupBy(x => x.RefundRecord?.RefundAccount ?? 0)
+                        .ToList();
+
+                    foreach (var refundGroup in groupByRefundAccount)
+                    {
+                        var refundCustomersInGroup = refundGroup.ToList();
+                        var refundAccount = refundGroup.Key;
+
+                        var custRecord = _mapper.Map<AirTicketFeeOpInfo>(airInfo);
+                        custRecord.RecordType = 1; // 退票记录
+                        custRecord.OriginalReservationId = custRecord.Id; // 关联原始记录
+                        custRecord.ClientNum = refundCustomersInGroup.Count;
+                        custRecord.ClientName = string.Join(",", refundCustomersInGroup.Select(x => x.ClientId));
+                        custRecord.CustTicketInfos = refundCustomersInGroup;
+
+                        // 计算退款金额(负数)
+                        decimal totalRefundAmount = refundCustomersInGroup.Sum(x =>
+                            (x.RefundRecord?.RefundAmount ?? 0) + (x.RefundRecord?.NonRefundableTax ?? 0));
+                        custRecord.Price = totalRefundAmount;
+                        custRecord.Currency = airInfo.PaymentCurrency;
+                        custRecord.PayMoney = totalRefundAmount;
+                        custRecord.CTDId = refundAccount; // 退款账户
+
+                        result.Add(custRecord);
+                    }
                 }
             }
         }
@@ -1835,6 +1851,10 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
     /// <summary>
     /// 新增记录
     /// </summary>
+    /// <param name="airFeeInfo"></param>
+    /// <param name="cardPaymentInfo"></param>
+    /// <param name="index"></param>
+    /// <returns></returns>
     private async Task<(bool Success, string ErrorMessage, int AirId, int CardId)> InsertNewRecord(
         Grp_AirTicketReservations airFeeInfo,
         Grp_CreditCardPayment cardPaymentInfo,
@@ -1880,6 +1900,12 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
     /// <summary>
     /// 更新记录
     /// </summary>
+    /// <param name="airFeeInfo"></param>
+    /// <param name="cardPaymentInfo"></param>
+    /// <param name="existingAir"></param>
+    /// <param name="groupId"></param>
+    /// <param name="index"></param>
+    /// <returns></returns>
     private async Task<(bool Success, bool IsSkip, string ErrorMessage, int AirId, int CardId)> UpdateExistingRecord(
         Grp_AirTicketReservations airFeeInfo,
         Grp_CreditCardPayment cardPaymentInfo,
@@ -1975,12 +2001,13 @@ public class AirTicketResRepository : BaseRepository<Grp_AirTicketReservations,
         return (true, false, string.Empty, airId, cardId);
     }
 
-
     #endregion
 
     /// <summary>
     /// 单条删除 2026版
     /// </summary>
+    /// <param name="userId"></param>
+    /// <param name="id"></param>
     /// <returns></returns>
     public async Task<JsonView> SoftDelAsync(int userId, int id)
     {