Bläddra i källkod

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

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

+ 72 - 63
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -186,7 +186,7 @@ namespace OASystem.API.Controllers
         /// <param name="taskAssignmentRep"></param>
         /// <param name="airTicketResRep"></param>
         /// <param name="decreasePaymentsRep"></param>
-        /// <param name="InvitationOfficialActivitiesRep"></param>
+        /// <param name="invitationOfficialActivitiesRep"></param>
         /// <param name="delegationEnDataRep"></param>
         /// <param name="enterExitCostRep"></param>
         /// <param name="delegationVisaRep"></param>
@@ -222,6 +222,7 @@ namespace OASystem.API.Controllers
         /// <param name="processOverviewRep"></param>
         /// <param name="groupSearchService"></param>
         /// <param name="deepSeekService"></param>
+        /// <param name="doubaoService"></param>
         public GroupsController(
             ILogger<GroupsController> logger,
             ITextFileLogger eec_textLogger,
@@ -289,7 +290,7 @@ namespace OASystem.API.Controllers
                 System.IO.Directory.CreateDirectory(path);//不存在就创建文件夹
             }
             _decreasePaymentsRep = decreasePaymentsRep;
-            _InvitationOfficialActivitiesRep = InvitationOfficialActivitiesRep;
+            _InvitationOfficialActivitiesRep = invitationOfficialActivitiesRep;
             _delegationEnDataRep = delegationEnDataRep;
             _enterExitCostRep = enterExitCostRep;
             _delegationVisaRep = delegationVisaRep;
@@ -12138,6 +12139,9 @@ FROM
         #endregion
 
         #region 机票费用录入
+
+        #region 机票费用录入 - 旧版
+
         /// <summary>
         /// 机票录入当前登录人可操作团组
         /// </summary>
@@ -13220,6 +13224,9 @@ FROM
             }
         }
 
+
+        #endregion
+
         #region 机票费用录入 - 2026版
 
         /// <summary>
@@ -16558,64 +16565,71 @@ FROM
                     return Ok(JsonView(false, data.Msg));
                 }
 
-                //生成默认文件pdf并且通知人员
-                var fileView = await GeneralMethod.EnterExitCostDownload(new EnterExitCostDownloadDto()
-                {
-                    DiId = dto.DiId,
-                    ExportType = 1,
-                    SubTypeId = 1005
-                }, "pdf");
 
-                //发送通知
-                string fileUrl = (string)fileView.Data.GetType().GetProperty("Url").GetValue(fileView.Data, null);
-                int sign = (int)data.Data.GetType().GetProperty("sign").GetValue(data.Data, null);
-                string md5Sign = GeneralMethod.Encrypt($"{sign}&fileName={fileUrl}");
-                string url = string.Format("http://oa.pan-american-intl.com:4399/#/Sankunginfo?sign={0}&fileName={1}", sign, fileUrl);
-                await AppNoticeLibrary.SendUserMsg_GroupShare_ToGM(dto.DiId, new List<string>() { "208", "233", "21" }, dto.UserId, url);
-                //汇率信息记录
-                await GeneralMethod.RateRecordSave(dto.UserId, sign, "出入境费用");
+                #region 异步 通知、日志记录等后续操作
 
