2 Commits f18475a89e ... baaafb9769

Author SHA1 Message Date
  yuanrf baaafb9769 Merge branch 'develop' of http://132.232.92.186:3000/yuanrf/OA2023 into develop 6 days ago
  yuanrf 54a072a46c ++ 6 days ago

+ 89 - 20
OASystem/OASystem.Api/Controllers/AITestController.cs

@@ -1,3 +1,5 @@
+using Microsoft.AspNetCore.Mvc;
+using OASystem.API.OAMethodLib.DeepSeekAPI;
 using Flurl.Http.Configuration;
 using Microsoft.Extensions.Options;
 using OASystem.API.OAMethodLib.DoubaoAPI;
@@ -30,16 +32,19 @@ namespace OASystem.API.Controllers
         private readonly IMicrosoftGraphMailboxService _microsoftGraphMailboxService;
         private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _microsoftGraphMailboxOptions;
 
+        private readonly IDeepSeekService _deepSeekService;
+
         public AITestController(
-            IHunyuanService hunyuanService, 
-            IDoubaoService doubaoService, 
-            ILogger<AITestController> logger, 
-            IQiYeWeChatApiService qiYeWeChatApiService, 
-            HotmailService hotmailService, 
-            System.Net.Http.IHttpClientFactory httpClientFactory, 
+            IHunyuanService hunyuanService,
+            IDoubaoService doubaoService,
+            ILogger<AITestController> logger,
+            IQiYeWeChatApiService qiYeWeChatApiService,
+            HotmailService hotmailService,
+            System.Net.Http.IHttpClientFactory httpClientFactory,
             IConfiguration config,
             IMicrosoftGraphMailboxService microsoftGraphMailboxService,
-            IOptionsMonitor<MicrosoftGraphMailboxOptions> microsoftGraphMailboxOptions
+            IOptionsMonitor<MicrosoftGraphMailboxOptions> microsoftGraphMailboxOptions,
+            IDeepSeekService deepSeekService
             )
         {
             _hunyuanService = hunyuanService;
@@ -50,6 +55,7 @@ namespace OASystem.API.Controllers
             _httpClientFactory = httpClientFactory;
             _config = config;
             _microsoftGraphMailboxService = microsoftGraphMailboxService;
+            _deepSeekService = deepSeekService;
             _microsoftGraphMailboxOptions = microsoftGraphMailboxOptions;
         }
 
@@ -346,6 +352,71 @@ namespace OASystem.API.Controllers
         }
         #endregion
 
+        #region DeepSeek 测试
+
+        /// <summary>
+        /// DeepSeek 带上下文的流式对话测试。响应为 NDJSON:每行一条 JSON,phase 为 reasoning、content 或 error。
+        /// </summary>
+        [HttpPost("deepseek-chat-stream-with-history")]
+        public async Task<IActionResult> DeepSeekChatStreamWithHistory(
+            [FromBody] DeepSeekChatStreamHistoryTestRequest request,
+            CancellationToken cancellationToken = default)
+        {
+            if (request?.Messages == null || request.Messages.Count == 0)
+                return BadRequest(new { message = "Messages 不能为空,且至少包含一条 user/system/assistant 消息。" });
+
+            Response.ContentType = "application/x-ndjson; charset=utf-8";
+            Response.Headers["Cache-Control"] = "no-cache";
+
+            static string NdjsonLine(DeepSeekStreamChunk c) => JsonConvert.SerializeObject(new
+            {
+                phase = c.Phase == DeepSeekStreamPhase.Reasoning ? "reasoning" : "content",
+                text = c.Text
+            });
+
+            try
+            {
+                await foreach (var chunk in _deepSeekService.ChatStreamWithHistoryAsync(
+                    request.Messages,
+                    string.IsNullOrWhiteSpace(request.Model) ? "deepseek-chat" : request.Model!.Trim(),
+                    request.Temperature,
+                    request.MaxTokens))
+                {
+                    cancellationToken.ThrowIfCancellationRequested();
+                    await Response.WriteAsync(NdjsonLine(chunk) + "\n", cancellationToken);
+                    await Response.Body.FlushAsync(cancellationToken);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "DeepSeek 带历史流式对话失败");
+                await Response.WriteAsync(
+                    JsonConvert.SerializeObject(new { phase = "error", text = ex.Message }) + "\n",
+                    cancellationToken);
+            }
+
+            return new EmptyResult();
+        }
+
+        /// <summary>
+        /// DeepSeek 流式对话(含多轮)请求体
+        /// </summary>
+        public class DeepSeekChatStreamHistoryTestRequest
+        {
+            public List<DeepSeekHistoryMessage> Messages { get; set; } = new();
+
+            public string? Model { get; set; } = "deepseek-chat";
+
+            public float Temperature { get; set; } = 0.7f;
+
+            public int MaxTokens { get; set; } = 4000;
+        }
+
+        #endregion
+
         /// <summary>
         /// hotmail 发送邮件
         /// </summary>
