AITestController.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. using Flurl.Http.Configuration;
  2. using Humanizer;
  3. using Microsoft.AspNetCore.Mvc;
  4. using Microsoft.Extensions.Options;
  5. using NodaTime;
  6. using OASystem.API.OAMethodLib;
  7. using OASystem.API.OAMethodLib.DeepSeekAPI;
  8. using OASystem.API.OAMethodLib.DoubaoAPI;
  9. using OASystem.API.OAMethodLib.Hotmail;
  10. using OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
  11. using OASystem.API.OAMethodLib.QiYeWeChatAPI;
  12. using OASystem.API.OAMethodLib.Quartz.Business;
  13. using OASystem.API.OAMethodLib.TokenHubAI;
  14. using OASystem.API.OAMethodLib.TokenHubAI.Models;
  15. using OASystem.Domain.AesEncryption;
  16. using OASystem.Domain.Entities.Customer;
  17. using OASystem.Domain.Entities.PersonnelModule;
  18. using OASystem.Domain.ViewModels.QiYeWeChat;
  19. using OASystem.RedisRepository;
  20. using System.IdentityModel.Tokens.Jwt;
  21. using System.Text.Json;
  22. using static OASystem.API.OAMethodLib.Hotmail.HotmailService;
  23. using JsonSerializer = System.Text.Json.JsonSerializer;
  24. namespace OASystem.API.Controllers
  25. {
  26. /// <summary>
  27. /// AI测试控制器
  28. /// </summary>
  29. [Route("api/[controller]")]
  30. public class AITestController : ControllerBase
  31. {
  32. private readonly IDoubaoService _doubaoService;
  33. private readonly ILogger<AITestController> _logger;
  34. private readonly IConfiguration _config;
  35. private readonly IQiYeWeChatApiService _qiYeWeChatApiService;
  36. private readonly System.Net.Http.IHttpClientFactory _httpClientFactory;
  37. private readonly HotmailService _hotmailService;
  38. private readonly IMicrosoftGraphMailboxService _microsoftGraphMailboxService;
  39. private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _microsoftGraphMailboxOptions;
  40. private readonly MessageRepository _message;
  41. private readonly SqlSugarClient _sqlSugar;
  42. private readonly ITokenHubService _tokenHubService;
  43. private readonly IDeepSeekService _deepSeekService;
  44. public AITestController(
  45. IDoubaoService doubaoService,
  46. ILogger<AITestController> logger,
  47. IQiYeWeChatApiService qiYeWeChatApiService,
  48. HotmailService hotmailService,
  49. System.Net.Http.IHttpClientFactory httpClientFactory,
  50. IConfiguration config,
  51. IMicrosoftGraphMailboxService microsoftGraphMailboxService,
  52. IOptionsMonitor<MicrosoftGraphMailboxOptions> microsoftGraphMailboxOptions,
  53. IDeepSeekService deepSeekService,
  54. MessageRepository message,
  55. SqlSugarClient sqlSugar,
  56. ITokenHubService tokenHubService
  57. )
  58. {
  59. _doubaoService = doubaoService;
  60. _logger = logger;
  61. _qiYeWeChatApiService = qiYeWeChatApiService;
  62. _hotmailService = hotmailService;
  63. _httpClientFactory = httpClientFactory;
  64. _config = config;
  65. _microsoftGraphMailboxService = microsoftGraphMailboxService;
  66. _deepSeekService = deepSeekService;
  67. _microsoftGraphMailboxOptions = microsoftGraphMailboxOptions;
  68. _message = message;
  69. _sqlSugar = sqlSugar;
  70. _tokenHubService = tokenHubService;
  71. }
  72. #region 企业微信发送邮件测试
  73. /// <summary>
  74. /// 企业微信发送邮件测试
  75. /// </summary>
  76. [HttpPost("sendEmail")]
  77. public async Task<ActionResult<string>> SendEmail([FromForm] IFormFile[] feils)
  78. {
  79. try
  80. {
  81. var req = new EmailRequestDto()
  82. {
  83. ToEmails = new List<string> { "johnny.yang@pan-american-intl.com" },
  84. CcEmails = new List<string> { "Roy.lei@pan-american-intl.com" },
  85. BccEmails = new List<string> { "Roy.lei@pan-american-intl.com" },
  86. Subject = "测试邮件 - 来自企业微信API",
  87. Body = "这是一封通过企业微信API发送的测试邮件,包含附件。",
  88. Files = feils
  89. };
  90. var response = await _qiYeWeChatApiService.EmailSendAsync(req);
  91. return Ok(response);
  92. }
  93. catch (Exception ex)
  94. {
  95. _logger.LogError(ex, "调用企业微信邮件API失败。");
  96. return StatusCode(500, new { Message = "调用企业微信邮件API失败,请检查配置或网络。", Detail = ex.Message });
  97. }
  98. }
  99. #endregion
  100. #region 豆包 AI
  101. /// <summary>
  102. /// 豆包基础对话
  103. /// </summary>
  104. [HttpPost("doubao-chat")]
  105. public async Task<ActionResult<string>> DoubaoChat(string question, bool isThinking = false)
  106. {
  107. try
  108. {
  109. var messages = new List<DouBaoChatMessage>
  110. {
  111. new DouBaoChatMessage { Role = DouBaoRole.user, Content = question }
  112. };
  113. var options = new CompleteChatOptions
  114. {
  115. ThinkingOptions = new thinkingOptions { IsThinking = isThinking }
  116. };
  117. var response = await _doubaoService.CompleteChatAsync(messages, options);
  118. return Ok(response);
  119. }
  120. catch (Exception ex)
  121. {
  122. _logger.LogError(ex, "调用豆包API失败。");
  123. return StatusCode(500, new { Message = "调用豆包API失败", Detail = ex.Message });
  124. }
  125. }
  126. /// <summary>
  127. /// 豆包上传文件
  128. /// </summary>
  129. [HttpPost("doubao-upload")]
  130. public async Task<ActionResult<DoubaoFileResponse>> DoubaoUpload(IFormFile file, string purpose = "user_data")
  131. {
  132. if (file == null || file.Length == 0)
  133. return BadRequest("请选择要上传的文件");
  134. try
  135. {
  136. var stream = file.OpenReadStream();
  137. var existsFileExpand = new List<string> { "pdf", "docx" };
  138. if (!existsFileExpand.Contains(file.FileName.Split('.').Last().ToLower()))
  139. {
  140. return BadRequest("请上传pdf、docx文件!不支持其他文件");
  141. }
  142. if (file.FileName.Split('.').Last().ToLower() == "docx")
  143. {
  144. using var docxStream = file.OpenReadStream();
  145. var pdfStream = DoubaoService.ConvertDocxStreamToPdfStream(docxStream);
  146. stream = pdfStream;
  147. }
  148. var response = await _doubaoService.UploadFileAsync(stream, file.FileName, purpose);
  149. stream.Dispose();
  150. return Ok(response);
  151. }
  152. catch (Exception ex)
  153. {
  154. _logger.LogError(ex, "豆包上传文件失败");
  155. return StatusCode(500, new { Message = "上传失败", Detail = ex.Message });
  156. }
  157. }
  158. /// <summary>
  159. /// 豆包获取文件列表
  160. /// </summary>
  161. [HttpGet("doubao-files")]
  162. public async Task<ActionResult<DoubaoFileListResponse>> DoubaoListFiles()
  163. {
  164. try
  165. {
  166. var response = await _doubaoService.ListFilesAsync();
  167. return Ok(response);
  168. }
  169. catch (Exception ex)
  170. {
  171. _logger.LogError(ex, "获取豆包文件列表失败");
  172. return StatusCode(500, new { Message = "获取失败", Detail = ex.Message });
  173. }
  174. }
  175. /// <summary>
  176. /// 豆包删除文件
  177. /// </summary>
  178. [HttpDelete("doubao-file/{fileId}")]
  179. public async Task<ActionResult<bool>> DoubaoDeleteFile(string fileId)
  180. {
  181. try
  182. {
  183. var response = await _doubaoService.DeleteFileAsync(fileId);
  184. return Ok(response);
  185. }
  186. catch (Exception ex)
  187. {
  188. _logger.LogError(ex, "删除豆包文件失败");
  189. return StatusCode(500, new { Message = "删除失败", Detail = ex.Message });
  190. }
  191. }
  192. /// <summary>
  193. /// 豆包多模态对话(支持文本+图片)
  194. /// </summary>
  195. /// <param name="request">表单请求参数</param>
  196. [HttpPost("doubao-multimodal-chat")]
  197. public async Task<ActionResult<string>> DoubaoMultimodalChat([FromForm] DoubaoMultimodalChatRequest request)
  198. {
  199. if (string.IsNullOrWhiteSpace(request.Question))
  200. return BadRequest("问题不能为空");
  201. try
  202. {
  203. var contentItems = new List<DoubaoMultimodalContentItem>
  204. {
  205. new DoubaoMultimodalContentItem { Type = "text", Text = request.Question.Trim() }
  206. };
  207. if (!string.IsNullOrWhiteSpace(request.FileId))
  208. {
  209. contentItems.Add(new DoubaoMultimodalContentItem
  210. {
  211. Type = "file",
  212. FileId = request.FileId.Trim(),
  213. });
  214. }
  215. if (request.Image != null && request.Image.Length > 0)
  216. {
  217. using var ms = new MemoryStream();
  218. await request.Image.CopyToAsync(ms);
  219. var base64 = Convert.ToBase64String(ms.ToArray());
  220. var mimeType = request.Image.ContentType ?? "image/jpeg";
  221. var dataUrl = $"data:{mimeType};base64,{base64}";
  222. contentItems.Add(new DoubaoMultimodalContentItem
  223. {
  224. Type = "image_url",
  225. ImageUrl = new DoubaoMultimodalImageUrl { Url = dataUrl }
  226. });
  227. }
  228. var messages = new List<DoubaoMultimodalChatMessage>
  229. {
  230. new DoubaoMultimodalChatMessage
  231. {
  232. Role = "user",
  233. Content = contentItems
  234. }
  235. };
  236. var options = new CompleteMultimodalChatOptions
  237. {
  238. ThinkingOptions = new DoubaoMultimodalThinkingOptions
  239. {
  240. IsThinking = request.IsThinking,
  241. ReasoningEffort = "medium"
  242. }
  243. };
  244. var response = await _doubaoService.CompleteMultimodalChatAsync(messages, options);
  245. return Ok(response ?? string.Empty);
  246. }
  247. catch (Exception ex)
  248. {
  249. _logger.LogError(ex, "调用豆包多模态API失败。");
  250. return StatusCode(500, new { Message = "调用豆包多模态API失败", Detail = ex.Message });
  251. }
  252. }
  253. #endregion
  254. #region 腾讯 TokenHub AI
  255. /// <summary>
  256. /// 简单对话
  257. /// </summary>
  258. /// <param name="request">对话请求</param>
  259. /// <returns>AI 回复</returns>
  260. [HttpPost("simple")]
  261. public async Task<IActionResult> SimpleChat([FromBody] ChatRequest request)
  262. {
  263. try
  264. {
  265. _logger.LogInformation("简单对话请求: {Message}", request.Messages);
  266. var reply = await _tokenHubService.ChatAsync(request);
  267. return Ok(new
  268. {
  269. success = true,
  270. data = reply,
  271. model = request.Model
  272. });
  273. }
  274. catch (TokenHubException ex)
  275. {
  276. _logger.LogError(ex, "TokenHub 调用失败");
  277. return StatusCode(ex.StatusCode, new JsonView()
  278. {
  279. Msg = ex.Message,
  280. Code = ex.StatusCode,
  281. Data = ex.ErrorDetail
  282. });
  283. }
  284. catch (Exception ex)
  285. {
  286. _logger.LogError(ex, "对话失败");
  287. return StatusCode(StatusCodes.Status400BadRequest, new JsonView()
  288. {
  289. Msg = ex.Message,
  290. Code = StatusCodes.Status400BadRequest,
  291. Data = ex.Message
  292. });
  293. }
  294. }
  295. /// <summary>
  296. /// 带系统提示的对话
  297. /// </summary>
  298. [HttpPost("system")]
  299. public async Task<IActionResult> ChatWithSystem([FromBody] ChatRequest request)
  300. {
  301. try
  302. {
  303. var reply = await _tokenHubService.ChatAsync(request);
  304. return Ok(new
  305. {
  306. success = true,
  307. data = reply,
  308. model = request.Model ?? "hunyuan-2.0-instruct-20251111"
  309. });
  310. }
  311. catch (TokenHubException ex)
  312. {
  313. return StatusCode(ex.StatusCode, new
  314. {
  315. success = false,
  316. error = ex.Message,
  317. code = ex.ErrorCode,
  318. requestId = ex.RequestId
  319. });
  320. }
  321. catch (Exception ex)
  322. {
  323. _logger.LogError(ex, "对话失败");
  324. return StatusCode(500, new
  325. {
  326. success = false,
  327. error = ex.Message
  328. });
  329. }
  330. }
  331. /// <summary>
  332. /// 完整参数对话
  333. /// </summary>
  334. [HttpPost("full")]
  335. public async Task<IActionResult> FullChat([FromBody] ChatRequest request)
  336. {
  337. try
  338. {
  339. _logger.LogInformation("完整对话请求: Model={Model}, Messages={Count}",
  340. request.Model, request.Messages?.Count ?? 0);
  341. // 添加系统提示
  342. if (!string.IsNullOrEmpty(request.SystemPrompt))
  343. {
  344. request = request.WithSystemPrompt(request.SystemPrompt);
  345. }
  346. var response = await _tokenHubService.ChatAsync(request);
  347. if (!response.IsSuccess)
  348. {
  349. return BadRequest(new
  350. {
  351. success = false,
  352. error = response.ErrorMessage
  353. });
  354. }
  355. return Ok(new
  356. {
  357. success = true,
  358. data = response.Content,
  359. model = response.Model,
  360. usage = response.Usage,
  361. finishReason = response.FinishReason,
  362. toolCalls = response.ToolCalls
  363. });
  364. }
  365. catch (TokenHubException ex)
  366. {
  367. return StatusCode(ex.StatusCode, new
  368. {
  369. success = false,
  370. error = ex.Message,
  371. code = ex.ErrorCode,
  372. requestId = ex.RequestId
  373. });
  374. }
  375. catch (Exception ex)
  376. {
  377. _logger.LogError(ex, "对话失败");
  378. return StatusCode(500, new
  379. {
  380. success = false,
  381. error = ex.Message
  382. });
  383. }
  384. }
  385. /// <summary>
  386. /// 流式对话(Server-Sent Events)
  387. /// </summary>
  388. [HttpPost("stream")]
  389. public async Task StreamChat([FromBody] ChatRequest request)
  390. {
  391. try
  392. {
  393. _logger.LogInformation("流式对话请求: {Message}", request.Messages.FirstOrDefault());
  394. // 设置响应头
  395. Response.ContentType = "text/event-stream; charset=utf-8";
  396. Response.Headers.Append("Cache-Control", "no-cache");
  397. Response.Headers.Append("Connection", "keep-alive");
  398. Response.Headers.Append("X-Accel-Buffering", "no");
  399. if (!string.IsNullOrEmpty(request.SystemPrompt))
  400. {
  401. request = request.WithSystemPrompt(request.SystemPrompt);
  402. }
  403. var fullContent = new StringBuilder();
  404. await _tokenHubService.StreamChatAsync(
  405. request,
  406. chunk =>
  407. {
  408. if (chunk.IsDone)
  409. {
  410. // 发送结束标记
  411. var endData = new
  412. {
  413. type = "done",
  414. fullContent = fullContent.ToString(),
  415. usage = chunk.Usage,
  416. model = chunk.Model
  417. };
  418. Response.WriteAsync($"data: {JsonSerializer.Serialize(endData)}\n\n");
  419. Response.Body.Flush();
  420. return;
  421. }
  422. if (!string.IsNullOrEmpty(chunk.Content))
  423. {
  424. fullContent.Append(chunk.Content);
  425. // 发送数据块
  426. var chunkData = new
  427. {
  428. type = "chunk",
  429. content = chunk.Content,
  430. model = chunk.Model
  431. };
  432. Response.WriteAsync($"data: {JsonSerializer.Serialize(chunkData)}\n\n");
  433. Response.Body.Flush();
  434. }
  435. if (chunk.ToolCalls != null && chunk.ToolCalls.Count > 0)
  436. {
  437. var toolData = new
  438. {
  439. type = "tool_call",
  440. tools = chunk.ToolCalls
  441. };
  442. Response.WriteAsync($"data: {JsonSerializer.Serialize(toolData)}\n\n");
  443. Response.Body.Flush();
  444. }
  445. }
  446. );
  447. }
  448. catch (Exception ex)
  449. {
  450. _logger.LogError(ex, "流式对话失败");
  451. var errorData = new { type = "error", error = ex.Message };
  452. await Response.WriteAsync($"data: {JsonSerializer.Serialize(errorData)}\n\n");
  453. }
  454. }
  455. /// <summary>
  456. /// 带重试的对话
  457. /// </summary>
  458. [HttpPost("retry")]
  459. public async Task<IActionResult> ChatWithRetry([FromBody] ChatRequest request)
  460. {
  461. try
  462. {
  463. var response = await _tokenHubService.ChatWithRetryAsync(request);
  464. if (!response.IsSuccess)
  465. {
  466. return BadRequest(new
  467. {
  468. success = false,
  469. error = response.ErrorMessage
  470. });
  471. }
  472. return Ok(new
  473. {
  474. success = true,
  475. data = response.Content,
  476. model = response.Model,
  477. usage = response.Usage
  478. });
  479. }
  480. catch (TokenHubException ex)
  481. {
  482. return StatusCode(ex.StatusCode, new
  483. {
  484. success = false,
  485. error = ex.Message,
  486. code = ex.ErrorCode,
  487. requestId = ex.RequestId
  488. });
  489. }
  490. catch (Exception ex)
  491. {
  492. _logger.LogError(ex, "对话失败");
  493. return StatusCode(500, new
  494. {
  495. success = false,
  496. error = ex.Message
  497. });
  498. }
  499. }
  500. /// <summary>
  501. /// 获取可用模型列表
  502. /// </summary>
  503. [HttpGet("models")]
  504. public async Task<IActionResult> GetModels()
  505. {
  506. try
  507. {
  508. var models = await _tokenHubService.GetAvailableModelsAsync();
  509. return Ok(new
  510. {
  511. success = true,
  512. data = models,
  513. count = models.Count
  514. });
  515. }
  516. catch (Exception ex)
  517. {
  518. _logger.LogError(ex, "获取模型列表失败");
  519. return StatusCode(500, new
  520. {
  521. success = false,
  522. error = ex.Message
  523. });
  524. }
  525. }
  526. /// <summary>
  527. /// 豆包多模态对话请求体(form-data)
  528. /// </summary>
  529. public class DoubaoMultimodalChatRequest
  530. {
  531. public string Question { get; set; } = string.Empty;
  532. public IFormFile? Image { get; set; }
  533. public bool IsThinking { get; set; } = false;
  534. public string FileId { get; set; } = string.Empty;
  535. }
  536. #endregion
  537. #region DeepSeek 测试
  538. /// <summary>
  539. /// DeepSeek 带上下文的流式对话测试。响应为 NDJSON:每行一条 JSON,phase 为 reasoning、content 或 error。
  540. /// system | user | assistant 角色。
  541. /// </summary>
  542. [HttpPost("deepseek-chat-stream-with-history")]
  543. public async Task<IActionResult> DeepSeekChatStreamWithHistory(
  544. [FromBody] DeepSeekChatStreamHistoryTestRequest request,
  545. CancellationToken cancellationToken = default)
  546. {
  547. if (request?.Messages == null || request.Messages.Count == 0)
  548. return BadRequest(new { message = "Messages 不能为空,且至少包含一条 user/system/assistant 消息。" });
  549. Response.ContentType = "application/x-ndjson; charset=utf-8";
  550. Response.Headers["Cache-Control"] = "no-cache";
  551. static string NdjsonLine(DeepSeekStreamChunk c) => JsonConvert.SerializeObject(new
  552. {
  553. phase = c.Phase == DeepSeekStreamPhase.Reasoning ? "reasoning" : "content",
  554. text = c.Text
  555. });
  556. try
  557. {
  558. await foreach (var chunk in _deepSeekService.ChatStreamWithHistoryAsync(
  559. request.Messages,
  560. string.IsNullOrWhiteSpace(request.Model) ? "deepseek-chat" : request.Model!.Trim(),
  561. request.Temperature,
  562. request.MaxTokens))
  563. {
  564. cancellationToken.ThrowIfCancellationRequested();
  565. await Response.WriteAsync(NdjsonLine(chunk) + "\n", cancellationToken);
  566. await Response.Body.FlushAsync(cancellationToken);
  567. }
  568. await Response.WriteAsync(JsonConvert.SerializeObject(new { phase = "success", text = "结束" }) + "\n", cancellationToken);
  569. await Response.Body.FlushAsync(cancellationToken);
  570. }
  571. catch (OperationCanceledException)
  572. {
  573. }
  574. catch (Exception ex)
  575. {
  576. _logger.LogError(ex, "DeepSeek 带历史流式对话失败");
  577. await Response.WriteAsync(
  578. JsonConvert.SerializeObject(new { phase = "error", text = ex.Message }) + "\n",
  579. cancellationToken);
  580. }
  581. return new EmptyResult();
  582. }
  583. /// <summary>
  584. /// DeepSeek 流式对话(含多轮)请求体
  585. /// </summary>
  586. public class DeepSeekChatStreamHistoryTestRequest
  587. {
  588. public List<DeepSeekHistoryMessage> Messages { get; set; } = new();
  589. public string? Model { get; set; } = "deepseek-chat";
  590. public float Temperature { get; set; } = 0.7f;
  591. public int MaxTokens { get; set; } = 4000;
  592. }
  593. #endregion
  594. /// <summary>
  595. /// hotmail 发送邮件
  596. /// </summary>
  597. [HttpPost("hotmailSeed")]
  598. public async Task<ActionResult<string>> HotmailSeed()
  599. {
  600. await _hotmailService.SendMailAsync(
  601. //"Roy.Lei.Atom@hotmail.com",
  602. "925554512@qq.com",
  603. //"johnny.yang@pan-american-intl.com",
  604. new HotmailService.MailDto()
  605. {
  606. Subject = "系统提醒",
  607. Content = "<p>这是一封Homail 发送的测试邮件</p>",
  608. //To = "Roy.lei@pan-american-intl.com"
  609. To = "johnny.yang@pan-american-intl.com"
  610. });
  611. return StatusCode(200, new { Message = "操作成功。" });
  612. }
  613. /// <summary>
  614. /// hotmail 发送邮件
  615. /// </summary>
  616. [HttpPost("HotmailMerged")]
  617. public async Task<ActionResult<string>> HotmailMerged()
  618. {
  619. // 1. 获取当前北京时间 (CST)
  620. var cstZone = CommonFun.GetCstZone();
  621. var nowInCst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, cstZone);
  622. // 2. 构造昨天的北京时间范围:00:00:00 到 23:59:59
  623. var yesterdayStart = nowInCst.Date.AddDays(-1); // 昨天的 00:00:00
  624. var yesterdayEnd = yesterdayStart.AddDays(1).AddTicks(-1); // 昨天的 23:59:59
  625. var res = await _hotmailService.GetMergedMessagesAsync(
  626. new List<string>() { "925554512@qq.com" },
  627. yesterdayStart,
  628. yesterdayEnd
  629. );
  630. return StatusCode(200, res);
  631. }
  632. /// <summary>
  633. /// hotmail 定时发送邮件 汇总 测试
  634. /// </summary>
  635. [HttpPost("hotmailSummarySeedQW")]
  636. public async Task<ActionResult<string>> HotmailSummary()
  637. {
  638. ProcessAndNotifySummary.ProcessAndNotifySummaryAsync();
  639. return StatusCode(200, "发送成功");
  640. }
  641. #region Microsoft Graph 邮箱测试(仅访问令牌)
  642. private const string GraphAccessTokenHeader = "X-Graph-Access-Token";
  643. /// <summary>
  644. /// 优先级:请求头 X-Graph-Access-Token → 查询 graphAccessToken → bodyToken(发信)。
  645. /// </summary>
  646. private string? ResolveGraphAccessToken(string? queryToken = null, string? bodyToken = null)
  647. {
  648. var header = Request.Headers[GraphAccessTokenHeader].FirstOrDefault();
  649. if (!string.IsNullOrWhiteSpace(header))
  650. return header.Trim();
  651. if (!string.IsNullOrWhiteSpace(queryToken))
  652. return queryToken.Trim();
  653. if (!string.IsNullOrWhiteSpace(bodyToken))
  654. return bodyToken.Trim();
  655. return null;
  656. }
  657. /// <summary>
  658. /// 查询当前用户 GET /v1.0/me。必须提供 Graph 访问令牌。
  659. /// </summary>
  660. [HttpGet("graph-mail/me")]
  661. public async Task<IActionResult> GraphMailMe(
  662. [FromQuery] string? graphAccessToken = null,
  663. CancellationToken cancellationToken = default)
  664. {
  665. var bearer = ResolveGraphAccessToken(graphAccessToken);
  666. if (string.IsNullOrWhiteSpace(bearer))
  667. return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:请求头 X-Graph-Access-Token 或查询参数 graphAccessToken" });
  668. try
  669. {
  670. var json = await _microsoftGraphMailboxService.GetMeRawJsonAsync(bearer, cancellationToken);
  671. if (string.IsNullOrEmpty(json))
  672. return StatusCode(502, new { message = "Graph 返回空正文" });
  673. return Content(json, "application/json");
  674. }
  675. catch (Exception ex)
  676. {
  677. _logger.LogError(ex, "Graph Mail /me 失败");
  678. return StatusCode(500, new { message = ex.Message });
  679. }
  680. }
  681. /// <summary>
  682. /// 查询收件箱。必须提供 Graph 访问令牌(需 Mail.Read)。默认 sinceUtc 为 UTC 近 24 小时。
  683. /// </summary>
  684. /// <param name="sinceUtc">起始时间(UTC),ISO8601</param>
  685. /// <param name="graphAccessToken">或使用请求头 X-Graph-Access-Token</param>
  686. /// <param name="cancellationToken">取消标记</param>
  687. [HttpGet("graph-mail/inbox")]
  688. public async Task<IActionResult> GraphMailInbox(
  689. [FromQuery] DateTime? sinceUtc = null,
  690. [FromQuery] string? graphAccessToken = null,
  691. CancellationToken cancellationToken = default)
  692. {
  693. var bearer = ResolveGraphAccessToken(graphAccessToken);
  694. if (string.IsNullOrWhiteSpace(bearer))
  695. return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:请求头 X-Graph-Access-Token 或查询参数 graphAccessToken" });
  696. var since = sinceUtc ?? DateTime.UtcNow.AddHours(-24);
  697. try
  698. {
  699. var json = await _microsoftGraphMailboxService.GetInboxMessagesJsonSinceAsync(since, bearer, cancellationToken);
  700. if (string.IsNullOrEmpty(json))
  701. return StatusCode(502, new { message = "Graph 返回空正文" });
  702. return Content(json, "application/json");
  703. }
  704. catch (Exception ex)
  705. {
  706. _logger.LogError(ex, "Graph Mail inbox 失败");
  707. return StatusCode(500, new { message = ex.Message });
  708. }
  709. }
  710. /// <summary>
  711. /// Graph sendMail 纯文本。必须提供令牌(需 Mail.Send):头 / 查询 / Body.graphAccessToken。
  712. /// </summary>
  713. [HttpPost("graph-mail/send")]
  714. public async Task<IActionResult> GraphMailSend(
  715. [FromBody] GraphMailSendTestRequest request,
  716. [FromQuery] string? graphAccessToken = null,
  717. CancellationToken cancellationToken = default)
  718. {
  719. if (request == null || string.IsNullOrWhiteSpace(request.ToEmail))
  720. return BadRequest(new { message = "ToEmail 不能为空" });
  721. var bearer = ResolveGraphAccessToken(graphAccessToken, request.GraphAccessToken);
  722. if (string.IsNullOrWhiteSpace(bearer))
  723. return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:X-Graph-Access-Token、?graphAccessToken 或 Body.graphAccessToken" });
  724. var subject = string.IsNullOrWhiteSpace(request.Subject)
  725. ? $"OASystem Graph 测试邮件 {DateTime.Now:yyyy-MM-dd HH:mm:ss}"
  726. : request.Subject!;
  727. var body = request.Body ?? string.Empty;
  728. try
  729. {
  730. await _microsoftGraphMailboxService.SendMailAsync(request.ToEmail.Trim(), subject, body, bearer, cancellationToken);
  731. return Ok(new { ok = true, message = "sendMail 已提交", to = request.ToEmail.Trim(), subject });
  732. }
  733. catch (HttpRequestException ex)
  734. {
  735. return StatusCode(502, new { message = "Graph HTTP 错误", detail = ex.Message });
  736. }
  737. catch (Exception ex)
  738. {
  739. _logger.LogError(ex, "Graph Mail send 失败");
  740. return StatusCode(500, new { message = ex.Message });
  741. }
  742. }
  743. public class EmailAuthRedisCache
  744. {
  745. public string? AccessToken { get; set; }
  746. public string? HomeAccountId { get; set; }
  747. public string? UserTokenCacheBase64 { get; set; }
  748. public string? ClientId { get; set; }
  749. }
  750. /// <summary>
  751. /// 从 Redis 读取 MSAL 缓存与 HomeAccountId,静默刷新 Graph access_token。
  752. /// </summary>
  753. [HttpGet("graph-mail/refresh-token")]
  754. public async Task<IActionResult> RefreshAccessToken([FromQuery] string? redisKey = null)
  755. {
  756. var key = string.IsNullOrWhiteSpace(redisKey) ? "Email:AuthCache:345" : redisKey.Trim();
  757. var redis = RedisFactory.CreateRedisRepository();
  758. var json = await redis.StringGetRawAsync(key);
  759. if (string.IsNullOrWhiteSpace(json))
  760. {
  761. return BadRequest(new { message = $"Redis 键 {key} 不存在或为空" });
  762. }
  763. EmailAuthRedisCache? cacheEntry;
  764. try
  765. {
  766. cacheEntry = JsonConvert.DeserializeObject<EmailAuthRedisCache>(json);
  767. }
  768. catch (System.Text.Json.JsonException ex)
  769. {
  770. _logger.LogWarning(ex, "Redis 键 {Key} 内容不是合法 JSON(应用 StringGetRawAsync + JSON,勿用 StringGetAsync<T>,后者为 BinaryFormatter)", key);
  771. return BadRequest(new { message = "Redis 值为 JSON 文本时须用 StringGetRawAsync 再反序列化;StringGetAsync<T> 仅适用于 BinaryFormatter 写入的数据", detail = ex.Message });
  772. }
  773. if (cacheEntry == null
  774. || string.IsNullOrWhiteSpace(cacheEntry.UserTokenCacheBase64)
  775. || string.IsNullOrWhiteSpace(cacheEntry.HomeAccountId))
  776. {
  777. return BadRequest(new { message = "JSON 中缺少 UserTokenCacheBase64 或 HomeAccountId" });
  778. }
  779. var accessToken = await _microsoftGraphMailboxService.RefreshAccessTokenAsync(
  780. cacheEntry.ClientId,
  781. "common",
  782. new[] { "Mail.Read", "User.Read", "Mail.Send" },
  783. cacheEntry.UserTokenCacheBase64,
  784. cacheEntry.HomeAccountId);
  785. return Ok(new { accessToken });
  786. }
  787. /// <summary>
  788. /// Graph 发信测试请求体
  789. /// </summary>
  790. public class GraphMailSendTestRequest
  791. {
  792. public string ToEmail { get; set; } = string.Empty;
  793. public string? Subject { get; set; }
  794. public string? Body { get; set; }
  795. /// <summary>Microsoft Graph 访问令牌(也可用请求头 X-Graph-Access-Token)</summary>
  796. public string? GraphAccessToken { get; set; }
  797. }
  798. #endregion
  799. #region 客户名单解密
  800. /// <summary>
  801. /// 客户名单解密
  802. /// </summary>
  803. [HttpPost("deleClientList")]
  804. public async Task<ActionResult<string>> DeleClientList()
  805. {
  806. var data = await _message._sqlSugar.Queryable<Crm_DeleClient>().Where(x => x.IsDel == 0 )
  807. .LeftJoin<Crm_CustomerCompany>((x,y) => x.CrmCompanyId == y.Id)
  808. .Select((x, y) => new {
  809. x.Id,
  810. x.FirstName,
  811. x.LastName,
  812. y.CompanyFullName,
  813. x.ClientPhone,
  814. x.Tel,
  815. x.Phone,
  816. x.Job,
  817. x.CreateTime
  818. }).ToListAsync();
  819. var newData = new List<dynamic>();
  820. foreach (var item in data)
  821. {
  822. newData.Add(new
  823. {
  824. item.Id,
  825. LastName = AesEncryptionHelper.Decrypt(item.LastName),
  826. FirstName = AesEncryptionHelper.Decrypt(item.FirstName),
  827. CompanyFullName = AesEncryptionHelper.Decrypt(item.CompanyFullName),
  828. Job = AesEncryptionHelper.Decrypt(item.Job),
  829. ClientPhone = AesEncryptionHelper.Decrypt(item.ClientPhone),
  830. Tel = AesEncryptionHelper.Decrypt(item.Tel),
  831. Phone = AesEncryptionHelper.Decrypt(item.Phone),
  832. item.CreateTime
  833. });
  834. }
  835. return StatusCode(200, newData);
  836. }
  837. #endregion
  838. #region 工资计算测试
  839. /// <summary>
  840. /// 工资计算测试 - 企微考勤信息
  841. /// </summary>
  842. [HttpPost("SalaryCalculator")]
  843. public async Task<ActionResult<string>> SalaryCalculator()
  844. {
  845. // 计算本月工资起止时间 比如是2月的1号-28号,那就是2月1号的零点到3月1号的零点
  846. DateTime startDt = Convert.ToDateTime("2026-03-01");
  847. DateTime endDt = Convert.ToDateTime("2026-03-31");
  848. string thisYearMonth = "2026-03"; //本月工资年月
  849. string preYearMonth = "2026-03"; //上月工资年月
  850. // 检查是否存在
  851. //var isExists = await _sqlSugar.Queryable<Pm_WageSheet>().AnyAsync(it => it.YearMonth == thisYearMonth);
  852. //if (isExists) return Ok(JsonView(false, $"{thisYearMonth} 工资数据已存在。"));
  853. var preWageSheetItems = await _sqlSugar.Queryable<Pm_WageSheet>()
  854. .Where(it => it.IsDel == 0 && it.YearMonth == preYearMonth)
  855. .ToListAsync();
  856. if (!preWageSheetItems.Any()) return Ok(JsonView(false, "上月工资数据不存在。"));
  857. //处理上个月同月同人 多条数据
  858. List<Pm_WageSheet> preWageSheetItems1 = preWageSheetItems
  859. .GroupBy(it => new { it.YearMonth, it.UserId })
  860. .Select(it => it.FirstOrDefault(item => item.Basic != 0))
  861. .Where(it => it != null)
  862. .ToList()!;
  863. var res = await PayrollComputation_v1.SalaryCalculatorAsync(preWageSheetItems1,208, thisYearMonth, startDt, endDt);
  864. return Ok(res);
  865. }
  866. #endregion
  867. }
  868. }