From c092ee46e9f96077fd13c2072387a8e5b1164b09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=A9=99=E5=AD=90?= <454313500@qq.com>
Date: Wed, 5 Mar 2025 23:08:58 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90ai=E7=94=9F=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Dtos/StockMarket/BuyStockInputDto.cs | 20 ++
.../Dtos/StockMarket/SellStockInputDto.cs | 20 ++
.../IServices/IStockMarketService.cs | 14 +
.../Services/StockMarketService.cs | 43 ++-
.../Etos/StockTransactionEto.cs | 56 +++
.../Yi.Framework.Stock.Domain.Shared.csproj | 3 +
.../Managers/NewsManager.cs | 23 ++
.../SemanticKernel/Plugins/NewsPlugins.cs | 25 ++
.../SemanticKernel/Plugins/StockPlugins.cs | 26 ++
.../SemanticKernel/SemanticKernelClient.cs | 88 +++++
.../SemanticKernel/SemanticKernelOptions.cs | 6 +
.../Managers/StockMarketManager.cs | 323 ++++++++++++++++++
.../Yi.Framework.Stock.Domain.csproj | 2 +-
Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj | 6 +
14 files changed, 653 insertions(+), 2 deletions(-)
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/BuyStockInputDto.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/SellStockInputDto.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Etos/StockTransactionEto.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs
create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/BuyStockInputDto.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/BuyStockInputDto.cs
new file mode 100644
index 00000000..cc154d9d
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/BuyStockInputDto.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockMarket
+{
+ ///
+ /// 买入股票输入DTO
+ ///
+ public class BuyStockInputDto
+ {
+ ///
+ /// 股票ID
+ ///
+ public Guid StockId { get; set; }
+
+ ///
+ /// 买入数量
+ ///
+ public int Quantity { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/SellStockInputDto.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/SellStockInputDto.cs
new file mode 100644
index 00000000..d552750e
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/SellStockInputDto.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockMarket
+{
+ ///
+ /// 卖出股票输入DTO
+ ///
+ public class SellStockInputDto
+ {
+ ///
+ /// 股票ID
+ ///
+ public Guid StockId { get; set; }
+
+ ///
+ /// 卖出数量
+ ///
+ public int Quantity { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs
index 9426870a..b788c998 100644
--- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs
@@ -25,5 +25,19 @@ namespace Yi.Framework.Stock.Application.Contracts.IServices
/// 查询条件
/// 股价记录列表
Task> GetStockPriceRecordListAsync(StockPriceRecordGetListInputDto input);
+
+ ///
+ /// 买入股票
+ ///
+ /// 买入股票参数
+ /// 操作结果
+ Task BuyStockAsync(BuyStockInputDto input);
+
+ ///
+ /// 卖出股票
+ ///
+ /// 卖出股票参数
+ /// 操作结果
+ Task SellStockAsync(SellStockInputDto input);
}
}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs
index 44b0b349..8d69ecc2 100644
--- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs
@@ -5,11 +5,13 @@ using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
+using Volo.Abp.Users;
using Yi.Framework.Stock.Application.Contracts.Dtos.StockMarket;
using Yi.Framework.Stock.Application.Contracts.Dtos.StockPrice;
using Yi.Framework.Stock.Application.Contracts.IServices;
using Yi.Framework.Stock.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
+using Yi.Framework.Stock.Domain.Managers;
namespace Yi.Framework.Stock.Application.Services
{
@@ -20,13 +22,16 @@ namespace Yi.Framework.Stock.Application.Services
{
private readonly ISqlSugarRepository _stockMarketRepository;
private readonly ISqlSugarRepository _stockPriceRecordRepository;
+ private readonly StockMarketManager _stockMarketManager;
public StockMarketService(
ISqlSugarRepository stockMarketRepository,
- ISqlSugarRepository stockPriceRecordRepository)
+ ISqlSugarRepository stockPriceRecordRepository,
+ StockMarketManager stockMarketManager)
{
_stockMarketRepository = stockMarketRepository;
_stockPriceRecordRepository = stockPriceRecordRepository;
+ _stockMarketManager = stockMarketManager;
}
///
@@ -91,5 +96,41 @@ namespace Yi.Framework.Stock.Application.Services
return new PagedResultDto(total, list);
}
+
+ ///
+ /// 买入股票
+ ///
+ [HttpPost("stock/buy")]
+ [Authorize]
+ public async Task BuyStockAsync(BuyStockInputDto input)
+ {
+ // 获取当前登录用户ID
+ var userId = CurrentUser.GetId();
+
+ // 调用领域服务进行股票购买
+ await _stockMarketManager.BuyStockAsync(
+ userId,
+ input.StockId,
+ input.Quantity
+ );
+ }
+
+ ///
+ /// 卖出股票
+ ///
+ [HttpPost("stock/sell")]
+ [Authorize]
+ public async Task SellStockAsync(SellStockInputDto input)
+ {
+ // 获取当前登录用户ID
+ var userId = CurrentUser.GetId();
+
+ // 调用领域服务进行股票卖出
+ await _stockMarketManager.SellStockAsync(
+ userId,
+ input.StockId,
+ input.Quantity
+ );
+ }
}
}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Etos/StockTransactionEto.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Etos/StockTransactionEto.cs
new file mode 100644
index 00000000..e37f07d9
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Etos/StockTransactionEto.cs
@@ -0,0 +1,56 @@
+using System;
+using Yi.Framework.Stock.Domain.Shared;
+
+namespace Yi.Framework.Stock.Domain.Shared.Etos
+{
+ ///
+ /// 股票交易事件数据传输对象
+ ///
+ public class StockTransactionEto
+ {
+ ///
+ /// 用户ID
+ ///
+ public Guid UserId { get; set; }
+
+ ///
+ /// 股票ID
+ ///
+ public Guid StockId { get; set; }
+
+ ///
+ /// 股票代码
+ ///
+ public string StockCode { get; set; }
+
+ ///
+ /// 股票名称
+ ///
+ public string StockName { get; set; }
+
+ ///
+ /// 交易类型
+ ///
+ public TransactionTypeEnum TransactionType { get; set; }
+
+ ///
+ /// 交易价格
+ ///
+ public decimal Price { get; set; }
+
+ ///
+ /// 交易数量
+ ///
+ public int Quantity { get; set; }
+
+ ///
+ /// 交易总额
+ ///
+ public decimal TotalAmount { get; set; }
+
+ ///
+ /// 交易费用
+ ///
+ public decimal Fee { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Yi.Framework.Stock.Domain.Shared.csproj b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Yi.Framework.Stock.Domain.Shared.csproj
index 77927955..94311f04 100644
--- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Yi.Framework.Stock.Domain.Shared.csproj
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain.Shared/Yi.Framework.Stock.Domain.Shared.csproj
@@ -14,5 +14,8 @@
+
+
+
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs
new file mode 100644
index 00000000..7d845d5f
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs
@@ -0,0 +1,23 @@
+using Yi.Framework.Stock.Domain.Managers.Plugins;
+
+namespace Yi.Framework.Stock.Domain.Managers;
+
+public class NewsManager
+{
+ private SemanticKernelClient _skClient;
+
+ public NewsManager(SemanticKernelClient skClient)
+ {
+ _skClient = skClient;
+ }
+
+ ///
+ /// 生成一个新闻
+ ///
+ ///
+ public async Task GenerateNewsAsync()
+ {
+ _skClient.RegisterPlugins("news");
+ await _skClient.ChatCompletionAsync("帮我生成一个新闻");
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs
new file mode 100644
index 00000000..fa02ad6c
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.SemanticKernel;
+
+namespace Yi.Framework.Stock.Domain.Managers.Plugins;
+
+public class NewsPlugins
+{
+ [KernelFunction("save_news"), Description("生成并且保存一个新闻")]
+ public async Task SaveAsync(NewModel news)
+ {
+ return "成功";
+ }
+}
+
+public class NewModel
+{
+ [JsonPropertyName("title")]
+ [DisplayName("新闻标题")]
+ public string Title { get; set; }
+
+ [JsonPropertyName("content")]
+ [DisplayName("新闻内容")]
+ public string? Content { get; set; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs
new file mode 100644
index 00000000..ee53a073
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.SemanticKernel;
+
+namespace Yi.Framework.Stock.Domain.Managers.Plugins;
+
+public class StockPlugins
+{
+ [KernelFunction("save_stocks"), Description("生成并且保存多个股票记录")]
+ public async Task SaveAsync(List stockModels)
+ {
+ return "成功";
+ }
+}
+
+public class StockModel
+{
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("is_on")]
+ public bool? IsOn { get; set; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs
new file mode 100644
index 00000000..a9f7eb34
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs
@@ -0,0 +1,88 @@
+using Microsoft.Extensions.Options;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+
+namespace Yi.Framework.Stock.Domain.Managers;
+
+public class SemanticKernelClient
+{
+ private Kernel Kernel { get; }
+ private readonly IKernelBuilder _kernelBuilder;
+ private SemanticKernelOptions Options { get; }
+
+ public SemanticKernelClient(IOptions semanticKernelOption)
+ {
+ Options = semanticKernelOption.Value;
+ _kernelBuilder = Kernel.CreateBuilder();
+ RegisterChatCompletion();
+ Kernel = _kernelBuilder.Build();
+ RegisterDefautlPlugins();
+ }
+
+ ///
+ /// 注册
+ ///
+ private void RegisterChatCompletion()
+ {
+ _kernelBuilder.AddOpenAIChatCompletion(
+ modelId: "",
+ apiKey: "",
+ httpClient: new HttpClient() { BaseAddress = new Uri("") });
+ }
+
+ ///
+ /// 插件注册
+ ///
+ private void RegisterDefautlPlugins()
+ {
+ //动态导入插件
+ // this.Kernel.ImportPluginFromPromptDirectory(System.IO.Path.Combine("wwwroot", "plugin","stock"),"stock");
+ }
+
+ ///
+ /// 自定义插件
+ ///
+ ///
+ ///
+ public void RegisterPlugins(string pluginName)
+ {
+ this.Kernel.Plugins.AddFromType(pluginName);
+ }
+
+
+ ///
+ /// 执行插件
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task InovkerFunctionAsync(string input, string pluginName, string functionName)
+ {
+ KernelFunction jsonFun = this.Kernel.Plugins.GetFunction(pluginName, functionName);
+ var result = await this.Kernel.InvokeAsync(function: jsonFun, new KernelArguments() { ["input"] = input });
+ return result.GetValue();
+ }
+
+ ///
+ /// 聊天对话,调用方法
+ ///
+ ///
+ public async Task> ChatCompletionAsync(string question)
+ {
+ OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
+ MaxTokens = 1000
+ };
+
+ var chatCompletionService = this.Kernel.GetRequiredService();
+
+ var results =await chatCompletionService.GetChatMessageContentsAsync(
+ question,
+ executionSettings: openAIPromptExecutionSettings,
+ kernel: Kernel);
+ return results;
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs
new file mode 100644
index 00000000..95edd997
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs
@@ -0,0 +1,6 @@
+namespace Yi.Framework.Stock.Domain.Managers;
+
+public class SemanticKernelOptions
+{
+
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs
new file mode 100644
index 00000000..dbe00433
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs
@@ -0,0 +1,323 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.EventBus.Local;
+using Yi.Framework.Bbs.Domain.Shared.Etos;
+using Yi.Framework.Stock.Domain.Entities;
+using Yi.Framework.Stock.Domain.Shared;
+using Yi.Framework.Stock.Domain.Shared.Etos;
+using Yi.Framework.SqlSugarCore.Abstractions;
+using Yi.Framework.Stock.Domain.Managers.Plugins;
+
+namespace Yi.Framework.Stock.Domain.Managers
+{
+ ///
+ /// 股市领域服务
+ ///
+ ///
+ /// 处理股票交易相关业务,例如买入、卖出等
+ ///
+ public class StockMarketManager : DomainService
+ {
+ private readonly ISqlSugarRepository _stockHoldingRepository;
+ private readonly ISqlSugarRepository _stockTransactionRepository;
+ private readonly ISqlSugarRepository _stockPriceRecordRepository;
+ private readonly ISqlSugarRepository _stockMarketRepository;
+ private readonly ILocalEventBus _localEventBus;
+ private readonly SemanticKernelClient _skClient;
+ public StockMarketManager(
+ ISqlSugarRepository stockHoldingRepository,
+ ISqlSugarRepository stockTransactionRepository,
+ ISqlSugarRepository stockPriceRecordRepository,
+ ISqlSugarRepository stockMarketRepository,
+ ILocalEventBus localEventBus, SemanticKernelClient skClient)
+ {
+ _stockHoldingRepository = stockHoldingRepository;
+ _stockTransactionRepository = stockTransactionRepository;
+ _stockPriceRecordRepository = stockPriceRecordRepository;
+ _stockMarketRepository = stockMarketRepository;
+ _localEventBus = localEventBus;
+ _skClient = skClient;
+ }
+
+ ///
+ /// 购买股票
+ ///
+ /// 用户ID
+ /// 股票ID
+ /// 购买数量
+ ///
+ public async Task BuyStockAsync(Guid userId, Guid stockId, int quantity)
+ {
+ if (quantity <= 0)
+ {
+ throw new UserFriendlyException("购买数量必须大于0");
+ }
+
+ // 通过stockId查询获取股票信息
+ var stockInfo = await _stockMarketRepository.GetFirstAsync(s => s.Id == stockId);
+ if (stockInfo == null)
+ {
+ throw new UserFriendlyException("找不到指定的股票");
+ }
+
+ string stockCode = stockInfo.MarketCode; // 根据实际字段调整
+ string stockName = stockInfo.MarketName; // 根据实际字段调整
+
+ // 获取当前股票价格
+ decimal currentPrice = await GetCurrentStockPriceAsync(stockId);
+
+ // 计算总金额和手续费
+ decimal totalAmount = currentPrice * quantity;
+ decimal fee = CalculateTradingFee(totalAmount, TransactionTypeEnum.Buy);
+ decimal totalCost = totalAmount + fee;
+
+ // 扣减用户资金
+ await _localEventBus.PublishAsync(
+ new MoneyChangeEventArgs { UserId = userId, Number = -totalCost }, false);
+
+ // 更新或创建用户持仓
+ var holding = await _stockHoldingRepository.GetFirstAsync(h =>
+ h.UserId == userId &&
+ h.StockId == stockId &&
+ !h.IsDeleted);
+
+ if (holding == null)
+ {
+ // 创建新持仓
+ holding = new StockHoldingAggregateRoot(
+ userId,
+ stockId,
+ stockCode,
+ stockName,
+ quantity,
+ currentPrice);
+
+ await _stockHoldingRepository.InsertAsync(holding);
+ }
+ else
+ {
+ // 更新现有持仓
+ holding.AddQuantity(quantity, currentPrice);
+ await _stockHoldingRepository.UpdateAsync(holding);
+ }
+
+ // 创建交易记录
+ var transaction = new StockTransactionEntity(
+ userId,
+ stockId,
+ stockCode,
+ stockName,
+ TransactionTypeEnum.Buy,
+ currentPrice,
+ quantity,
+ fee);
+
+ await _stockTransactionRepository.InsertAsync(transaction);
+
+ // 发布交易事件
+ await _localEventBus.PublishAsync(new StockTransactionEto
+ {
+ UserId = userId,
+ StockId = stockId,
+ StockCode = stockCode,
+ StockName = stockName,
+ TransactionType = TransactionTypeEnum.Buy,
+ Price = currentPrice,
+ Quantity = quantity,
+ TotalAmount = totalAmount,
+ Fee = fee
+ }, false);
+ }
+
+ ///
+ /// 卖出股票
+ ///
+ /// 用户ID
+ /// 股票ID
+ /// 卖出数量
+ ///
+ public async Task SellStockAsync(Guid userId, Guid stockId, int quantity)
+ {
+ // 验证卖出时间
+ VerifySellTime();
+
+ if (quantity <= 0)
+ {
+ throw new UserFriendlyException("卖出数量必须大于0");
+ }
+
+ // 获取用户持仓
+ var holding = await _stockHoldingRepository.GetFirstAsync(h =>
+ h.UserId == userId &&
+ h.StockId == stockId &&
+ !h.IsDeleted);
+
+ if (holding == null)
+ {
+ throw new UserFriendlyException("您没有持有该股票");
+ }
+
+ if (holding.Quantity < quantity)
+ {
+ throw new UserFriendlyException("持仓数量不足");
+ }
+
+ // 获取当前股票价格
+ decimal currentPrice = await GetCurrentStockPriceAsync(stockId);
+
+ // 计算总金额和手续费
+ decimal totalAmount = currentPrice * quantity;
+ decimal fee = CalculateTradingFee(totalAmount, TransactionTypeEnum.Sell);
+ decimal actualIncome = totalAmount - fee;
+
+ // 增加用户资金
+ await _localEventBus.PublishAsync(
+ new MoneyChangeEventArgs { UserId = userId, Number = actualIncome }, false);
+
+ // 更新用户持仓
+ holding.ReduceQuantity(quantity);
+
+ if (holding.Quantity > 0)
+ {
+ await _stockHoldingRepository.UpdateAsync(holding);
+ }
+ else
+ {
+ await _stockHoldingRepository.DeleteAsync(holding);
+ }
+
+ // 创建交易记录
+ var transaction = new StockTransactionEntity(
+ userId,
+ stockId,
+ holding.StockCode,
+ holding.StockName,
+ TransactionTypeEnum.Sell,
+ currentPrice,
+ quantity,
+ fee);
+
+ await _stockTransactionRepository.InsertAsync(transaction);
+
+ // 发布交易事件
+ await _localEventBus.PublishAsync(new StockTransactionEto
+ {
+ UserId = userId,
+ StockId = stockId,
+ StockCode = holding.StockCode,
+ StockName = holding.StockName,
+ TransactionType = TransactionTypeEnum.Sell,
+ Price = currentPrice,
+ Quantity = quantity,
+ TotalAmount = totalAmount,
+ Fee = fee
+ }, false);
+ }
+
+ ///
+ /// 获取股票当前价格
+ ///
+ /// 股票ID
+ /// 当前价格
+ public async Task GetCurrentStockPriceAsync(Guid stockId)
+ {
+ // 获取最新的价格记录
+ var latestPriceRecord = await _stockPriceRecordRepository._DbQueryable
+ .Where(p => p.StockId == stockId)
+ .OrderByDescending(p => p.CreationTime)
+ .FirstAsync();
+
+ if (latestPriceRecord == null)
+ {
+ throw new UserFriendlyException("无法获取股票价格信息");
+ }
+
+ return latestPriceRecord.CurrentPrice;
+ }
+
+ ///
+ /// 计算交易手续费
+ ///
+ /// 交易金额
+ /// 交易类型
+ /// 手续费
+ private decimal CalculateTradingFee(decimal amount, TransactionTypeEnum transactionType)
+ {
+ // 示例费率:买入0.1%,卖出0.2%
+ decimal feeRate = transactionType == TransactionTypeEnum.Buy ? 0.001m : 0.002m;
+ return amount * feeRate;
+ }
+
+ ///
+ /// 验证卖出时间
+ ///
+ /// 如果不在允许卖出的时间范围内
+ private void VerifySellTime()
+ {
+ DateTime now = DateTime.Now;
+
+ // 检查是否为工作日(周一到周五)
+ if (now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday)
+ {
+ throw new UserFriendlyException("股票只能在工作日(周一至周五)卖出");
+ }
+
+ // 检查是否在下午5点到6点之间
+ if (now.Hour < 17 || now.Hour >= 18)
+ {
+ throw new UserFriendlyException("股票只能在下午5点到6点之间卖出");
+ }
+ }
+
+
+ ///
+ /// 批量保存多个股票的最新价格记录
+ ///
+ /// 价格记录列表
+ /// 保存的记录数量
+ public async Task BatchSaveStockPriceRecordsAsync(List priceRecords)
+ {
+ if (priceRecords == null || !priceRecords.Any())
+ {
+ return 0;
+ }
+
+ // 验证数据
+ foreach (var record in priceRecords)
+ {
+ if (record.CurrentPrice <= 0)
+ {
+ throw new UserFriendlyException($"股票ID {record.StockId} 的价格必须大于0");
+ }
+
+ // 设置创建时间为当前时间(如果未设置)
+ if (record.CreationTime == default)
+ {
+ record.CreationTime = DateTime.Now;
+ }
+
+ // 计算交易额(如果未设置)
+ if (record.Turnover == 0 && record.Volume > 0)
+ {
+ record.Turnover = record.CurrentPrice * record.Volume;
+ }
+ }
+
+ await _stockPriceRecordRepository.InsertManyAsync(priceRecords);
+ return priceRecords.Count;
+ }
+
+
+ ///
+ /// 生成最新股票记录
+ ///
+ ///
+ public async Task GenerateStocksAsync()
+ {
+ _skClient.RegisterPlugins("stock");
+ await _skClient.ChatCompletionAsync("帮我生成多个股市内容");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Yi.Framework.Stock.Domain.csproj b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Yi.Framework.Stock.Domain.csproj
index e65240f4..55c6a318 100644
--- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Yi.Framework.Stock.Domain.csproj
+++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Yi.Framework.Stock.Domain.csproj
@@ -1,6 +1,7 @@
+
@@ -14,7 +15,6 @@
-
diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj b/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj
index d85ce09d..eb81b112 100644
--- a/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj
+++ b/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj
@@ -47,6 +47,12 @@
Always
+
+ Always
+
+
+ Always
+