@@ -356,11 +427,12 @@ namespace OASystem.API.Controllers
                 //"Roy.Lei.Atom@hotmail.com",
                 "925554512@qq.com",
                 //"johnny.yang@pan-american-intl.com",
-                new HotmailService.MailDto() { 
-                Subject = "系统提醒",
-                Content = "<p>这是一封Homail 发送的测试邮件</p>",
-                //To = "Roy.lei@pan-american-intl.com"
-                To = "johnny.yang@pan-american-intl.com"
+                new HotmailService.MailDto()
+                {
+                    Subject = "系统提醒",
+                    Content = "<p>这是一封Homail 发送的测试邮件</p>",
+                    //To = "Roy.lei@pan-american-intl.com"
+                    To = "johnny.yang@pan-american-intl.com"
                 });
 
             return StatusCode(200, new { Message = "操作成功。" });
@@ -382,11 +454,11 @@ namespace OASystem.API.Controllers
             var yesterdayEnd = yesterdayStart.AddDays(1).AddTicks(-1); // 昨天的 23:59:59
 
 
-           var res = await _hotmailService.GetMergedMessagesAsync(
-               new List<string>() { "925554512@qq.com" },
-               yesterdayStart,
-               yesterdayEnd
-               );
+            var res = await _hotmailService.GetMergedMessagesAsync(
+                new List<string>() { "925554512@qq.com" },
+                yesterdayStart,
+                yesterdayEnd
+                );
 
             return StatusCode(200, res);
         }
@@ -519,8 +591,6 @@ namespace OASystem.API.Controllers
             }
         }
 
-
-
         public class EmailAuthRedisCache
         {
             public string? AccessToken { get; set; }
@@ -529,7 +599,6 @@ namespace OASystem.API.Controllers
             public string? ClientId { get; set; }
         }
 
-
         /// <summary>
         /// 从 Redis 读取 MSAL 缓存与 HomeAccountId,静默刷新 Graph access_token。
         /// </summary>

+ 112 - 5
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -2708,7 +2708,7 @@ FROM
         }
 
         /// <summary>
-        /// 团组合同下载(1.付款80% 2.付全款)
+        /// 团组合同下载(1.付款80% 2.付全款 政府团 ---- 3.企业团-出访前结算合同 4.政府团-预付款及尾款结算 5.政府团-归国后结算 6.政府团-出访前结算)
         /// </summary>
         /// <param name="dto"></param>
         /// <returns></returns>
@@ -2814,7 +2814,8 @@ FROM
                                                 .Where(x => x.DiId == dto.GroupId && x.IsDel == 0)
                                                 .ToListAsync();
 