-                var checkCurrencys = await _enterExitCostRep.GetCheckCurrencies(dto.DiId);
-                //汇率验证、反推 
-                (bool isSendMsg, string msgContent, List<CurrencyInfo> liveRates) = await GeneralMethod.EnterExitCostCheckRate(dto.DiId, checkCurrencys);
-                if (isSendMsg)
+                _ = Task.Run(async () =>
                 {
-                    //消息推送
-                    await AppNoticeLibrary.SendUserMsg_GroupShare_ToFin(dto.DiId, dto.UserId, msgContent);
-                }
+                    // 生成默认文件pdf并且通知人员
+                    var fileView = await GeneralMethod.EnterExitCostDownload(new EnterExitCostDownloadDto()
+                    {
+                        DiId = dto.DiId,
+                        ExportType = 1,
+                        SubTypeId = 1005
+                    }, "pdf");
 
-                #region 日志汇率记录
+                    // 发送通知
+                    string fileUrl = (string)fileView.Data.GetType().GetProperty("Url").GetValue(fileView.Data, null);
+                    int sign = (int)data.Data.GetType().GetProperty("sign").GetValue(data.Data, null);
+                    string md5Sign = GeneralMethod.Encrypt($"{sign}&fileName={fileUrl}");
+                    string url = string.Format("http://oa.pan-american-intl.com:4399/#/Sankunginfo?sign={0}&fileName={1}", sign, fileUrl);
+                    await AppNoticeLibrary.SendUserMsg_GroupShare_ToGM(dto.DiId, new List<string>() { "208", "233", "21" }, dto.UserId, url);
+                    // 汇率信息记录
+                    await GeneralMethod.RateRecordSave(dto.UserId, sign, "出入境费用");
 
-                //按照已使用币种记录
-                if (checkCurrencys != null && checkCurrencys.Count > 0)
-                {
-                    rateRecords = rateRecords.Where(x => checkCurrencys.Select(x1 => x1.CurrencyName).ToList().Contains(x.CurrencyName)).ToList();
+                    var checkCurrencys = await _enterExitCostRep.GetCheckCurrencies(dto.DiId);
+                    // 汇率验证、反推 
+                    (bool isSendMsg, string msgContent, List<CurrencyInfo> liveRates) = await GeneralMethod.EnterExitCostCheckRate(dto.DiId, checkCurrencys);
+                    if (isSendMsg)
+                    {
+                        //消息推送
+                        await AppNoticeLibrary.SendUserMsg_GroupShare_ToFin(dto.DiId, dto.UserId, msgContent);
+                    }
 
-                    //汇率记录中加入实时汇率
-                    if (liveRates != null && liveRates.Count > 0)
+                    #region 日志汇率记录
+
+                    //按照已使用币种记录
+                    if (checkCurrencys != null && checkCurrencys.Count > 0)
                     {
-                        foreach (var item in rateRecords)
+                        rateRecords = rateRecords.Where(x => checkCurrencys.Select(x1 => x1.CurrencyName).ToList().Contains(x.CurrencyName)).ToList();
+
+                        //汇率记录中加入实时汇率
+                        if (liveRates != null && liveRates.Count > 0)
                         {
-                            item.CurrLiveRate = liveRates.Where(x => x.CurrencyName.Equals(item.CurrencyName)).FirstOrDefault()?.Rate ?? 0.00M;
+                            foreach (var item in rateRecords)
+                            {
+                                item.CurrLiveRate = liveRates.Where(x => x.CurrencyName.Equals(item.CurrencyName)).FirstOrDefault()?.Rate ?? 0.00M;
+                            }
                         }
-                    }
 
-                    var teamName = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.Id == dto.DiId && x.IsDel == 0).Select(x => x.TeamName).FirstAsync();
-                    var operationName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.UserId && x.IsDel == 0).Select(x => x.CnName).FirstAsync();
+                        var teamName = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.Id == dto.DiId && x.IsDel == 0).Select(x => x.TeamName).FirstAsync();
+                        var operationName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.UserId && x.IsDel == 0).Select(x => x.CnName).FirstAsync();
 
-                    var logContent = new StringBuilder();
-                    foreach (var item in rateRecords)
-                    {
+                        var logContent = new StringBuilder();
+                        foreach (var item in rateRecords)
+                        {
+                            var blRate = (item.CurrLiveRate * 1.0350M).TruncDecimals(4);
 
-                        var blRate = (item.CurrLiveRate * 1.0350M).TruncDecimals(4);
+                            string msg = $">- {item.CurrencyName}汇率:(页面保存前){item.UpdatePreRate:#0.0000}/(页面保存){item.UpdateBreRate:#0.0000}/实时汇率(已上浮1.0350):{blRate:#0.0000}【注:原接口输出汇率为:{item.CurrLiveRate:#0.0000}】";
 
-                        string msg = $">- {item.CurrencyName}汇率:(页面保存前){item.UpdatePreRate:#0.0000}/(页面保存){item.UpdateBreRate:#0.0000}/实时汇率(已上浮1.0350):{blRate:#0.0000}【注:原接口输出汇率为:{item.CurrLiveRate:#0.0000}】";
+                            logContent.AppendLine(msg);
+                        }
 
-                        logContent.AppendLine(msg);
+                        _eec_textLogger.LogInformation("团组:【{teamName}({groupId})】 操作人:【{operationName}】 操作时间:【{operationTime}】\r\n{logContent}", teamName, dto.DiId, operationName, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), logContent.ToString());
                     }
+                    #endregion
+                });
 
-                    _eec_textLogger.LogInformation("团组:【{teamName}({groupId})】 操作人:【{operationName}】 操作时间:【{operationTime}】\r\n{logContent}", teamName, dto.DiId, operationName, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), logContent.ToString());
-                }
                 #endregion
 
                 return Ok(JsonView(true, data.Msg, data.Data));
@@ -16669,10 +16683,13 @@ FROM
                                                 1 明细表 --> 1005(默认明细表) 1006(因公出国(境)经费测算明细表) 1007(四川省商务厅出国经费财政先行审核表)  
                                                 2 表格 --> 1008(派员单位出(境)任务和预算审批意见表) 1009(省级单位出(境)经费报销单)
                                                 3 团组成员名单 1 团组成员名单", ""));
