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,421 @@
using Hua.Todo.Application.Repositories;
using Hua.Todo.Application.Services.CloudSync.Models;
using Hua.Todo.Application.Services.CloudSync.Services;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Hua.Todo.Core.Entities;
using Xunit;
namespace Hua.Todo.Host.Tests;
/// <summary>
/// CloudTaskSyncService SQLite 集成测试(验证 UNIQUE 约束不会在生产环境中触发)。
/// </summary>
public class CloudTaskSyncServiceSqliteTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly TodoDbContext _dbContext;
private readonly CloudTaskSyncService _service;
private readonly Guid _testUserId;
public CloudTaskSyncServiceSqliteTests()
{
_testUserId = Guid.NewGuid();
// 使用共享缓存的 SQLite 内存数据库
_connection = new SqliteConnection("Data Source=CloudTaskSyncTests;Mode=Memory;Cache=Shared");
_connection.Open();
var options = new DbContextOptionsBuilder<TodoDbContext>()
.UseSqlite(_connection)
.Options;
_dbContext = new TodoDbContext(options);
_dbContext.Database.EnsureCreated();
// 创建测试用户(满足 UserId 外键约束)
_dbContext.Users.Add(new UserEntity
{
Id = _testUserId,
UserName = "test_user",
PasswordHash = "hash",
PasswordSalt = "salt",
Role = "User"
});
_dbContext.SaveChanges();
_service = new CloudTaskSyncService(_dbContext, NullLogger<CloudTaskSyncService>.Instance);
}
public void Dispose()
{
_dbContext.Dispose();
_connection.Dispose();
}
/// <summary>
/// 复现用户报告的 Bug:同一批次中同步父子任务(均有已知 IdlastModificationTime 为 null)。
/// 期望:首次同步成功创建两个任务。
/// </summary>
[Fact]
public async Task SyncAsync_ParentChildWithNullTime_FirstSync_Succeeds()
{
var parentId = Guid.NewGuid();
var childId = Guid.NewGuid();
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = childId,
Title = "子任务",
Priority = TaskPriority.Medium,
Code = "2",
ParentTaskId = parentId,
LastModificationTime = null
},
new CloudTaskUpsert
{
Id = parentId,
Title = "父任务",
Priority = TaskPriority.High,
Code = "1",
ParentTaskId = null,
LastModificationTime = null
}
}
};
var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, response.Tasks.Count);
var parent = response.Tasks.First(t => t.Title == "父任务");
Assert.Null(parent.ParentTaskId);
var child = response.Tasks.First(t => t.Title == "子任务");
Assert.Equal(parentId, child.ParentTaskId);
}
/// <summary>
/// 复现 Bug 场景:首次同步成功后再用相同数据重新同步(模拟客户端重试)。
/// lastModificationTime 为 null 时 LWW 应跳过更新,不应抛出 UNIQUE 约束。
/// </summary>
[Fact]
public async Task SyncAsync_ParentChildWithNullTime_Resync_NoUniqueViolation()
{
var parentId = Guid.NewGuid();
var childId = Guid.NewGuid();
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = childId, Title = "子任务", Priority = TaskPriority.Medium,
Code = "2", ParentTaskId = parentId, LastModificationTime = null
},
new CloudTaskUpsert
{
Id = parentId, Title = "父任务", Priority = TaskPriority.High,
Code = "1", ParentTaskId = null, LastModificationTime = null
}
}
};
// 首次同步
var firstResponse = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, firstResponse.Tasks.Count);
// 重新同步(同一 DbContext,模拟客户端重试)
var resyncResponse = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, resyncResponse.Tasks.Count);
}
/// <summary>
/// 测试多次重试同步不引发 UNIQUE 约束。
/// </summary>
[Fact]
public async Task SyncAsync_ParentChildWithNullTime_MultipleResyncs_NoUniqueViolation()
{
var parentId = Guid.NewGuid();
var childId = Guid.NewGuid();
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = childId, Title = "子任务", Priority = TaskPriority.Medium,
Code = "2", ParentTaskId = parentId, LastModificationTime = null
},
new CloudTaskUpsert
{
Id = parentId, Title = "父任务", Priority = TaskPriority.High,
Code = "1", ParentTaskId = null, LastModificationTime = null
}
}
};
for (int i = 0; i < 5; i++)
{
var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, response.Tasks.Count);
}
}
/// <summary>
/// 测试已有 DbContext 跟踪实体的场景下同步不会引发 UNIQUE 冲突。
/// 模拟场景:任务已通过其他 API 创建并仍被跟踪,然后通过 CloudSync 同步同一数据。
/// </summary>
[Fact]
public async Task SyncAsync_TaskExistsThenSynced_NoUniqueViolation()
{
var taskId = Guid.NewGuid();
var existingTask = new TaskEntity
{
Id = taskId,
UserId = _testUserId,
Title = "已有任务",
Priority = TaskPriority.Medium,
CreationTime = DateTime.UtcNow,
CreatorId = _testUserId,
LastModificationTime = DateTime.UtcNow,
LastModifierId = _testUserId
};
_dbContext.Tasks.Add(existingTask);
await _dbContext.SaveChangesAsync();
var syncRequest = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = taskId,
Title = "已有任务",
LastModificationTime = null
}
}
};
var response = await _service.SyncAsync(_testUserId, syncRequest, CancellationToken.None);
Assert.Single(response.Tasks);
Assert.Equal("已有任务", response.Tasks.First().Title);
}
/// <summary>
/// 复现用户 Bug 的核心场景:使用独立 DbContext 模拟跨请求重试。
/// Context A 创建任务并提交,Context B 再次同步相同数据时不应触发 UNIQUE 约束。
/// </summary>
[Fact]
public async Task SyncAsync_CrossContextResync_NoUniqueViolation()
{
var parentId = Guid.NewGuid();
var childId = Guid.NewGuid();
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = childId, Title = "子任务", Priority = TaskPriority.Medium,
Code = "2", ParentTaskId = parentId, LastModificationTime = null
},
new CloudTaskUpsert
{
Id = parentId, Title = "父任务", Priority = TaskPriority.High,
Code = "1", ParentTaskId = null, LastModificationTime = null
}
}
};
// Context A:首次同步
var firstResponse = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, firstResponse.Tasks.Count);
// Context B:使用全新 DbContext 重新同步(模拟另一个请求)
var options = new DbContextOptionsBuilder<TodoDbContext>()
.UseSqlite(_connection)
.Options;
using var dbContextB = new TodoDbContext(options);
var serviceB = new CloudTaskSyncService(dbContextB, NullLogger<CloudTaskSyncService>.Instance);
var resyncResponse = await serviceB.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, resyncResponse.Tasks.Count);
}
/// <summary>
/// 使用报错场景中的精确 UUID 复现 Bug:
/// 父子任务在同一批次中同步,lastModificationTime 均为 null。
/// </summary>
[Fact]
public async Task SyncAsync_ExactBugReportUuids_NoUniqueViolation()
{
// 使用报错日志中的精确 UUID
var parentId = Guid.Parse("6afeea6f-296f-4da1-a1e1-279e818f932c");
var childId = Guid.Parse("412290d0-01b7-4348-af2b-a6abb55dd580");
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = childId,
Title = "测试",
Priority = (TaskPriority)1,
IsCompleted = false,
Code = "2",
ParentTaskId = parentId,
LastModificationTime = null
},
new CloudTaskUpsert
{
Id = parentId,
Title = "测试",
Priority = (TaskPriority)1,
IsCompleted = false,
Code = "1",
ParentTaskId = null,
LastModificationTime = null
}
}
};
// 首次同步
var response1 = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, response1.Tasks.Count);
// 使用全新 DbContext 重新同步(模拟跨请求重试)
var options = new DbContextOptionsBuilder<TodoDbContext>()
.UseSqlite(_connection)
.Options;
using var dbContextB = new TodoDbContext(options);
var serviceB = new CloudTaskSyncService(dbContextB, NullLogger<CloudTaskSyncService>.Instance);
// 重试同步 - 不应抛出 UNIQUE 约束
var response2 = await serviceB.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Equal(2, response2.Tasks.Count);
}
/// <summary>
/// 测试同一次同步请求中,父任务已存在于 DB 但 lastModificationTime 为 null 的重同步场景。
/// </summary>
[Fact]
public async Task SyncAsync_ExistingTaskWithNullTime_NoUniqueViolation()
{
// 使用精确 UUID
var parentId = Guid.Parse("6afeea6f-296f-4da1-a1e1-279e818f932c");
var childId = Guid.Parse("412290d0-01b7-4348-af2b-a6abb55dd580");
// 先创建一个已存在的父任务(模拟前置同步已完成)
var existingParent = new TaskEntity
{
Id = parentId,
UserId = _testUserId,
Title = "测试",
Priority = (TaskPriority)1,
IsCompleted = false,
Code = "1",
ParentTaskId = null,
CreationTime = DateTime.UtcNow,
CreatorId = _testUserId,
LastModificationTime = DateTime.UtcNow,
LastModifierId = _testUserId
};
_dbContext.Tasks.Add(existingParent);
await _dbContext.SaveChangesAsync();
// 同步请求(lastModificationTime 为 null,应被 LWW 跳过)
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = parentId,
Title = "测试",
Priority = (TaskPriority)1,
IsCompleted = false,
Code = "1",
ParentTaskId = null,
LastModificationTime = null // 🔑 关键:null 时间戳
}
}
};
// 不应抛出 UNIQUE 约束
var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Single(response.Tasks);
}
/// <summary>
/// 验证跨 UserId 同步相同 Id 不再触发 UNIQUE 约束。
/// 任务已存在于 DB(通过其他 UserId 创建),用不同 UserId 同步相同 Id,
/// 应被 ProcessUpsertAsync 的 DB 级别二次查重发现并安全跳过。
/// </summary>
[Fact]
public async Task SyncAsync_TaskExistsWithDifferentUserId_HandlesGracefully()
{
// 创建另一个用户的 DbContext
var otherUserId = Guid.NewGuid();
_dbContext.Users.Add(new UserEntity
{
Id = otherUserId,
UserName = "other_user",
PasswordHash = "hash",
PasswordSalt = "salt",
Role = "User"
});
await _dbContext.SaveChangesAsync();
// 使用报错 UUID 创建任务,但属于 otherUserId
var parentId = Guid.Parse("6afeea6f-296f-4da1-a1e1-279e818f932c");
_dbContext.Tasks.Add(new TaskEntity
{
Id = parentId,
UserId = otherUserId,
Title = "other user task",
Priority = (TaskPriority)1,
CreationTime = DateTime.UtcNow,
CreatorId = otherUserId,
LastModificationTime = DateTime.UtcNow,
LastModifierId = otherUserId
});
await _dbContext.SaveChangesAsync();
// 使用独立 DbContext 同步相同 Id(模拟跨请求)
var options = new DbContextOptionsBuilder<TodoDbContext>()
.UseSqlite(_connection)
.Options;
using var dbContextB = new TodoDbContext(options);
var serviceB = new CloudTaskSyncService(dbContextB, NullLogger<CloudTaskSyncService>.Instance);
var request = new SyncRequest
{
Upserts = new List<CloudTaskUpsert>
{
new CloudTaskUpsert
{
Id = parentId,
Title = "测试",
Priority = (TaskPriority)1,
IsCompleted = false,
Code = "1",
ParentTaskId = null,
LastModificationTime = null
}
}
};
// 应正常完成(不抛出 DbUpdateException),DB 级别二次查重发现已存在实体并跳过
// 注意:response.Tasks 为空,因为该任务属于 otherUserId,不属于 _testUserId
var response = await serviceB.SyncAsync(_testUserId, request, CancellationToken.None);
Assert.Empty(response.Tasks);
}
}