-            if (!blackCodeList.Any())
+            var blackType = new List<int>() { 1, 2 };
+            if (!blackCodeList.Any() && blackType.Contains(dto.FileType))
             {
                 jw.Msg = "该团无机票行程代码!";
                 return Ok(jw);
@@ -2933,7 +2934,7 @@ FROM
 
             Document doc = null;
             var percentage = 0.8M;
-            var fileNameadd = "预付";
+            var fileNameadd = "预付版本合同文件";
 
             if (dto.FileType == 1)
             {
@@ -2943,7 +2944,31 @@ FROM
             {
                 doc = new Document($"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Template/团组合同全款无表格.doc");
                 percentage = 1;
-                fileNameadd = "全款";
+                fileNameadd = "全款版本合同文件";
+            }
+            else if (dto.FileType == 3)
+            {
+                doc = new Document($"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Template/企业合同模板.doc");
+                percentage = 1;
+                fileNameadd = "企业团-出访前结算合同";
+            }
+            else if (dto.FileType == 4)
+            {
+                doc = new Document($"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Template/出访考察协议模版-预付款及尾款结算.doc");
+                percentage = 0.85M;
+                fileNameadd = "政府团-预付款及尾款结算合同";
+            }
+            else if (dto.FileType == 5)
+            {
+                doc = new Document($"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Template/出访考察协议模版-归国后结算.doc");
+                percentage = 1M;
+                fileNameadd = "政府团-归国后结算合同";
+            }
+            else if (dto.FileType == 6)
+            {
+                doc = new Document($"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Template/出访考察协议模板(出访前结算).doc");
+                percentage = 1M;
+                fileNameadd = "政府团-出访前结算合同";
             }
             else
             {
@@ -3029,6 +3054,7 @@ FROM
             bookMarkDic.Add("TotalPrice", (totalPrice).ToString("#0.00"));
             bookMarkDic.Add("TotalPrice1", (totalPrice * percentage).ToString("#0.00"));
             bookMarkDic.Add("TotalPriceChinese", decimal.Parse((totalPrice * percentage).ToString("#0.00")).ConvertCNYUpper());
+            bookMarkDic.Add("TotalPriceUp", decimal.Parse((totalPrice * percentage).ToString("#0.00")).ConvertCNYUpper());
 
             //--合同表格部分
             bookMarkDic.Add("TimeNowDate", DateTime.Now.ToString("yyyy年MM月dd日"));
@@ -3348,7 +3374,7 @@ FROM
                 }
             }
 
-            string savePaht = $"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Export/{di.TeamName}_{fileNameadd}版本合同文件.doc";
+            string savePaht = $"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Export/{di.TeamName}_{fileNameadd}.doc";
             doc.Save(savePaht);
 
             jw.Msg = "生成成功!";
@@ -3357,6 +3383,87 @@ FROM
             return Ok(jw);
         }
 
