feat: 添加用户授权支持并重构组合服务
重构组合服务接口和实现,添加用户ID参数以实现多租户隔离 更新组合控制器,从JWT令牌中提取用户ID并验证权限 完善组合服务数据库操作,包括组合创建、查询和交易处理 更新README文档,补充组合API详细说明
This commit is contained in:
parent
2d1fbd37d8
commit
9741468f3e
@ -4,6 +4,7 @@ using AssetManager.Services;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace AssetManager.API.Controllers;
|
namespace AssetManager.API.Controllers;
|
||||||
|
|
||||||
@ -21,22 +22,30 @@ public class PortfolioController : ControllerBase
|
|||||||
_portfolioService = portfolioService;
|
_portfolioService = portfolioService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private string GetCurrentUserId()
|
||||||
/// 创建新投资组合
|
{
|
||||||
/// </summary>
|
return User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
/// <param name="request">投资组合创建请求参数</param>
|
}
|
||||||
/// <returns>创建的投资组合详情</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 此接口用于创建新的投资组合。
|
|
||||||
/// </remarks>
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public ActionResult<ApiResponse<CreatePortfolioResponse>> CreatePortfolio([FromBody] CreatePortfolioRequest request)
|
public ActionResult<ApiResponse<CreatePortfolioResponse>> CreatePortfolio([FromBody] CreatePortfolioRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized(new ApiResponse<CreatePortfolioResponse>
|
||||||
|
{
|
||||||
|
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||||
|
data = null,
|
||||||
|
message = "用户未授权"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Request to create portfolio");
|
_logger.LogInformation("Request to create portfolio");
|
||||||
|
|
||||||
var response = _portfolioService.CreatePortfolio(request);
|
var response = _portfolioService.CreatePortfolio(request, userId);
|
||||||
|
|
||||||
_logger.LogInformation("Portfolio created successfully");
|
_logger.LogInformation("Portfolio created successfully");
|
||||||
|
|
||||||
@ -60,28 +69,32 @@ public class PortfolioController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取投资组合列表
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>投资组合列表</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 此接口用于获取投资组合列表。
|
|
||||||
/// </remarks>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult<ApiResponse<GetPortfoliosResponse>> GetPortfolios()
|
public ActionResult<ApiResponse<GetPortfoliosResponse>> GetPortfolios()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized(new ApiResponse<GetPortfoliosResponse>
|
||||||
|
{
|
||||||
|
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||||
|
data = null,
|
||||||
|
message = "用户未授权"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Request to get portfolios");
|
_logger.LogInformation("Request to get portfolios");
|
||||||
|
|
||||||
var response = _portfolioService.GetPortfolios();
|
var response = _portfolioService.GetPortfolios(userId);
|
||||||
|
|
||||||
_logger.LogInformation("Portfolios retrieved successfully");
|
_logger.LogInformation("Portfolios retrieved successfully");
|
||||||
|
|
||||||
return Ok(new ApiResponse<GetPortfoliosResponse>
|
return Ok(new ApiResponse<GetPortfoliosResponse>
|
||||||
{
|
{
|
||||||
code = AssetManager.Models.StatusCodes.Success,
|
code = AssetManager.Models.StatusCodes.Success,
|
||||||
data = response,
|
data = new GetPortfoliosResponse { items = response },
|
||||||
message = "success"
|
message = "success"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -98,21 +111,25 @@ public class PortfolioController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取总资产情况
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>总资产情况</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 此接口用于获取用户的总资产情况。
|
|
||||||
/// </remarks>
|
|
||||||
[HttpGet("assets")]
|
[HttpGet("assets")]
|
||||||
public ActionResult<ApiResponse<TotalAssetsResponse>> GetTotalAssets()
|
public ActionResult<ApiResponse<TotalAssetsResponse>> GetTotalAssets()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized(new ApiResponse<TotalAssetsResponse>
|
||||||
|
{
|
||||||
|
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||||
|
data = null,
|
||||||
|
message = "用户未授权"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Request to get total assets");
|
_logger.LogInformation("Request to get total assets");
|
||||||
|
|
||||||
var response = _portfolioService.GetTotalAssets();
|
var response = _portfolioService.GetTotalAssets(userId);
|
||||||
|
|
||||||
_logger.LogInformation("Total assets retrieved successfully");
|
_logger.LogInformation("Total assets retrieved successfully");
|
||||||
|
|
||||||
@ -136,22 +153,25 @@ public class PortfolioController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取单个投资组合详情
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">投资组合ID</param>
|
|
||||||
/// <returns>投资组合详情</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 此接口用于获取指定投资组合的详细信息。
|
|
||||||
/// </remarks>
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public ActionResult<ApiResponse<PortfolioDetailResponse>> GetPortfolioById(string id)
|
public ActionResult<ApiResponse<PortfolioDetailResponse>> GetPortfolioById(string id)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized(new ApiResponse<PortfolioDetailResponse>
|
||||||
|
{
|
||||||
|
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||||
|
data = null,
|
||||||
|
message = "用户未授权"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"Request to get portfolio by id: {id}");
|
_logger.LogInformation($"Request to get portfolio by id: {id}");
|
||||||
|
|
||||||
var response = _portfolioService.GetPortfolioById(id);
|
var response = _portfolioService.GetPortfolioById(id, userId);
|
||||||
|
|
||||||
_logger.LogInformation("Portfolio retrieved successfully");
|
_logger.LogInformation("Portfolio retrieved successfully");
|
||||||
|
|
||||||
@ -175,24 +195,25 @@ public class PortfolioController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取交易记录
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="portfolioId">投资组合ID</param>
|
|
||||||
/// <param name="limit">每页记录数</param>
|
|
||||||
/// <param name="offset">偏移量</param>
|
|
||||||
/// <returns>交易记录列表</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 此接口用于获取指定投资组合的交易记录。
|
|
||||||
/// </remarks>
|
|
||||||
[HttpGet("transactions")]
|
[HttpGet("transactions")]
|
||||||
public ActionResult<ApiResponse<GetTransactionsResponse>> GetTransactions([FromQuery] string portfolioId, [FromQuery] int limit = 10, [FromQuery] int offset = 0)
|
public ActionResult<ApiResponse<GetTransactionsResponse>> GetTransactions([FromQuery] string portfolioId, [FromQuery] int limit = 10, [FromQuery] int offset = 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized(new ApiResponse<GetTransactionsResponse>
|
||||||
|
{
|
||||||
|
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||||
|
data = null,
|
||||||
|
message = "用户未授权"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"Request to get transactions for portfolio: {portfolioId}");
|
_logger.LogInformation($"Request to get transactions for portfolio: {portfolioId}");
|
||||||
|
|
||||||
var response = _portfolioService.GetTransactions(portfolioId, limit, offset);
|
var response = _portfolioService.GetTransactions(portfolioId, userId, limit, offset);
|
||||||
|
|
||||||
_logger.LogInformation("Transactions retrieved successfully");
|
_logger.LogInformation("Transactions retrieved successfully");
|
||||||
|
|
||||||
@ -216,22 +237,25 @@ public class PortfolioController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建新交易
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">交易创建请求参数</param>
|
|
||||||
/// <returns>创建的交易详情</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 此接口用于创建新的交易。
|
|
||||||
/// </remarks>
|
|
||||||
[HttpPost("transactions")]
|
[HttpPost("transactions")]
|
||||||
public ActionResult<ApiResponse<CreateTransactionResponse>> CreateTransaction([FromBody] CreateTransactionRequest request)
|
public ActionResult<ApiResponse<CreateTransactionResponse>> CreateTransaction([FromBody] CreateTransactionRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return Unauthorized(new ApiResponse<CreateTransactionResponse>
|
||||||
|
{
|
||||||
|
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||||
|
data = null,
|
||||||
|
message = "用户未授权"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Request to create transaction");
|
_logger.LogInformation("Request to create transaction");
|
||||||
|
|
||||||
var response = _portfolioService.CreateTransaction(request);
|
var response = _portfolioService.CreateTransaction(request, userId);
|
||||||
|
|
||||||
_logger.LogInformation("Transaction created successfully");
|
_logger.LogInformation("Transaction created successfully");
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,10 @@ namespace AssetManager.Services;
|
|||||||
|
|
||||||
public interface IPortfolioService
|
public interface IPortfolioService
|
||||||
{
|
{
|
||||||
CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request);
|
CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId);
|
||||||
PortfolioDetailResponse GetPortfolioById(string id);
|
List<PortfolioListItem> GetPortfolios(string userId);
|
||||||
GetTransactionsResponse GetTransactions(string portfolioId, int limit, int offset);
|
TotalAssetsResponse GetTotalAssets(string userId);
|
||||||
CreateTransactionResponse CreateTransaction(CreateTransactionRequest request);
|
PortfolioDetailResponse GetPortfolioById(string id, string userId);
|
||||||
GetPortfoliosResponse GetPortfolios();
|
GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset);
|
||||||
TotalAssetsResponse GetTotalAssets();
|
CreateTransactionResponse CreateTransaction(CreateTransactionRequest request, string userId);
|
||||||
}
|
}
|
||||||
13
AssetManager.Services/IStrategyService.cs
Normal file
13
AssetManager.Services/IStrategyService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using AssetManager.Data;
|
||||||
|
using AssetManager.Models.DTOs;
|
||||||
|
|
||||||
|
namespace AssetManager.Services;
|
||||||
|
|
||||||
|
public interface IStrategyService
|
||||||
|
{
|
||||||
|
Strategy CreateStrategy(CreateStrategyRequest request, string userId);
|
||||||
|
List<Strategy> GetStrategies(string userId);
|
||||||
|
Strategy GetStrategyById(string id, string userId);
|
||||||
|
Strategy UpdateStrategy(string id, UpdateStrategyRequest request, string userId);
|
||||||
|
bool DeleteStrategy(string id, string userId);
|
||||||
|
}
|
||||||
@ -1,180 +1,330 @@
|
|||||||
|
using AssetManager.Data;
|
||||||
using AssetManager.Models.DTOs;
|
using AssetManager.Models.DTOs;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
namespace AssetManager.Services;
|
namespace AssetManager.Services;
|
||||||
|
|
||||||
public class PortfolioService : IPortfolioService
|
public class PortfolioService : IPortfolioService
|
||||||
{
|
{
|
||||||
public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request)
|
private readonly ISqlSugarClient _db;
|
||||||
|
|
||||||
|
public PortfolioService(ISqlSugarClient db)
|
||||||
{
|
{
|
||||||
// 模拟创建投资组合
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId)
|
||||||
|
{
|
||||||
|
var portfolio = new Portfolio
|
||||||
|
{
|
||||||
|
Id = "port-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||||||
|
UserId = userId,
|
||||||
|
StrategyId = request.strategyId,
|
||||||
|
Name = request.name,
|
||||||
|
Currency = request.currency,
|
||||||
|
TotalValue = (decimal)request.stocks.Sum(s => s.price * s.amount),
|
||||||
|
ReturnRate = 0,
|
||||||
|
Status = "运行中",
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Insertable(portfolio).ExecuteCommand();
|
||||||
|
|
||||||
|
// 创建初始持仓
|
||||||
|
foreach (var stock in request.stocks)
|
||||||
|
{
|
||||||
|
var position = new Position
|
||||||
|
{
|
||||||
|
Id = "pos-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||||||
|
PortfolioId = portfolio.Id,
|
||||||
|
StockCode = stock.code,
|
||||||
|
StockName = stock.name,
|
||||||
|
AssetType = "Stock",
|
||||||
|
Shares = (decimal)stock.amount,
|
||||||
|
AvgPrice = (decimal)stock.price,
|
||||||
|
Currency = request.currency,
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Insertable(position).ExecuteCommand();
|
||||||
|
|
||||||
|
// 创建交易记录
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Id = "trans-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||||||
|
PortfolioId = portfolio.Id,
|
||||||
|
Type = "buy",
|
||||||
|
StockCode = stock.code,
|
||||||
|
AssetType = "Stock",
|
||||||
|
Title = "初始建仓",
|
||||||
|
Amount = (decimal)stock.amount,
|
||||||
|
Price = (decimal)stock.price,
|
||||||
|
TotalAmount = (decimal)(stock.price * stock.amount),
|
||||||
|
Currency = request.currency,
|
||||||
|
Status = "completed",
|
||||||
|
Remark = "初始建仓",
|
||||||
|
TransactionTime = DateTime.Now,
|
||||||
|
CreatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Insertable(transaction).ExecuteCommand();
|
||||||
|
}
|
||||||
|
|
||||||
return new CreatePortfolioResponse
|
return new CreatePortfolioResponse
|
||||||
{
|
{
|
||||||
id = "port-" + Guid.NewGuid().ToString().Substring(0, 8),
|
id = portfolio.Id,
|
||||||
totalValue = request.stocks.Sum(s => s.price * s.amount),
|
totalValue = (double)portfolio.TotalValue,
|
||||||
returnRate = 0,
|
returnRate = 0,
|
||||||
currency = request.currency,
|
currency = portfolio.Currency,
|
||||||
createdAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
createdAt = portfolio.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public PortfolioDetailResponse GetPortfolioById(string id)
|
public List<PortfolioListItem> GetPortfolios(string userId)
|
||||||
{
|
{
|
||||||
// 模拟获取投资组合详情
|
var portfolios = _db.Queryable<Portfolio>()
|
||||||
|
.Where(p => p.UserId == userId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return portfolios.Select(p => new PortfolioListItem
|
||||||
|
{
|
||||||
|
id = p.Id,
|
||||||
|
name = p.Name,
|
||||||
|
tags = $"{p.Status} · {p.Currency}",
|
||||||
|
status = p.Status,
|
||||||
|
statusType = p.Status == "运行中" ? "green" : "gray",
|
||||||
|
iconChar = p.Name.Substring(0, 1).ToUpper(),
|
||||||
|
iconBgClass = "bg-blue-100",
|
||||||
|
iconTextClass = "text-blue-700",
|
||||||
|
value = (double)p.TotalValue,
|
||||||
|
currency = p.Currency,
|
||||||
|
returnRate = (double)p.ReturnRate,
|
||||||
|
returnType = p.ReturnRate >= 0 ? "positive" : "negative"
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TotalAssetsResponse GetTotalAssets(string userId)
|
||||||
|
{
|
||||||
|
var totalValue = _db.Queryable<Portfolio>()
|
||||||
|
.Where(p => p.UserId == userId)
|
||||||
|
.Sum(p => p.TotalValue);
|
||||||
|
|
||||||
|
// 这里简化处理,实际应该计算今日盈亏和总收益率
|
||||||
|
return new TotalAssetsResponse
|
||||||
|
{
|
||||||
|
totalValue = (double)totalValue,
|
||||||
|
currency = "CNY", // 假设统一转换为CNY
|
||||||
|
todayProfit = 0,
|
||||||
|
todayProfitCurrency = "CNY",
|
||||||
|
totalReturnRate = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortfolioDetailResponse GetPortfolioById(string id, string userId)
|
||||||
|
{
|
||||||
|
var portfolio = _db.Queryable<Portfolio>()
|
||||||
|
.Where(p => p.Id == id && p.UserId == userId)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
if (portfolio == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Portfolio not found or access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
var positions = _db.Queryable<Position>()
|
||||||
|
.Where(pos => pos.PortfolioId == id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var transactions = _db.Queryable<Transaction>()
|
||||||
|
.Where(t => t.PortfolioId == id)
|
||||||
|
.OrderByDescending(t => t.TransactionTime)
|
||||||
|
.Take(5)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return new PortfolioDetailResponse
|
return new PortfolioDetailResponse
|
||||||
{
|
{
|
||||||
id = id,
|
id = portfolio.Id,
|
||||||
name = "我的投资组合",
|
name = portfolio.Name,
|
||||||
currency = "CNY",
|
currency = portfolio.Currency,
|
||||||
strategy = new StrategyInfo
|
strategy = new StrategyInfo
|
||||||
{
|
{
|
||||||
id = "hfea",
|
id = portfolio.StrategyId,
|
||||||
name = "HFEA 风险平价策略",
|
name = "策略名称", // 实际应该从策略表获取
|
||||||
description = "高风险高收益策略"
|
description = "策略描述"
|
||||||
},
|
},
|
||||||
portfolioValue = 125000,
|
portfolioValue = (double)portfolio.TotalValue,
|
||||||
totalReturn = 12500,
|
totalReturn = (double)(portfolio.TotalValue * portfolio.ReturnRate),
|
||||||
todayProfit = 2500,
|
todayProfit = 0,
|
||||||
todayProfitCurrency = "CNY",
|
todayProfitCurrency = portfolio.Currency,
|
||||||
positions = new List<PositionItem>
|
positions = positions.Select(pos => new PositionItem
|
||||||
{
|
{
|
||||||
new PositionItem
|
id = pos.Id,
|
||||||
{
|
stockCode = pos.StockCode,
|
||||||
id = "pos-001",
|
stockName = pos.StockName,
|
||||||
stockCode = "UPRO",
|
amount = (int)pos.Shares,
|
||||||
stockName = "标普500三倍杠杆ETF",
|
averagePrice = (double)pos.AvgPrice,
|
||||||
amount = 100,
|
currentPrice = (double)pos.AvgPrice, // 实际应该从市场数据获取
|
||||||
averagePrice = 50,
|
totalValue = (double)(pos.Shares * pos.AvgPrice),
|
||||||
currentPrice = 55,
|
profit = 0,
|
||||||
totalValue = 5500,
|
profitRate = 0,
|
||||||
profit = 500,
|
currency = pos.Currency
|
||||||
profitRate = 10,
|
}).ToList(),
|
||||||
currency = "USD"
|
transactions = transactions.Select(t => new TransactionItem
|
||||||
}
|
|
||||||
},
|
|
||||||
transactions = new List<TransactionItem>
|
|
||||||
{
|
{
|
||||||
new TransactionItem
|
id = t.Id,
|
||||||
{
|
portfolioId = t.PortfolioId,
|
||||||
id = "trans-001",
|
date = t.TransactionTime.ToString("yyyy-MM-dd"),
|
||||||
portfolioId = id,
|
time = t.TransactionTime.ToString("HH:mm:ss"),
|
||||||
date = "2026-02-24",
|
type = t.Type,
|
||||||
time = "14:30:00",
|
title = t.Title,
|
||||||
type = "buy",
|
amount = (double)t.TotalAmount,
|
||||||
title = "购买 UPRO",
|
currency = t.Currency,
|
||||||
amount = 5000,
|
status = t.Status,
|
||||||
currency = "USD",
|
remark = t.Remark
|
||||||
status = "completed",
|
}).ToList()
|
||||||
remark = "初始建仓"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetTransactionsResponse GetTransactions(string portfolioId, int limit, int offset)
|
public GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset)
|
||||||
{
|
{
|
||||||
// 模拟获取交易记录
|
// 验证投资组合是否属于该用户
|
||||||
|
var portfolio = _db.Queryable<Portfolio>()
|
||||||
|
.Where(p => p.Id == portfolioId && p.UserId == userId)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
if (portfolio == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Portfolio not found or access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
var transactions = _db.Queryable<Transaction>()
|
||||||
|
.Where(t => t.PortfolioId == portfolioId)
|
||||||
|
.OrderByDescending(t => t.TransactionTime)
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(limit)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var total = _db.Queryable<Transaction>()
|
||||||
|
.Where(t => t.PortfolioId == portfolioId)
|
||||||
|
.Count();
|
||||||
|
|
||||||
return new GetTransactionsResponse
|
return new GetTransactionsResponse
|
||||||
{
|
{
|
||||||
items = new List<TransactionItem>
|
items = transactions.Select(t => new TransactionItem
|
||||||
{
|
{
|
||||||
new TransactionItem
|
id = t.Id,
|
||||||
{
|
portfolioId = t.PortfolioId,
|
||||||
id = "trans-001",
|
date = t.TransactionTime.ToString("yyyy-MM-dd"),
|
||||||
portfolioId = portfolioId,
|
time = t.TransactionTime.ToString("HH:mm:ss"),
|
||||||
date = "2026-02-24",
|
type = t.Type,
|
||||||
time = "14:30:00",
|
title = t.Title,
|
||||||
type = "buy",
|
amount = (double)t.TotalAmount,
|
||||||
title = "购买 UPRO",
|
currency = t.Currency,
|
||||||
amount = 5000,
|
status = t.Status,
|
||||||
currency = "USD",
|
remark = t.Remark
|
||||||
status = "completed",
|
}).ToList(),
|
||||||
remark = "初始建仓"
|
total = total,
|
||||||
}
|
|
||||||
},
|
|
||||||
total = 1,
|
|
||||||
page = offset / limit + 1,
|
page = offset / limit + 1,
|
||||||
pageSize = limit
|
pageSize = limit
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateTransactionResponse CreateTransaction(CreateTransactionRequest request)
|
public CreateTransactionResponse CreateTransaction(CreateTransactionRequest request, string userId)
|
||||||
{
|
{
|
||||||
// 模拟执行交易
|
// 验证投资组合是否属于该用户
|
||||||
return new CreateTransactionResponse
|
var portfolio = _db.Queryable<Portfolio>()
|
||||||
{
|
.Where(p => p.Id == request.portfolioId && p.UserId == userId)
|
||||||
id = "trans-" + Guid.NewGuid().ToString().Substring(0, 8),
|
.First();
|
||||||
totalAmount = request.price * request.amount,
|
|
||||||
status = "processing",
|
|
||||||
createdAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetPortfoliosResponse GetPortfolios()
|
if (portfolio == null)
|
||||||
{
|
|
||||||
// 模拟获取投资组合列表
|
|
||||||
return new GetPortfoliosResponse
|
|
||||||
{
|
{
|
||||||
items = new List<PortfolioListItem>
|
throw new Exception("Portfolio not found or access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Id = "trans-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||||||
|
PortfolioId = request.portfolioId,
|
||||||
|
Type = request.type,
|
||||||
|
StockCode = request.stockCode,
|
||||||
|
AssetType = "Stock",
|
||||||
|
Title = request.remark ?? "交易",
|
||||||
|
Amount = (decimal)request.amount,
|
||||||
|
Price = (decimal)request.price,
|
||||||
|
TotalAmount = (decimal)(request.price * request.amount),
|
||||||
|
Currency = request.currency,
|
||||||
|
Status = "processing",
|
||||||
|
Remark = request.remark,
|
||||||
|
TransactionTime = DateTime.Now,
|
||||||
|
CreatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Insertable(transaction).ExecuteCommand();
|
||||||
|
|
||||||
|
// 更新持仓
|
||||||
|
var position = _db.Queryable<Position>()
|
||||||
|
.Where(pos => pos.PortfolioId == request.portfolioId && pos.StockCode == request.stockCode)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
if (position != null)
|
||||||
|
{
|
||||||
|
if (request.type == "buy")
|
||||||
{
|
{
|
||||||
new PortfolioListItem
|
// 计算新的平均价格
|
||||||
|
var newTotalShares = position.Shares + (decimal)request.amount;
|
||||||
|
var newTotalCost = (position.Shares * position.AvgPrice) + ((decimal)request.amount * (decimal)request.price);
|
||||||
|
position.AvgPrice = newTotalCost / newTotalShares;
|
||||||
|
position.Shares = newTotalShares;
|
||||||
|
}
|
||||||
|
else if (request.type == "sell")
|
||||||
|
{
|
||||||
|
position.Shares -= (decimal)request.amount;
|
||||||
|
if (position.Shares <= 0)
|
||||||
{
|
{
|
||||||
id = "hfea-001",
|
_db.Deleteable(position).ExecuteCommand();
|
||||||
name = "美股全天候杠杆",
|
}
|
||||||
tags = "HFEA · 季度调仓",
|
else
|
||||||
status = "监控中",
|
|
||||||
statusType = "green",
|
|
||||||
iconChar = "H",
|
|
||||||
iconBgClass = "bg-green-100",
|
|
||||||
iconTextClass = "text-green-700",
|
|
||||||
value = 156240,
|
|
||||||
currency = "USD",
|
|
||||||
returnRate = 42.82,
|
|
||||||
returnType = "positive"
|
|
||||||
},
|
|
||||||
new PortfolioListItem
|
|
||||||
{
|
{
|
||||||
id = "ma-002",
|
_db.Updateable(position).ExecuteCommand();
|
||||||
name = "纳指双均线趋势",
|
|
||||||
tags = "趋势跟踪 · 日线",
|
|
||||||
status = "等待信号",
|
|
||||||
statusType = "gray",
|
|
||||||
iconChar = "T",
|
|
||||||
iconBgClass = "bg-blue-100",
|
|
||||||
iconTextClass = "text-blue-700",
|
|
||||||
value = 412500,
|
|
||||||
currency = "USD",
|
|
||||||
returnRate = -1.79,
|
|
||||||
returnType = "negative"
|
|
||||||
},
|
|
||||||
new PortfolioListItem
|
|
||||||
{
|
|
||||||
id = "hk-003",
|
|
||||||
name = "港股价值投资",
|
|
||||||
tags = "价值投资 · 蓝筹",
|
|
||||||
status = "持有中",
|
|
||||||
statusType = "green",
|
|
||||||
iconChar = "H",
|
|
||||||
iconBgClass = "bg-green-100",
|
|
||||||
iconTextClass = "text-green-700",
|
|
||||||
value = 896000,
|
|
||||||
currency = "HKD",
|
|
||||||
returnRate = 12.56,
|
|
||||||
returnType = "positive"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
else if (request.type == "buy")
|
||||||
|
|
||||||
public TotalAssetsResponse GetTotalAssets()
|
|
||||||
{
|
|
||||||
// 模拟获取总资产情况
|
|
||||||
return new TotalAssetsResponse
|
|
||||||
{
|
{
|
||||||
totalValue = 1284592.4,
|
// 创建新持仓
|
||||||
currency = "CNY",
|
position = new Position
|
||||||
todayProfit = 12482,
|
{
|
||||||
todayProfitCurrency = "CNY",
|
Id = "pos-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||||||
totalReturnRate = 24.82
|
PortfolioId = request.portfolioId,
|
||||||
|
StockCode = request.stockCode,
|
||||||
|
StockName = request.remark ?? request.stockCode,
|
||||||
|
AssetType = "Stock",
|
||||||
|
Shares = (decimal)request.amount,
|
||||||
|
AvgPrice = (decimal)request.price,
|
||||||
|
Currency = request.currency,
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
_db.Insertable(position).ExecuteCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新投资组合总价值
|
||||||
|
var totalValue = _db.Queryable<Position>()
|
||||||
|
.Where(pos => pos.PortfolioId == request.portfolioId)
|
||||||
|
.Sum(pos => pos.Shares * pos.AvgPrice);
|
||||||
|
|
||||||
|
portfolio.TotalValue = totalValue;
|
||||||
|
portfolio.UpdatedAt = DateTime.Now;
|
||||||
|
_db.Updateable(portfolio).ExecuteCommand();
|
||||||
|
|
||||||
|
return new CreateTransactionResponse
|
||||||
|
{
|
||||||
|
id = transaction.Id,
|
||||||
|
totalAmount = (double)transaction.TotalAmount,
|
||||||
|
status = transaction.Status,
|
||||||
|
createdAt = transaction.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,15 +4,6 @@ using SqlSugar;
|
|||||||
|
|
||||||
namespace AssetManager.Services;
|
namespace AssetManager.Services;
|
||||||
|
|
||||||
public interface IStrategyService
|
|
||||||
{
|
|
||||||
Strategy CreateStrategy(CreateStrategyRequest request, string userId);
|
|
||||||
List<Strategy> GetStrategies(string userId);
|
|
||||||
Strategy GetStrategyById(string id, string userId);
|
|
||||||
Strategy UpdateStrategy(string id, UpdateStrategyRequest request, string userId);
|
|
||||||
bool DeleteStrategy(string id, string userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StrategyService : IStrategyService
|
public class StrategyService : IStrategyService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly ISqlSugarClient _db;
|
||||||
|
|||||||
317
README.md
317
README.md
@ -141,3 +141,320 @@ AssetManager
|
|||||||
"Status": "created"
|
"Status": "created"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 💼 投资组合 API (Portfolio API)
|
||||||
|
|
||||||
|
### 1. 创建投资组合
|
||||||
|
|
||||||
|
**请求 URL**: `POST /api/v1/portfolio`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "我的投资组合",
|
||||||
|
"strategyId": "strategy-123",
|
||||||
|
"currency": "USD",
|
||||||
|
"stocks": [
|
||||||
|
{
|
||||||
|
"name": "Apple Inc.",
|
||||||
|
"code": "AAPL",
|
||||||
|
"price": 150.50,
|
||||||
|
"amount": 100,
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft Corp.",
|
||||||
|
"code": "MSFT",
|
||||||
|
"price": 300.25,
|
||||||
|
"amount": 50,
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"currency": "USD"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"id": "port-abc12345",
|
||||||
|
"totalValue": 30075.00,
|
||||||
|
"returnRate": 0.0,
|
||||||
|
"currency": "USD",
|
||||||
|
"createdAt": "2024-01-01 10:30:00"
|
||||||
|
},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取投资组合列表
|
||||||
|
|
||||||
|
**请求 URL**: `GET /api/v1/portfolio`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "port-abc12345",
|
||||||
|
"name": "我的投资组合",
|
||||||
|
"tags": "科技股",
|
||||||
|
"status": "运行中",
|
||||||
|
"statusType": "active",
|
||||||
|
"iconChar": "P",
|
||||||
|
"iconBgClass": "bg-blue-500",
|
||||||
|
"iconTextClass": "text-white",
|
||||||
|
"value": 30075.00,
|
||||||
|
"currency": "USD",
|
||||||
|
"returnRate": 0.15,
|
||||||
|
"returnType": "positive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "port-def67890",
|
||||||
|
"name": "保守组合",
|
||||||
|
"tags": "债券",
|
||||||
|
"status": "运行中",
|
||||||
|
"statusType": "active",
|
||||||
|
"iconChar": "C",
|
||||||
|
"iconBgClass": "bg-green-500",
|
||||||
|
"iconTextClass": "text-white",
|
||||||
|
"value": 50000.00,
|
||||||
|
"currency": "USD",
|
||||||
|
"returnRate": 0.08,
|
||||||
|
"returnType": "positive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 获取总资产情况
|
||||||
|
|
||||||
|
**请求 URL**: `GET /api/v1/portfolio/assets`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"totalValue": 80075.00,
|
||||||
|
"currency": "USD",
|
||||||
|
"todayProfit": 1250.50,
|
||||||
|
"todayProfitCurrency": "USD",
|
||||||
|
"totalReturnRate": 0.12
|
||||||
|
},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 获取单个投资组合详情
|
||||||
|
|
||||||
|
**请求 URL**: `GET /api/v1/portfolio/{id}`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"id": "port-abc12345",
|
||||||
|
"name": "我的投资组合",
|
||||||
|
"currency": "USD",
|
||||||
|
"strategy": {
|
||||||
|
"id": "strategy-123",
|
||||||
|
"name": "双均线策略",
|
||||||
|
"description": "基于短期和长期移动平均线的趋势跟踪策略"
|
||||||
|
},
|
||||||
|
"portfolioValue": 30075.00,
|
||||||
|
"totalReturn": 0.15,
|
||||||
|
"todayProfit": 1250.50,
|
||||||
|
"todayProfitCurrency": "USD",
|
||||||
|
"positions": [
|
||||||
|
{
|
||||||
|
"id": "pos-xyz12345",
|
||||||
|
"stockCode": "AAPL",
|
||||||
|
"stockName": "Apple Inc.",
|
||||||
|
"amount": 100,
|
||||||
|
"averagePrice": 150.50,
|
||||||
|
"currentPrice": 155.00,
|
||||||
|
"totalValue": 15500.00,
|
||||||
|
"profit": 450.00,
|
||||||
|
"profitRate": 0.03,
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pos-xyz67890",
|
||||||
|
"stockCode": "MSFT",
|
||||||
|
"stockName": "Microsoft Corp.",
|
||||||
|
"amount": 50,
|
||||||
|
"averagePrice": 300.25,
|
||||||
|
"currentPrice": 291.50,
|
||||||
|
"totalValue": 14575.00,
|
||||||
|
"profit": -437.50,
|
||||||
|
"profitRate": -0.03,
|
||||||
|
"currency": "USD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"id": "trans-abc12345",
|
||||||
|
"portfolioId": "port-abc12345",
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"time": "10:30:00",
|
||||||
|
"type": "buy",
|
||||||
|
"title": "初始建仓",
|
||||||
|
"amount": 100,
|
||||||
|
"currency": "USD",
|
||||||
|
"status": "completed",
|
||||||
|
"remark": "初始建仓"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 获取交易记录
|
||||||
|
|
||||||
|
**请求 URL**: `GET /api/v1/portfolio/transactions`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `portfolioId` (必填): 投资组合ID
|
||||||
|
- `limit` (可选): 每页记录数,默认 10
|
||||||
|
- `offset` (可选): 偏移量,默认 0
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /api/v1/portfolio/transactions?portfolioId=port-abc12345&limit=10&offset=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "trans-abc12345",
|
||||||
|
"portfolioId": "port-abc12345",
|
||||||
|
"date": "2024-01-15",
|
||||||
|
"time": "14:30:00",
|
||||||
|
"type": "buy",
|
||||||
|
"title": "买入AAPL",
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "USD",
|
||||||
|
"status": "completed",
|
||||||
|
"remark": "加仓"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trans-def67890",
|
||||||
|
"portfolioId": "port-abc12345",
|
||||||
|
"date": "2024-01-10",
|
||||||
|
"time": "10:15:00",
|
||||||
|
"type": "sell",
|
||||||
|
"title": "卖出MSFT",
|
||||||
|
"amount": 25,
|
||||||
|
"currency": "USD",
|
||||||
|
"status": "completed",
|
||||||
|
"remark": "减仓"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 25,
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 10
|
||||||
|
},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 创建交易
|
||||||
|
|
||||||
|
**请求 URL**: `POST /api/v1/portfolio/transactions`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"portfolioId": "port-abc12345",
|
||||||
|
"type": "buy",
|
||||||
|
"stockCode": "AAPL",
|
||||||
|
"amount": 50,
|
||||||
|
"price": 155.00,
|
||||||
|
"currency": "USD",
|
||||||
|
"remark": "加仓"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"id": "trans-xyz12345",
|
||||||
|
"totalAmount": 7750.00,
|
||||||
|
"status": "processing",
|
||||||
|
"createdAt": "2024-01-15 14:30:00"
|
||||||
|
},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应格式
|
||||||
|
|
||||||
|
所有接口在发生错误时都会返回统一的错误格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 401,
|
||||||
|
"data": null,
|
||||||
|
"message": "用户未授权"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见错误码**:
|
||||||
|
- `401`: 用户未授权
|
||||||
|
- `500`: 服务器内部错误
|
||||||
Loading…
Reference in New Issue
Block a user