From 592054f062dc710f9d63c43f9df754330508ddf4 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sat, 1 Aug 2026 15:55:30 +0800 Subject: [PATCH] feat: extract global AI service and persist sales-product publish AI reports Move provider/chat APIs from /xhs/ai to /ai, rename t_ai_provider, and require adopted AI analysis on price publish. Co-authored-by: Cursor --- db.sql | 9 +-- sql/20260801_global_ai_provider.sql | 16 +++++ ...260801_sales_product_publish_ai_report.sql | 3 + src/Oms.Application/Dtos/Ai/AiDtos.cs | 72 +++++++++++++++++++ .../Channels/SalesProductPriceSaveAllInput.cs | 5 ++ .../Dtos/Channels/SalesProductPublishInput.cs | 5 ++ .../SalesProductPublishRecordOutput.cs | 5 ++ src/Oms.Application/Dtos/Xhs/XhsDtos.cs | 69 ------------------ src/Oms.Application/OmsApplicationModule.cs | 2 +- .../{Xhs/XhsAiService.cs => Ai/AiService.cs} | 47 ++++++------ .../Channels/SalesProductService.Helpers.cs | 8 ++- .../Services/Channels/SalesProductService.cs | 23 +++++- .../Services/Mcp/McpToolService.cs | 8 +-- .../OrderService.ReceiverAddressParse.cs | 12 ++-- .../Services/Xhs/XhsAnalysisService.cs | 5 +- .../Services/Xhs/XhsCreationService.cs | 5 +- .../{XhsLlmClient.cs => AiLlmClient.cs} | 25 +++---- .../Entities/AiProvider.cs} | 9 ++- .../Repositories/IAiProviderRepository.cs | 9 +++ .../Entities/SalesProductPublishRecord.cs | 6 ++ .../Repositories/IXhsRepositories.cs | 5 -- .../Ai/AiProviderRepository.cs | 16 +++++ src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs | 10 --- .../Auth/XhsRolePermissionSeedContributor.cs | 4 +- 24 files changed, 233 insertions(+), 145 deletions(-) create mode 100644 sql/20260801_global_ai_provider.sql create mode 100644 sql/20260801_sales_product_publish_ai_report.sql create mode 100644 src/Oms.Application/Dtos/Ai/AiDtos.cs rename src/Oms.Application/Services/{Xhs/XhsAiService.cs => Ai/AiService.cs} (74%) rename src/Oms.Application/Shared/{XhsLlmClient.cs => AiLlmClient.cs} (93%) rename src/Oms.Domain/{XhsAggregate/Entities/XhsAiProvider.cs => AiAggregate/Entities/AiProvider.cs} (72%) create mode 100644 src/Oms.Domain/AiAggregate/Repositories/IAiProviderRepository.cs create mode 100644 src/Oms.SqlSugarCore/Ai/AiProviderRepository.cs diff --git a/db.sql b/db.sql index b1afb07..5f5f713 100644 --- a/db.sql +++ b/db.sql @@ -29,6 +29,7 @@ DROP TABLE IF EXISTS `role_data_scope`; DROP TABLE IF EXISTS `role_org_perm`; DROP TABLE IF EXISTS `user_org_perm`; DROP TABLE IF EXISTS `t_xhs_media`; +DROP TABLE IF EXISTS `t_ai_provider`; DROP TABLE IF EXISTS `t_xhs_ai_provider`; DROP TABLE IF EXISTS `t_xhs_human_config`; DROP TABLE IF EXISTS `t_xhs_skill`; @@ -1733,7 +1734,7 @@ CREATE TABLE `t_xhs_human_config` ( UNIQUE KEY `uk_xhs_human_customer` (`CustomerId`) ) COMMENT='小红书拟人行为与防封配置表'; -CREATE TABLE `t_xhs_ai_provider` ( +CREATE TABLE `t_ai_provider` ( `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局', `ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等', @@ -1751,9 +1752,9 @@ CREATE TABLE `t_xhs_ai_provider` ( `UpdateUserId` INT NULL, `UpdateTime` DATETIME NULL, PRIMARY KEY (`Id`), - UNIQUE KEY `uk_xhs_ai_provider` (`CustomerId`, `ProviderKey`), - KEY `idx_xhs_ai_customer` (`CustomerId`, `Enabled`) -) COMMENT='小红书 AI 提供商配置表'; + UNIQUE KEY `uk_ai_provider` (`CustomerId`, `ProviderKey`), + KEY `idx_ai_customer` (`CustomerId`, `Enabled`) +) COMMENT='全局 AI 提供商配置表(CustomerId=0 表示平台级)'; CREATE TABLE `t_xhs_media` ( `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', diff --git a/sql/20260801_global_ai_provider.sql b/sql/20260801_global_ai_provider.sql new file mode 100644 index 0000000..33788ab --- /dev/null +++ b/sql/20260801_global_ai_provider.sql @@ -0,0 +1,16 @@ +-- 全局 AI 提供商:从小红书域表迁出(未上线环境可直接改名) +-- 若表尚不存在则跳过;若已是 t_ai_provider 则无需执行。 + +RENAME TABLE `t_xhs_ai_provider` TO `t_ai_provider`; + +ALTER TABLE `t_ai_provider` + COMMENT = '全局 AI 提供商配置表(CustomerId=0 表示平台级)'; + +-- 索引名顺带规范化(可选,失败可忽略) +ALTER TABLE `t_ai_provider` + DROP INDEX `uk_xhs_ai_provider`, + ADD UNIQUE KEY `uk_ai_provider` (`CustomerId`, `ProviderKey`); + +ALTER TABLE `t_ai_provider` + DROP INDEX `idx_xhs_ai_customer`, + ADD KEY `idx_ai_customer` (`CustomerId`, `Enabled`); diff --git a/sql/20260801_sales_product_publish_ai_report.sql b/sql/20260801_sales_product_publish_ai_report.sql new file mode 100644 index 0000000..d2fd354 --- /dev/null +++ b/sql/20260801_sales_product_publish_ai_report.sql @@ -0,0 +1,3 @@ +-- 销售产品价格发布记录:保存 AI 风险分析报告 +ALTER TABLE `t_sales_product_publish_record` + ADD COLUMN `AiAnalysisReport` MEDIUMTEXT NULL COMMENT '价格发布AI风险分析报告' AFTER `Description`; diff --git a/src/Oms.Application/Dtos/Ai/AiDtos.cs b/src/Oms.Application/Dtos/Ai/AiDtos.cs new file mode 100644 index 0000000..7cb0f98 --- /dev/null +++ b/src/Oms.Application/Dtos/Ai/AiDtos.cs @@ -0,0 +1,72 @@ +using System.ComponentModel.DataAnnotations; + +namespace Oms.Application.Dtos.Ai; + +public class AiProviderOutput +{ + public int Id { get; set; } + + public string ProviderKey { get; set; } = string.Empty; + + public string ProviderType { get; set; } = string.Empty; + + public string DisplayName { get; set; } = string.Empty; + + public string BaseUrl { get; set; } = string.Empty; + + public string? ApiKey { get; set; } + + public string? ApiSecret { get; set; } + + public bool IsDefault { get; set; } + + public bool Enabled { get; set; } +} + +public class AiProviderSaveInput +{ + [Required] + public string ProviderKey { get; set; } = string.Empty; + + [Required] + public string ProviderType { get; set; } = string.Empty; + + [Required] + public string DisplayName { get; set; } = string.Empty; + + [Required] + public string BaseUrl { get; set; } = string.Empty; + + public string? ApiKey { get; set; } + + public string? ApiSecret { get; set; } + + public bool IsDefault { get; set; } + + public bool Enabled { get; set; } = true; +} + +public class AiChatInput +{ + [Required] + public string ProviderKey { get; set; } = string.Empty; + + public string? Model { get; set; } + + [Required] + public List Messages { get; set; } = []; + + public string Scene { get; set; } = "assistant"; +} + +public class AiChatMessageInput +{ + public string Role { get; set; } = "user"; + + public string Content { get; set; } = string.Empty; +} + +public class AiChatOutput +{ + public string Content { get; set; } = string.Empty; +} diff --git a/src/Oms.Application/Dtos/Channels/SalesProductPriceSaveAllInput.cs b/src/Oms.Application/Dtos/Channels/SalesProductPriceSaveAllInput.cs index 7888c56..a68959a 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductPriceSaveAllInput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductPriceSaveAllInput.cs @@ -15,6 +15,11 @@ public class SalesProductPriceSaveAllInput /// public bool Publish { get; set; } + /// + /// 价格发布 AI 风险分析报告(Publish=true 时必填)。 + /// + public string? AiAnalysisReport { get; set; } + /// /// 价格项集合。 /// diff --git a/src/Oms.Application/Dtos/Channels/SalesProductPublishInput.cs b/src/Oms.Application/Dtos/Channels/SalesProductPublishInput.cs index ca283c9..b75caa8 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductPublishInput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductPublishInput.cs @@ -19,4 +19,9 @@ public class SalesProductPublishInput /// 描述。 /// public string? Description { get; set; } + + /// + /// 价格发布 AI 风险分析报告(operatorType=price 时必填)。 + /// + public string? AiAnalysisReport { get; set; } } diff --git a/src/Oms.Application/Dtos/Channels/SalesProductPublishRecordOutput.cs b/src/Oms.Application/Dtos/Channels/SalesProductPublishRecordOutput.cs index baca778..243de9d 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductPublishRecordOutput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductPublishRecordOutput.cs @@ -45,6 +45,11 @@ public class SalesProductPublishRecordOutput /// public string? Description { get; set; } + /// + /// 价格发布 AI 风险分析报告。 + /// + public string? AiAnalysisReport { get; set; } + /// /// 内容。 /// diff --git a/src/Oms.Application/Dtos/Xhs/XhsDtos.cs b/src/Oms.Application/Dtos/Xhs/XhsDtos.cs index 7bd4919..100c7b2 100644 --- a/src/Oms.Application/Dtos/Xhs/XhsDtos.cs +++ b/src/Oms.Application/Dtos/Xhs/XhsDtos.cs @@ -320,75 +320,6 @@ public class XhsSkillSaveInput public object? Recipe { get; set; } } -public class XhsAiProviderOutput -{ - public int Id { get; set; } - - public string ProviderKey { get; set; } = string.Empty; - - public string ProviderType { get; set; } = string.Empty; - - public string DisplayName { get; set; } = string.Empty; - - public string BaseUrl { get; set; } = string.Empty; - - public string? ApiKey { get; set; } - - public string? ApiSecret { get; set; } - - public bool IsDefault { get; set; } - - public bool Enabled { get; set; } -} - -public class XhsAiProviderSaveInput -{ - [Required] - public string ProviderKey { get; set; } = string.Empty; - - [Required] - public string ProviderType { get; set; } = string.Empty; - - [Required] - public string DisplayName { get; set; } = string.Empty; - - [Required] - public string BaseUrl { get; set; } = string.Empty; - - public string? ApiKey { get; set; } - - public string? ApiSecret { get; set; } - - public bool IsDefault { get; set; } - - public bool Enabled { get; set; } = true; -} - -public class XhsAiChatInput -{ - [Required] - public string ProviderKey { get; set; } = string.Empty; - - public string? Model { get; set; } - - [Required] - public List Messages { get; set; } = []; - - public string Scene { get; set; } = "assistant"; -} - -public class XhsAiChatMessageInput -{ - public string Role { get; set; } = "user"; - - public string Content { get; set; } = string.Empty; -} - -public class XhsAiChatOutput -{ - public string Content { get; set; } = string.Empty; -} - public class XhsAnalysisStartInput { [Required] diff --git a/src/Oms.Application/OmsApplicationModule.cs b/src/Oms.Application/OmsApplicationModule.cs index ec7976d..f44ca86 100644 --- a/src/Oms.Application/OmsApplicationModule.cs +++ b/src/Oms.Application/OmsApplicationModule.cs @@ -55,7 +55,7 @@ public class OmsApplicationModule : FakeModule context.Services.AddOmsItfxIntegration(configuration); context.Services.AddOmsPlatformShopIntegration(configuration); context.Services.AddOmsOrderBatchMessaging(configuration); - context.Services.AddXhsLlmClient(); + context.Services.AddAiLlmClient(); context.Services.AddHostedService(); context.Services.AddHostedService(); } diff --git a/src/Oms.Application/Services/Xhs/XhsAiService.cs b/src/Oms.Application/Services/Ai/AiService.cs similarity index 74% rename from src/Oms.Application/Services/Xhs/XhsAiService.cs rename to src/Oms.Application/Services/Ai/AiService.cs index 4ef37a6..cc9ecbd 100644 --- a/src/Oms.Application/Services/Xhs/XhsAiService.cs +++ b/src/Oms.Application/Services/Ai/AiService.cs @@ -1,48 +1,51 @@ using Fake.Application; using Microsoft.AspNetCore.Mvc; +using Oms.Application.Dtos.Ai; using Oms.Application.Dtos.Shared; -using Oms.Application.Dtos.Xhs; using Oms.Application.Shared; +using Oms.Domain.AiAggregate.Entities; +using Oms.Domain.AiAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories; -using Oms.Domain.XhsAggregate.Entities; -using Oms.Domain.XhsAggregate.Repositories; -namespace Oms.Application.Services.Xhs; +namespace Oms.Application.Services.Ai; -[ApiExplorerSettings(GroupName = "OMS-Xhs")] -public class XhsAiService : ApplicationService +/// +/// 全局 AI 提供商与对话服务(系统级,非业务域专属)。 +/// +[ApiExplorerSettings(GroupName = "OMS-Ai")] +public class AiService : ApplicationService { private const string SecretMask = "******"; - private IXhsAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService(); + private IAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); /// - /// 查询AI服务商列表 + /// 查询 AI 服务商列表。 /// - [HttpPost("/xhs/ai/provider/list")] - public async Task> ListProvidersAsync() + [HttpPost("/ai/provider/list")] + public async Task> ListProvidersAsync() { var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var items = await AiProviderRepository.GetListAsync(x => x.CustomerId == customerId || x.CustomerId == 0); - return new OmsItemsResult + return new OmsItemsResult { Items = items.Select(x => MapProvider(x, maskSecret: true)).ToList() }; } /// - /// 保存AI服务商配置 + /// 保存 AI 服务商配置。 /// - [HttpPost("/xhs/ai/provider/save")] - public async Task SaveProviderAsync(XhsAiProviderSaveInput input) + [HttpPost("/ai/provider/save")] + public async Task SaveProviderAsync(AiProviderSaveInput input) { var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var providerKey = input.ProviderKey.Trim(); var entity = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey); if (entity == null) { - entity = new XhsAiProvider { CustomerId = customerId, ProviderKey = providerKey }; + entity = new AiProvider { CustomerId = customerId, ProviderKey = providerKey }; } entity.ProviderType = input.ProviderType.Trim(); @@ -85,22 +88,22 @@ public class XhsAiService : ApplicationService } /// - /// AI对话 + /// AI 对话。 /// - [HttpPost("/xhs/ai/chat")] - public async Task ChatAsync(XhsAiChatInput input) + [HttpPost("/ai/chat")] + public async Task ChatAsync(AiChatInput input) { var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var provider = await ResolveProviderAsync(customerId, input.ProviderKey.Trim()); - var llm = LazyServiceProvider.GetRequiredService(); + var llm = LazyServiceProvider.GetRequiredService(); var content = await llm.ChatCompletionAsync( provider, input.Messages.Select(x => (x.Role, x.Content)).ToList(), input.Model); - return new XhsAiChatOutput { Content = content }; + return new AiChatOutput { Content = content }; } - internal async Task ResolveProviderAsync(int customerId, string providerKey) + internal async Task ResolveProviderAsync(int customerId, string providerKey) { var provider = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey); if (provider == null || !provider.Enabled) @@ -116,7 +119,7 @@ public class XhsAiService : ApplicationService return provider; } - private static XhsAiProviderOutput MapProvider(XhsAiProvider entity, bool maskSecret) => new() + private static AiProviderOutput MapProvider(AiProvider entity, bool maskSecret) => new() { Id = entity.Id, ProviderKey = entity.ProviderKey, diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index 859d4ee..e363b3b 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -153,6 +153,7 @@ public partial class SalesProductService PublishUser = entity.PublishUser, PublishTime = entity.PublishTime, Description = entity.Description, + AiAnalysisReport = entity.AiAnalysisReport, Content = operatorType == PriceOperatorType ? OmsDocHelper.ParseSalesProductPricePublishContentForApi(entity.Content) : OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeRulesSnapshot(entity.Content)), @@ -227,7 +228,11 @@ public partial class SalesProductService /// /// 为当前配置生成发布快照。 /// - private async Task CreatePublishRecordAsync(int salesProductId, string operatorType, string? description) + private async Task CreatePublishRecordAsync( + int salesProductId, + string operatorType, + string? description, + string? aiAnalysisReport = null) { object snapshot = operatorType == PriceOperatorType ? await BuildPricePublishSnapshotAsync(salesProductId) @@ -243,6 +248,7 @@ public partial class SalesProductService PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId().ToString() : CurrentUser.UserName!, PublishTime = DateTime.Now, Description = description, + AiAnalysisReport = string.IsNullOrWhiteSpace(aiAnalysisReport) ? null : aiAnalysisReport.Trim(), Content = OmsDocHelper.SerializeSnapshotJson(snapshot) }); } diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index 5d4c535..afad134 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -262,7 +262,16 @@ public partial class SalesProductService : ApplicationService await SalesProductRepository.ReplacePricesAsync(input.SalesProductId, prices, factors, details); if (input.Publish) { - await CreatePublishRecordAsync(input.SalesProductId, PriceOperatorType, "保存并发布价格"); + if (string.IsNullOrWhiteSpace(input.AiAnalysisReport)) + { + throw new Fake.Application.BusinessException("价格发布前须完成 AI 风险分析并保存报告"); + } + + await CreatePublishRecordAsync( + input.SalesProductId, + PriceOperatorType, + "保存并发布价格", + input.AiAnalysisReport); } } @@ -349,7 +358,17 @@ public partial class SalesProductService : ApplicationService [HttpPost("/channel/sales-product/publish")] public async Task PublishAsync(SalesProductPublishInput input) { - await CreatePublishRecordAsync(input.SalesProductId, NormalizeOperatorType(input.OperatorType), input.Description); + var operatorType = NormalizeOperatorType(input.OperatorType); + if (operatorType == PriceOperatorType && string.IsNullOrWhiteSpace(input.AiAnalysisReport)) + { + throw new Fake.Application.BusinessException("价格发布前须完成 AI 风险分析并保存报告"); + } + + await CreatePublishRecordAsync( + input.SalesProductId, + operatorType, + input.Description, + input.AiAnalysisReport); } /// diff --git a/src/Oms.Application/Services/Mcp/McpToolService.cs b/src/Oms.Application/Services/Mcp/McpToolService.cs index c404907..2985f1c 100644 --- a/src/Oms.Application/Services/Mcp/McpToolService.cs +++ b/src/Oms.Application/Services/Mcp/McpToolService.cs @@ -7,10 +7,10 @@ using Oms.Application.Dtos.Mcp; using Oms.Application.Dtos.Shared; using Oms.Application.Mcp; using Oms.Application.Shared; +using Oms.Domain.AiAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.McpAggregate.Entities; using Oms.Domain.McpAggregate.Repositories; -using Oms.Domain.XhsAggregate.Repositories; namespace Oms.Application.Services.Mcp; @@ -19,9 +19,9 @@ public class McpToolService : ApplicationService { private IMcpToolRepository McpToolRepository => LazyServiceProvider.GetRequiredService(); private IMcpToolExecutor ToolExecutor => LazyServiceProvider.GetRequiredService(); - private IXhsAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService(); + private IAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); - private XhsLlmClient LlmClient => LazyServiceProvider.GetRequiredService(); + private AiLlmClient LlmClient => LazyServiceProvider.GetRequiredService(); /// 列出 MCP 工具(内置目录 + 库表覆盖) [HttpPost("/mcp/tool/list")] @@ -307,7 +307,7 @@ public class McpToolService : ApplicationService return enabled; } - private async Task ResolveProviderAsync( + private async Task ResolveProviderAsync( int customerId, string providerKey) { diff --git a/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs b/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs index 8bf89d1..92d95ca 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs @@ -4,8 +4,8 @@ using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Shared; -using Oms.Domain.XhsAggregate.Entities; -using Oms.Domain.XhsAggregate.Repositories; +using Oms.Domain.AiAggregate.Entities; +using Oms.Domain.AiAggregate.Repositories; namespace Oms.Application.Services.Orders; @@ -20,10 +20,10 @@ public partial class OrderService "地址1", ]; - private IXhsAiProviderRepository AiProviderRepository => - LazyServiceProvider.GetRequiredService(); + private IAiProviderRepository AiProviderRepository => + LazyServiceProvider.GetRequiredService(); - private XhsLlmClient LlmClient => LazyServiceProvider.GetRequiredService(); + private AiLlmClient LlmClient => LazyServiceProvider.GetRequiredService(); /// /// AI 解析粘贴的收件地址文本,并校验是否完整。 @@ -122,7 +122,7 @@ public partial class OrderService }; } - private async Task ResolveAiProviderAsync(int customerId, string? providerKey) + private async Task ResolveAiProviderAsync(int customerId, string? providerKey) { var key = (providerKey ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(key)) diff --git a/src/Oms.Application/Services/Xhs/XhsAnalysisService.cs b/src/Oms.Application/Services/Xhs/XhsAnalysisService.cs index 2d34d44..7445b82 100644 --- a/src/Oms.Application/Services/Xhs/XhsAnalysisService.cs +++ b/src/Oms.Application/Services/Xhs/XhsAnalysisService.cs @@ -2,6 +2,7 @@ using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Xhs; +using Oms.Application.Services.Ai; using Oms.Application.Shared; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.XhsAggregate.Consts; @@ -110,9 +111,9 @@ public class XhsAnalysisService : ApplicationService throw new BusinessException("请先完成笔记采集"); } - var aiService = LazyServiceProvider.GetRequiredService(); + var aiService = LazyServiceProvider.GetRequiredService(); var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim()); - var llm = LazyServiceProvider.GetRequiredService(); + var llm = LazyServiceProvider.GetRequiredService(); var skillHint = string.Empty; if (input.SkillId is > 0) diff --git a/src/Oms.Application/Services/Xhs/XhsCreationService.cs b/src/Oms.Application/Services/Xhs/XhsCreationService.cs index a556482..e518103 100644 --- a/src/Oms.Application/Services/Xhs/XhsCreationService.cs +++ b/src/Oms.Application/Services/Xhs/XhsCreationService.cs @@ -1,6 +1,7 @@ using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Xhs; +using Oms.Application.Services.Ai; using Oms.Application.Shared; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.XhsAggregate.Consts; @@ -66,9 +67,9 @@ public class XhsCreationService : ApplicationService public async Task GenerateAsync(XhsCreationGenerateInput input) { var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); - var aiService = LazyServiceProvider.GetRequiredService(); + var aiService = LazyServiceProvider.GetRequiredService(); var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim()); - var llm = LazyServiceProvider.GetRequiredService(); + var llm = LazyServiceProvider.GetRequiredService(); var skillHint = string.Empty; if (input.SkillId is > 0) diff --git a/src/Oms.Application/Shared/XhsLlmClient.cs b/src/Oms.Application/Shared/AiLlmClient.cs similarity index 93% rename from src/Oms.Application/Shared/XhsLlmClient.cs rename to src/Oms.Application/Shared/AiLlmClient.cs index c76d48f..6f9f825 100644 --- a/src/Oms.Application/Shared/XhsLlmClient.cs +++ b/src/Oms.Application/Shared/AiLlmClient.cs @@ -1,11 +1,11 @@ -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Fake.Application; using Microsoft.Extensions.DependencyInjection; using Oms.Application.Mcp; -using Oms.Domain.XhsAggregate.Entities; +using Oms.Domain.AiAggregate.Entities; namespace Oms.Application.Shared; @@ -40,21 +40,21 @@ public class LlmChatResult } /// -/// OpenAI 兼容 Chat Completions 客户端(智谱 / DeepSeek / 豆包等)。 +/// OpenAI 兼容 Chat Completions 客户端(通义 / DeepSeek / 豆包等)。 /// -public class XhsLlmClient +public class AiLlmClient { - public const string HttpClientName = nameof(XhsLlmClient); + public const string HttpClientName = nameof(AiLlmClient); private readonly IHttpClientFactory _httpClientFactory; - public XhsLlmClient(IHttpClientFactory httpClientFactory) + public AiLlmClient(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task ChatCompletionAsync( - XhsAiProvider provider, + AiProvider provider, IReadOnlyList<(string Role, string Content)> messages, string? modelOverride = null, CancellationToken cancellationToken = default) @@ -69,7 +69,7 @@ public class XhsLlmClient } public async Task ChatCompletionDetailedAsync( - XhsAiProvider provider, + AiProvider provider, IReadOnlyList messages, IReadOnlyList? tools = null, string? modelOverride = null, @@ -258,11 +258,11 @@ public class XhsLlmClient } } -public static class XhsLlmServiceExtensions +public static class AiLlmServiceExtensions { - public static IServiceCollection AddXhsLlmClient(this IServiceCollection services) + public static IServiceCollection AddAiLlmClient(this IServiceCollection services) { - services.AddHttpClient(XhsLlmClient.HttpClientName, client => + services.AddHttpClient(AiLlmClient.HttpClientName, client => { client.Timeout = TimeSpan.FromMinutes(3); }); @@ -270,7 +270,8 @@ public static class XhsLlmServiceExtensions { client.Timeout = TimeSpan.FromSeconds(45); }); - services.AddTransient(); + services.AddTransient(); return services; } } + diff --git a/src/Oms.Domain/XhsAggregate/Entities/XhsAiProvider.cs b/src/Oms.Domain/AiAggregate/Entities/AiProvider.cs similarity index 72% rename from src/Oms.Domain/XhsAggregate/Entities/XhsAiProvider.cs rename to src/Oms.Domain/AiAggregate/Entities/AiProvider.cs index 0504237..18c9360 100644 --- a/src/Oms.Domain/XhsAggregate/Entities/XhsAiProvider.cs +++ b/src/Oms.Domain/AiAggregate/Entities/AiProvider.cs @@ -1,10 +1,13 @@ using Oms.Domain.Shared; using SqlSugar; -namespace Oms.Domain.XhsAggregate.Entities; +namespace Oms.Domain.AiAggregate.Entities; -[SugarTable("t_xhs_ai_provider")] -public class XhsAiProvider : OmsAuditedAggregateRoot +/// +/// 全局 AI 提供商配置(CustomerId=0 表示平台级)。 +/// +[SugarTable("t_ai_provider")] +public class AiProvider : OmsAuditedAggregateRoot { public int CustomerId { get; set; } diff --git a/src/Oms.Domain/AiAggregate/Repositories/IAiProviderRepository.cs b/src/Oms.Domain/AiAggregate/Repositories/IAiProviderRepository.cs new file mode 100644 index 0000000..f36c2dd --- /dev/null +++ b/src/Oms.Domain/AiAggregate/Repositories/IAiProviderRepository.cs @@ -0,0 +1,9 @@ +using Fake.Domain.Repositories; +using Oms.Domain.AiAggregate.Entities; + +namespace Oms.Domain.AiAggregate.Repositories; + +public interface IAiProviderRepository : IRepository +{ + Task GetByProviderKeyAsync(int customerId, string providerKey, CancellationToken cancellationToken = default); +} diff --git a/src/Oms.Domain/ChannelsAggregate/Entities/SalesProductPublishRecord.cs b/src/Oms.Domain/ChannelsAggregate/Entities/SalesProductPublishRecord.cs index a0c42a4..aedd8c3 100644 --- a/src/Oms.Domain/ChannelsAggregate/Entities/SalesProductPublishRecord.cs +++ b/src/Oms.Domain/ChannelsAggregate/Entities/SalesProductPublishRecord.cs @@ -21,6 +21,12 @@ public class SalesProductPublishRecord : OmsAuditedEntity public string? Description { get; set; } + /// + /// 价格发布 AI 风险分析报告(operatorType=price 时必填)。 + /// + [SugarColumn(ColumnDataType = "mediumtext", IsNullable = true)] + public string? AiAnalysisReport { get; set; } + [SugarColumn(ColumnDataType = "json", IsNullable = true)] public string? Content { get; set; } } diff --git a/src/Oms.Domain/XhsAggregate/Repositories/IXhsRepositories.cs b/src/Oms.Domain/XhsAggregate/Repositories/IXhsRepositories.cs index c37a36e..a59230d 100644 --- a/src/Oms.Domain/XhsAggregate/Repositories/IXhsRepositories.cs +++ b/src/Oms.Domain/XhsAggregate/Repositories/IXhsRepositories.cs @@ -51,9 +51,4 @@ public interface IXhsHumanConfigRepository : IRepository Task GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default); } -public interface IXhsAiProviderRepository : IRepository -{ - Task GetByProviderKeyAsync(int customerId, string providerKey, CancellationToken cancellationToken = default); -} - public interface IXhsMediaRepository : IRepository; diff --git a/src/Oms.SqlSugarCore/Ai/AiProviderRepository.cs b/src/Oms.SqlSugarCore/Ai/AiProviderRepository.cs new file mode 100644 index 0000000..ad1f961 --- /dev/null +++ b/src/Oms.SqlSugarCore/Ai/AiProviderRepository.cs @@ -0,0 +1,16 @@ +using Fake.DependencyInjection; +using Oms.Domain.AiAggregate.Entities; +using Oms.Domain.AiAggregate.Repositories; +using Oms.SqlSugarCore.Repositories; + +namespace Oms.SqlSugarCore.Ai; + +public class AiProviderRepository + : OmsSqlSugarRepository, IAiProviderRepository, IScopedDependency +{ + public Task GetByProviderKeyAsync( + int customerId, + string providerKey, + CancellationToken cancellationToken = default) => + FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ProviderKey == providerKey, cancellationToken); +} diff --git a/src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs b/src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs index ab355c9..d833163 100644 --- a/src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs +++ b/src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs @@ -162,14 +162,4 @@ public class XhsHumanConfigRepository FirstOrDefaultAsync(x => x.CustomerId == customerId, cancellationToken); } -public class XhsAiProviderRepository - : OmsSqlSugarRepository, IXhsAiProviderRepository, IScopedDependency -{ - public Task GetByProviderKeyAsync( - int customerId, - string providerKey, - CancellationToken cancellationToken = default) => - FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ProviderKey == providerKey, cancellationToken); -} - public class XhsMediaRepository : OmsSqlSugarRepository, IXhsMediaRepository, IScopedDependency; diff --git a/src/SimpleAdmin.Api/Auth/XhsRolePermissionSeedContributor.cs b/src/SimpleAdmin.Api/Auth/XhsRolePermissionSeedContributor.cs index 86a4f85..5935dc2 100644 --- a/src/SimpleAdmin.Api/Auth/XhsRolePermissionSeedContributor.cs +++ b/src/SimpleAdmin.Api/Auth/XhsRolePermissionSeedContributor.cs @@ -22,8 +22,8 @@ public class XhsRolePermissionSeedContributor(RoleService roleService) : IDataSe "Xhs.Skills.Save", "Xhs.Skills.Run", "Xhs.HumanSettings.Save", - "Xhs.Ai.Use", - "Xhs.Ai.Save" + "System.Ai.Use", + "System.Ai.Save" ]; public async Task SeedAsync()