+        /// <summary>
+        /// 会务团
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> DownGroupContractConferenceFile(DownGroupContractFileDto dto)
+        {
+            var jw = JsonView(false);
+
+            var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
+            .Where(x => x.IsDel == 0 && x.Id == dto.GroupId)
+            .FirstAsync();
+            if (groupInfo == null)
+            {
+                jw.Msg = "团组信息不存在!";
+                return Ok(jw);
+            }
+
+            var GroupPrice = 0.00M;
+            string VisitCountry = Regex.Replace(groupInfo.VisitCountry, @"[|-]", "、"); ;
+
+            GroupPrice = _sqlSugar.Queryable<Grp_ConferenceAffairsCost>()
+                .First(x => x.Diid == dto.GroupId && x.IsDel == 0)?.BaoJiaAll ?? 0.00M;
+
+            Dictionary<string, string> bookMarkDic = new Dictionary<string, string>();
+            bookMarkDic.Add("UpGroupPrice", GeneralMethod.ConvertCNYUpper(GroupPrice.ToString("F2").ObjToDecimal()));
+            bookMarkDic.Add("ClientUnit", groupInfo.ClientUnit);
+            bookMarkDic.Add("GroupPrice", GroupPrice.ToString("F2"));
+            bookMarkDic.Add("VisitCountry", VisitCountry);
+            bookMarkDic.Add("VisitPNumber", groupInfo.VisitPNumber.ToString());
+            bookMarkDic.Add("VisitStartDate", groupInfo.VisitStartDate.ToString("yyyy年MM月dd日"));
+            bookMarkDic.Add("StartYear", groupInfo.VisitStartDate.Year.ToString());
+            bookMarkDic.Add("EndYear", groupInfo.VisitEndDate.Year.ToString());
+            bookMarkDic.Add("StartMonth", groupInfo.VisitStartDate.Month.ToString());
+            bookMarkDic.Add("EndMonth", groupInfo.VisitEndDate.Month.ToString());
+            bookMarkDic.Add("StartDay", groupInfo.VisitStartDate.Day.ToString());
+            bookMarkDic.Add("EndDay", groupInfo.VisitEndDate.Day.ToString());
+            bookMarkDic.Add("GroupPrice1", (GroupPrice * 0.80M).ToString("F2"));
+            bookMarkDic.Add("GroupPrice2", (GroupPrice * 0.20M).ToString("F2"));
+            bookMarkDic.Add("UpGroupPrice1", GeneralMethod.ConvertCNYUpper((GroupPrice * 0.80M).ToString("F2").ObjToDecimal()));
+            bookMarkDic.Add("UpGroupPrice2", GeneralMethod.ConvertCNYUpper((GroupPrice * 0.20M).ToString("F2").ObjToDecimal()));
+
+            var underlineFont = new List<String>{
+                "ClientUnit",
+                "VisitStartDate"
+            };
+
+            var doc = new Document($"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Template/会务合同模板.doc");
+            var builder = new DocumentBuilder(doc);
+            foreach (var bookmarkName in bookMarkDic.Keys)
+            {
+                Bookmark bookmark = doc.Range.Bookmarks[bookmarkName];
+                if (bookmark == null)
+                {
+                    continue;
+                }
+
+                builder.MoveTo(bookmark.BookmarkStart);
+                if (underlineFont.Contains(bookmarkName))
+                {
+                    builder.Font.Underline = Aspose.Words.Underline.Single;
+                }
+
+                builder.Write(bookMarkDic[bookmarkName]);
+                if (underlineFont.Contains(bookmarkName))
+                {
+                    builder.Font.ClearFormatting();
+                }
+            }
+
+            string savePaht = $"{AppSettingsHelper.Get("WordBasePath")}GroupContractFile/Export/{groupInfo.TeamName}_会务服务合同.doc";
+            doc.Save(savePaht);
+
+            jw.Msg = "生成成功!";
+            jw.Code = 200;
+            jw.Data = new { Url = AppSettingsHelper.Get("WordBaseUrl") + savePaht.Replace(AppSettingsHelper.Get("WordBasePath"), AppSettingsHelper.Get("WordFtpPath")) };
+
+            return Ok(jw);
+        }
+
         static int FindMergeStartRowIndex(Table table, int rowIndex)
         {
             int index = rowIndex;

+ 13 - 0
OASystem/OASystem.Api/OAMethodLib/DeepSeekAPI/DeepSeekModels.cs

@@ -44,6 +44,19 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
         public string Text { get; set; } = "";
     }
 
+    /// <summary>
+    /// 连续对话中的一条上下文消息(纯文本,对应 chat/completions 的 messages 项)。
+    /// 调用方按时间顺序传入完整历史(含 system、多轮 user/assistant),每轮流式结束后将 assistant 全文追加再发下一轮。
+    /// </summary>
+    public class DeepSeekHistoryMessage
+    {
+        /// <summary>system | user | assistant</summary>
+        public string Role { get; set; } = "";
+
+        /// <summary>该轮文本内容</summary>
+        public string Content { get; set; } = "";
+    }
+
     /// <summary>
     /// 文件上传到DeepSeek API的请求参数
     /// </summary>

+ 104 - 0
OASystem/OASystem.Api/OAMethodLib/DeepSeekAPI/DeepSeekService.cs

@@ -480,6 +480,110 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
             }
         }
 
