Lyyyi пре 2 недеља
родитељ
комит
88b82cfbb6

+ 254 - 4
OASystem/OASystem.Api/Controllers/ResourceController.cs

@@ -2,6 +2,8 @@
 using Aspose.Words;
 using Aspose.Words.Tables;
 using EyeSoft.Extensions;
+using Microsoft.AspNetCore.Http.Features;
+using Newtonsoft.Json.Serialization;
 using NodaTime;
 using NPOI.SS.Formula.Functions;
 using NPOI.SS.UserModel;
@@ -2320,17 +2322,30 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 .Where(x => x.IsDel == 0 && x.InvName == name)
                 .FirstAsync();
 
+            var groupInfo = new Grp_DelegationInfo();
+
             if (info == null)
             {
+                groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.TeamName.Equals(name)).FirstAsync();
+
+                var entry = new EntryInfo() {
+                    OriginUnit = groupInfo?.ClientUnit ?? "",
+                    TargetCountry = _delegationInfoRep.GroupSplitCountry(groupInfo?.VisitCountry ?? ""),
+                };
+
                 return Ok(JsonView(true, $"暂无数据",new {
                     Id = 0,
-                    GroupId= 0,
-                    InvName = "",
+                    GroupId= groupInfo?.Id ?? 0,
+                    InvName = name,
                     AiCrawledDetails = new List<InvitationAIInfo>(),
+                    Entry = entry
                 }));
             }
 
-            var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.Id == info.GroupId).FirstAsync();
+            if (info.GroupId != 0)
+            {
+                groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.Id == info.GroupId).FirstAsync();
+            }
             // 设置国家、单位默认值
             if (string.IsNullOrEmpty(info.EntryInfo.OriginUnit))
             {
@@ -2383,6 +2398,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
 
             var localInvDatas = new List<InvitationAIInfo>(); // 本地数据源(商邀资料)
             var aiTasks = new List<CountryAIPormptInfo>();    // 记录:国家 -> 需要补齐的数量
+
             #region 本地数据源(商邀资料)
 
             var datas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
@@ -2606,6 +2622,239 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             }));
         }
 
