feat: 完成云同步、语音控制与多平台扩展基础架构搭建

本次提交完成了项目核心基础架构升级:
1. 新增动态API中间件与权限控制系统,支持匿名/鉴权接口分离
2. 搭建云同步服务体系,包含认证、任务同步、安全策略等核心模块
3. 实现语音控制全链路,从STT/意图解析到命令执行
4. 新增任务类型、附件实体与相关仓储接口
5. 重构前端配置与代理规则,统一后端端口为5057
6. 新增多平台测试项目与CI脚本优化
7. 完善项目文档与代码注释规范

移除了旧版迁移文件与冗余代理配置,调整项目结构适配跨平台部署需求。
This commit is contained in:
ShaoHua
2026-06-21 03:26:04 +08:00
parent 65cee20006
commit 4fe0b5a963
200 changed files with 12728 additions and 3550 deletions
@@ -0,0 +1,138 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Hua.Todo.Application.Common;
using Hua.Todo.Application.Repositories;
using Hua.Todo.Application.Services.CloudSync.Models;
using Hua.Todo.Core.Entities;
using Hua.Todo.HttpApi.AspNetCore.CloudSync.Services;
using Hua.Todo.HttpApi.AspNetCore.DynamicApi;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Hua.Todo.Host.Tests.CloudSync;
/// <summary>
/// CloudSync 登录后访问安全策略的端到端测试。
/// 覆盖 DynamicApi 响应包装、SessionAuthenticationHandler 认证和 SecurityPolicyService 当前用户解析链路。
/// </summary>
public sealed class CloudSyncAuthSecurityPolicyTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly WebApplication _app;
private readonly HttpClient _client;
/// <summary>
/// 创建使用 SQLite 内存库的最小 Host 管道。
/// </summary>
public CloudSyncAuthSecurityPolicyTests()
{
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddApplicationServices("Data Source=:memory:");
builder.Services.AddDbContext<TodoDbContext>(options => options.UseSqlite(_connection));
builder.Services.AddCloudSyncServer();
_app = builder.Build();
_app.UseAuthentication();
_app.UseAuthorization();
_app.UseDynamicApi();
using (var scope = _app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<TodoDbContext>();
db.Database.EnsureCreated();
SeedAdmin(db, scope.ServiceProvider.GetRequiredService<IPasswordHasher<UserEntity>>());
}
_app.StartAsync().GetAwaiter().GetResult();
_client = new HttpClient { BaseAddress = new Uri(_app.Urls.First()) };
}
/// <summary>
/// 验证登录成功后,携带返回的 Bearer Token 能获取当前用户安全策略。
/// </summary>
[Fact]
public async Task GetSecurityPolicy_AfterLogin_ReturnsPolicy()
{
var loginResponse = await _client.PostAsJsonAsync("/api/auth/login", new LoginRequest
{
UserName = "admin",
Password = "123456"
});
Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
var loginPayload = await ReadDynamicApiResponseAsync<LoginResponse>(loginResponse);
Assert.True(loginPayload.Success);
Assert.False(string.IsNullOrWhiteSpace(loginPayload.Data?.AccessToken));
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginPayload.Data.AccessToken);
var policyResponse = await _client.GetAsync("/api/security/policy");
Assert.Equal(HttpStatusCode.OK, policyResponse.StatusCode);
var policyPayload = await ReadDynamicApiResponseAsync<SecurityPolicyDto>(policyResponse);
Assert.True(policyPayload.Success);
Assert.True(policyPayload.Data?.AllowPersist);
Assert.True(policyPayload.Data?.AllowSync);
}
/// <summary>
/// 释放测试 Host 与 SQLite 连接。
/// </summary>
public void Dispose()
{
_client.Dispose();
_app.StopAsync().GetAwaiter().GetResult();
_app.DisposeAsync().GetAwaiter().GetResult();
_connection.Dispose();
}
private static void SeedAdmin(TodoDbContext db, IPasswordHasher<UserEntity> hasher)
{
var user = new UserEntity
{
Id = Guid.NewGuid(),
UserName = "admin",
Role = "admin",
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow
};
user.PasswordHash = hasher.HashPassword(user, "123456");
db.Users.Add(user);
db.SecurityPolicies.Add(new SecurityPolicyEntity
{
Id = Guid.NewGuid(),
UserId = user.Id,
AllowPersist = true,
AllowSync = true,
IsTrustedDeviceOnly = false
});
db.SaveChanges();
}
private static async Task<DynamicApiPayload<T>> ReadDynamicApiResponseAsync<T>(HttpResponseMessage response)
{
var payload = await response.Content.ReadFromJsonAsync<DynamicApiPayload<T>>(new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return payload ?? throw new InvalidOperationException("DynamicApi 响应为空。");
}
private sealed class DynamicApiPayload<T>
{
public bool Success { get; set; }
public T? Data { get; set; }
}
}
@@ -0,0 +1,82 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Hua.Todo.Application.Common;
using Hua.Todo.Application.Repositories;
using Hua.Todo.Application.Services.CloudSync.Services;
using Hua.Todo.HttpApi.AspNetCore.CloudSync.Services;
using Xunit;
namespace Hua.Todo.Host.Tests.CloudSync;
/// <summary>
/// 云同步核心服务 DI 注册集成测试。
/// 模拟 Host 的完整 DI 链路:AddApplicationServices + AddCloudSyncServer
/// 验证所有关键服务可被正确解析。
/// </summary>
public class CloudSyncEndpointRegistrationTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly ServiceProvider _provider;
public CloudSyncEndpointRegistrationTests()
{
// 使用共享缓存的 SQLite 内存数据库
_connection = new SqliteConnection("Data Source=CloudSyncRegTests;Mode=Memory;Cache=Shared");
_connection.Open();
var services = new ServiceCollection();
services.AddLogging();
// 模拟 Host 的完整 DI 链路
services.AddApplicationServices("Data Source=CloudSyncRegTests;Mode=Memory;Cache=Shared");
services.AddCloudSyncServer();
_provider = services.BuildServiceProvider();
// 确保数据库表已创建
using var scope = _provider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<TodoDbContext>();
db.Database.EnsureCreated();
}
[Fact]
public void ResolveCloudProbeService_ShouldSucceed()
{
var service = _provider.GetRequiredService<CloudProbeService>();
Assert.NotNull(service);
}
[Fact]
public void ResolveCloudAuthService_ShouldSucceed()
{
var service = _provider.GetRequiredService<CloudAuthService>();
Assert.NotNull(service);
}
[Fact]
public void ResolveCloudTaskSyncService_ShouldSucceed()
{
var service = _provider.GetRequiredService<CloudTaskSyncService>();
Assert.NotNull(service);
}
[Fact]
public void ResolveSecurityPolicyService_ShouldSucceed()
{
var service = _provider.GetRequiredService<SecurityPolicyService>();
Assert.NotNull(service);
}
[Fact]
public void ResolveCloudAdminService_ShouldSucceed()
{
var service = _provider.GetRequiredService<CloudAdminService>();
Assert.NotNull(service);
}
public void Dispose()
{
_provider.Dispose();
_connection.Dispose();
}
}
@@ -0,0 +1,95 @@
using System.Net;
using System.Net.Http.Json;
using Hua.Todo.HttpApi.AspNetCore.CloudSync.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Hua.Todo.Host.Tests.CloudSync;
/// <summary>
/// CloudSyncProxyMiddleware 行为集成测试。
/// 验证云同步路径代理转发、无 URL 时返回 503。
/// settings 端点已迁移至 DynamicApi/api/cloudSyncProxySettings),不在本测试覆盖范围。
/// </summary>
public class CloudSyncProxyMiddlewareTests : IDisposable
{
private readonly WebApplication _app;
private readonly HttpClient _client;
public CloudSyncProxyMiddlewareTests()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddCloudSyncProxy();
var app = builder.Build();
app.UseCloudSyncProxy();
_app = app;
_app.StartAsync().GetAwaiter().GetResult();
_client = new HttpClient { BaseAddress = new Uri(_app.Urls.First()) };
}
[Fact]
public async Task Probe_NoCloudSyncUrl_Returns503()
{
var response = await _client.PostAsJsonAsync("/api/cloud-sync/probe",
new { targetUrl = "http://localhost:5173" });
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("cloud sync server URL not configured", content);
}
[Fact]
public async Task AuthLogin_NoCloudSyncUrl_Returns503()
{
var response = await _client.PostAsJsonAsync("/api/auth/login",
new { username = "admin", password = "123456" });
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}
[Fact]
public async Task Tasks_NoCloudSyncUrl_Returns503()
{
var response = await _client.GetAsync("/api/tasks/");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}
[Fact]
public async Task Tasks_WithoutTrailingSlash_NoCloudSyncUrl_Returns503()
{
var response = await _client.PostAsJsonAsync("/api/tasks", new { });
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}
[Fact]
public async Task SecurityPolicy_NoCloudSyncUrl_Returns503()
{
var response = await _client.GetAsync("/api/security/policy");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}
[Fact]
public async Task Sync_NoCloudSyncUrl_Returns503()
{
var response = await _client.PostAsJsonAsync("/api/sync/", new { });
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}
public void Dispose()
{
_client.Dispose();
_app.StopAsync().GetAwaiter().GetResult();
_app.DisposeAsync().GetAwaiter().GetResult();
}
}