refactor: 安全性和代码质量改进

🔴 高优先级修复:

1. JWT 密钥安全 (Program.cs)
   - 移除硬编码默认密钥
   - 启动时强制检查环境变量/配置
   - 密钥长度必须 >= 32 字符

2. 数据库事务 (PortfolioService.cs)
   - CreateTransaction 添加事务保护
   - 交易创建、持仓更新、组合更新原子性保证
   - 异常时自动回滚

3. 异步方法改进 (PortfolioService.cs)
   - 移除 .GetAwaiter().GetResult() 阻塞调用
   - 统一使用 async/await 模式

🟡 中优先级:

4. 接口统一 (IPortfolioService.cs)
   - 移除同步方法,只保留异步版本
   - 简化接口,降低维护成本
This commit is contained in:
OpenClaw Agent 2026-03-25 06:35:42 +00:00
parent 42d3fc91c4
commit 02e199faf2
3 changed files with 125 additions and 137 deletions

View File

@ -56,10 +56,20 @@ builder.Services.AddCors(options =>
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
// 优先从环境变量读取JWT配置 // 强制从环境变量或配置文件读取JWT配置不允许硬编码默认值
var jwtSecretKey = Environment.GetEnvironmentVariable("Jwt__SecretKey") var jwtSecretKey = Environment.GetEnvironmentVariable("Jwt__SecretKey")
?? builder.Configuration["Jwt:SecretKey"] ?? builder.Configuration["Jwt:SecretKey"];
?? "your-strong-secret-key-here-2026";
if (string.IsNullOrEmpty(jwtSecretKey))
{
throw new InvalidOperationException("JWT SecretKey is required. Please set Jwt__SecretKey environment variable or Jwt:SecretKey in configuration.");
}
if (jwtSecretKey.Length < 32)
{
throw new InvalidOperationException("JWT SecretKey must be at least 32 characters long for security.");
}
var jwtIssuer = Environment.GetEnvironmentVariable("Jwt__Issuer") var jwtIssuer = Environment.GetEnvironmentVariable("Jwt__Issuer")
?? builder.Configuration["Jwt:Issuer"] ?? builder.Configuration["Jwt:Issuer"]
?? "AssetManager"; ?? "AssetManager";

View File

@ -4,17 +4,12 @@ namespace AssetManager.Services;
public interface IPortfolioService public interface IPortfolioService
{ {
CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId);
Task<CreatePortfolioResponse> CreatePortfolioAsync(CreatePortfolioRequest request, string userId); Task<CreatePortfolioResponse> CreatePortfolioAsync(CreatePortfolioRequest request, string userId);
Task<bool> UpdatePortfolioAsync(string portfolioId, UpdatePortfolioRequest request, string userId); Task<bool> UpdatePortfolioAsync(string portfolioId, UpdatePortfolioRequest request, string userId);
List<PortfolioListItem> GetPortfolios(string userId);
Task<List<PortfolioListItem>> GetPortfolioListAsync(string userId); Task<List<PortfolioListItem>> GetPortfolioListAsync(string userId);
TotalAssetsResponse GetTotalAssets(string userId);
Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId); Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId);
PortfolioDetailResponse GetPortfolioById(string id, string userId);
Task<PortfolioDetailResponse> GetPortfolioDetailAsync(string portfolioId, string userId); Task<PortfolioDetailResponse> GetPortfolioDetailAsync(string portfolioId, string userId);
GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset); Task<GetTransactionsResponse> GetTransactionsAsync(string portfolioId, string userId, int limit, int offset);
Task<List<TransactionItem>> GetTransactionsAsync(string portfolioId, GetTransactionsRequest request, string userId);
Task<TransactionItem> CreateTransactionAsync(string portfolioId, CreateTransactionRequest request, string userId); Task<TransactionItem> CreateTransactionAsync(string portfolioId, CreateTransactionRequest request, string userId);
Task<bool> DeletePortfolioAsync(string portfolioId, string userId); Task<bool> DeletePortfolioAsync(string portfolioId, string userId);
} }

View File

