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 <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-08-01 15:55:30 +08:00
parent d30375ba49
commit 592054f062
24 changed files with 233 additions and 145 deletions

9
db.sql
View File

@ -29,6 +29,7 @@ DROP TABLE IF EXISTS `role_data_scope`;
DROP TABLE IF EXISTS `role_org_perm`; DROP TABLE IF EXISTS `role_org_perm`;
DROP TABLE IF EXISTS `user_org_perm`; DROP TABLE IF EXISTS `user_org_perm`;
DROP TABLE IF EXISTS `t_xhs_media`; 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_ai_provider`;
DROP TABLE IF EXISTS `t_xhs_human_config`; DROP TABLE IF EXISTS `t_xhs_human_config`;
DROP TABLE IF EXISTS `t_xhs_skill`; DROP TABLE IF EXISTS `t_xhs_skill`;
@ -1733,7 +1734,7 @@ CREATE TABLE `t_xhs_human_config` (
UNIQUE KEY `uk_xhs_human_customer` (`CustomerId`) UNIQUE KEY `uk_xhs_human_customer` (`CustomerId`)
) COMMENT='小红书拟人行为与防封配置表'; ) COMMENT='小红书拟人行为与防封配置表';
CREATE TABLE `t_xhs_ai_provider` ( CREATE TABLE `t_ai_provider` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局', `CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局',
`ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等', `ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等',
@ -1751,9 +1752,9 @@ CREATE TABLE `t_xhs_ai_provider` (
`UpdateUserId` INT NULL, `UpdateUserId` INT NULL,
`UpdateTime` DATETIME NULL, `UpdateTime` DATETIME NULL,
PRIMARY KEY (`Id`), PRIMARY KEY (`Id`),
UNIQUE KEY `uk_xhs_ai_provider` (`CustomerId`, `ProviderKey`), UNIQUE KEY `uk_ai_provider` (`CustomerId`, `ProviderKey`),
KEY `idx_xhs_ai_customer` (`CustomerId`, `Enabled`) KEY `idx_ai_customer` (`CustomerId`, `Enabled`)
) COMMENT='小红书 AI 提供商配置表'; ) COMMENT='全局 AI 提供商配置表(CustomerId=0 表示平台级)';
CREATE TABLE `t_xhs_media` ( CREATE TABLE `t_xhs_media` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',

View File

@ -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`);

View File

@ -0,0 +1,3 @@
-- 销售产品价格发布记录:保存 AI 风险分析报告
ALTER TABLE `t_sales_product_publish_record`
ADD COLUMN `AiAnalysisReport` MEDIUMTEXT NULL COMMENT '价格发布AI风险分析报告' AFTER `Description`;

View File

@ -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<AiChatMessageInput> 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;
}

View File

@ -15,6 +15,11 @@ public class SalesProductPriceSaveAllInput
/// </summary> /// </summary>
public bool Publish { get; set; } public bool Publish { get; set; }
/// <summary>
/// 价格发布 AI 风险分析报告(Publish=true 时必填)。
/// </summary>
public string? AiAnalysisReport { get; set; }
/// <summary> /// <summary>
/// 价格项集合。 /// 价格项集合。
/// </summary> /// </summary>

View File

@ -19,4 +19,9 @@ public class SalesProductPublishInput
/// 描述。 /// 描述。
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
/// <summary>
/// 价格发布 AI 风险分析报告(operatorType=price 时必填)。
/// </summary>
public string? AiAnalysisReport { get; set; }
} }

View File

@ -45,6 +45,11 @@ public class SalesProductPublishRecordOutput
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
/// <summary>
/// 价格发布 AI 风险分析报告。
/// </summary>
public string? AiAnalysisReport { get; set; }
/// <summary> /// <summary>
/// 内容。 /// 内容。
/// </summary> /// </summary>

View File

@ -320,75 +320,6 @@ public class XhsSkillSaveInput
public object? Recipe { get; set; } 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<XhsAiChatMessageInput> 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 public class XhsAnalysisStartInput
{ {
[Required] [Required]

View File

@ -55,7 +55,7 @@ public class OmsApplicationModule : FakeModule
context.Services.AddOmsItfxIntegration(configuration); context.Services.AddOmsItfxIntegration(configuration);
context.Services.AddOmsPlatformShopIntegration(configuration); context.Services.AddOmsPlatformShopIntegration(configuration);
context.Services.AddOmsOrderBatchMessaging(configuration); context.Services.AddOmsOrderBatchMessaging(configuration);
context.Services.AddXhsLlmClient(); context.Services.AddAiLlmClient();
context.Services.AddHostedService<XhsScheduledPublishWorker>(); context.Services.AddHostedService<XhsScheduledPublishWorker>();
context.Services.AddHostedService<MarketingBackgroundWorker>(); context.Services.AddHostedService<MarketingBackgroundWorker>();
} }

View File

@ -1,48 +1,51 @@
using Fake.Application; using Fake.Application;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Ai;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Xhs;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.AiAggregate.Entities;
using Oms.Domain.AiAggregate.Repositories;
using Oms.Domain.CustomersAggregate.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")] /// <summary>
public class XhsAiService : ApplicationService /// 全局 AI 提供商与对话服务(系统级,非业务域专属)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Ai")]
public class AiService : ApplicationService
{ {
private const string SecretMask = "******"; private const string SecretMask = "******";
private IXhsAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService<IXhsAiProviderRepository>(); private IAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService<IAiProviderRepository>();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
/// <summary> /// <summary>
/// 查询AI服务商列表 /// 查询 AI 服务商列表。
/// </summary> /// </summary>
[HttpPost("/xhs/ai/provider/list")] [HttpPost("/ai/provider/list")]
public async Task<OmsItemsResult<XhsAiProviderOutput>> ListProvidersAsync() public async Task<OmsItemsResult<AiProviderOutput>> ListProvidersAsync()
{ {
var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser);
var items = await AiProviderRepository.GetListAsync(x => x.CustomerId == customerId || x.CustomerId == 0); var items = await AiProviderRepository.GetListAsync(x => x.CustomerId == customerId || x.CustomerId == 0);
return new OmsItemsResult<XhsAiProviderOutput> return new OmsItemsResult<AiProviderOutput>
{ {
Items = items.Select(x => MapProvider(x, maskSecret: true)).ToList() Items = items.Select(x => MapProvider(x, maskSecret: true)).ToList()
}; };
} }
/// <summary> /// <summary>
/// 保存AI服务商配置 /// 保存 AI 服务商配置。
/// </summary> /// </summary>
[HttpPost("/xhs/ai/provider/save")] [HttpPost("/ai/provider/save")]
public async Task<XhsAiProviderOutput> SaveProviderAsync(XhsAiProviderSaveInput input) public async Task<AiProviderOutput> SaveProviderAsync(AiProviderSaveInput input)
{ {
var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser);
var providerKey = input.ProviderKey.Trim(); var providerKey = input.ProviderKey.Trim();
var entity = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey); var entity = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey);
if (entity == null) if (entity == null)
{ {
entity = new XhsAiProvider { CustomerId = customerId, ProviderKey = providerKey }; entity = new AiProvider { CustomerId = customerId, ProviderKey = providerKey };
} }
entity.ProviderType = input.ProviderType.Trim(); entity.ProviderType = input.ProviderType.Trim();
@ -85,22 +88,22 @@ public class XhsAiService : ApplicationService
} }
/// <summary> /// <summary>
/// AI对话 /// AI 对话。
/// </summary> /// </summary>
[HttpPost("/xhs/ai/chat")] [HttpPost("/ai/chat")]
public async Task<XhsAiChatOutput> ChatAsync(XhsAiChatInput input) public async Task<AiChatOutput> ChatAsync(AiChatInput input)
{ {
var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser);
var provider = await ResolveProviderAsync(customerId, input.ProviderKey.Trim()); var provider = await ResolveProviderAsync(customerId, input.ProviderKey.Trim());
var llm = LazyServiceProvider.GetRequiredService<XhsLlmClient>(); var llm = LazyServiceProvider.GetRequiredService<AiLlmClient>();
var content = await llm.ChatCompletionAsync( var content = await llm.ChatCompletionAsync(
provider, provider,
input.Messages.Select(x => (x.Role, x.Content)).ToList(), input.Messages.Select(x => (x.Role, x.Content)).ToList(),
input.Model); input.Model);
return new XhsAiChatOutput { Content = content }; return new AiChatOutput { Content = content };
} }
internal async Task<XhsAiProvider> ResolveProviderAsync(int customerId, string providerKey) internal async Task<AiProvider> ResolveProviderAsync(int customerId, string providerKey)
{ {
var provider = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey); var provider = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey);
if (provider == null || !provider.Enabled) if (provider == null || !provider.Enabled)
@ -116,7 +119,7 @@ public class XhsAiService : ApplicationService
return provider; return provider;
} }
private static XhsAiProviderOutput MapProvider(XhsAiProvider entity, bool maskSecret) => new() private static AiProviderOutput MapProvider(AiProvider entity, bool maskSecret) => new()
{ {
Id = entity.Id, Id = entity.Id,
ProviderKey = entity.ProviderKey, ProviderKey = entity.ProviderKey,

View File

@ -153,6 +153,7 @@ public partial class SalesProductService
PublishUser = entity.PublishUser, PublishUser = entity.PublishUser,
PublishTime = entity.PublishTime, PublishTime = entity.PublishTime,
Description = entity.Description, Description = entity.Description,
AiAnalysisReport = entity.AiAnalysisReport,
Content = operatorType == PriceOperatorType Content = operatorType == PriceOperatorType
? OmsDocHelper.ParseSalesProductPricePublishContentForApi(entity.Content) ? OmsDocHelper.ParseSalesProductPricePublishContentForApi(entity.Content)
: OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeRulesSnapshot(entity.Content)), : OmsDocHelper.ToSnapshotApiContent(OmsDocHelper.DeserializeRulesSnapshot(entity.Content)),
@ -227,7 +228,11 @@ public partial class SalesProductService
/// <summary> /// <summary>
/// 为当前配置生成发布快照。 /// 为当前配置生成发布快照。
/// </summary> /// </summary>
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 object snapshot = operatorType == PriceOperatorType
? await BuildPricePublishSnapshotAsync(salesProductId) ? await BuildPricePublishSnapshotAsync(salesProductId)
@ -243,6 +248,7 @@ public partial class SalesProductService
PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId<int>().ToString() : CurrentUser.UserName!, PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId<int>().ToString() : CurrentUser.UserName!,
PublishTime = DateTime.Now, PublishTime = DateTime.Now,
Description = description, Description = description,
AiAnalysisReport = string.IsNullOrWhiteSpace(aiAnalysisReport) ? null : aiAnalysisReport.Trim(),
Content = OmsDocHelper.SerializeSnapshotJson(snapshot) Content = OmsDocHelper.SerializeSnapshotJson(snapshot)
}); });
} }

View File

@ -262,7 +262,16 @@ public partial class SalesProductService : ApplicationService
await SalesProductRepository.ReplacePricesAsync(input.SalesProductId, prices, factors, details); await SalesProductRepository.ReplacePricesAsync(input.SalesProductId, prices, factors, details);
if (input.Publish) 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")] [HttpPost("/channel/sales-product/publish")]
public async Task PublishAsync(SalesProductPublishInput input) 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);
} }
/// <summary> /// <summary>

View File

@ -7,10 +7,10 @@ using Oms.Application.Dtos.Mcp;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Mcp; using Oms.Application.Mcp;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.AiAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.McpAggregate.Entities; using Oms.Domain.McpAggregate.Entities;
using Oms.Domain.McpAggregate.Repositories; using Oms.Domain.McpAggregate.Repositories;
using Oms.Domain.XhsAggregate.Repositories;
namespace Oms.Application.Services.Mcp; namespace Oms.Application.Services.Mcp;
@ -19,9 +19,9 @@ public class McpToolService : ApplicationService
{ {
private IMcpToolRepository McpToolRepository => LazyServiceProvider.GetRequiredService<IMcpToolRepository>(); private IMcpToolRepository McpToolRepository => LazyServiceProvider.GetRequiredService<IMcpToolRepository>();
private IMcpToolExecutor ToolExecutor => LazyServiceProvider.GetRequiredService<IMcpToolExecutor>(); private IMcpToolExecutor ToolExecutor => LazyServiceProvider.GetRequiredService<IMcpToolExecutor>();
private IXhsAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService<IXhsAiProviderRepository>(); private IAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService<IAiProviderRepository>();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private XhsLlmClient LlmClient => LazyServiceProvider.GetRequiredService<XhsLlmClient>(); private AiLlmClient LlmClient => LazyServiceProvider.GetRequiredService<AiLlmClient>();
/// <summary>列出 MCP 工具(内置目录 + 库表覆盖)</summary> /// <summary>列出 MCP 工具(内置目录 + 库表覆盖)</summary>
[HttpPost("/mcp/tool/list")] [HttpPost("/mcp/tool/list")]
@ -307,7 +307,7 @@ public class McpToolService : ApplicationService
return enabled; return enabled;
} }
private async Task<Oms.Domain.XhsAggregate.Entities.XhsAiProvider> ResolveProviderAsync( private async Task<Oms.Domain.AiAggregate.Entities.AiProvider> ResolveProviderAsync(
int customerId, int customerId,
string providerKey) string providerKey)
{ {

View File

@ -4,8 +4,8 @@ using Fake.Application;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.XhsAggregate.Entities; using Oms.Domain.AiAggregate.Entities;
using Oms.Domain.XhsAggregate.Repositories; using Oms.Domain.AiAggregate.Repositories;
namespace Oms.Application.Services.Orders; namespace Oms.Application.Services.Orders;
@ -20,10 +20,10 @@ public partial class OrderService
"地址1", "地址1",
]; ];
private IXhsAiProviderRepository AiProviderRepository => private IAiProviderRepository AiProviderRepository =>
LazyServiceProvider.GetRequiredService<IXhsAiProviderRepository>(); LazyServiceProvider.GetRequiredService<IAiProviderRepository>();
private XhsLlmClient LlmClient => LazyServiceProvider.GetRequiredService<XhsLlmClient>(); private AiLlmClient LlmClient => LazyServiceProvider.GetRequiredService<AiLlmClient>();
/// <summary> /// <summary>
/// AI 解析粘贴的收件地址文本,并校验是否完整。 /// AI 解析粘贴的收件地址文本,并校验是否完整。
@ -122,7 +122,7 @@ public partial class OrderService
}; };
} }
private async Task<XhsAiProvider> ResolveAiProviderAsync(int customerId, string? providerKey) private async Task<AiProvider> ResolveAiProviderAsync(int customerId, string? providerKey)
{ {
var key = (providerKey ?? string.Empty).Trim(); var key = (providerKey ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(key)) if (!string.IsNullOrWhiteSpace(key))

View File

@ -2,6 +2,7 @@ using Fake.Application;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Xhs; using Oms.Application.Dtos.Xhs;
using Oms.Application.Services.Ai;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.XhsAggregate.Consts; using Oms.Domain.XhsAggregate.Consts;
@ -110,9 +111,9 @@ public class XhsAnalysisService : ApplicationService
throw new BusinessException("请先完成笔记采集"); throw new BusinessException("请先完成笔记采集");
} }
var aiService = LazyServiceProvider.GetRequiredService<XhsAiService>(); var aiService = LazyServiceProvider.GetRequiredService<AiService>();
var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim()); var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim());
var llm = LazyServiceProvider.GetRequiredService<XhsLlmClient>(); var llm = LazyServiceProvider.GetRequiredService<AiLlmClient>();
var skillHint = string.Empty; var skillHint = string.Empty;
if (input.SkillId is > 0) if (input.SkillId is > 0)

View File

@ -1,6 +1,7 @@
using Fake.Application; using Fake.Application;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Xhs; using Oms.Application.Dtos.Xhs;
using Oms.Application.Services.Ai;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.XhsAggregate.Consts; using Oms.Domain.XhsAggregate.Consts;
@ -66,9 +67,9 @@ public class XhsCreationService : ApplicationService
public async Task<XhsCreationGenerateOutput> GenerateAsync(XhsCreationGenerateInput input) public async Task<XhsCreationGenerateOutput> GenerateAsync(XhsCreationGenerateInput input)
{ {
var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser);
var aiService = LazyServiceProvider.GetRequiredService<XhsAiService>(); var aiService = LazyServiceProvider.GetRequiredService<AiService>();
var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim()); var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim());
var llm = LazyServiceProvider.GetRequiredService<XhsLlmClient>(); var llm = LazyServiceProvider.GetRequiredService<AiLlmClient>();
var skillHint = string.Empty; var skillHint = string.Empty;
if (input.SkillId is > 0) if (input.SkillId is > 0)

View File

@ -1,11 +1,11 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Fake.Application; using Fake.Application;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Oms.Application.Mcp; using Oms.Application.Mcp;
using Oms.Domain.XhsAggregate.Entities; using Oms.Domain.AiAggregate.Entities;
namespace Oms.Application.Shared; namespace Oms.Application.Shared;
@ -40,21 +40,21 @@ public class LlmChatResult
} }
/// <summary> /// <summary>
/// OpenAI 兼容 Chat Completions 客户端(智谱 / DeepSeek / 豆包等)。 /// OpenAI 兼容 Chat Completions 客户端(通义 / DeepSeek / 豆包等)。
/// </summary> /// </summary>
public class XhsLlmClient public class AiLlmClient
{ {
public const string HttpClientName = nameof(XhsLlmClient); public const string HttpClientName = nameof(AiLlmClient);
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
public XhsLlmClient(IHttpClientFactory httpClientFactory) public AiLlmClient(IHttpClientFactory httpClientFactory)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
} }
public async Task<string> ChatCompletionAsync( public async Task<string> ChatCompletionAsync(
XhsAiProvider provider, AiProvider provider,
IReadOnlyList<(string Role, string Content)> messages, IReadOnlyList<(string Role, string Content)> messages,
string? modelOverride = null, string? modelOverride = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@ -69,7 +69,7 @@ public class XhsLlmClient
} }
public async Task<LlmChatResult> ChatCompletionDetailedAsync( public async Task<LlmChatResult> ChatCompletionDetailedAsync(
XhsAiProvider provider, AiProvider provider,
IReadOnlyList<LlmChatMessage> messages, IReadOnlyList<LlmChatMessage> messages,
IReadOnlyList<LlmToolDefinition>? tools = null, IReadOnlyList<LlmToolDefinition>? tools = null,
string? modelOverride = 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); client.Timeout = TimeSpan.FromMinutes(3);
}); });
@ -270,7 +270,8 @@ public static class XhsLlmServiceExtensions
{ {
client.Timeout = TimeSpan.FromSeconds(45); client.Timeout = TimeSpan.FromSeconds(45);
}); });
services.AddTransient<XhsLlmClient>(); services.AddTransient<AiLlmClient>();
return services; return services;
} }
} }

View File

@ -1,10 +1,13 @@
using Oms.Domain.Shared; using Oms.Domain.Shared;
using SqlSugar; using SqlSugar;
namespace Oms.Domain.XhsAggregate.Entities; namespace Oms.Domain.AiAggregate.Entities;
[SugarTable("t_xhs_ai_provider")] /// <summary>
public class XhsAiProvider : OmsAuditedAggregateRoot /// 全局 AI 提供商配置(CustomerId=0 表示平台级)。
/// </summary>
[SugarTable("t_ai_provider")]
public class AiProvider : OmsAuditedAggregateRoot
{ {
public int CustomerId { get; set; } public int CustomerId { get; set; }

View File

@ -0,0 +1,9 @@
using Fake.Domain.Repositories;
using Oms.Domain.AiAggregate.Entities;
namespace Oms.Domain.AiAggregate.Repositories;
public interface IAiProviderRepository : IRepository<AiProvider>
{
Task<AiProvider?> GetByProviderKeyAsync(int customerId, string providerKey, CancellationToken cancellationToken = default);
}

View File

@ -21,6 +21,12 @@ public class SalesProductPublishRecord : OmsAuditedEntity
public string? Description { get; set; } public string? Description { get; set; }
/// <summary>
/// 价格发布 AI 风险分析报告(operatorType=price 时必填)。
/// </summary>
[SugarColumn(ColumnDataType = "mediumtext", IsNullable = true)]
public string? AiAnalysisReport { get; set; }
[SugarColumn(ColumnDataType = "json", IsNullable = true)] [SugarColumn(ColumnDataType = "json", IsNullable = true)]
public string? Content { get; set; } public string? Content { get; set; }
} }

View File

@ -51,9 +51,4 @@ public interface IXhsHumanConfigRepository : IRepository<XhsHumanConfig>
Task<XhsHumanConfig?> GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default); Task<XhsHumanConfig?> GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default);
} }
public interface IXhsAiProviderRepository : IRepository<XhsAiProvider>
{
Task<XhsAiProvider?> GetByProviderKeyAsync(int customerId, string providerKey, CancellationToken cancellationToken = default);
}
public interface IXhsMediaRepository : IRepository<XhsMedia>; public interface IXhsMediaRepository : IRepository<XhsMedia>;

View File

@ -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<AiProvider>, IAiProviderRepository, IScopedDependency
{
public Task<AiProvider?> GetByProviderKeyAsync(
int customerId,
string providerKey,
CancellationToken cancellationToken = default) =>
FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ProviderKey == providerKey, cancellationToken);
}

View File

@ -162,14 +162,4 @@ public class XhsHumanConfigRepository
FirstOrDefaultAsync(x => x.CustomerId == customerId, cancellationToken); FirstOrDefaultAsync(x => x.CustomerId == customerId, cancellationToken);
} }
public class XhsAiProviderRepository
: OmsSqlSugarRepository<XhsAiProvider>, IXhsAiProviderRepository, IScopedDependency
{
public Task<XhsAiProvider?> GetByProviderKeyAsync(
int customerId,
string providerKey,
CancellationToken cancellationToken = default) =>
FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ProviderKey == providerKey, cancellationToken);
}
public class XhsMediaRepository : OmsSqlSugarRepository<XhsMedia>, IXhsMediaRepository, IScopedDependency; public class XhsMediaRepository : OmsSqlSugarRepository<XhsMedia>, IXhsMediaRepository, IScopedDependency;

View File

@ -22,8 +22,8 @@ public class XhsRolePermissionSeedContributor(RoleService roleService) : IDataSe
"Xhs.Skills.Save", "Xhs.Skills.Save",
"Xhs.Skills.Run", "Xhs.Skills.Run",
"Xhs.HumanSettings.Save", "Xhs.HumanSettings.Save",
"Xhs.Ai.Use", "System.Ai.Use",
"Xhs.Ai.Save" "System.Ai.Save"
]; ];
public async Task SeedAsync() public async Task SeedAsync()