+        /// <summary>
+        /// 商邀资料AI 混元AI查询资料(SSE流式推送)
+        /// </summary>
+        [HttpPost()]
+        [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+        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(); // 立即清空缓冲区发送
+            }
+
+            try
+            {
+                await SendStep(5, "正在加载配置参数...");
+
+                #region 1. 参数与权限验证
+                var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
+                if (invAiInfo?.EntryInfo == null)
+                {
+                    await SendStep(-1, "未找到有效的关键字配置信息。");
+                    return;
+                }
+
+                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() ?? "-";
+                #endregion
+
+                await Task.Delay(2000);
+
+                await SendStep(20, "正在翻阅本地典籍,执行高性能并行解密...");
+
+                #region 2. 本地数据并行解密 (Performance Boost)
+                var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
+                    .Where(x => x.IsDel == 0)
+                    .ToListAsync();
+
+                // 使用 PLINQ 并行解密,极大提升解密密集型任务速度
+                var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAIInfo
+                {
+                    Guid = Guid.NewGuid().ToString("N"),
+                    Source = 0,
+                    Region = AesEncryptionHelper.Decrypt(item.Country),
+                    NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
+                    Address = AesEncryptionHelper.Decrypt(item.Address),
+                    Scope = AesEncryptionHelper.Decrypt(item.Field),
+                    Contact = AesEncryptionHelper.Decrypt(item.Contact),
+                    Phone = AesEncryptionHelper.Decrypt(item.Tel),
+                    Email = AesEncryptionHelper.Decrypt(item.Email),
+                    OperatedAt = DateTime.Now,
+                    Operator = operatorName,
+                }).ToList();
+
+                // 筛选目标国家并计算 AI 缺口
+                var localInvDatas = new List<InvitationAIInfo>();
+                var aiTasks = new List<CountryAIPormptInfo>();
+                int targetPerCountry = entryInfo.NeedCount;
+
+                foreach (var countryName in entryInfo.TargetCountry)
+                {
+                    var countryMatched = allDecrypted.Where(x => x.Region == countryName).Take(targetPerCountry).ToList();
+                    localInvDatas.AddRange(countryMatched);
+
+                    int gap = targetPerCountry - countryMatched.Count;
+                    if (gap > 0) aiTasks.Add(new() { Country = countryName, Count = gap });
+                }
+                #endregion
+
+                #region 3. 混元 AI 远程炼金
+                var hunyuanAIInvDatas = new List<InvitationAIInfo>();
+                if (aiTasks.Any())
+                {
+                    await SendStep(60, $"正在召唤混元 AI:检索 {aiTasks.Count} 个国家的邀请信息...");
+
+                    string question = BuildHunyuanPrompt(aiTasks, entryInfo); // 抽离 Prompt 构造
+                    string aiRawResponse = string.Empty;
+
+                    try
+                    {
+                        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, "AI 接口调用或解析失败");
+                        await SendStep(60, "警告:AI 检索部分失败,将仅展示本地数据。");
+                    }
+                }
+                #endregion
+
+                await SendStep(90, "执行数据合并与持久化...");
+
+                #region 4. 数据合并与入库
+                var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
+                invAiInfo.AiCrawledDetails = finalResult;
+
+                var updateSuccess = await _sqlSugar.Updateable(invAiInfo).ExecuteCommandAsync() > 0;
+                if (!updateSuccess)
+                {
+                    await SendStep(-1, "数据库更新异常。");
+                    return;
+                }
+                #endregion
+
+                // 5. 最终推送:带上全量结果
+                await SendStep(100, "操作成功!资料已全部就绪。", new
+                {
+                    invAiInfo.Id,
+                    invAiInfo.InvName,
+                    AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList(),
+                    Entry = invAiInfo.EntryInfo
+                });
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "SSE 发生不可预知异常");
+                await SendStep(-1, $"炼金炸炉:{ex.Message}");
+            }
+            finally
+            {
+                // 确保最后一次冲刷缓冲区
+                await HttpContext.Response.Body.FlushAsync();
+
+                // 此时方法结束,ASP.NET Core 会自动处理连接关闭
+                _logger.LogInformation("SSE 炼金通道已安全关闭");
+                // :直接完成响应流
+
+                await response.CompleteAsync();
+
+            }
+            return;
+        }
+
+        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
+
+# [INPUT_CONFIG]
+- Tasks: {{{JsonConvert.SerializeObject(tasks)}}}
+- OriginUnit: {{{entryInfo.OriginUnit}}} 
+- Objective: {{{entryInfo.Objective}}}
+- OtherConstraints: {{{entryInfo.OtherConstraints}}}
+# [ROLE_DEFINITION]
+你是一位精通全球跨境经贸、海外园区政策及 Tasks 中的国家本地准入法规的【顶级商务咨询顾问】。你擅长通过“产业链对等原则(Chain-Parity Principle)”为跨国企业精准匹配具备落地价值的合作伙伴。
+
+# [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 企业号。
+
+# [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**: 确保推荐机构不涉及敏感黑名单或已破产企业。
+
+# [INFORMATION_SCHEMA]
+请将结果填充至以下结构的 JSON 数组中:
+- Region:国家(必须与 Tasks 中的 Country 完全匹配)
+- NameCn: 单位名称(中文)
+- NameEn: 单位名称(英文)
+- Address: 详细地理位置(含省市区街道)
+- Scope: 经营范围(需强调其出口配额、生产能力或行业地位)
+- Contact: 联系人姓名及职务
+- Phone: 拨打全号(含区号)
+- Email: 商务联络邮箱
+- SiteUrl: 官方网站或权威社媒主页
+- PostUrl: 近一年内的商务动态/新闻链接
+- RecLevel: 推荐等级(枚举值:Core, Backup)
+- IntgAdvice: 对接深度建议(基于产业链交合、互补逻辑)
+
+# [OUTPUT_PROTOCOL]
+- 仅输出一个标准的 JSON Array 字符串。
+- 严禁任何 Markdown 说明文字、代码块之外的解释或开场白。
+- 确保 JSON 语法在 .NET `JsonSerializer.Deserialize` 下可直接解析。
+
+# [EXECUTION]
+根据以上配置,开始生成。"; ;
+        }
+    
         /// <summary>
         /// 商邀资料AI 设置词条
         /// </summary>
@@ -2661,11 +2910,12 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     CreateUserId = dto.CurrUserId
                 };
 
-                var insert = await _sqlSugar.Insertable(dataInfo).ExecuteCommandAsync();
+                var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
                 if (insert < 1)
                 {
                     return Ok(JsonView(false, $"词条信息新增失败!"));
                 }
+                dataInfo.Id = insert;
             }
             else
             {

+ 1 - 1
OASystem/OASystem.Domain/Entities/Resource/Res_InvitationAI.cs

@@ -152,7 +152,7 @@ namespace OASystem.Domain.Entities.Resource
         /// <summary>
         /// 操作时间
         /// </summary>
-        public DateTime OperatedAt { get; set; }
+        public DateTime OperatedAt { get; set; } = new DateTime();
         /// <summary>
         /// 操作人
         /// </summary>

+ 1 - 0
OASystem/OASystem.Domain/ViewModels/JsonView.cs

@@ -47,3 +47,4 @@ public class JsonView
     }
 
 }
+