|
@@ -4908,7 +4908,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
[ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
|
public async Task<IActionResult> InvitationAI_NoGroupInit()
|
|
public async Task<IActionResult> InvitationAI_NoGroupInit()
|
|
|
{
|
|
{
|
|
|
- var itemNames = await InvitationAIInvName();
|
|
|
|
|
|
|
+ var itemNames = await InvitationAI_NoGroupInvName();
|
|
|
var unitNames = await InvitationAIClientName();
|
|
var unitNames = await InvitationAIClientName();
|
|
|
var countries = await InvitationAICountryName();
|
|
var countries = await InvitationAICountryName();
|
|
|
var industryNodes = IndustryTree.Build().Select(x => x.NameCn).ToList();
|
|
var industryNodes = IndustryTree.Build().Select(x => x.NameCn).ToList();
|
|
@@ -5001,156 +5001,56 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// 商邀资料AI-无团组版 混元AI查询资料(SSE流式推送)
|
|
|
|
|
|
|
+ /// 商邀资料AI-无团组版 新增公务资料
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
|
|
+ /// <returns></returns>
|
|
|
[HttpPost]
|
|
[HttpPost]
|
|
|
- public async Task InvitationAI_NoGroupSearchStreamProgress([FromBody] InvitationAI_NoGroupSearchDto dto)
|
|
|
|
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
|
|
|
+ public async Task<IActionResult> InvitationAI_NoGroupAdd(InvitationAI_NoGroupAddDto dto)
|
|
|
{
|
|
{
|
|
|
- HttpContext.InitializeSse();
|
|
|
|
|
-
|
|
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- await HttpContext.SendSseStepAsync(5, "正在加载配置...");
|
|
|
|
|
|
|
+ // 基础校验
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.InvName))
|
|
|
|
|
+ return Ok(JsonView(false, "请传入有效的公务名称!"));
|
|
|
|
|
|
|
|
- #region 1. 异步并行化验证 (Performance Boost)
|
|
|
|
|
- var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().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();
|
|
|
|
|
|
|
+ if (dto.CurrUserId <= 0)
|
|
|
|
|
+ return Ok(JsonView(false, "用户身份无效"));
|
|
|
|
|
|
|
|
- if (invAiInfo?.EntryInfo == null)
|
|
|
|
|
- {
|
|
|
|
|
- await HttpContext.SendSseStepAsync(-1, "未找到有效的关键字配置。");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ var invName = dto.InvName;
|
|
|
|
|
|
|
|
- var entryInfo = invAiInfo.EntryInfo;
|
|
|
|
|
- var targetCountrySet = new HashSet<string>(entryInfo.TargetCountry);
|
|
|
|
|
- #endregion
|
|
|
|
|
|
|
+ #region 数据库操作
|
|
|
|
|
|
|
|
- await HttpContext.SendSseStepAsync(20, "正在解析本地典籍 (并行解密)...");
|
|
|
|
|
|
|
+ // 数据库信息获取方式
|
|
|
|
|
+ var invExist = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().AnyAsync(x => x.IsDel == 0 && x.InvName == invName);
|
|
|
|
|
+ if (invExist)
|
|
|
|
|
+ return Ok(JsonView(false, $"公务名称 [{invName}] 已存在,请更换"));
|
|
|
|
|
|
|
|
- #region 2. 内存计算优化
|
|
|
|
|
- // 仅查询必要字段,减少 DataReader 压力
|
|
|
|
|
- var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
|
|
|
|
|
- .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.UnitName))
|
|
|
|
|
- .Select(x => new { x.Country, x.UnitName, x.Address, x.Field, x.Contact, x.Tel, x.Email })
|
|
|
|
|
- .ToListAsync();
|
|
|
|
|
|
|
+ string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
|
|
|
|
|
- // PLINQ 核心炼金:利用多核并行解密
|
|
|
|
|
- var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAI_NoGroupInfo
|
|
|
|
|
|
|
+ var dataInfo = new Res_InvitationAI_NoGroup()
|
|
|
|
|
+ {
|
|
|
|
|
+ InvName = invName,
|
|
|
|
|
+ EntryInfo = new Entry_NoGroupInfo()
|
|
|
{
|
|
{
|
|
|
- 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,
|
|
Operator = operatorName,
|
|
|
- }).ToList();
|
|
|
|
|
-
|
|
|
|
|
- // 筛选符合国家的本地数据
|
|
|
|
|
- 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 => !string.IsNullOrEmpty(x.NameCn) && matchedNames.Contains(x.NameCn)).Distinct().ToList();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // --- 阶段 B: 差额补全 (Gap Filling) ---
|
|
|
|
|
- var localInvDatas = new List<InvitationAI_NoGroupInfo>();
|
|
|
|
|
- var aiTasks = new List<CountryAIPormptInfo>();
|
|
|
|
|
-
|
|
|
|
|
- foreach (var country in entryInfo.TargetCountry)
|
|
|
|
|
- {
|
|
|
|
|
- var countryData = matchedCountries.Where(x => x.Region == country).Take(entryInfo.NeedCount).ToList();
|
|
|
|
|
- localInvDatas.AddRange(countryData);
|
|
|
|
|
-
|
|
|
|
|
- int gap = entryInfo.NeedCount - countryData.Count;
|
|
|
|
|
- if (gap > 0) aiTasks.Add(new() { Country = country, Count = gap });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var hunyuanAIInvDatas = new List<InvitationAI_NoGroupInfo>();
|
|
|
|
|
- if (aiTasks.Any())
|
|
|
|
|
- {
|
|
|
|
|
- // 强制冷却(应对 QPS 限制)
|
|
|
|
|
- // 混元免费版或低阶版本通常有 1s/1次 的频率限制
|
|
|
|
|
- await Task.Delay(1000);
|
|
|
|
|
- // 任务配置计算
|
|
|
|
|
- var countryTasks = QuotaScheduler.GenerateTasks(aiTasks, entryInfo.Industries, entryInfo.ScaleTypes);
|
|
|
|
|
-
|
|
|
|
|
- var countryTasksGroupBy = countryTasks.GroupBy(x => x.Region).Select(g => new
|
|
|
|
|
- {
|
|
|
|
|
- Region = g.Key,
|
|
|
|
|
- Counrt = g.Count()
|
|
|
|
|
- }).ToList();
|
|
|
|
|
-
|
|
|
|
|
- await HttpContext.SendSseStepAsync(60, $"AI 正在跨境检索缺失的 {string.Join(", ", countryTasksGroupBy.Select(x => $"{x.Region}({x.Counrt}条)"))} 单位资料...");
|
|
|
|
|
- string searchQuestion = BuildHunyuanPrompt(aiTasks, countryTasks, entryInfo);
|
|
|
|
|
-
|
|
|
|
|
- _logger.LogInformation(@"公务名称:{InvName}; 混元AI查询提示词:{searchQuestion}", invAiInfo.InvName, searchQuestion);
|
|
|
|
|
- string searchRaw = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(searchQuestion);
|
|
|
|
|
-
|
|
|
|
|
- var aiParsed = CleanAndParseJson<List<InvitationAI_NoGroupInfo>>(searchRaw);
|
|
|
|
|
- if (aiParsed != null)
|
|
|
|
|
- {
|
|
|
|
|
- hunyuanAIInvDatas = aiParsed.Select(x => {
|
|
|
|
|
- x.Guid = Guid.NewGuid().ToString("N");
|
|
|
|
|
- x.Source = 1;
|
|
|
|
|
- x.Operator = operatorName;
|
|
|
|
|
- x.OperatedAt = DateTime.Now;
|
|
|
|
|
- return x;
|
|
|
|
|
- }).ToList();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- #endregion
|
|
|
|
|
-
|
|
|
|
|
- await HttpContext.SendSseStepAsync(90, "正在同步成果至持久化仓库...");
|
|
|
|
|
-
|
|
|
|
|
- #region 4. 数据融合与更新
|
|
|
|
|
- var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
|
|
|
|
|
-
|
|
|
|
|
- // 仅更新指定列,性能更优
|
|
|
|
|
- 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
|
|
|
|
|
|
|
+ OperatedAt = DateTime.Now
|
|
|
|
|
+ },
|
|
|
|
|
+ CreateUserId = dto.CurrUserId
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 5. 终焉推送
|
|
|
|
|
- await HttpContext.SendSseStepAsync(100, "操作成功!资料已全部就绪。", new
|
|
|
|
|
- {
|
|
|
|
|
- invAiInfo.Id,
|
|
|
|
|
- AiCrawledDetails = finalResult.OrderByDescending(x => x.Source).ThenBy(x => x.Region).ToList()
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- catch (Exception ex)
|
|
|
|
|
|
|
+ var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
|
|
|
|
|
+ if (insert < 1)
|
|
|
{
|
|
{
|
|
|
- _logger.LogError(ex, "SSE 管道熔断");
|
|
|
|
|
- await HttpContext.SendSseStepAsync(-1, $"SSE 错误:{ex.InnerException.Message}({ex.Message})");
|
|
|
|
|
|
|
+ return Ok(JsonView(false, $"词条信息新增失败!"));
|
|
|
}
|
|
}
|
|
|
- finally
|
|
|
|
|
|
|
+
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ return Ok(JsonView(true, $"新增成功!", new
|
|
|
{
|
|
{
|
|
|
- await HttpContext.FinalizeSseAsync();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ id = insert,
|
|
|
|
|
+ invName = dataInfo.InvName,
|
|
|
|
|
+ entry = dataInfo.EntryInfo,
|
|
|
|
|
+ }));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -5161,29 +5061,25 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
[ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
|
public async Task<IActionResult> InvitationAI_NoGroupSetPrompt(InvitationAI_NoGroupSetPromptDto dto)
|
|
public async Task<IActionResult> InvitationAI_NoGroupSetPrompt(InvitationAI_NoGroupSetPromptDto dto)
|
|
|
{
|
|
{
|
|
|
- // 基础校验
|
|
|
|
|
- if (string.IsNullOrWhiteSpace(dto.InvName) || string.IsNullOrWhiteSpace(dto.Objective) || 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 invName = dto.InvName;
|
|
|
|
|
-
|
|
|
|
|
- #region 数据库操作
|
|
|
|
|
|
|
+ if (dto.Id < 1) return Ok(JsonView(false, "当前公务ID无效"));
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.Objective)) return Ok(JsonView(false, "请输入出访目的"));
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.OriginUnit)) return Ok(JsonView(false, "请输入单位名称"));
|
|
|
|
|
+ if (dto.TargetCountry == null || !dto.TargetCountry.Any()) return Ok(JsonView(false, "请选择目标国家"));
|
|
|
|
|
+ if (dto.Industries == null || !dto.Industries.Any()) return Ok(JsonView(false, "请选择所属行业"));
|
|
|
|
|
+ if (dto.ScaleTypes == null || !dto.ScaleTypes.Any()) return Ok(JsonView(false, "请选择规模类型"));
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(dto.VisitDate)) return Ok(JsonView(false, "请输入出访时间"));
|
|
|
|
|
|
|
|
// 数据库信息获取方式
|
|
// 数据库信息获取方式
|
|
|
- var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.IsDel == 0 && x.InvName.Equals(invName)).FirstAsync();
|
|
|
|
|
-
|
|
|
|
|
- #region 词条信息
|
|
|
|
|
|
|
+ var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.IsDel == 0 && x.Id == dto.Id).FirstAsync();
|
|
|
|
|
+ if (dataInfo == null)
|
|
|
|
|
+ return Ok(JsonView(false, "请先新增公务信息"));
|
|
|
|
|
|
|
|
string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
-
|
|
|
|
|
- var entryInfo = new Entry_NoGroupInfo()
|
|
|
|
|
|
|
+ dataInfo.EntryInfo = new Entry_NoGroupInfo()
|
|
|
{
|
|
{
|
|
|
- OriginUnit = dto.OriginUnit,
|
|
|
|
|
|
|
+ OriginUnit = dto.OriginUnit?.Trim(),
|
|
|
TargetCountry = dto.TargetCountry,
|
|
TargetCountry = dto.TargetCountry,
|
|
|
- Objective = dto.Objective,
|
|
|
|
|
|
|
+ Objective = dto.Objective?.Trim(),
|
|
|
Industries = dto.Industries,
|
|
Industries = dto.Industries,
|
|
|
ScaleTypes = dto.ScaleTypes,
|
|
ScaleTypes = dto.ScaleTypes,
|
|
|
IsBackground = dto.IsBackground,
|
|
IsBackground = dto.IsBackground,
|
|
@@ -5191,53 +5087,28 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
Operator = operatorName,
|
|
Operator = operatorName,
|
|
|
OperatedAt = DateTime.Now
|
|
OperatedAt = DateTime.Now
|
|
|
};
|
|
};
|
|
|
- #endregion
|
|
|
|
|
-
|
|
|
|
|
- if (dataInfo == null)
|
|
|
|
|
- {
|
|
|
|
|
- dataInfo = new Res_InvitationAI_NoGroup()
|
|
|
|
|
- {
|
|
|
|
|
- InvName = invName,
|
|
|
|
|
- EntryInfo = entryInfo,
|
|
|
|
|
- CreateUserId = dto.CurrUserId
|
|
|
|
|
- };
|
|
|
|
|
|
|
|
|
|
- var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
|
|
|
|
|
- if (insert < 1)
|
|
|
|
|
- {
|
|
|
|
|
- return Ok(JsonView(false, $"词条信息新增失败!"));
|
|
|
|
|
- }
|
|
|
|
|
- dataInfo.Id = insert;
|
|
|
|
|
- }
|
|
|
|
|
- else
|
|
|
|
|
|
|
+ var updateResult = await _sqlSugar.Updateable(dataInfo)
|
|
|
|
|
+ .UpdateColumns(x => new { x.EntryInfo })
|
|
|
|
|
+ .ExecuteCommandHasChangeAsync();
|
|
|
|
|
+ if (!updateResult)
|
|
|
{
|
|
{
|
|
|
- dataInfo.EntryInfo = entryInfo;
|
|
|
|
|
-
|
|
|
|
|
- var update = await _sqlSugar.Updateable(dataInfo).ExecuteCommandAsync();
|
|
|
|
|
- if (update < 1)
|
|
|
|
|
- {
|
|
|
|
|
- return Ok(JsonView(false, $"词条信息更新失败!"));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return Ok(JsonView(false, $"词条信息更新失败!"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- #endregion
|
|
|
|
|
-
|
|
|
|
|
return Ok(JsonView(true, $"设置成功!", new
|
|
return Ok(JsonView(true, $"设置成功!", new
|
|
|
{
|
|
{
|
|
|
- dataInfo.Id,
|
|
|
|
|
- dataInfo.InvName,
|
|
|
|
|
- Entry = dataInfo.EntryInfo,
|
|
|
|
|
|
|
+ id = dataInfo.Id,
|
|
|
|
|
+ invName = dataInfo.InvName,
|
|
|
|
|
+ entry = dataInfo.EntryInfo
|
|
|
}));
|
|
}));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// 商邀资料AI-无团组版 设置复选框选中 批量
|
|
|
|
|
|
|
+ /// 商邀资料AI-无团组版 设置复选框选中 批量(支持传入空数组进行重置)
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
/// <param name="dto"></param>
|
|
/// <param name="dto"></param>
|
|
|
/// <returns></returns>
|
|
/// <returns></returns>
|
|
|
- /// <summary>
|
|
|
|
|
- /// 商邀资料AI-无团组版 设置复选框选中 批量 (支持传入空数组进行重置)
|
|
|
|
|
- /// </summary>
|
|
|
|
|
[HttpPost]
|
|
[HttpPost]
|
|
|
public async Task<IActionResult> InvitationAI_NoGroupSetChecked([FromBody] InvitationAI_NoGroupSetCheckedDto dto)
|
|
public async Task<IActionResult> InvitationAI_NoGroupSetChecked([FromBody] InvitationAI_NoGroupSetCheckedDto dto)
|
|
|
{
|
|
{
|
|
@@ -5452,6 +5323,159 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
: JsonView(false, $"操作失败:{result.ErrorMessage}"));
|
|
: JsonView(false, $"操作失败:{result.ErrorMessage}"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 商邀资料AI-无团组版 混元AI查询资料(SSE流式推送)
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [HttpPost]
|
|
|
|
|
+ public async Task InvitationAI_NoGroupSearchStreamProgress([FromBody] InvitationAI_NoGroupSearchDto dto)
|
|
|
|
|
+ {
|
|
|
|
|
+ HttpContext.InitializeSse();
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(5, "正在加载配置...");
|
|
|
|
|
+
|
|
|
|
|
+ #region 1. 异步并行化验证 (Performance Boost)
|
|
|
|
|
+ var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().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();
|
|
|
|
|
+
|
|
|
|
|
+ if (invAiInfo?.EntryInfo == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(-1, "未找到有效的关键字配置。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var entryInfo = invAiInfo.EntryInfo;
|
|
|
|
|
+ var targetCountrySet = new HashSet<string>(entryInfo.TargetCountry);
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(20, "正在解析本地典籍 (并行解密)...");
|
|
|
|
|
+
|
|
|
|
|
+ #region 2. 内存计算优化
|
|
|
|
|
+ // 仅查询必要字段,减少 DataReader 压力
|
|
|
|
|
+ var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
|
|
|
|
|
+ .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.UnitName))
|
|
|
|
|
+ .Select(x => new { x.Country, x.UnitName, x.Address, x.Field, x.Contact, x.Tel, x.Email })
|
|
|
|
|
+ .ToListAsync();
|
|
|
|
|
+
|
|
|
|
|
+ // PLINQ 核心炼金:利用多核并行解密
|
|
|
|
|
+ var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAI_NoGroupInfo
|
|
|
|
|
+ {
|
|
|
|
|
+ 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();
|
|
|
|
|
+
|
|
|
|
|
+ // 筛选符合国家的本地数据
|
|
|
|
|
+ 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 => !string.IsNullOrEmpty(x.NameCn) && matchedNames.Contains(x.NameCn)).Distinct().ToList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 阶段 B: 差额补全 (Gap Filling) ---
|
|
|
|
|
+ var localInvDatas = new List<InvitationAI_NoGroupInfo>();
|
|
|
|
|
+ var aiTasks = new List<CountryAIPormptInfo>();
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var country in entryInfo.TargetCountry)
|
|
|
|
|
+ {
|
|
|
|
|
+ var countryData = matchedCountries.Where(x => x.Region == country).Take(entryInfo.NeedCount).ToList();
|
|
|
|
|
+ localInvDatas.AddRange(countryData);
|
|
|
|
|
+
|
|
|
|
|
+ int gap = entryInfo.NeedCount - countryData.Count;
|
|
|
|
|
+ if (gap > 0) aiTasks.Add(new() { Country = country, Count = gap });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var hunyuanAIInvDatas = new List<InvitationAI_NoGroupInfo>();
|
|
|
|
|
+ if (aiTasks.Any())
|
|
|
|
|
+ {
|
|
|
|
|
+ // 强制冷却(应对 QPS 限制)
|
|
|
|
|
+ // 混元免费版或低阶版本通常有 1s/1次 的频率限制
|
|
|
|
|
+ await Task.Delay(1000);
|
|
|
|
|
+ // 任务配置计算
|
|
|
|
|
+ var countryTasks = QuotaScheduler.GenerateTasks(aiTasks, entryInfo.Industries, entryInfo.ScaleTypes);
|
|
|
|
|
+
|
|
|
|
|
+ var countryTasksGroupBy = countryTasks.GroupBy(x => x.Region).Select(g => new
|
|
|
|
|
+ {
|
|
|
|
|
+ Region = g.Key,
|
|
|
|
|
+ Counrt = g.Count()
|
|
|
|
|
+ }).ToList();
|
|
|
|
|
+
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(60, $"AI 正在跨境检索缺失的 {string.Join(", ", countryTasksGroupBy.Select(x => $"{x.Region}({x.Counrt}条)"))} 单位资料...");
|
|
|
|
|
+ string searchQuestion = BuildHunyuanPrompt(aiTasks, countryTasks, entryInfo);
|
|
|
|
|
+
|
|
|
|
|
+ _logger.LogInformation(@"公务名称:{InvName}; 混元AI查询提示词:{searchQuestion}", invAiInfo.InvName, searchQuestion);
|
|
|
|
|
+ string searchRaw = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(searchQuestion);
|
|
|
|
|
+
|
|
|
|
|
+ var aiParsed = CleanAndParseJson<List<InvitationAI_NoGroupInfo>>(searchRaw);
|
|
|
|
|
+ if (aiParsed != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ hunyuanAIInvDatas = aiParsed.Select(x => {
|
|
|
|
|
+ x.Guid = Guid.NewGuid().ToString("N");
|
|
|
|
|
+ x.Source = 1;
|
|
|
|
|
+ x.Operator = operatorName;
|
|
|
|
|
+ x.OperatedAt = DateTime.Now;
|
|
|
|
|
+ return x;
|
|
|
|
|
+ }).ToList();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(90, "正在同步成果至持久化仓库...");
|
|
|
|
|
+
|
|
|
|
|
+ #region 4. 数据融合与更新
|
|
|
|
|
+ var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
|
|
|
|
|
+
|
|
|
|
|
+ // 仅更新指定列,性能更优
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 终焉推送
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(100, "操作成功!资料已全部就绪。", new
|
|
|
|
|
+ {
|
|
|
|
|
+ invAiInfo.Id,
|
|
|
|
|
+ AiCrawledDetails = finalResult.OrderByDescending(x => x.Source).ThenBy(x => x.Region).ToList()
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogError(ex, "SSE 管道熔断");
|
|
|
|
|
+ await HttpContext.SendSseStepAsync(-1, $"SSE 错误:{ex.InnerException.Message}({ex.Message})");
|
|
|
|
|
+ }
|
|
|
|
|
+ finally
|
|
|
|
|
+ {
|
|
|
|
|
+ await HttpContext.FinalizeSseAsync();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 商邀资料AI-无团组版 混元AI续写(SSE流式推送)
|
|
/// 商邀资料AI-无团组版 混元AI续写(SSE流式推送)
|
|
|
/// </summary>
|
|
/// </summary>
|