|
@@ -2,6 +2,8 @@
|
|
|
using Aspose.Words;
|
|
using Aspose.Words;
|
|
|
using Aspose.Words.Tables;
|
|
using Aspose.Words.Tables;
|
|
|
using EyeSoft.Extensions;
|
|
using EyeSoft.Extensions;
|
|
|
|
|
+using Microsoft.AspNetCore.Http.Features;
|
|
|
|
|
+using Newtonsoft.Json.Serialization;
|
|
|
using NodaTime;
|
|
using NodaTime;
|
|
|
using NPOI.SS.Formula.Functions;
|
|
using NPOI.SS.Formula.Functions;
|
|
|
using NPOI.SS.UserModel;
|
|
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)
|
|
.Where(x => x.IsDel == 0 && x.InvName == name)
|
|
|
.FirstAsync();
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
+ var groupInfo = new Grp_DelegationInfo();
|
|
|
|
|
+
|
|
|
if (info == null)
|
|
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 {
|
|
return Ok(JsonView(true, $"暂无数据",new {
|
|
|
Id = 0,
|
|
Id = 0,
|
|
|
- GroupId= 0,
|
|
|
|
|
- InvName = "",
|
|
|
|
|
|
|
+ GroupId= groupInfo?.Id ?? 0,
|
|
|
|
|
+ InvName = name,
|
|
|
AiCrawledDetails = new List<InvitationAIInfo>(),
|
|
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))
|
|
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 localInvDatas = new List<InvitationAIInfo>(); // 本地数据源(商邀资料)
|
|
|
var aiTasks = new List<CountryAIPormptInfo>(); // 记录:国家 -> 需要补齐的数量
|
|
var aiTasks = new List<CountryAIPormptInfo>(); // 记录:国家 -> 需要补齐的数量
|
|
|
|
|
+
|
|
|
#region 本地数据源(商邀资料)
|
|
#region 本地数据源(商邀资料)
|
|
|
|
|
|
|
|
var datas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
|
|
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>
|
|
/// <summary>
|
|
|
/// 商邀资料AI 设置词条
|
|
/// 商邀资料AI 设置词条
|
|
|
/// </summary>
|
|
/// </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
|
|
CreateUserId = dto.CurrUserId
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- var insert = await _sqlSugar.Insertable(dataInfo).ExecuteCommandAsync();
|
|
|
|
|
|
|
+ var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
|
|
|
if (insert < 1)
|
|
if (insert < 1)
|
|
|
{
|
|
{
|
|
|
return Ok(JsonView(false, $"词条信息新增失败!"));
|
|
return Ok(JsonView(false, $"词条信息新增失败!"));
|
|
|
}
|
|
}
|
|
|
|
|
+ dataInfo.Id = insert;
|
|
|
}
|
|
}
|
|
|
else
|
|
else
|
|
|
{
|
|
{
|