feat: 完善对接接口

This commit is contained in:
橙子
2025-03-09 16:21:39 +08:00
parent b48f584db8
commit d605809932
9 changed files with 311 additions and 55 deletions

View File

@@ -56,6 +56,7 @@ namespace Yi.Framework.Stock.Application.Services
StockId = h.StockId,
StockCode = h.StockCode,
StockName = h.StockName,
Quantity = h.Quantity,
CreationTime = h.CreationTime
})
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);

View File

@@ -137,7 +137,7 @@ namespace Yi.Framework.Stock.Application.Services
/// <summary>
/// 卖出股票
/// </summary>
[HttpPost("stock/sell")]
[HttpDelete("stock/sell")]
[Authorize]
public async Task SellStockAsync(SellStockInputDto input)
{

View File

@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Yi.Framework.Stock.Domain.Entities;
using Yi.Framework.Stock.Domain.Shared.Etos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.Stock.Domain.EventHandlers
{
/// <summary>
/// 股票交易事件处理器
/// </summary>
public class StockTransactionEventHandler : ILocalEventHandler<StockTransactionEto>, ITransientDependency
{
private readonly ISqlSugarRepository<StockTransactionEntity> _transactionRepository;
public StockTransactionEventHandler(
ISqlSugarRepository<StockTransactionEntity> transactionRepository)
{
_transactionRepository = transactionRepository;
}
public async Task HandleEventAsync(StockTransactionEto eventData)
{
// 创建交易记录实体
var transaction = new StockTransactionEntity(
eventData.UserId,
eventData.StockId,
eventData.StockCode,
eventData.StockName,
eventData.TransactionType,
eventData.Price,
eventData.Quantity,
eventData.Fee
);
// 保存交易记录
await _transactionRepository.InsertAsync(transaction);
}
}
}

View File

@@ -10,6 +10,7 @@ using Yi.Framework.Stock.Domain.Shared.Etos;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.Stock.Domain.Managers.SemanticKernel;
using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins;
using Microsoft.Extensions.Hosting;
namespace Yi.Framework.Stock.Domain.Managers
{
@@ -27,12 +28,16 @@ namespace Yi.Framework.Stock.Domain.Managers
private readonly ISqlSugarRepository<StockMarketAggregateRoot> _stockMarketRepository;
private readonly ILocalEventBus _localEventBus;
private readonly SemanticKernelClient _skClient;
private readonly IHostEnvironment _hostEnvironment;
public StockMarketManager(
ISqlSugarRepository<StockHoldingAggregateRoot> stockHoldingRepository,
ISqlSugarRepository<StockTransactionEntity> stockTransactionRepository,
ISqlSugarRepository<StockPriceRecordEntity> stockPriceRecordRepository,
ISqlSugarRepository<StockMarketAggregateRoot> stockMarketRepository,
ILocalEventBus localEventBus, SemanticKernelClient skClient)
ILocalEventBus localEventBus,
SemanticKernelClient skClient,
IHostEnvironment hostEnvironment)
{
_stockHoldingRepository = stockHoldingRepository;
_stockTransactionRepository = stockTransactionRepository;
@@ -40,6 +45,7 @@ namespace Yi.Framework.Stock.Domain.Managers
_stockMarketRepository = stockMarketRepository;
_localEventBus = localEventBus;
_skClient = skClient;
_hostEnvironment = hostEnvironment;
}
/// <summary>
@@ -103,20 +109,6 @@ namespace Yi.Framework.Stock.Domain.Managers
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
{
@@ -189,19 +181,6 @@ namespace Yi.Framework.Stock.Domain.Managers
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
{
@@ -246,8 +225,8 @@ namespace Yi.Framework.Stock.Domain.Managers
/// <returns>手续费</returns>
private decimal CalculateTradingFee(decimal amount, TransactionTypeEnum transactionType)
{
// 示例费率买入0.1%,卖出0.2%
decimal feeRate = transactionType == TransactionTypeEnum.Buy ? 0.001m : 0.002m;
// 买入不收手续费,卖出2%
decimal feeRate = transactionType == TransactionTypeEnum.Buy ? 0m : 0.02m;
return amount * feeRate;
}
@@ -257,6 +236,12 @@ namespace Yi.Framework.Stock.Domain.Managers
/// <exception cref="UserFriendlyException">如果不在允许卖出的时间范围内</exception>
private void VerifySellTime()
{
// 如果是开发环境,跳过验证
if (_hostEnvironment.IsDevelopment())
{
return;
}
DateTime now = DateTime.Now;
// 检查是否为工作日(周一到周五)

View File

@@ -46,4 +46,22 @@ export function getStockMarkets() {
url: "/stock/markets",
method: "get"
});
}
// 买入股票
export function buyStock(data) {
return request({
url: "/stock/buy",
method: "post",
data
});
}
// 卖出股票
export function sellStock(params) {
return request({
url: "/stock/sell",
method: "delete",
params
});
}

View File

@@ -75,9 +75,9 @@ export function getUserProfile() {
}
// 查询bbs个人信息
export function getBbsUserProfile(userName) {
export function getBbsUserProfile(userNameOrId) {
return request({
url: `/bbs-user/${userName}`,
url: `/bbs-user/${userNameOrId}`,
method: "get",
});
}

View File

@@ -144,14 +144,6 @@ const router = createRouter({
title: "面试宝典",
},
},
{
name: "stock",
path: "/stock",
component: () => import("../views/stock/Index.vue"),
meta: {
title: "股票",
},
},
],
},
{
@@ -203,7 +195,14 @@ const router = createRouter({
},
],
},
{
name: "stock",
path: "/stock",
component: () => import("../views/stock/Index.vue"),
meta: {
title: "股票",
},
},
{
path: "/hub",
name: "hub",

View File

@@ -7,6 +7,8 @@
</div>
<div class="selector-area">
<div class="user-money">当前钱钱: <span class="money-value">{{ userInfo.money || 0 }}</span></div>
<el-select
v-model="currentStock"
placeholder="选择股票"
@@ -24,6 +26,15 @@
<span class="stock-code">{{ item.code }}</span>
</el-option>
</el-select>
<el-button
type="primary"
size="small"
class="return-button"
@click="returnToHome"
>
<i class="el-icon-back"></i> 返回社区
</el-button>
</div>
</div>
@@ -44,6 +55,7 @@
v-for="news in newsList"
:key="news.id"
class="news-item"
@click="showNewsDetail(news)"
>
<div class="news-date">
{{ dayjs(news.publishTime).format('YYYY-MM-DD') }}
@@ -144,7 +156,7 @@
<div class="section-header">
<span class="icon"><i class="el-icon-wallet"></i></span>
<h3>我的持仓</h3>
<span class="total-value">总资产: ¥{{ totalAssets }}</span>
<span class="total-value">持有钱钱: ¥{{ totalAssets }}</span>
</div>
<div v-if="isLoadingPortfolio" class="loading-portfolio">
@@ -170,14 +182,52 @@
</el-table>
</template>
</div>
<!-- 添加新闻详情弹窗 -->
<el-dialog
v-model="newsDialogVisible"
:title="currentNewsDetail.title"
width="50%"
class="news-detail-dialog"
>
<div class="news-detail-header">
<span class="news-detail-time">{{ dayjs(currentNewsDetail.publishTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
<span class="news-detail-source" v-if="currentNewsDetail.source">来源: {{ currentNewsDetail.source }}</span>
</div>
<div class="news-detail-content">{{ currentNewsDetail.content }}</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue';
import StockChart from './components/StockChart.vue';
import { getStockNews, getUserHoldings, getUserTransactions, getStockPriceRecords, getStockMarkets } from '@/apis/stockApi.js';
import { getStockNews, getUserHoldings, getUserTransactions, getStockPriceRecords, getStockMarkets, buyStock as buyStockApi, sellStock as sellStockApi } from '@/apis/stockApi.js';
import { getBbsUserProfile } from '@/apis/userApi.js';
import { dayjs } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useRouter } from 'vue-router';
import useUserStore from "@/stores/user";
// 获取路由器实例
const router = useRouter();
const userStore = useUserStore();
const userInfo = ref({});
// 返回首页函数
const returnToHome = () => {
router.push('/index');
};
// 获取用户信息,包括钱钱
const loadUserInfo = async () => {
try {
const { data } = await getBbsUserProfile(userStore.id);
userInfo.value = data;
} catch (error) {
console.error('获取用户信息失败:', error);
}
};
// 股票列表数据
const stockList = ref([]);
@@ -221,7 +271,6 @@ const fetchStockMarkets = async () => {
// 加载默认选中股票的数据
await fetchStockPriceRecords();
await fetchTradeHistory();
}
} catch (error) {
console.error('获取股市列表失败:', error);
@@ -385,7 +434,7 @@ const totalAssets = computed(() => {
const stockValue = portfolioList.value.reduce((sum, stock) => {
return sum + parseFloat(stock.currentPrice) * stock.amount;
}, 0);
return (stockValue + 50000).toFixed(2); // 假设有50000现金
return (stockValue + (userInfo.value.money || 0)).toFixed(2); // 使用API获取的钱钱而不是固定值
});
// 切换股票
@@ -408,20 +457,98 @@ const changeStock = async (stockId) => {
};
// 买入股票
const buyStock = () => {
// 实现买入逻辑
alert(`买入${tradeAmount.value}${currentStockInfo.value.name}`);
const buyStock = async () => {
if (!currentStock.value) {
ElMessage.warning('请先选择股票');
return;
}
try {
await ElMessageBox.confirm(
`确定要买入 ${tradeAmount.value}${currentStockInfo.value.name} 吗?`,
'买入确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await buyStockApi({
stockId: currentStock.value,
quantity: tradeAmount.value
});
ElMessage.success(`已成功买入 ${tradeAmount.value}${currentStockInfo.value.name}`);
// 刷新数据
await Promise.all([
fetchTradeHistory(),
fetchPortfolioList(),
loadUserInfo() // 买入后更新用户钱钱
]);
} catch (error) {
if (error !== 'cancel') {
console.error('买入股票失败:', error);
}
}
};
// 卖出股票
const sellStock = () => {
// 实现卖出逻辑
alert(`卖出${tradeAmount.value}${currentStockInfo.value.name}`);
const sellStock = async () => {
if (!currentStock.value) {
ElMessage.warning('请先选择股票');
return;
}
try {
await ElMessageBox.confirm(
`确定要卖出 ${tradeAmount.value}${currentStockInfo.value.name} 吗?`,
'卖出确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await sellStockApi({
StockId: currentStock.value,
Quantity: tradeAmount.value
});
ElMessage.success(`已成功卖出 ${tradeAmount.value}${currentStockInfo.value.name}`);
// 刷新数据
await Promise.all([
fetchTradeHistory(),
fetchPortfolioList(),
loadUserInfo() // 卖出后更新用户钱钱
]);
} catch (error) {
if (error !== 'cancel') {
console.error('卖出股票失败:', error);
}
}
};
// 新闻详情弹窗
const newsDialogVisible = ref(false);
const currentNewsDetail = ref({
title: '',
content: '',
publishTime: '',
source: ''
});
// 显示新闻详情
const showNewsDetail = (news) => {
currentNewsDetail.value = news;
newsDialogVisible.value = true;
};
// 生命周期钩子
onMounted(async () => {
// 先获取股市列表
await fetchStockMarkets();
@@ -431,6 +558,9 @@ onMounted(async () => {
fetchPortfolioList(),
fetchTradeHistory()
]);
// 加载用户信息
await loadUserInfo();
});
</script>
@@ -469,8 +599,9 @@ onMounted(async () => {
}
.selector-area {
flex: 0 0 150px;
text-align: right;
display: flex;
align-items: center;
gap: 10px;
}
.title {
@@ -803,4 +934,84 @@ onMounted(async () => {
margin-top: 5px;
font-size: 12px;
}
/* 新闻详情弹窗样式 */
.news-detail-dialog :deep(.el-dialog__header) {
padding: 15px 20px;
border-bottom: 1px solid #30363d;
}
.news-detail-dialog :deep(.el-dialog__title) {
font-size: 18px;
font-weight: bold;
color: #ffffff !important;
text-shadow: 0 0 3px rgba(255, 255, 255, 0.3);
}
.news-detail-dialog :deep(.el-dialog__body) {
padding: 20px;
color: #e6edf3;
}
.news-detail-header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #30363d;
color: #8b949e;
font-size: 0.9em;
}
.news-detail-content {
line-height: 1.6;
white-space: pre-line;
}
/* 修改Element弹窗适应深色主题 */
.stock-dashboard :deep(.el-dialog) {
background-color: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.stock-dashboard :deep(.el-dialog__headerbtn .el-dialog__close) {
color: #8b949e;
}
.stock-dashboard :deep(.el-dialog__headerbtn:hover .el-dialog__close) {
color: #58a6ff;
}
.return-button {
margin-left: 10px;
background-color: #58a6ff;
border-color: #58a6ff;
transition: all 0.3s;
}
.return-button:hover {
background-color: #388bfd;
border-color: #388bfd;
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(88, 166, 255, 0.4);
}
.user-money {
color: #e6edf3;
font-size: 14px;
margin-right: 15px;
padding: 4px 8px;
background-color: #21262d;
border-radius: 4px;
border: 1px solid #30363d;
}
.money-value {
font-weight: bold;
color: #7ee787;
margin-left: 4px;
}
</style>