Browse Source

1. 重构SSE组件,优化stream方式。
2. 新增商邀AI关键字条件筛选相关接口更改。
3. 本地数据新增AI行业分析。
4. AI访问词优化。

Lyyyi 1 week ago
parent
commit
338511138b

+ 423 - 210
OASystem/OASystem.Api/Controllers/ResourceController.cs

@@ -2628,63 +2628,42 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
         [HttpPost]
         [HttpPost]
         public async Task InvitationAISearchStreamProgress([FromBody] InvitationAISearchDto dto)
         public async Task InvitationAISearchStreamProgress([FromBody] InvitationAISearchDto dto)
         {
         {
-            // 强制关闭响应缓冲
-            var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
-            if (syncIOFeature != null) syncIOFeature.AllowSynchronousIO = true;
-
-            var response = HttpContext.Response;
-
-            // --- SSE 核心协议头配置 ---
-            response.Headers.Add("Content-Type", "text/event-stream");
-            response.Headers.Add("Cache-Control", "no-cache");
-            response.Headers.Add("Connection", "keep-alive");
-            response.Headers.Add("X-Accel-Buffering", "no");
-
-            // 定义流式推送匿名函数
-            async Task SendStep(int progress, string message, object data = null)
-            {
-                var settings = new JsonSerializerSettings
-                {
-                    ContractResolver = new CamelCasePropertyNamesContractResolver() // 强制小驼峰转换
-                };
-
-                var payload = JsonConvert.SerializeObject(new { progress, message, data }, settings);
-                var bytes = Encoding.UTF8.GetBytes($"data: {payload}\n\n");
-
-                await response.Body.WriteAsync(bytes, 0, bytes.Length);
-                await response.Body.FlushAsync(); // 立即清空缓冲区发送
-            }
+            HttpContext.InitializeSse();
 
 
             try
             try
             {
             {
-                await SendStep(5, "正在加载配置参数...");
+                await HttpContext.SendSseStepAsync(5, "正在加载配置...");
+
+                #region 1. 异步并行化验证 (Performance Boost)
+                var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.IsDel == 0 && x.Id == dto.Id).FirstAsync();
+                var operatorName = await _sqlSugar.Queryable<Sys_Users>()
+                    .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
+                    .Select(x => x.CnName).FirstAsync();
 
 
-                #region 1. 参数与权限验证
-                var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
                 if (invAiInfo?.EntryInfo == null)
                 if (invAiInfo?.EntryInfo == null)
                 {
                 {
-                    await SendStep(-1, "未找到有效的关键字配置信息。");
+                    await HttpContext.SendSseStepAsync(-1, "未找到有效的关键字配置。");
                     return;
                     return;
                 }
                 }
 
 
                 var entryInfo = invAiInfo.EntryInfo;
                 var entryInfo = invAiInfo.EntryInfo;
-                var operatorName = await _sqlSugar.Queryable<Sys_Users>()
-                    .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
-                    .Select(x => x.CnName).FirstAsync() ?? "-";
+                var targetCountrySet = new HashSet<string>(entryInfo.TargetCountry);
                 #endregion
                 #endregion
 
 
-                await SendStep(20, "正在翻阅本地典籍,执行高性能并行解密...");
+                await HttpContext.SendSseStepAsync(20, "正在解析本地典籍 (并行解密)...");
 
 
-                #region 2. 本地数据并行解密 (Performance Boost)
+                #region 2. 内存计算优化
+                // 仅查询必要字段,减少 DataReader 压力
                 var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
                 var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
                     .Where(x => x.IsDel == 0)
                     .Where(x => x.IsDel == 0)
+                    .Select(x => new { x.Country, x.UnitName, x.Address, x.Field, x.Contact, x.Tel, x.Email })
                     .ToListAsync();
                     .ToListAsync();
 
 
-                // 使用 PLINQ 并行解密,极大提升解密密集型任务速度
+                // PLINQ 核心炼金:利用多核并行解密
                 var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAIInfo
                 var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAIInfo
                 {
                 {
                     Guid = Guid.NewGuid().ToString("N"),
                     Guid = Guid.NewGuid().ToString("N"),
-                    Source = 0,
+                    Source = 0, // 标识来源:本地
                     Region = AesEncryptionHelper.Decrypt(item.Country),
                     Region = AesEncryptionHelper.Decrypt(item.Country),
                     NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
                     NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
                     Address = AesEncryptionHelper.Decrypt(item.Address),
                     Address = AesEncryptionHelper.Decrypt(item.Address),
@@ -2696,161 +2675,272 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     Operator = operatorName,
                     Operator = operatorName,
                 }).ToList();
                 }).ToList();
 
 
-                // 筛选目标国家并计算 AI 缺口
+                // 筛选符合国家的本地数据
+                var matchedCountries = allDecrypted.Where(x => targetCountrySet.Contains(x.Region)).ToList();
+                #endregion
+
+                #region 3. 混元 AI 协同炼金 (双阶段)
+
+                // --- 阶段 A: 行业匹配分析 ---
+                if (matchedCountries.Any())
+                {
+                    await HttpContext.SendSseStepAsync(40, $"AI 正在执行 {entryInfo.TargetCountry.Count} 国的行业契合度分析...");
+                    string industryQuestion = BuildIndustryPrompt(entryInfo, matchedCountries);
+                    string industryRaw = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(industryQuestion);
+
+                    var industryMatches = CleanAndParseJson<List<IndustryMatchResult>>(industryRaw) ?? new();
+                    var matchedNames = new HashSet<string>(industryMatches.Select(x => x.TargetUnitName));
+                    // 重新过滤:仅保留 AI 认为匹配的本地数据
+                    matchedCountries = matchedCountries.Where(x => matchedNames.Contains(x.NameCn)).Distinct().ToList();
+                }
+
+                // --- 阶段 B: 差额补全 (Gap Filling) ---
                 var localInvDatas = new List<InvitationAIInfo>();
                 var localInvDatas = new List<InvitationAIInfo>();
                 var aiTasks = new List<CountryAIPormptInfo>();
                 var aiTasks = new List<CountryAIPormptInfo>();
-                int targetPerCountry = entryInfo.NeedCount;
 
 
-                foreach (var countryName in entryInfo.TargetCountry)
+                foreach (var country in entryInfo.TargetCountry)
                 {
                 {
-                    var countryMatched = allDecrypted.Where(x => x.Region == countryName).Take(targetPerCountry).ToList();
-                    localInvDatas.AddRange(countryMatched);
+                    var countryData = matchedCountries.Where(x => x.Region == country).Take(entryInfo.NeedCount).ToList();
+                    localInvDatas.AddRange(countryData);
 
 
-                    int gap = targetPerCountry - countryMatched.Count;
-                    if (gap > 0) aiTasks.Add(new() { Country = countryName, Count = gap });
+                    int gap = entryInfo.NeedCount - countryData.Count;
+                    if (gap > 0) aiTasks.Add(new() { Country = country, Count = gap });
                 }
                 }
-                #endregion
 
 
-                #region 3. 混元 AI 远程炼金
                 var hunyuanAIInvDatas = new List<InvitationAIInfo>();
                 var hunyuanAIInvDatas = new List<InvitationAIInfo>();
                 if (aiTasks.Any())
                 if (aiTasks.Any())
                 {
                 {
-                    await SendStep(60, $"Hunyuan AI 正在跨境深度检索 {aiTasks.Count} 个国家的邀请单位信息...");
+                    // 强制冷却(应对 QPS 限制)
+                    // 混元免费版或低阶版本通常有 1s/1次 的频率限制
+                    await Task.Delay(1000);
 
 
-                    string question = BuildHunyuanPrompt(aiTasks, entryInfo); // 抽离 Prompt 构造
-                    string aiRawResponse = string.Empty;
+                    await HttpContext.SendSseStepAsync(60, $"AI 正在跨境检索缺失的 {aiTasks.Sum(x => x.Count)} 条单位资料...");
+                    string searchQuestion = BuildHunyuanPrompt(aiTasks, entryInfo);
+                    string searchRaw = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(searchQuestion);
 
 
-                    try
+                    var aiParsed = CleanAndParseJson<List<InvitationAIInfo>>(searchRaw);
+                    if (aiParsed != null)
                     {
                     {
-                        aiRawResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(question);
-
-                        // 清理 Markdown 代码块包裹
-                        string cleanJson = aiRawResponse.Trim();
-                        if (cleanJson.StartsWith("```json")) cleanJson = cleanJson[7..^3].Trim();
-                        else if (cleanJson.StartsWith("```")) cleanJson = cleanJson[3..^3].Trim();
-
-                        var aiParsed = JsonConvert.DeserializeObject<List<InvitationAIInfo>>(cleanJson);
-                        if (aiParsed != null)
-                        {
-                            hunyuanAIInvDatas = aiParsed.Select(x => {
-                                x.Guid = Guid.NewGuid().ToString("N");
-                                x.Source = 1; // AI 来源
-                                x.Operator = operatorName;
-                                x.OperatedAt = DateTime.Now;
-                                return x;
-                            }).ToList();
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Hunyuan AI 接口调用或解析失败");
-                        await SendStep(60, "警告:Hunyuan AI 检索部分失败,将仅展示本地数据。");
+                        hunyuanAIInvDatas = aiParsed.Select(x => {
+                            x.Guid = Guid.NewGuid().ToString("N");
+                            x.Source = 1; // 标识来源:AI 炼金
+                            x.Operator = operatorName;
+                            x.OperatedAt = DateTime.Now;
+                            return x;
+                        }).ToList();
                     }
                     }
                 }
                 }
                 #endregion
                 #endregion
 
 
-                await SendStep(90, "执行数据合并与持久化...");
+                await HttpContext.SendSseStepAsync(90, "正在同步成果至持久化仓库...");
 
 
-                #region 4. 数据合并与入库
+                #region 4. 数据融合与更新
                 var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
                 var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
-                invAiInfo.AiCrawledDetails = finalResult;
 
 
-                var updateSuccess = await _sqlSugar.Updateable(invAiInfo).ExecuteCommandAsync() > 0;
-                if (!updateSuccess)
-                {
-                    await SendStep(-1, "数据库更新异常。");
-                    return;
-                }
+                // 仅更新指定列,性能更优
+                invAiInfo.AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList();
+                bool isOk = await _sqlSugar.Updateable(invAiInfo)
+                    .UpdateColumns(x => x.AiCrawledDetails)
+                    .ExecuteCommandHasChangeAsync();
+
+                if (!isOk) await HttpContext.SendSseStepAsync(-1, $"数据持久化失败。"); 
                 #endregion
                 #endregion
 
 
-                // 5. 终推送:带上全量结果
-                await SendStep(100, "操作成功!资料已全部就绪。", new
+                // 5. 终推送
+                await HttpContext.SendSseStepAsync(100, "操作成功!资料已全部就绪。", new
                 {
                 {
                     invAiInfo.Id,
                     invAiInfo.Id,
-                    invAiInfo.InvName,
-                    AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList(),
-                    Entry = invAiInfo.EntryInfo
+                    AiCrawledDetails = finalResult.OrderByDescending(x => x.Source).ThenBy(x => x.Region).ToList()
                 });
                 });
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
-                _logger.LogError(ex, "SSE 发生不可预知异常");
-                await SendStep(-1, $"SSE Error:{ex.Message}");
+                _logger.LogError(ex, "SSE 管道熔断");
+                await HttpContext.SendSseStepAsync(-1, $"SSE 错误:{ex.InnerException.Message}({ex.Message})");
             }
             }
             finally
             finally
             {
             {
-                // 确保最后一次冲刷缓冲区
-                await HttpContext.Response.Body.FlushAsync();
+                await HttpContext.FinalizeSseAsync();
+            }
+        }
 
 
-                // 此时方法结束,ASP.NET Core 会自动处理连接关闭
-                _logger.LogInformation("SSE 通道已安全关闭");
+        /// <summary>
+        /// 炼金辅助:清洗并解析 AI 返回的 JSON 块
+        /// </summary>
+        private static T? CleanAndParseJson<T>(string rawResponse)
+        {
+            if (string.IsNullOrWhiteSpace(rawResponse)) return default;
 
 
-                // 直接完成响应流
-                await response.CompleteAsync();
+            string cleanJson = rawResponse.Trim();
+            // 自动剥离 Markdown 语法糖
+            if (cleanJson.Contains("```json"))
+                cleanJson = cleanJson.Split("```json")[1].Split("```")[0];
+            else if (cleanJson.Contains("```"))
+                cleanJson = cleanJson.Split("```")[1].Split("```")[0];
 
 
-            }
-            return;
+            return JsonConvert.DeserializeObject<T>(cleanJson.Trim());
         }
         }
 
 