+        /// <inheritdoc />
+        public async IAsyncEnumerable<DeepSeekStreamChunk> ChatStreamWithHistoryAsync(
+            IReadOnlyList<DeepSeekHistoryMessage> messages,
+            string model = "deepseek-chat",
+            float temperature = 0.7f,
+            int maxTokens = 4000)
+        {
+            if (messages == null)
+                throw new ArgumentNullException(nameof(messages));
+            if (messages.Count == 0)
+                throw new ArgumentException("至少需要一条消息。", nameof(messages));
+
+            var apiMessages = new List<FileMessage>(messages.Count);
+            foreach (var m in messages)
+            {
+                if (m == null)
+                    continue;
+                var role = (m.Role ?? "").Trim().ToLowerInvariant();
+                if (role.Length == 0)
+                    throw new ArgumentException("每条消息的 Role 不能为空。", nameof(messages));
+                if (role is not ("system" or "user" or "assistant"))
+                    throw new ArgumentException($"不支持的 Role,仅允许 system、user、assistant: {m.Role}", nameof(messages));
+
+                apiMessages.Add(new FileMessage
+                {
+                    Role = role,
+                    Content = m.Content ?? ""
+                });
+            }
+
+            if (apiMessages.Count == 0)
+                throw new ArgumentException("至少需要一条有效消息。", nameof(messages));
+
+            var request = new DeepSeekChatWithFilesRequest
+            {
+                Model = model,
+                Messages = apiMessages,
+                Stream = true,
+                Temperature = temperature,
+                MaxTokens = maxTokens,
+                TopP = 0.9M,
+                FrequencyPenalty = 0.2M,
+                PresencePenalty = 0.1M,
+            };
+
+            var jsonContent = JsonSerializer.Serialize(request, new JsonSerializerOptions
+            {
+                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+            });
+
+            using var requestMsg = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
+            {
+                Content = new StringContent(jsonContent, Encoding.UTF8, "application/json")
+            };
+
+            using var response = await _httpClient.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
+
+            using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+            using var reader = new StreamReader(responseStream, Encoding.UTF8);
+
+            string? line;
+            while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
+            {
+                if (string.IsNullOrWhiteSpace(line))
+                    continue;
+                if (!line.StartsWith("data: ", StringComparison.Ordinal))
+                    continue;
+
+                var data = line["data: ".Length..];
+                if (data == "[DONE]")
+                    yield break;
+
+                string? reasoningPiece = null;
+                string? contentPiece = null;
+                try
+                {
+                    using var jsonDoc = JsonDocument.Parse(data);
+                    if (!jsonDoc.RootElement.TryGetProperty("choices", out var choices) || choices.GetArrayLength() == 0)
+                        continue;
+                    if (!choices[0].TryGetProperty("delta", out var delta))
+                        continue;
+
+                    if (delta.TryGetProperty("reasoning_content", out var reasoningVal))
+                    {
+                        var r = reasoningVal.GetString();
+                        if (!string.IsNullOrEmpty(r))
+                            reasoningPiece = r;
+                    }
+
+                    if (delta.TryGetProperty("content", out var contentVal))
+                        contentPiece = contentVal.GetString();
+                }
+                catch (System.Text.Json.JsonException)
+                {
+                }
+
+                if (!string.IsNullOrEmpty(reasoningPiece))
+                    yield return new DeepSeekStreamChunk { Phase = DeepSeekStreamPhase.Reasoning, Text = reasoningPiece };
+                if (!string.IsNullOrEmpty(contentPiece))
+                    yield return new DeepSeekStreamChunk { Phase = DeepSeekStreamPhase.Content, Text = contentPiece };
+            }
+        }
+
         /// <summary>
         /// 等待文件处理完成
         /// </summary>

+ 14 - 0
OASystem/OASystem.Api/OAMethodLib/DeepSeekAPI/IDeepSeekService.cs

@@ -84,6 +84,20 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
         /// <param name="maxTokens">最大token数</param>
         IAsyncEnumerable<DeepSeekStreamChunk> ChatStreamAsync(string question, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000);
 
+        /// <summary>
+        /// 带完整上下文的流式对话:将 <paramref name="messages"/> 原样提交给 chat/completions(stream=true)。
+        /// 与 <see cref="ChatStreamAsync"/> 行为一致(含 reasoner 的 reasoning/content 分阶段),但支持多轮记忆。
+        /// </summary>
+        /// <param name="messages">已排序的会话历史,最后一条一般应为 user</param>
+        /// <param name="model">模型名称</param>
+        /// <param name="temperature">温度参数</param>
+        /// <param name="maxTokens">最大 token 数</param>
+        IAsyncEnumerable<DeepSeekStreamChunk> ChatStreamWithHistoryAsync(
+            IReadOnlyList<DeepSeekHistoryMessage> messages,
+            string model = "deepseek-chat",
+            float temperature = 0.7f,
+            int maxTokens = 4000);
+
         /// <summary>
         /// 等待文件处理完成
         /// </summary>