@ -326,11 +326,6 @@ public class PortfolioService : IPortfolioService
return result; return result;
} }
public List<PortfolioListItem> GetPortfolios(string userId)
{
return GetPortfolioListAsync(userId).GetAwaiter().GetResult();
}
public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId) public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
{ {
// 获取用户信息 // 获取用户信息
@ -464,12 +459,6 @@ public class PortfolioService : IPortfolioService
}; };
} }
// 保留同步方法作为兼容
public TotalAssetsResponse GetTotalAssets(string userId)
{
return GetTotalAssetsAsync(userId).GetAwaiter().GetResult();
}
public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId) public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
{ {
var portfolio = _db.Queryable<Portfolio>() var portfolio = _db.Queryable<Portfolio>()
@ -592,13 +581,7 @@ public class PortfolioService : IPortfolioService
}; };
} }
// 保留同步方法作为兼容(内部调用异步) public async Task<GetTransactionsResponse> GetTransactionsAsync(string portfolioId, string userId, int limit, int offset)
public PortfolioDetailResponse GetPortfolioById(string id, string userId)
{
return GetPortfolioByIdAsync(id, userId).GetAwaiter().GetResult();
}
public GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset)
{ {
// 验证投资组合是否属于该用户 // 验证投资组合是否属于该用户
var portfolio = _db.Queryable<Portfolio>() var portfolio = _db.Queryable<Portfolio>()
@ -737,9 +720,15 @@ public class PortfolioService : IPortfolioService
CreatedAt = DateTime.Now CreatedAt = DateTime.Now
}; };
// 使用事务包裹所有数据库操作
try
{
_db.BeginTran();
// 1. 插入交易记录
_db.Insertable(transaction).ExecuteCommand(); _db.Insertable(transaction).ExecuteCommand();
// 更新持仓 // 2. 更新持仓
var position = _db.Queryable<Position>() var position = _db.Queryable<Position>()
.Where(pos => pos.PortfolioId == request.PortfolioId && pos.StockCode == request.StockCode) .Where(pos => pos.PortfolioId == request.PortfolioId && pos.StockCode == request.StockCode)
.First(); .First();
@ -809,7 +798,7 @@ public class PortfolioService : IPortfolioService
_db.Insertable(position).ExecuteCommand(); _db.Insertable(position).ExecuteCommand();
} }
// 更新投资组合总价值(使用实时市值而不是成本价) // 3. 更新投资组合总价值
var Positions = _db.Queryable<Position>() var Positions = _db.Queryable<Position>()
.Where(pos => pos.PortfolioId == request.PortfolioId) .Where(pos => pos.PortfolioId == request.PortfolioId)
.ToList(); .ToList();
@ -822,11 +811,11 @@ public class PortfolioService : IPortfolioService
continue; continue;
} }
// 获取实时价格(自动路由数据源),失败则降级使用成本价 // 获取实时价格(异步调用),失败则降级使用成本价
decimal CurrentPrice = pos.AvgPrice; decimal CurrentPrice = pos.AvgPrice;
try try
{ {
var priceResponse = _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock").GetAwaiter().GetResult(); var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
if (priceResponse.Price > 0) if (priceResponse.Price > 0)
{ {
CurrentPrice = priceResponse.Price; CurrentPrice = priceResponse.Price;
@ -844,6 +833,16 @@ public class PortfolioService : IPortfolioService
portfolio.UpdatedAt = DateTime.Now; portfolio.UpdatedAt = DateTime.Now;
_db.Updateable(portfolio).ExecuteCommand(); _db.Updateable(portfolio).ExecuteCommand();
// 提交事务
_db.CommitTran();
}
catch (Exception ex)
{
_db.RollbackTran();
_logger.LogError(ex, "创建交易失败,已回滚: {PortfolioId}, {StockCode}", request.PortfolioId, request.StockCode);
throw;
}
// 删除该交易日期之后的净值历史记录,下次请求收益曲线时会自动重新计算 // 删除该交易日期之后的净值历史记录,下次请求收益曲线时会自动重新计算
try try
{ {
@ -870,22 +869,6 @@ public class PortfolioService : IPortfolioService
// ===== 异步方法实现 ===== // ===== 异步方法实现 =====
public Task<CreatePortfolioResponse> CreatePortfolioAsync(CreatePortfolioRequest request, string userId)
{
return Task.FromResult(CreatePortfolio(request, userId));
}
public Task<PortfolioDetailResponse> GetPortfolioDetailAsync(string portfolioId, string userId)
{
return GetPortfolioByIdAsync(portfolioId, userId);
}
public Task<List<TransactionItem>> GetTransactionsAsync(string portfolioId, GetTransactionsRequest request, string userId)
{
var response = GetTransactions(portfolioId, userId, request.Limit, request.Offset);
return Task.FromResult(response.Items ?? new List<TransactionItem>());
}
public async Task<TransactionItem> CreateTransactionAsync(string portfolioId, CreateTransactionRequest request, string userId) public async Task<TransactionItem> CreateTransactionAsync(string portfolioId, CreateTransactionRequest request, string userId)
{ {
request.PortfolioId = portfolioId; request.PortfolioId = portfolioId;