-        private string BuildHunyuanPrompt(List<CountryAIPormptInfo> tasks, EntryInfo entryInfo)
-        {
-            // 保持你原有的高质量 Prompt 结构
-            return  @$"# [SYSTEM_CONTEXT]
-- Role: Senior Business Consultant (Global Supply Chain & Cross-border Trade Expert)
-- Framework: S.P.A.R. (Situation-Problem-Action-Result)
-- Target Architecture: .NET 6 DTO Compatible Interface
+        /// <summary>
+        /// 构建行业匹配提示词(含预筛选逻辑)
+        /// <param name="entryInfo">【必填】基础配置信息</param>
+        /// <param name="localData">【数据源】来自 SqlSugar 查询的本地候选数据集,需包含 Region 和 NameCn 字段</param>
+        /// <returns>返回经过结构化处理的 Markdown 格式 Prompt 字符串</returns>
+        /// </summary>
+        public static string BuildIndustryPrompt(
+            EntryInfo entryInfo,
+            IEnumerable<InvitationAIInfo> localData)
+        {
+            // 业务逻辑预处理:仅保留目标国家数据,大幅节省 Token 成本
+            var filteredData = localData
+                .Where(x => entryInfo.TargetCountry.Contains(x.Region))
+                .Select(x => new { x.Region, x.NameCn }) // 仅提取 AI 需要的字段
+                .Distinct()
+                .ToList();
 
 
-# [INPUT_CONFIG]
-- Tasks: {{{JsonConvert.SerializeObject(tasks)}}}
-- OriginUnit: {{{entryInfo.OriginUnit}}} 
-- Objective: {{{entryInfo.Objective}}}
-- OtherConstraints: {{{entryInfo.OtherConstraints}}}
-# [ROLE_DEFINITION]
-你是一位精通全球跨境经贸、海外园区政策及 Tasks 中的国家本地准入法规的【顶级商务咨询顾问】。你擅长通过“产业链对等原则(Chain-Parity Principle)”为跨国企业精准匹配具备落地价值的合作伙伴。
+            // 序列化数据
+            var industryStandard = IndustryNode.BuildInitialData().Select(x => x.NameCn).ToList();
+            string industryStandardStr = string.Join("、", industryStandard);
+            string countriesStr = string.Join(", ", entryInfo.TargetCountry);
+            string jsonData = JsonConvert.SerializeObject(filteredData);
+            string sourceUnit = entryInfo.OriginUnit ?? "未知单位";
+            string sourceIndustry = string.Join(", ", entryInfo.Industries);
 
 
-# [BUSINESS_LOGIC_CoT]
-在生成结果前,请严格执行以下思维链拆解:
-1. **ScopePartitioning**: 遍历 Tasks 中的每个国家。
-1. **EntityProfiling**: 识别 {{OriginUnit}} 的核心生态位(如:Supply/Demand/Capital/Logistics)。
-2. **ParityMatching**: 针对每个国家,检索境内业务闭环对等机构。(例如:若 Origin 为分销,则 Target 为源头工厂/种植园)。
-3. **RegulatoryCheck**: 验证目标机构的合规性(如:GACC 备案、SPS 协议、或当地政府特许经营权)。
-4. **DataSynthesis**: 必须严格根据每个国家对应的 Count 生成数据条数。针对无法直接获取的动态(如 PostUrl),优先检索其官网 News 频道或 LinkedIn 企业号。
+            // 将提示词模板定义为常量,方便维护和查阅
+            string promptTemplate = @"
+# Role
+你是一位精通全球产业结构与大数据清洗的【智能匹配专家】。
 
 
-# [CONSTRAINTS_&_STANDARDS]
-- **NamingConvention**: 所有 JSON Key 必须严格遵循 **PascalCase**(例如:`UnitNameCn` 而非 `unit_name_cn`)。
-- **DataIntegrity**: 
-    - 总条数必须等于 Tasks 中所有 Count 的总和。
-    - 优先级:Core (核心机构) > Backup (关联替代机构)。
-- **ValidationRules**:
-    - `Phone`: 必须包含 Tasks 中的国家国际区号(如 +856, +66 等)。
-    - `IntgAdvice`: 必须包含“对等性分析”,解释该机构如何与 {{OriginUnit}} 形成业务闭环(50-100字)。
-    - `Status`: 若字段确实无法获取,统一填充 'N/A',禁止编造。
-- **Safety**: 确保推荐机构不涉及敏感黑名单或已破产企业。
+# Standard Industry Categories (Dynamic)
+请将目标单位严格归类为以下指定的行业类别:
+{0}
+*注意:若无法完全匹配,请归类至语义最接近的一项。*
 
 
-# [INFORMATION_SCHEMA]
-请将结果填充至以下结构的 JSON 数组中:
-- Region:国家(必须与 Tasks 中的 Country 完全匹配)
-- NameCn: 单位名称(中文)
-- NameEn: 单位名称(英文)
-- Address: 详细地理位置(含省市区街道)
-- Scope: 经营范围(需强调其出口配额、生产能力或行业地位)
-- Contact: 联系人姓名及职务
-- Phone: 拨打全号(含区号)
-- Email: 商务联络邮箱
-- SiteUrl: 官方网站或权威社媒主页
-- PostUrl: 近一年内的商务动态/新闻链接
-- RecLevel: 推荐等级(枚举值:Core, Backup)
-- IntgAdvice: 对接深度建议(基于产业链交合、互补逻辑)
+# Situation (Dynamic Input)
+- **出访单位**:{1}
+- **其所属行业**:{2}
+- **目标出访国家**:{3}
 
 
-# [OUTPUT_PROTOCOL]
-- 仅输出一个标准的 JSON Array 字符串。
-- 严禁任何 Markdown 说明文字、代码块之外的解释或开场白。
-- 确保 JSON 语法在 .NET `JsonSerializer.Deserialize` 下可直接解析。
+# Task
+分析【本地待匹配数据集】,执行以下逻辑:
+1. **实体一致性 (Critical)**:返回结果中的 TargetUnitName 必须与输入数据集中的 NameCn 完全一致,严禁修改。
+2. **语义归类**:分析业务关键词,并映射至上述标准行业分类。
+3. **关联度计算**:计算与出访单位的业务契合度 (ConfidenceScore: 0.0-1.0)。
+
+# Rules
+- **输出格式**:仅返回纯 JSON 数组,严禁任何解释性文字。
+- **命名规范**:JSON 键名严格使用 PascalCase。
+
+# Data Source (JSON Array)
+{4}
+
+# Output Format (Required JSON)
+[
+  {{
+    ""SourceUnitName"": ""{1}"",
+    ""TargetUnitName"": ""必须与输入数据原样一致"",
+    ""TargetCountry"": ""所在国家"",
+    ""MatchedIndustry"": ""必须匹配标准行业分类中的原词"",
+    ""ConfidenceScore"": 0.00,
+    ""MatchReason"": ""匹配理由""
+  }}
+]
+";
+            // 填充模板并返回
+            return string.Format(
+                promptTemplate,
+                industryStandardStr, // {0}
+                sourceUnit,          // {1}
+                sourceIndustry,      // {2}
+                countriesStr,        // {3}
+                jsonData             // {4}
+            );
+        }
+
+        /// <summary>
+        /// 混元提示词构建(高度定制化,适配商邀资料场景)
+        /// </summary>
+        /// <param name="tasks">包含国家及 Count 数量的任务列表</param>
+        /// <param name="entryInfo">包含规则基础信息</param>
+        /// <returns>最终构建的 System Prompt 字符串</returns>
+        private static string BuildHunyuanPrompt(List<CountryAIPormptInfo> tasks, EntryInfo entryInfo)
+        {
+            // 行业信息,用于提示词硬约束
+            string industryEnum = string.Join("、", IndustryNode.Roots.Select(x => x.NameCn));
+
+            //构建其他约束条件字符串
+            var otherConstraintsStr = new StringBuilder();
+            if (entryInfo.Industries.Any()) otherConstraintsStr.Append($"- 行业信息: {string.Join("、", entryInfo.Industries)};");
+            if (entryInfo.ScaleTypes.Any()) otherConstraintsStr.Append($"- 单位规模: {string.Join("、", entryInfo.ScaleTypes)};");
+            if (entryInfo.IsBackground) otherConstraintsStr.Append($"- 单位是否包含华人背景: 是;");
+            if (!string.IsNullOrEmpty(entryInfo.OtherConstraints)) otherConstraintsStr.Append($"- 其他规则: {entryInfo.OtherConstraints};");
+
+            return @$"
+# [SYSTEM_ROLE]
+你是一位精通全球跨境经贸、海外园区政策及准入法规的【顶级商务咨询顾问】。同时,你具备资深的 .NET 6 软件架构思维,擅长生成高精度、符合强类型反序列化要求的结构化 JSON 数据。
+
+# [CONTEXT_ANALYSIS]
+- **发起单位 (OriginUnit)**: {entryInfo.OriginUnit}
+- **核心目标 (Objective)**: {entryInfo.Objective}
+- **行业归口枚举**: [{industryEnum}]
+- **业务约束条件**: {otherConstraintsStr}
+
+# [THOUGHT_PROCESS_LOGIC (CoT)]
+在构建每一条匹配数据前,请严格执行以下逻辑拆解,严禁盲目生成:
+1. **产业链定位分析 (Parity Logic)**: 深度分析 {entryInfo.OriginUnit} 在产业链中的位置。若其为制造方,则必须匹配具备当地分销能力的“贸易商”或“工程承包商”;若其为下游单位,则匹配上游“工厂”。严禁匹配直接竞争对手。
+2. **地域合规性校验**: 确保匹配的单位在指定任务国家(Tasks)真实存在,并具备当地市场准入资质。
+3. **时效性过滤**: 检索目标单位在 **2023-2026** 年间的动态。若无公开新闻,必须确保其官方网站(SiteUrl)具有真实度。
+4. **数据量校验**: 严格执行 Tasks 中的 `Count` 数量要求,总生成条数必须精准等于所有任务 Count 之和。
+
+# [STRICT_DATA_CONTRACT]
+1. **命名规范 (Mandatory)**: 所有 JSON Key 必须严格遵循 **PascalCase**(大驼峰命名法)。
+   - 正确示例: `NameCn`, `PostUrl`, `RecLevel`
+   - 错误示例: `name_cn`, `nameCn`
+2. **字段取值约束**:
+   - `Industry`: 必须且只能从枚举值 [{industryEnum}] 中选择其一,不得自行发明。
+   - `RecLevel`: 仅限使用 `Core` 或 `Backup`。
+3. **空值与集合处理**:
+   - 若某字符串字段缺失,填充 `""N/A""`。
+   - 若 `PostUrl` 数组无动态,必须返回 `[]`,严禁返回 `null`。
+4. **格式规范**: 
+   - `Phone`: 必须包含国际区号(如 +86, +856)。
+   - `Date`: 统一使用 `yyyy-MM-dd`。
+
+# [INFORMATION_SCHEMA]
+请将结果填充至以下结构的 JSON Array 中:
+{{
+  ""Region"": ""国家名称"",
+  ""Industry"": ""所属行业(必须是指定的枚举值)"",
+  ""NameCn"": ""单位名称(中文)"",
+  ""NameEn"": ""单位名称(英文)"",
+  ""Address"": ""详细地理位置/总部地址"",
+  ""Scope"": ""经营范围(侧重描述其在当地的行业地位或配套能力)"",
+  ""Contact"": ""联系人姓名及职务"",
+  ""Phone"": ""含区号的联系电话"",
+  ""Email"": ""商务联络邮箱"",
+  ""SiteUrl"": ""官方网站或权威主页链接"",
+  ""PostUrl"": [
+    {{
+      ""Date"": ""yyyy-MM-dd"",
+      ""Description"": ""近三年企业动态简述 (15-30字)"",
+      ""Url"": ""动态/新闻链接""
+    }}
+  ],
+  ""RecLevel"": ""推荐等级枚举值"",
+  ""IntgAdvice"": ""对接深度建议(50-100字,基于产业链对等逻辑分析匹配原因)""
+}}
+
+# [OUTPUT_PROTOCOL - CRITICAL]
+- **MODE**: RAW_TEXT_STREAM
+- **FORBIDDEN**: 严禁包含任何 Markdown 格式标识符(如 ```json )。
+- **FORBIDDEN**: 严禁输出任何开场白、中间解释、结尾客套话或补充说明。
+- **REQUIREMENT**: 只允许输出一个以 `[` 开头,以 `]` 结尾的纯 JSON 字符串,确保其可直接被 `JsonConvert.DeserializeObject<List<T>>` 解析。
 
 
 # [EXECUTION]
 # [EXECUTION]
-根据以上配置,开始生成。"; ;
+基于以上配置,立即处理以下任务列表:
+{JsonConvert.SerializeObject(tasks)}";
         }
         }
-    
+
         /// <summary>
         /// <summary>
         /// 商邀资料AI 设置词条
         /// 商邀资料AI 设置词条
         /// </summary>
         /// </summary>
@@ -2860,8 +2950,10 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
         public async Task<IActionResult> InvitationAISetPrompt(InvitationAISetPromptDto dto)
         public async Task<IActionResult> InvitationAISetPrompt(InvitationAISetPromptDto dto)
         {
         {
             // 基础校验
             // 基础校验
-            if (string.IsNullOrWhiteSpace(dto.OriginUnit) || dto.TargetCountry == null || dto.TargetCountry.Count == 0)
-                return Ok(JsonView(false, "请传入有效的单位名称和国家!"));
+            if (string.IsNullOrWhiteSpace(dto.OriginUnit) || dto.TargetCountry == null || dto.TargetCountry.Count == 0
+                || dto.Industries == null || dto.Industries.Count == 0 || dto.ScaleTypes == null || dto.ScaleTypes.Count == 0
+                )
+                return Ok(JsonView(false, "请传入有效的单位名称、国家、行业和规模类型!"));
 
 
             var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
             var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
                 .Where(x => x.IsDel == 0 && x.Id == dto.GroupId)
                 .Where(x => x.IsDel == 0 && x.Id == dto.GroupId)
@@ -2888,6 +2980,9 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 OriginUnit = dto.OriginUnit,
                 OriginUnit = dto.OriginUnit,
                 TargetCountry = dto.TargetCountry,
                 TargetCountry = dto.TargetCountry,
                 Objective = groupInfo?.VisitPurpose ?? "商务考察与合作对接",
                 Objective = groupInfo?.VisitPurpose ?? "商务考察与合作对接",
+                Industries = dto.Industries,
+                ScaleTypes = dto.ScaleTypes,
+                IsBackground = dto.IsBackground,
                 OtherConstraints = dto.OtherConstraints,
                 OtherConstraints = dto.OtherConstraints,
                 Operator = operatorName,
                 Operator = operatorName,
                 OperatedAt = DateTime.Now
                 OperatedAt = DateTime.Now
@@ -3369,24 +3464,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
         [HttpPost]
         [HttpPost]
         public async Task InvitationAICompleteTextStream([FromBody] InvitationAICompleteTextDto dto)
         public async Task InvitationAICompleteTextStream([FromBody] InvitationAICompleteTextDto dto)
         {
         {
-            var response = HttpContext.Response;
-            response.Headers.Add("Content-Type", "text/event-stream");
-            response.Headers.Add("Cache-Control", "no-cache");
-            response.Headers.Add("Connection", "keep-alive");
-            response.Headers.Add("X-Accel-Buffering", "no"); // 禁用 Nginx 缓存
-
-            // 内部推送工具
-            async Task SendStep(int progress, string msg, object data = null)
-            {
-                var payload = JsonConvert.SerializeObject(new
-                {
-                    progress,
-                    message = msg,
-                    data
-                });
-                await response.WriteAsync($"data: {payload}\n\n");
-                await response.Body.FlushAsync();
-            }
+            HttpContext.InitializeSse();
 
 
             try
             try
             {
             {
@@ -3394,10 +3472,10 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
                 var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
                 if (invAiInfo?.EntryInfo == null)
                 if (invAiInfo?.EntryInfo == null)
                 {
                 {
-                    await SendStep(-1, "请先设置关键字信息!");
+                    await HttpContext.SendSseStepAsync(-1, "请先设置关键字信息!");
                     return;
                     return;
                 }
                 }
-                await SendStep(10, "任务初始化完成,正在计算补齐缺口...");
+                await HttpContext.SendSseStepAsync(10, "任务初始化完成,正在计算补齐缺口...");
 
 
                 // 任务拆解逻辑
                 // 任务拆解逻辑
                 var entryInfo = invAiInfo.EntryInfo;
                 var entryInfo = invAiInfo.EntryInfo;
@@ -3413,7 +3491,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
 
 
                 if (!aiTasks.Any())
                 if (!aiTasks.Any())
                 {
                 {
-                    await SendStep(100, "数据已满额,无需续写", invAiInfo.AiCrawledDetails);
+                    await HttpContext.SendSseStepAsync(100, "数据已满额,无需续写", invAiInfo.AiCrawledDetails);
                     return;
                     return;
                 }
                 }
 
 
@@ -3425,14 +3503,12 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 // 构建 Question 
                 // 构建 Question 
                 string question = BuildHunyuanPrompt(aiTasks, entryInfo);
                 string question = BuildHunyuanPrompt(aiTasks, entryInfo);
 
 
-                await SendStep(30, "Hunyuan AI 正在深度检索跨境商邀数据,请稍候...");
+                await HttpContext.SendSseStepAsync(30, "AI 正在深度检索跨境商邀数据,请稍候...");
 
 
                 // 调用 AI (Progress: 30% - 80%)
                 // 调用 AI (Progress: 30% - 80%)
-                // 注意:这里如果能换成流式接口(ChatCompletionsStream)会更好,
-                // 如果依然用非流式,这里会有一段较长的等待。
                 string aiRawResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(question);
                 string aiRawResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(question);
 
 
-                await SendStep(85, "数据已捕获,正在进行格式校验与去重...");
+                await HttpContext.SendSseStepAsync(85, "数据已捕获,正在进行格式校验与去重...");
 
 
                 // 5. 解析与清洗数据
                 // 5. 解析与清洗数据
                 var hunyuanAIInvDatas = ProcessAIResponse(aiRawResponse);
                 var hunyuanAIInvDatas = ProcessAIResponse(aiRawResponse);
@@ -3454,22 +3530,21 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
 
 
                 if (update > 0)
                 if (update > 0)
                 {
                 {
-                    await SendStep(100, $"Hunyuan AI 续写成功!新增 {hunyuanAIInvDatas.Count} 条数据", invAiInfo.AiCrawledDetails);
+                    await HttpContext.SendSseStepAsync(100, $"AI 续写成功!新增 {hunyuanAIInvDatas.Count} 条数据", invAiInfo.AiCrawledDetails);
                 }
                 }
                 else
                 else
                 {
                 {
-                    await SendStep(-1, "数据库更新失败");
+                    await HttpContext.SendSseStepAsync(-1, "数据库更新失败");
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 _logger.LogError(ex, "SSE 续写异常");
                 _logger.LogError(ex, "SSE 续写异常");
-                await SendStep(-1, $"炼金炸炉:{ex.Message}");
+                await HttpContext.SendSseStepAsync(-1, $"炼金炸炉:{ex.Message}");
             }
             }
             finally
             finally
             {
             {
-                await response.Body.FlushAsync();
-                await response.CompleteAsync(); // 彻底关闭连接
+                await HttpContext.FinalizeSseAsync(); 
             }
             }
         }
         }
 
 
@@ -3574,7 +3649,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
 
 
                 AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "官方网站", item.SiteUrl);
                 AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "官方网站", item.SiteUrl);
 
 
-                AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "最新动态", item.PostUrl);
+                AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "最近三年动态", string.Join(", ", item.PostUrl.Select(p => p.Url)));
 
 
                 AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "推荐等级", item.RecLevel);
                 AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "推荐等级", item.RecLevel);
 
 
@@ -3946,42 +4021,24 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
         [HttpPost]
         [HttpPost]
         public async Task InvitationAIGenerateEmailStream([FromBody] InvitationAIGenerateEmailDto dto)
         public async Task InvitationAIGenerateEmailStream([FromBody] InvitationAIGenerateEmailDto dto)
         {
         {
-            var response = HttpContext.Response;
-            response.Headers.Add("Content-Type", "text/event-stream");
-            response.Headers.Add("Cache-Control", "no-cache");
-            response.Headers.Add("Connection", "keep-alive");
-            response.Headers.Add("X-Accel-Buffering", "no");
-
-            async Task SendStep(int progress, string msg, object data = null)
-            {
-                var payload = JsonConvert.SerializeObject(new
-                {
-                    progress,
-                    message = msg,
-                    data,
-                    operatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
-                });
-                await response.WriteAsync($"data: {payload}\n\n");
-                await response.Body.FlushAsync();
-            }
-
+            HttpContext.InitializeSse();
             try
             try
             {
             {
                 // 1. 基础校验与资源加载 (Progress: 5%)
                 // 1. 基础校验与资源加载 (Progress: 5%)
                 if (dto.Id < 1 || dto.Guids == null || !dto.Guids.Any())
                 if (dto.Id < 1 || dto.Guids == null || !dto.Guids.Any())
                 {
                 {
-                    await SendStep(-1, "请求参数不完整,请选择单位");
+                    await HttpContext.SendSseStepAsync(-1, "请求参数不完整,请选择单位");
                     return;
                     return;
                 }
                 }
 
 
                 var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
                 var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
                 if (invAiInfo?.AiCrawledDetails == null)
                 if (invAiInfo?.AiCrawledDetails == null)
                 {
                 {
-                    await SendStep(-1, "基础商邀资料不存在");
+                    await HttpContext.SendSseStepAsync(-1, "基础商邀资料不存在");
                     return;
                     return;
                 }
                 }
 
 
-                await SendStep(10, "Hunyuan AI 正在分析考察团组背景与访问意图...");
+                await HttpContext.SendSseStepAsync(10, "AI 正在分析考察团组背景与访问意图...");
 
 
                 // 2. 准备 AI 上下文数据 (Progress: 15%)
                 // 2. 准备 AI 上下文数据 (Progress: 15%)
                 var clientInfoSources = invAiInfo.AiCrawledDetails.Where(x => dto.Guids.Contains(x.Guid)).ToList();
                 var clientInfoSources = invAiInfo.AiCrawledDetails.Where(x => dto.Guids.Contains(x.Guid)).ToList();
@@ -4001,7 +4058,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     .Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
                     .Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
 
 
                 // 3. 构建 Prompt 并调用 AI (Progress: 25%)
                 // 3. 构建 Prompt 并调用 AI (Progress: 25%)
-                await SendStep(30, $"Hunyuan AI 正在为 {clientInfosForAI.Count} 家单位撰写定制化正式邮件...");
+                await HttpContext.SendSseStepAsync(30, $"AI 正在为 {clientInfosForAI.Count} 家单位撰写定制化正式邮件...");
 
 
                 // 此处沿用你原有的庞大 Prompt 逻辑
                 // 此处沿用你原有的庞大 Prompt 逻辑
                 string pormpt = $@"
                 string pormpt = $@"
@@ -4074,7 +4131,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 // 调用 AI (此处为阻塞式等待 AI 结果,若混元支持 Stream 可进一步拆解)
                 // 调用 AI (此处为阻塞式等待 AI 结果,若混元支持 Stream 可进一步拆解)
                 string aiResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(pormpt);
                 string aiResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(pormpt);
 
 
-                await SendStep(80, "邮件初稿已生成,正在进行 HTML 格式校验与转义处理...");
+                await HttpContext.SendSseStepAsync(80, "邮件初稿已生成,正在进行 HTML 格式校验与转义处理...");
 
 
                 // 4. 解析结果 (Progress: 85%)
                 // 4. 解析结果 (Progress: 85%)
                 var hunyuanAIEmailDatas = new List<AICreateEmailInfo>();
                 var hunyuanAIEmailDatas = new List<AICreateEmailInfo>();
@@ -4101,7 +4158,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 }
                 }
                 if (hunyuanAIEmailDatas == null || !hunyuanAIEmailDatas.Any())
                 if (hunyuanAIEmailDatas == null || !hunyuanAIEmailDatas.Any())
                 {
                 {
-                    await SendStep(-1, "AI 格式解析失败,请尝试重新生成");
+                    await HttpContext.SendSseStepAsync(-1, "AI 格式解析失败,请尝试重新生成");
                     return;
                     return;
                 }
                 }
 
 
@@ -4124,21 +4181,21 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
                 var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
                 if (update > 0)
                 if (update > 0)
                 {
                 {
-                    await SendStep(100, "邮件全部生成完毕并已存入团组资料库", invAiInfo.AiCrawledDetails);
+                    await HttpContext.SendSseStepAsync(100, "邮件全部生成完毕并已存入团组资料库", invAiInfo.AiCrawledDetails);
                 }
                 }
                 else
                 else
                 {
                 {
-                    await SendStep(-1, "数据库写入失败,请联系管理员");
+                    await HttpContext.SendSseStepAsync(-1, "数据库写入失败,请联系管理员");
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 _logger.LogError(ex, "邮件生成异常");
                 _logger.LogError(ex, "邮件生成异常");
-                await SendStep(-1, $"生成失败:{ex.Message}");
+                await HttpContext.SendSseStepAsync(-1, $"生成失败:{ex.Message}");
             }
             }
             finally
             finally
             {
             {
-                await response.CompleteAsync();
+                await HttpContext.FinalizeSseAsync();
             }
             }
         }
         }
 
 
@@ -4283,6 +4340,162 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             return Ok(JsonView(true, msgSb.ToString()));
             return Ok(JsonView(true, msgSb.ToString()));
         }
         }
 
 
+        /// <summary>
+        /// 商邀资料AI 发送邮件(SSE 流式推送)
+        /// </summary>
+        [HttpPost]
+        public async Task InvitationAISeedEmailStream([FromBody] InvitationAISeedEmailDto dto)
+        {
+            // 1. 初始化 SSE 
+            HttpContext.InitializeSse();
+
+            try
+            {
+                await HttpContext.SendSseStepAsync(5, "正在准备发送队列...");
+
+                #region 1. 参数与权限前置校验
+                if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
+                {
+                    await HttpContext.SendSseStepAsync(-1, "参数验证失败,请检查选择的单位及用户状态。");
+                    return;
+                }
+
+                // 并行获取团组信息和用户信息
+                var invAiTask = _sqlSugar.Queryable<Res_InvitationAI>().InSingleAsync(dto.Id);
+                var userTask = _sqlSugar.Queryable<Sys_Users>()
+                    .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
+                    .Select(x => new { x.Email, x.CnName }).FirstAsync();
+
+                await Task.WhenAll(invAiTask, userTask);
+
+                var invAiInfo = await invAiTask;
+                var userInfo = await userTask;
+
+                if (invAiInfo?.AiCrawledDetails == null)
+                {
+                    await HttpContext.SendSseStepAsync(-1, "未找到有效的邀请方数据。");
+                    return;
+                }
+
+                if (string.IsNullOrEmpty(userInfo?.Email))
+                {
+                    await HttpContext.SendSseStepAsync(-1, "当前账号未配置邮箱,无法执行发送。");
+                    return;
+                }
+
+                // 提取待发送的目标集合
+                var guidSet = new HashSet<string>(dto.Guids);
+                var seedInvInfos = invAiInfo.AiCrawledDetails.Where(x => guidSet.Contains(x.Guid)).ToList();
+
+                if (!seedInvInfos.Any())
+                {
+                    await HttpContext.SendSseStepAsync(-1, "所选单位信息在原始库中不存在。");
+                    return;
+                }
+                #endregion
+
+                await HttpContext.SendSseStepAsync(15, $"准备就绪,共计 {seedInvInfos.Count} 封邮件待发送...");
+
+                #region 2. 批量发送逻辑 (流式反馈)
+                int total = seedInvInfos.Count;
+                int current = 0;
+                var successCount = 0;
+                var failCount = 0;
+
+                foreach (var item in seedInvInfos)
+                {
+                    current++;
+                    // 计算进度:从 20% 到 90%
+                    int progress = 20 + (int)((double)current / total * 70);
+
+                    if (string.IsNullOrEmpty(item.EmailInfo?.EmailTitle) || string.IsNullOrEmpty(item.EmailInfo?.EmailContent))
+                    {
+                        await HttpContext.SendSseStepAsync(progress, $"跳过:{item.NameCn} (邮件标题或内容缺失)");
+                        failCount++;
+                        continue;
+                    }
+
+                    try
+                    {
+                        var req = new EmailRequestDto()
+                        {
+                            ToEmails = new List<string> { item.Email },
+                            Subject = item.EmailInfo.EmailTitle,
+                            Body = item.EmailInfo.EmailContent,
+                            Files = Array.Empty<IFormFile>()
+                        };
+
+                        // 调用企微 API
+                        var response = await _qiYeWeChatApiService.EmailSendAsync(req);
+
+                        if (response.errcode == 0)
+                        {
+                            successCount++;
+                            // 更新本地状态
+                            item.EmailInfo.Status = 4; // 发送成功状态
+                            item.EmailInfo.Operator = userInfo.CnName;
+                            item.EmailInfo.OperatedAt = DateTime.Now;
+
+                            await HttpContext.SendSseStepAsync(progress, $"成功:已向 {item.NameCn}({item.Email}) 发送邮件");
+                        }
+                        else
+                        {
+                            failCount++;
+                            await HttpContext.SendSseStepAsync(progress, $"失败:{item.NameCn} 发送失败 ({response.errmsg})");
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        failCount++;
+                        _logger.LogError(ex, $"企微邮件推送异常:{item.NameCn}");
+                        await HttpContext.SendSseStepAsync(progress, $"异常:{item.NameCn} 连接超时");
+                    }
+
+                    // 频率控制:防止企微 API 触发 QPS 限制 (炼金建议:根据实际情况调整)
+                    await Task.Delay(300);
+                }
+                #endregion
+
+                await HttpContext.SendSseStepAsync(95, "正在归档发送记录...");
+
+                #region 3. 数据同步与收尾
+                // 更新内存中的全量数据:排除旧的,加入已更新状态的
+                invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
+                    .Where(x => !guidSet.Contains(x.Guid))
+                    .Concat(seedInvInfos)
+                    .OrderByDescending(x => x.EmailInfo.OperatedAt)
+                    .ToList();
+
+                // 精准更新 JSON 列
+                var updateOk = await _sqlSugar.Updateable(invAiInfo)
+                    .UpdateColumns(x => x.AiCrawledDetails)
+                    .ExecuteCommandHasChangeAsync();
+
+                if (!updateOk)
+                {
+                    await HttpContext.SendSseStepAsync(-1, "记录保存失败,请检查数据库连接。");
+                    return;
+                }
+
+                await HttpContext.SendSseStepAsync(100, $"任务完成!成功: {successCount}, 失败: {failCount}", new
+                {
+                    Id = invAiInfo.Id,
+                    SuccessCount = successCount,
+                    FailCount = failCount
+                });
+                #endregion
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "邮件发送流发生崩溃");
+                await HttpContext.SendSseStepAsync(-1, $"系统错误:{ex.Message}");
+            }
+            finally
+            {
+                await HttpContext.FinalizeSseAsync();
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region 公务出访
         #region 公务出访

+ 29 - 41
OASystem/OASystem.Api/OAMethodLib/HotmailEmail/HotmailEmailService.cs

@@ -1,89 +1,77 @@
-
-using MailKit.Net.Smtp;
+using MailKit.Net.Smtp;
 using MailKit.Security;
 using MailKit.Security;
 using MimeKit;
 using MimeKit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
 
 
 namespace OASystem.API.OAMethodLib.HotmailEmail
 namespace OASystem.API.OAMethodLib.HotmailEmail
 {
 {
-    /// <summary>
-    /// Hotemail 服务
-    /// </summary>
     public class HotmailEmailService : IHotmailEmailService
     public class HotmailEmailService : IHotmailEmailService
     {
     {
-        private readonly IConfiguration _configuration;
+        private readonly IConfiguration _config;
+        private readonly ILogger<HotmailEmailService> _logger;
 
 
-        public HotmailEmailService(IConfiguration configuration)
+        public HotmailEmailService(IConfiguration config, ILogger<HotmailEmailService> logger)
         {
         {
-            _configuration = configuration;
+            _config = config;
+            _logger = logger;
         }
         }
 
 
         public async Task<bool> SendEmailAsync(string toEmail, string subject, string htmlContent)
         public async Task<bool> SendEmailAsync(string toEmail, string subject, string htmlContent)
         {
         {
-            // 从配置读取信息
-            var smtpServer = _configuration["HotEmailConfig:SmtpServer"];
-            var smtpPort = int.Parse(_configuration["HotEmailConfig:SmtpPort"]);
-            var senderEmail = _configuration["HotEmailConfig:SenderEmail"];
-            var senderName = _configuration["HotEmailConfig:SenderName"];
-            var appPassword = _configuration["HotEmailConfig:AppPassword"];
+            // 1. 配置前置校验
+            string smtpServer = _config["HotEmailConfig:SmtpServer"] ?? string.Empty;
+            int smtpPort = _config.GetValue<int>("HotEmailConfig:SmtpPort", 587);
+            string appPassword = _config["HotEmailConfig:AppPassword"];
+            string senderEmail = _config["HotEmailConfig:SenderEmail"];
+            string senderName = _config["HotEmailConfig:SenderName"];
 
 
-            // 移除应用密码中的所有 "-" 分隔符
-            appPassword = appPassword?.Replace("-", "");
-
-            // 验证配置必填项
-            if (string.IsNullOrEmpty(smtpServer) || string.IsNullOrEmpty(senderEmail) || string.IsNullOrEmpty(appPassword))
+            if (string.IsNullOrEmpty(smtpServer) || string.IsNullOrEmpty(appPassword))
             {
             {
-                Console.WriteLine("[Email Error]: 邮件配置不完整");
+                _logger.LogError("[Config Alchemy] 无法从 JSON 获取必要的邮件配置节点。");
                 return false;
                 return false;
             }
             }
 
 
+            // 2. 构建邮件内容
             var message = new MimeMessage();
             var message = new MimeMessage();
             message.From.Add(new MailboxAddress(senderName, senderEmail));
             message.From.Add(new MailboxAddress(senderName, senderEmail));
 
 
-            // 校验收件人邮箱格式,避免无效地址报错
             if (!MailboxAddress.TryParse(toEmail, out var recipient))
             if (!MailboxAddress.TryParse(toEmail, out var recipient))
             {
             {
-                Console.WriteLine($"[Email Error]: 收件人邮箱格式错误 - {toEmail}");
+                _logger.LogWarning("[Email] 收件人地址非法: {ToEmail}", toEmail);
                 return false;
                 return false;
             }
             }
             message.To.Add(recipient);
             message.To.Add(recipient);
-
             message.Subject = subject;
             message.Subject = subject;
 
 
             var bodyBuilder = new BodyBuilder { HtmlBody = htmlContent };
             var bodyBuilder = new BodyBuilder { HtmlBody = htmlContent };
             message.Body = bodyBuilder.ToMessageBody();
             message.Body = bodyBuilder.ToMessageBody();
 
 
+            // 3. 客户端连接与发送
             using var client = new SmtpClient();
             using var client = new SmtpClient();
             try
             try
             {
             {
-                // 显式设置超时时间,避免连接超时
-                client.Timeout = 30000; // 30秒超时
+                // Hotmail/Outlook 通常使用 587 + StartTls 或 465 + SslOnConnect
+                var secureOption = smtpPort == 465
+                    ? SecureSocketOptions.SslOnConnect
+                    : SecureSocketOptions.StartTls;
 
 
-                // 连接服务器 (Hotmail/Outlook 必须使用 StartTls)
-                await client.ConnectAsync(smtpServer, smtpPort, SecureSocketOptions.StartTls);
+                // 设置超时 (建议 30-60 秒)
+                client.Timeout = 30000;
 
 
-                // 身份验证
+                await client.ConnectAsync(smtpServer, smtpPort, secureOption);
                 await client.AuthenticateAsync(senderEmail, appPassword);
                 await client.AuthenticateAsync(senderEmail, appPassword);
-
-                // 执行发送
                 await client.SendAsync(message);
                 await client.SendAsync(message);
                 await client.DisconnectAsync(true);
                 await client.DisconnectAsync(true);
 
 
-                Console.WriteLine($"[Email Success]: 邮件已发送至 {toEmail}");
+                _logger.LogInformation("[Email] 成功发送至: {ToEmail}", toEmail);
                 return true;
                 return true;
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
-                // 打印完整异常链,定位真实错误
-                var errorMsg = $"[Email Error]: {ex.Message}";
-                var innerEx = ex.InnerException;
-                while (innerEx != null)
-                {
-                    errorMsg += $"\n[Inner Error]: {innerEx.Message}";
-                    innerEx = innerEx.InnerException;
-                }
-                Console.WriteLine(errorMsg);
+                _logger.LogError(ex, "[Email] 发送失败,目标: {ToEmail}", toEmail);
                 return false;
                 return false;
             }
             }
         }
         }
     }
     }
-}
+}

+ 2 - 2
OASystem/OASystem.Api/appsettings.json

@@ -602,8 +602,8 @@
     "SmtpServer": "smtp-mail.outlook.com",
     "SmtpServer": "smtp-mail.outlook.com",
     "SmtpPort": 587,
     "SmtpPort": 587,
     "SenderEmail": "Roy.Lei.Atom@hotmail.com",
     "SenderEmail": "Roy.Lei.Atom@hotmail.com",
+    //"SenderEmail": "Roy.Lei.Atom@hotmail.com",
     "SenderName": "Roy Lei",
     "SenderName": "Roy Lei",
-    "AppPassword": "mbjefaowzicprbfx"
-    //"AppPassword": "apirccbinnuditof"
+    "AppPassword": "pqqrwkszdodzhift"
   }
   }
 }
 }

+ 17 - 0
OASystem/OASystem.Domain/Dtos/Resource/InvitationAI.cs

@@ -21,6 +21,23 @@ namespace OASystem.Domain.Dtos.Resource
         /// 出访国家
         /// 出访国家
         /// </summary>
         /// </summary>
         public List<string> TargetCountry { get; set; }
         public List<string> TargetCountry { get; set; }
+
+        /// <summary>
+        /// 行业信息
+        /// 信息技术、金融与财会、工业制造、医疗保健、政府与公共服务、消费与贸易
+        /// </summary>
+        public List<string> Industries { get; set; } = new List<string>();
+
+        /// <summary>
+        /// 规模类型
+        /// </summary>
+        public List<string> ScaleTypes { get; set; } = new List<string>();
+
+        /// <summary>
+        /// 是否需要华人单位背景
+        /// </summary>
+        public bool IsBackground { get; set; } = false;
+
         /// <summary>
         /// <summary>
         /// 备注信息
         /// 备注信息
         /// </summary>
         /// </summary>

+ 198 - 4
OASystem/OASystem.Domain/Entities/Resource/Res_InvitationAI.cs

@@ -1,7 +1,9 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
+using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
 namespace OASystem.Domain.Entities.Resource
 namespace OASystem.Domain.Entities.Resource
@@ -64,6 +66,11 @@ namespace OASystem.Domain.Entities.Resource
         /// </summary>
         /// </summary>
         public string Region { get; set; }
         public string Region { get; set; }
 
 
+        /// <summary>
+        /// 标准行业:信息技术、金融与财会、工业制造、医疗保健、政府与公共服务、消费与贸易
+        /// </summary>
+        public string Industry { get; set; }
+
         /// <summary>
         /// <summary>
         /// 名称(中文)
         /// 名称(中文)
         /// </summary>
         /// </summary>
@@ -99,7 +106,7 @@ namespace OASystem.Domain.Entities.Resource
         /// <summary>
         /// <summary>
         /// 文章地址
         /// 文章地址
         /// </summary>
         /// </summary>
-        public string PostUrl { get; set; }
+        public List<PostNewsItem> PostUrl { get; set; } = new List<PostNewsItem>();
 
 
         /// <summary>
         /// <summary>
         /// 推荐等级
         /// 推荐等级
@@ -131,6 +138,16 @@ namespace OASystem.Domain.Entities.Resource
         public string Operator { get; set; }
         public string Operator { get; set; }
     }
     }
 
 
+    /// <summary>
+    /// 新闻链接信息
+    /// </summary>
+    public class PostNewsItem
+    {
+        public string Date { get; set; }
+        public string Description { get; set; }
+        public string Url { get; set; }
+    }
+
     public class EntryInfo
     public class EntryInfo
     {
     {
         /// <summary>
         /// <summary>
@@ -141,6 +158,23 @@ namespace OASystem.Domain.Entities.Resource
         /// 拜访国家
         /// 拜访国家
         /// </summary>
         /// </summary>
         public List<string> TargetCountry { get; set; } = new List<string>();
         public List<string> TargetCountry { get; set; } = new List<string>();
+
+        /// <summary>
+        /// 行业信息
+        /// 信息技术、金融与财会、工业制造、医疗保健、政府与公共服务、消费与贸易
+        /// </summary>
+        public List<string> Industries { get; set; } = new List<string>();
+
+        /// <summary>
+        /// 规模类型
+        /// </summary>
+        public List<string> ScaleTypes { get; set; } = new List<string>();
+
+        /// <summary>
+        /// 是否需要华人单位背景
+        /// </summary>
+        public bool IsBackground { get; set; } = false;
+
         /// <summary>
         /// <summary>
         /// 出访目的
         /// 出访目的
         /// </summary>
         /// </summary>
@@ -149,7 +183,7 @@ namespace OASystem.Domain.Entities.Resource
         /// 数据条数
         /// 数据条数
         /// 每个国家获取数据总条数,默认10条
         /// 每个国家获取数据总条数,默认10条
         /// </summary>
         /// </summary>
-        public int NeedCount { get; set; } = 10;
+        public int NeedCount { get; set; } = 5;
         /// <summary>
         /// <summary>
         /// 其他规则
         /// 其他规则
         /// </summary>
         /// </summary>
@@ -196,7 +230,6 @@ namespace OASystem.Domain.Entities.Resource
         public string Operator { get; set; }
         public string Operator { get; set; }
     }
     }
 
 
-
     public class AICreateEmailInfo
     public class AICreateEmailInfo
     {
     {
 
 
@@ -207,9 +240,170 @@ namespace OASystem.Domain.Entities.Resource
         public string Content { get; set; }
         public string Content { get; set; }
     }
     }
 
 
-
     public class CountryAIPormptInfo {
     public class CountryAIPormptInfo {
         public string Country { get; set; }
         public string Country { get; set; }
         public int Count { get; set; }
         public int Count { get; set; }
     }
     }
+
+    #region 行业信息
+
+    /// <summary>
+    /// 行业信息
+    /// </summary>
+    public class IndustryNode
+    {
+        public string Code { get; init; }
+        public string NameCn { get; init; }
+        public string NameEn { get; init; }
+        public string ParentCode { get; init; }
+        public List<IndustryNode> Children { get; init; } = new();
+        public string Keywords { get; init; }
+
+        public IndustryNode() { }
+
+        /// <summary>
+        /// 静态缓存:扁平化字典,用于 O(1) 效率查找
+        /// </summary>
+        private static readonly Dictionary<string, IndustryNode> _flatCache;
+        public static List<IndustryNode> Roots { get; }
+
+        static IndustryNode()
+        {
+            Roots = BuildInitialData();
+            // 预先扁平化所有节点,方便后续根据 Code 查找
+            _flatCache = Roots.SelectMany(GetSelfAndChildren).ToDictionary(x => x.Code);
+        }
+
+        /// <summary>
+        /// 递归获取所有节点(平铺)
+        /// </summary>
+        private static IEnumerable<IndustryNode> GetSelfAndChildren(IndustryNode node)
+        {
+            yield return node;
+            if (node.Children == null) yield break;
+            foreach (var child in node.Children.SelectMany(GetSelfAndChildren))
+                yield return child;
+        }
+
+        /// <summary>
+        /// 快速查找节点 (O(1))
+        /// </summary>
+        public static IndustryNode FindByCode(string code) =>
+            !string.IsNullOrEmpty(code) && _flatCache.TryGetValue(code, out var node) ? node : null;
+
+        /// <summary>
+        /// 初始化全球行业分类静态数据
+        /// </summary>
+        public static List<IndustryNode> BuildInitialData() => new()
+        {
+            new() { Code = "TECH", NameCn = "信息技术", NameEn = "Technology", Keywords = "IT,互联网,软件,AI", Children = new() {
+                new() { Code = "TECH01", ParentCode = "TECH", NameCn = "软件开发与服务", NameEn = "Software & Services" },
+                new() { Code = "TECH02", ParentCode = "TECH", NameCn = "硬件与半导体", NameEn = "Hardware & Semiconductors" },
+                new() { Code = "TECH03", ParentCode = "TECH", NameCn = "人工智能与大数据", NameEn = "AI & Big Data" }
+            }},
+            new() { Code = "FIN", NameCn = "金融与财会", NameEn = "Financials", Keywords = "银行,保险,证券", Children = new() {
+                new() { Code = "FIN01", ParentCode = "FIN", NameCn = "银行业", NameEn = "Banking" },
+                new() { Code = "FIN02", ParentCode = "FIN", NameCn = "保险与风险管理", NameEn = "Insurance" },
+                new() { Code = "FIN03", ParentCode = "FIN", NameCn = "资本市场与证券", NameEn = "Capital Markets" }
+            }},
+            new() { Code = "MANU", NameCn = "工业制造", NameEn = "Manufacturing", Keywords = "工厂,机械,自动化", Children = new() {
+                new() { Code = "MANU01", ParentCode = "MANU", NameCn = "汽车与运输设备", NameEn = "Automotive" },
+                new() { Code = "MANU02", ParentCode = "MANU", NameCn = "机械与电气设备", NameEn = "Machinery & Electrical" },
+                new() { Code = "MANU03", ParentCode = "MANU", NameCn = "消费电子制造", NameEn = "Consumer Electronics" }
+            }},
+            new() { Code = "HLT", NameCn = "医疗保健", NameEn = "Healthcare", Keywords = "制药,医院,器械", Children = new() {
+                new() { Code = "HLT01", ParentCode = "HLT", NameCn = "制药与生物技术", NameEn = "Pharmaceuticals & Biotech" },
+                new() { Code = "HLT02", ParentCode = "HLT", NameCn = "医疗器械与供应", NameEn = "Medical Devices" },
+                new() { Code = "HLT03", ParentCode = "HLT", NameCn = "医疗机构与诊所", NameEn = "Health Institutions" }
+            }},
+            new() { Code = "GOV", NameCn = "政府与公共服务", NameEn = "Government", Keywords = "行政,机关,组织,NGO", Children = new() {
+                new() { Code = "GOV01", ParentCode = "GOV", NameCn = "政府行政机关", NameEn = "Administrative Bodies" },
+                new() { Code = "GOV02", ParentCode = "GOV", NameCn = "国际组织与NGO", NameEn = "International Orgs & NGOs" },
+                new() { Code = "GOV03", ParentCode = "GOV", NameCn = "公共教育与科研单位", NameEn = "Education & Research" }
+            }},
+            new() { Code = "CONS", NameCn = "消费与贸易", NameEn = "Consumer & Trade", Keywords = "零售,电商,物流", Children = new() {
+                new() { Code = "CONS01", ParentCode = "CONS", NameCn = "电子商务与零售", NameEn = "E-commerce & Retail" },
+                new() { Code = "CONS02", ParentCode = "CONS", NameCn = "物流与供应链", NameEn = "Logistics & Supply Chain" },
+                new() { Code = "CONS03", ParentCode = "CONS", NameCn = "酒店与旅游餐饮", NameEn = "Hospitality & Tourism" }
+            }}
+        };
+    }
+
+    #endregion
+
+    #region 单位规模信息
+
+    /// <summary>
+    /// 单位规模
+    /// </summary>
+    public class OrgScale
+    {
+        public string Label { get; init; }
+        public int MinStaff { get; init; }
+        public int MaxStaff { get; init; }
+
+        public static List<OrgScale> GetScales() => new()
+        {
+            new() { Label = "微型 (1-10人)", MinStaff = 1, MaxStaff = 10 },
+            new() { Label = "小型 (11-50人)", MinStaff = 11, MaxStaff = 50 },
+            new() { Label = "中型 (51-200人)", MinStaff = 51, MaxStaff = 200 },
+            new() { Label = "大型 (201-1000人)", MinStaff = 201, MaxStaff = 1000 },
+            new() { Label = "超大型 (1000人以上)", MinStaff = 1001, MaxStaff = int.MaxValue }
+        };
+
+        /// <summary>
+        /// 根据人数自动匹配规模标签
+        /// </summary>
+        public static string GetLabel(int count) =>
+            GetScales().FirstOrDefault(s => count >= s.MinStaff && count <= s.MaxStaff)?.Label ?? "未知规模";
+    }
+
+    #endregion
+
+    #region AI 行业匹配结果实体
+
+    /// <summary>
+    /// AI 行业匹配结果实体
+    /// </summary>
+    public class IndustryMatchResult
+    {
+        /// <summary>
+        /// 原始出访单位名称
+        /// </summary>
+        public string SourceUnitName { get; set; } = string.Empty;
+
+        /// <summary>
+        /// 匹配到的目标单位名称(必须与输入 NameCn 一致)
+        /// </summary>
+        public string TargetUnitName { get; set; } = string.Empty;
+
+        /// <summary>
+        /// 目标单位所属国家
+        /// </summary>
+        public string TargetCountry { get; set; } = string.Empty;
+
+        /// <summary>
+        /// 识别出的行业分类(来自动态传入的行业标准)
+        /// </summary>
+        public string MatchedIndustry { get; set; } = string.Empty;
+
+        /// <summary>
+        /// 匹配置信度 (0.0 - 1.0)
+        /// </summary>
+        public double ConfidenceScore { get; set; }
+
+        /// <summary>
+        /// AI 给出的匹配理由说明
+        /// </summary>
+        public string MatchReason { get; set; } = string.Empty;
+
+        // --- 辅助属性 (非 AI 返回内容) ---
+
+        /// <summary>
+        /// 判断是否为高价值匹配(业务逻辑判定)
+        /// </summary>
+        [JsonIgnore]
+        public bool IsHighValue => ConfidenceScore >= 0.8;
+    }
+    #endregion
 }
 }

+ 68 - 0
OASystem/OASystem.Infrastructure/Tools/SseAlchemyHelper.cs

@@ -0,0 +1,68 @@
+using Microsoft.AspNetCore.Http.Features;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+/// <summary>
+/// SSE 流式数据助手类,提供初始化 SSE、发送数据包和结束流的方法
+/// </summary>
+public static class SseAlchemyHelper
+{
+    private static readonly JsonSerializerSettings _jsonSettings = new()
+    {
+        ContractResolver = new CamelCasePropertyNamesContractResolver(),
+        DateTimeZoneHandling = DateTimeZoneHandling.Local,
+        NullValueHandling = NullValueHandling.Ignore
+    };
+
+    /// <summary>
+    /// 初始化 SSE
+    /// </summary>
+    public static void InitializeSse(this HttpContext context)
+    {
+        var syncIOFeature = context.Features.Get<IHttpBodyControlFeature>();
+        if (syncIOFeature != null) syncIOFeature.AllowSynchronousIO = true;
+
+        var response = context.Response;
+        response.Headers.Append("Content-Type", "text/event-stream");
+        response.Headers.Append("Cache-Control", "no-cache");
+        response.Headers.Append("Connection", "keep-alive");
+        response.Headers.Append("X-Accel-Buffering", "no");
+    }
+
+    /// <summary>
+    /// 发送流数据包
+    /// </summary>
+    public static async Task SendSseStepAsync(
+        this HttpContext context,
+        int progress,
+        string message,
+        object? data = null)
+    {
+        // 检查客户端是否已断开连接
+        if (context.RequestAborted.IsCancellationRequested) return;
+
+        var payload = JsonConvert.SerializeObject(new { progress, message, data }, _jsonSettings);
+        string sseFormattedData = $"data: {payload}\n\n";
+        byte[] bytes = Encoding.UTF8.GetBytes(sseFormattedData);
+
+        try
+        {
+            // 写入并立即冲刷到客户端
+            await context.Response.Body.WriteAsync(bytes, 0, bytes.Length, context.RequestAborted);
+            await context.Response.Body.FlushAsync(context.RequestAborted);
+        }
+        catch (OperationCanceledException) { /* 客户端取消,优雅退出 */ }
+        catch (Exception) { /* 忽略其他写入异常 */ }
+    }
+
+    /// <summary>
+    /// 结束 SSE 流
+    /// </summary>
+    public static async Task FinalizeSseAsync(this HttpContext context)
+    {
+        if (!context.RequestAborted.IsCancellationRequested)
+        {
+            await context.Response.Body.FlushAsync();
+        }
+    }
+}