+                
+                var _DelegationInfo = _sqlSugar.Queryable<Grp_DelegationInfo>().Where(it => it.IsDel == 0 && it.Id == dto.DiId).First();
+                if (_DelegationInfo == null) return Ok(JsonView(400, "团组信息为空", ""));
 
                 var _EnterExitCosts = _sqlSugar.Queryable<Grp_EnterExitCost>().Where(it => it.IsDel == 0 && it.DiId == dto.DiId).OrderByDescending(x => x.CreateTime).First();
                 var _DayAndCosts = _sqlSugar.Queryable<Grp_DayAndCost>().Where(it => it.IsDel == 0 && it.DiId == dto.DiId).ToList();
-                if (_EnterExitCosts == null) return Ok(JsonView(400, "该团组未填写出入境费用", ""));
+                if (_EnterExitCosts == null) return Ok(JsonView(400, "该团组未填写出入境费用", ""));
 
                 var currencyInit = _sqlSugar.Queryable<Sys_SetData>().Where(x => x.IsDel == 0 && x.STid == 66).ToList();
                 var enterExitCostCurrencys = (List<CurrencyInfo>?)CommonFun.GetCurrencyChinaToList(_EnterExitCosts.CurrencyRemark);
@@ -16718,13 +16735,7 @@ FROM
                 }
 
                 var _CurrDatas = _sqlSugar.Queryable<Sys_SetData>().Where(it => it.IsDel == 0 && it.STid == 66).ToList();
-                var _DelegationInfo = _sqlSugar.Queryable<Grp_DelegationInfo>().Where(it => it.IsDel == 0 && it.Id == dto.DiId).First();
-
-                if (_DelegationInfo == null)
-                {
-                    return Ok(JsonView(400, "团组信息为空!", ""));
-                }
-
+                
                 var DeleClientList = _sqlSugar.Queryable<Grp_TourClientList>()
                     .LeftJoin<Crm_DeleClient>((tcl, dc) => tcl.ClientId == dc.Id && dc.IsDel == 0)
                     .LeftJoin<Crm_CustomerCompany>((tcl, dc, cc) => dc.CrmCompanyId == cc.Id && dc.IsDel == 0)
@@ -34294,20 +34305,18 @@ AirHotelPrice
                     new SeedMessages { Role = KimiRole.system, Content = "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。" },
                     //new SeedMessages { Role = KimiRole.user, Content = "根据文件内容 ,按照“姓、名、姓名拼音、性别、单位、职务、出生日期”的json格式来排列整理(性别和出生日期通过身份证来识别 , 当识别到性别为男时值给0女给1 未识别到性别的情况下值给-1,出生日期按照yyyy-MM-dd来整理(若出生日期没有值则为‘-’),文件存在拼音的情况读取对应的拼音不存在则设置拼音为中文名字全拼),只输出JSON数组数据 从'['开始 到']'结尾 ,不要除JSON数据外的任何文字以及符号" }
                     new SeedMessages { Role = KimiRole.user, Content =
-                    @" 严格按照以下规则处理上传的文件内容,只输出纯JSON数组,不要输出任何其他文字、解释、标记或符号:
+                    @" 从图片提取人员信息,输出纯JSON数组,不要任何其他文字。
 
-                       输出格式:
-                       [{""姓"":"""",""名"":"""",""姓名拼音"":"""",""性别"":0,""单位"":"""",""职务"":"""",""出生日期"":""yyyy-MM-dd""}]
+格式:[{""姓"":"""",""名"":"""",""姓名拼音"":"""",""性别"":0,""单位"":"""",""职务"":"""",""出生日期"":""yyyy-MM-dd""}]
 
-                       规则:
-                       1. 姓、名:从姓名中拆分(中文姓名姓在前名在后
-                       2. 姓名拼音:文件中存在则使用,不存在则生成中文全拼(小写)
-                       3. 性别:通过身份证第17位判断,奇数为男→0,偶数为女→1,无法识别→-1
-                       4. 出生日期:从身份证第7-14位提取,格式yyyy-MM-dd,无身份证则填""-""
-                       5. 单位、职务:按文件实际内容填写
+规则:
+- 姓:首字;复姓取前两字(欧阳/司马/上官等
+- 拼音:大写,姓/名用/隔开,名多字空格分隔,ü→v。例:张三→ZHANG/SAN,欧阳明→OU/YANG MING
+- 性别:男0女1,无则-1
+- 出生日期:年月补01,无则""-""
+- 单位职务:机构名称为单位,职位为职务,仅有其一另一填""-""
 
-                       严格要求:输出必须以[开头,以]结尾,不要```json标记,不要任何解释文字。
-                    "
+要求:以[开头]结尾,无标记无解释,合法JSON。 "
                     }
                 };
 

+ 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)
     {