feat: 添加用户授权支持并重构组合服务

重构组合服务接口和实现,添加用户ID参数以实现多租户隔离
更新组合控制器,从JWT令牌中提取用户ID并验证权限
完善组合服务数据库操作,包括组合创建、查询和交易处理
更新README文档,补充组合API详细说明
This commit is contained in:
niannian zheng 2026-03-02 14:47:16 +08:00
parent 2d1fbd37d8
commit 9741468f3e
6 changed files with 702 additions and 207 deletions

View File

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

View File

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

View 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);
}

View File

@ -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")
}; };
} }
} }

View File

@ -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
View File

@ -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`: 服务器内部错误