From b587e8c1e11f6de26443faa95430afcac3569de5 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 22 Nov 2025 21:20:49 +0800 Subject: [PATCH 01/56] feat(shared-web): add shared swagger and tracing utilities --- 0_Document/10_TODO.md | 62 +++++++++++ .../Controllers/HealthController.cs | 28 +++++ src/Api/TakeoutSaaS.AdminApi/Program.cs | 76 +++++++++++++ .../Controllers/HealthController.cs | 28 +++++ src/Api/TakeoutSaaS.MiniApi/Program.cs | 76 +++++++++++++ .../Controllers/HealthController.cs | 28 +++++ src/Api/TakeoutSaaS.UserApi/Program.cs | 76 +++++++++++++ .../Diagnostics/TraceContext.cs | 25 +++++ .../Results/ApiResponse.NonGeneric.cs | 31 ++++++ .../Results/ApiResponse.cs | 105 ++++++++++++++++++ .../ApplicationBuilderExtensions.cs | 22 ++++ .../Extensions/ServiceCollectionExtensions.cs | 48 ++++++++ .../Filters/ValidateModelAttribute.cs | 29 +++++ .../Middleware/CorrelationIdMiddleware.cs | 85 ++++++++++++++ .../Middleware/ExceptionHandlingMiddleware.cs | 86 ++++++++++++++ .../Middleware/RequestLoggingMiddleware.cs | 43 +++++++ .../Middleware/SecurityHeadersMiddleware.cs | 27 +++++ .../Swagger/ConfigureSwaggerOptions.cs | 60 ++++++++++ .../Swagger/SwaggerDocumentSettings.cs | 22 ++++ .../Swagger/SwaggerExtensions.cs | 65 +++++++++++ .../TakeoutSaaS.Shared.Web.csproj | 23 ++++ .../TakeoutSaaS.Module.Tenancy.csproj | 13 +++ 22 files changed, 1058 insertions(+) create mode 100644 0_Document/10_TODO.md create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/Program.cs create mode 100644 src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs create mode 100644 src/Api/TakeoutSaaS.MiniApi/Program.cs create mode 100644 src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs create mode 100644 src/Api/TakeoutSaaS.UserApi/Program.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj diff --git a/0_Document/10_TODO.md b/0_Document/10_TODO.md new file mode 100644 index 0000000..7157dbf --- /dev/null +++ b/0_Document/10_TODO.md @@ -0,0 +1,62 @@ +# TODO Roadmap + +说明:本清单覆盖当前阶段的骨架搭建与核心基础能力(不含部署与CI/CD,留到项目跑通后再做)。 + +## A. 基础骨架与规范 +- [x] 统一返回结果/异常处理中间件(Shared.Web) +- [x] 模型验证、验证失败统一输出(Shared.Web) +- [x] 统一日志(Serilog)与请求日志/TraceId(Shared.Web) +- [x] API 版本化与分组(AdminApi、MiniApi、UserApi) +- [x] Swagger 定制(鉴权按钮、分组说明、示例) +- [x] 安全中间件:Security Headers、CORS 策略(按端区分) + +## B. 认证与权限 +- [ ] JWT 颁发与刷新(AdminApi、MiniApi) +- [ ] RBAC 权限模型(角色/权限/策略)与特性授权(AdminApi) +- [ ] 小程序登录(微信 code2Session)并绑定用户账户(MiniApi) +- [ ] 登录防刷限流(MiniApi) + +## C. 多租户与参数字典 +- [ ] 多租户中间件:从 Header/域名解析租户(Shared.Web + Tenancy) +- [ ] EF Core 全局查询过滤(tenant_id) +- [ ] 参数字典模块(系统参数/业务参数)CRUD 与缓存(Dictionary 模块) + +## D. 数据访问与多数据源 +- [ ] EF Core 10 基础上下文、实体基类、审计字段 +- [ ] 读写分离/多数据源配置(主写、从读;或按租户切库预留) +- [ ] Dapper 基础设施封装(统计/报表类查询) + +## E. 文件与存储 +- [ ] 存储模块抽象(本地/MinIO/云厂商适配) +- [ ] 上传接口(AdminApi、MiniApi)与签名直传预留 +- [ ] 图片/文件访问安全策略(防盗链、过期签名) + +## F. 短信与消息队列 +- [ ] 短信模块(阿里云/腾讯云 适配占位)与验证码发送 +- [ ] MQ 模块(RabbitMQ)Publisher/Subscriber 抽象 +- [ ] 业务事件定义(订单创建/支付成功等)与事件发布入口 + +## G. 调度与定时任务 +- [ ] 调度模块(Quartz/Hangfire 二选一,默认 Hangfire) +- [ ] 基础任务:订单超时取消、优惠券过期处理、日志清理 +- [ ] 调度面板(后续 AdminUI 对接) + +## H. 第三方配送对接(仅第三方) +- [ ] 配送适配抽象(达达/闪送/顺丰同城等) +- [ ] 统一下单/取消/查询接口与回调验签 +- [ ] AdminApi 后台运力单查询与补单 + +## I. 网关与横切能力 +- [ ] YARP 路由拆分(/api/admin、/api/mini、/api/user) +- [ ] 网关级限流与请求日志 +- [ ] 透传鉴权/租户标识与统一错误页 + +## J. 测试与质量 +- [ ] 单元测试工程骨架(xUnit + FluentAssertions) +- [ ] 集成测试基座(WebApplicationFactory、测试容器) +- [ ] 静态分析与风格规范(.editorconfig) + +## K. 文档与规范落地 +- [ ] 在文档中补充:仅第三方配送的接口与回调规范 +- [ ] MiniApi 认证流程图(微信登录)与错误码 +- [ ] 模块间调用关系图与依赖边界 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs new file mode 100644 index 0000000..68ca72d --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 管理后台 - 健康检查。 +/// +[ApiVersion("1.0")] +[Route("api/admin/v{version:apiVersion}/[controller]")] +public class HealthController : BaseApiController +{ + /// + /// 获取服务健康状态。 + /// + /// 健康状态 + [HttpGet] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public IActionResult Get() + { + var payload = new { status = "OK", service = "AdminApi", time = DateTime.UtcNow }; + return Ok(ApiResponse.Ok(payload)); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs new file mode 100644 index 0000000..7e650e0 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using TakeoutSaaS.Module.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Web.Extensions; +using TakeoutSaaS.Shared.Web.Swagger; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseSerilog((context, _, configuration) => +{ + configuration + .Enrich.FromLogContext() + .Enrich.WithProperty("Service", "AdminApi") + .WriteTo.Console(); +}); + +builder.Services.AddSharedWebCore(); +builder.Services.AddSharedSwagger(options => +{ + options.Title = "外卖SaaS - 管理后台"; + options.Description = "管理后台 API 文档"; + options.EnableAuthorization = true; +}); + +var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin"); +builder.Services.AddCors(options => +{ + options.AddPolicy("AdminApiCors", policy => + { + ConfigureCorsPolicy(policy, adminOrigins); + }); +}); + +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.UseCors("AdminApiCors"); +app.UseSharedWebCore(); +app.UseSharedSwagger(); + +app.MapControllers(); +app.Run(); + +static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey) +{ + var origins = configuration.GetSection(sectionKey).Get(); + return origins? + .Where(origin => !string.IsNullOrWhiteSpace(origin)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? Array.Empty(); +} + +static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) +{ + if (origins.Length == 0) + { + policy.AllowAnyOrigin(); + } + else + { + policy.WithOrigins(origins) + .AllowCredentials(); + } + + policy + .AllowAnyHeader() + .AllowAnyMethod(); +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs new file mode 100644 index 0000000..e915adf --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.MiniApi.Controllers; + +/// +/// 小程序端 - 健康检查。 +/// +[ApiVersion("1.0")] +[AllowAnonymous] +[Route("api/mini/v{version:apiVersion}/[controller]")] +public class HealthController : BaseApiController +{ + /// + /// 获取服务健康状态。 + /// + /// 健康状态 + [HttpGet] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public IActionResult Get() + { + var payload = new { status = "OK", service = "MiniApi", time = DateTime.UtcNow }; + return Ok(ApiResponse.Ok(payload)); + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs new file mode 100644 index 0000000..d6621d5 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using TakeoutSaaS.Module.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Web.Extensions; +using TakeoutSaaS.Shared.Web.Swagger; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseSerilog((context, _, configuration) => +{ + configuration + .Enrich.FromLogContext() + .Enrich.WithProperty("Service", "MiniApi") + .WriteTo.Console(); +}); + +builder.Services.AddSharedWebCore(); +builder.Services.AddSharedSwagger(options => +{ + options.Title = "外卖SaaS - 小程序端"; + options.Description = "小程序 API 文档"; + options.EnableAuthorization = true; +}); + +var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini"); +builder.Services.AddCors(options => +{ + options.AddPolicy("MiniApiCors", policy => + { + ConfigureCorsPolicy(policy, miniOrigins); + }); +}); + +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.UseCors("MiniApiCors"); +app.UseSharedWebCore(); +app.UseSharedSwagger(); + +app.MapControllers(); +app.Run(); + +static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey) +{ + var origins = configuration.GetSection(sectionKey).Get(); + return origins? + .Where(origin => !string.IsNullOrWhiteSpace(origin)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? Array.Empty(); +} + +static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) +{ + if (origins.Length == 0) + { + policy.AllowAnyOrigin(); + } + else + { + policy.WithOrigins(origins) + .AllowCredentials(); + } + + policy + .AllowAnyHeader() + .AllowAnyMethod(); +} diff --git a/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs new file mode 100644 index 0000000..1648b83 --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.UserApi.Controllers; + +/// +/// 用户端 - 健康检查。 +/// +[ApiVersion("1.0")] +[AllowAnonymous] +[Route("api/user/v{version:apiVersion}/[controller]")] +public class HealthController : BaseApiController +{ + /// + /// 获取服务健康状态。 + /// + /// 健康状态 + [HttpGet] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public IActionResult Get() + { + var payload = new { status = "OK", service = "UserApi", time = DateTime.UtcNow }; + return Ok(ApiResponse.Ok(payload)); + } +} diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs new file mode 100644 index 0000000..fc30e4f --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using TakeoutSaaS.Module.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Web.Extensions; +using TakeoutSaaS.Shared.Web.Swagger; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseSerilog((context, _, configuration) => +{ + configuration + .Enrich.FromLogContext() + .Enrich.WithProperty("Service", "UserApi") + .WriteTo.Console(); +}); + +builder.Services.AddSharedWebCore(); +builder.Services.AddSharedSwagger(options => +{ + options.Title = "外卖SaaS - 用户端"; + options.Description = "C 端用户 API 文档"; + options.EnableAuthorization = true; +}); + +var userOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:User"); +builder.Services.AddCors(options => +{ + options.AddPolicy("UserApiCors", policy => + { + ConfigureCorsPolicy(policy, userOrigins); + }); +}); + +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.UseCors("UserApiCors"); +app.UseSharedWebCore(); +app.UseSharedSwagger(); + +app.MapControllers(); +app.Run(); + +static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey) +{ + var origins = configuration.GetSection(sectionKey).Get(); + return origins? + .Where(origin => !string.IsNullOrWhiteSpace(origin)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? Array.Empty(); +} + +static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) +{ + if (origins.Length == 0) + { + policy.AllowAnyOrigin(); + } + else + { + policy.WithOrigins(origins) + .AllowCredentials(); + } + + policy + .AllowAnyHeader() + .AllowAnyMethod(); +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs new file mode 100644 index 0000000..0715cf1 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs @@ -0,0 +1,25 @@ +using System.Threading; + +namespace TakeoutSaaS.Shared.Abstractions.Diagnostics; + +/// +/// 轻量级 TraceId 上下文,便于跨层访问当前请求的追踪标识。 +/// +public static class TraceContext +{ + private static readonly AsyncLocal TraceIdHolder = new(); + + /// + /// 当前请求的 TraceId。 + /// + public static string? TraceId + { + get => TraceIdHolder.Value; + set => TraceIdHolder.Value = value; + } + + /// + /// 清理 TraceId,避免 AsyncLocal 污染其它请求。 + /// + public static void Clear() => TraceIdHolder.Value = null; +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs new file mode 100644 index 0000000..d3e8d6d --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs @@ -0,0 +1,31 @@ +namespace TakeoutSaaS.Shared.Abstractions.Results; + +/// +/// 非泛型便捷封装。 +/// +public static class ApiResponse +{ + /// + /// 仅返回成功消息(无数据)。 + /// + public static ApiResponse Success(string? message = "操作成功") + => ApiResponse.Ok(message: message); + + /// + /// 成功且携带数据。 + /// + public static ApiResponse Ok(object? data, string? message = "操作成功") + => data is null ? ApiResponse.Ok(message: message) : ApiResponse.Ok(data, message); + + /// + /// 错误返回。 + /// + public static ApiResponse Failure(int code, string message) + => ApiResponse.Error(code, message); + + /// + /// 错误返回(附带详情)。 + /// + public static ApiResponse Error(int code, string message, object? errors = null) + => ApiResponse.Error(code, message, errors); +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs new file mode 100644 index 0000000..6669468 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using TakeoutSaaS.Shared.Abstractions.Diagnostics; + +namespace TakeoutSaaS.Shared.Abstractions.Results; + +/// +/// 统一的 API 返回结果包装。 +/// +/// 数据载荷类型 +public sealed record class ApiResponse +{ + /// + /// 是否成功。 + /// + public bool Success { get; init; } + + /// + /// 状态/错误码(默认 200)。 + /// + public int Code { get; init; } = 200; + + /// + /// 提示信息。 + /// + public string? Message { get; init; } + + /// + /// 业务数据。 + /// + public T? Data { get; init; } + + /// + /// 错误详情(如字段验证错误)。 + /// + public object? Errors { get; init; } + + /// + /// TraceId,便于链路追踪。 + /// + public string TraceId { get; init; } = string.Empty; + + /// + /// 时间戳(UTC)。 + /// + public DateTime Timestamp { get; init; } = DateTime.UtcNow; + + /// + /// 成功返回。 + /// + public static ApiResponse Ok(T data, string? message = "操作成功") + => Create(true, 200, message, data); + + /// + /// 无数据的成功返回。 + /// + public static ApiResponse Ok(string? message = "操作成功") + => Create(true, 200, message, default); + + /// + /// 兼容旧名称:成功结果。 + /// + public static ApiResponse SuccessResult(T data, string? message = "操作成功") + => Ok(data, message); + + /// + /// 错误返回。 + /// + public static ApiResponse Error(int code, string message, object? errors = null) + => Create(false, code, message, default, errors); + + /// + /// 兼容旧名称:失败结果。 + /// + public static ApiResponse Failure(int code, string message) + => Error(code, message); + + /// + /// 附加错误详情。 + /// + public ApiResponse WithErrors(object? errors) + => this with { Errors = errors }; + + private static ApiResponse Create(bool success, int code, string? message, T? data, object? errors = null) + => new() + { + Success = success, + Code = code, + Message = message, + Data = data, + Errors = errors, + TraceId = ResolveTraceId(), + Timestamp = DateTime.UtcNow + }; + + private static string ResolveTraceId() + { + if (!string.IsNullOrWhiteSpace(TraceContext.TraceId)) + { + return TraceContext.TraceId!; + } + + return Activity.Current?.Id ?? Guid.NewGuid().ToString("N"); + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..2c25aaa --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Builder; +using TakeoutSaaS.Shared.Web.Middleware; + +namespace TakeoutSaaS.Shared.Web.Extensions; + +/// +/// Web 应用中间件扩展。 +/// +public static class ApplicationBuilderExtensions +{ + /// + /// 按规范启用 TraceId、请求日志、异常映射与安全响应头。 + /// + public static IApplicationBuilder UseSharedWebCore(this IApplicationBuilder app) + { + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + return app; + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..781364d --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Shared.Web.Filters; + +namespace TakeoutSaaS.Shared.Web.Extensions; + +/// +/// Shared.Web 服务注册扩展。 +/// +public static class ServiceCollectionExtensions +{ + /// + /// 注册控制器、模型验证、API 版本化等基础能力。 + /// + public static IServiceCollection AddSharedWebCore(this IServiceCollection services) + { + services.AddHttpContextAccessor(); + services.AddEndpointsApiExplorer(); + + services + .AddControllers(options => + { + options.Filters.Add(); + }) + .AddNewtonsoftJson(); + + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + + services.AddApiVersioning(options => + { + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; + }); + + services.AddVersionedApiExplorer(setup => + { + setup.GroupNameFormat = "'v'VVV"; + setup.SubstituteApiVersionInUrl = true; + }); + + return services; + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs b/src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs new file mode 100644 index 0000000..4c802e0 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs @@ -0,0 +1,29 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Shared.Web.Filters; + +/// +/// 模型验证过滤器:将模型验证错误统一为ApiResponse输出 +/// +public sealed class ValidateModelAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + var errors = context.ModelState + .Where(kv => kv.Value?.Errors.Count > 0) + .ToDictionary( + kv => kv.Key, + kv => kv.Value!.Errors.Select(e => string.IsNullOrWhiteSpace(e.ErrorMessage) ? "Invalid" : e.ErrorMessage).ToArray() + ); + + var response = ApiResponse.Error(ErrorCodes.ValidationFailed, "一个或多个验证错误", errors); + context.Result = new UnprocessableEntityObjectResult(response); + } + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs new file mode 100644 index 0000000..b3cf763 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Shared.Abstractions.Diagnostics; + +namespace TakeoutSaaS.Shared.Web.Middleware; + +/// +/// 统一 TraceId/CorrelationId,贯穿日志与响应。 +/// +public sealed class CorrelationIdMiddleware +{ + private const string TraceHeader = "X-Trace-Id"; + private const string RequestHeader = "X-Request-Id"; + + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public CorrelationIdMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var traceId = ResolveTraceId(context); + context.TraceIdentifier = traceId; + TraceContext.TraceId = traceId; + + context.Response.OnStarting(() => + { + context.Response.Headers[TraceHeader] = traceId; + return Task.CompletedTask; + }); + + using (_logger.BeginScope(new Dictionary + { + ["TraceId"] = traceId + })) + { + try + { + await _next(context); + } + finally + { + TraceContext.Clear(); + } + } + } + + private static string ResolveTraceId(HttpContext context) + { + if (TryGetHeader(context, TraceHeader, out var traceId)) + { + return traceId; + } + + if (TryGetHeader(context, RequestHeader, out var requestId)) + { + return requestId; + } + + return Guid.NewGuid().ToString("N"); + } + + private static bool TryGetHeader(HttpContext context, string headerName, out string value) + { + if (context.Request.Headers.TryGetValue(headerName, out var values)) + { + var headerValue = values.ToString(); + if (!string.IsNullOrWhiteSpace(headerValue)) + { + value = headerValue; + return true; + } + } + + value = string.Empty; + return false; + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..da85777 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Shared.Web.Middleware; + +/// +/// 全局异常处理中间件,将异常统一映射为 ApiResponse。 +/// +public sealed class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IHostEnvironment _environment; + + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger, IHostEnvironment environment) + { + _next = next; + _logger = logger; + _environment = environment; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "未处理异常:{Message}", ex.Message); + await HandleExceptionAsync(context, ex); + } + } + + private Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var (statusCode, response) = BuildErrorResponse(exception); + + if (_environment.IsDevelopment()) + { + response = response with + { + Message = exception.Message, + Errors = new + { + response.Errors, + detail = exception.ToString() + } + }; + } + + context.Response.StatusCode = statusCode; + context.Response.ContentType = "application/json"; + return context.Response.WriteAsJsonAsync(response, SerializerOptions); + } + + private static (int StatusCode, ApiResponse Response) BuildErrorResponse(Exception exception) + { + return exception switch + { + ValidationException validationException => ( + StatusCodes.Status422UnprocessableEntity, + ApiResponse.Error(ErrorCodes.ValidationFailed, "请求参数验证失败", validationException.Errors)), + BusinessException businessException => ( + StatusCodes.Status422UnprocessableEntity, + ApiResponse.Error(businessException.ErrorCode, businessException.Message)), + _ => ( + StatusCodes.Status500InternalServerError, + ApiResponse.Error(ErrorCodes.InternalServerError, "服务器开小差啦,请稍后再试")) + }; + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs new file mode 100644 index 0000000..8121499 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Shared.Abstractions.Diagnostics; + +namespace TakeoutSaaS.Shared.Web.Middleware; + +/// +/// 基础请求日志(方法、路径、耗时、状态码、TraceId)。 +/// +public sealed class RequestLoggingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RequestLoggingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var stopwatch = Stopwatch.StartNew(); + try + { + await _next(context); + } + finally + { + stopwatch.Stop(); + var traceId = TraceContext.TraceId ?? context.TraceIdentifier; + _logger.LogInformation( + "HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}", + context.Request.Method, + context.Request.Path, + context.Response.StatusCode, + stopwatch.Elapsed.TotalMilliseconds, + traceId); + } + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs new file mode 100644 index 0000000..7bb1b0c --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; + +namespace TakeoutSaaS.Shared.Web.Middleware; + +/// +/// 安全响应头中间件 +/// +public sealed class SecurityHeadersMiddleware +{ + private readonly RequestDelegate _next; + + public SecurityHeadersMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var headers = context.Response.Headers; + headers["X-Content-Type-Options"] = "nosniff"; + headers["X-Frame-Options"] = "DENY"; + headers["X-XSS-Protection"] = "1; mode=block"; + headers["Referrer-Policy"] = "no-referrer"; + await _next(context); + } +} + diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs new file mode 100644 index 0000000..e620d67 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace TakeoutSaaS.Shared.Web.Swagger; + +/// +/// 根据 API 版本动态注册 Swagger 文档。 +/// +internal sealed class ConfigureSwaggerOptions : IConfigureOptions +{ + private readonly IApiVersionDescriptionProvider _provider; + private readonly SwaggerDocumentSettings _settings; + + public ConfigureSwaggerOptions( + IApiVersionDescriptionProvider provider, + IOptions settings) + { + _provider = provider; + _settings = settings.Value; + } + + public void Configure(SwaggerGenOptions options) + { + foreach (var description in _provider.ApiVersionDescriptions) + { + var info = new OpenApiInfo + { + Title = $"{_settings.Title} {description.ApiVersion}", + Version = description.ApiVersion.ToString(), + Description = description.IsDeprecated + ? $"{_settings.Description}(该版本已弃用)" + : _settings.Description + }; + + options.SwaggerGeneratorOptions.SwaggerDocs[description.GroupName] = info; + } + + if (_settings.EnableAuthorization) + { + var scheme = new OpenApiSecurityScheme + { + Name = "Authorization", + Description = "在下方输入 Bearer Token,格式:Bearer {token}", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }; + + options.SwaggerGeneratorOptions.SecuritySchemes["Bearer"] = scheme; + options.SwaggerGeneratorOptions.SecurityRequirements.Add(new OpenApiSecurityRequirement + { + { scheme, Array.Empty() } + }); + } + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs new file mode 100644 index 0000000..f0410e0 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Shared.Web.Swagger; + +/// +/// Swagger 文档配置。 +/// +public class SwaggerDocumentSettings +{ + /// + /// 文档标题。 + /// + public string Title { get; set; } = "TakeoutSaaS API"; + + /// + /// 描述信息。 + /// + public string? Description { get; set; } + + /// + /// 是否启用 JWT Authorize 按钮。 + /// + public bool EnableAuthorization { get; set; } = true; +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs new file mode 100644 index 0000000..c192adb --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace TakeoutSaaS.Shared.Web.Swagger; + +/// +/// Swagger 注册/启用扩展。 +/// +public static class SwaggerExtensions +{ + /// + /// 注入统一的 Swagger 服务。 + /// + public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action? configure = null) + { + services.AddSwaggerGen(); + services.AddSingleton(provider => + { + var settings = new SwaggerDocumentSettings(); + configure?.Invoke(settings); + return settings; + }); + services.AddSingleton>(provider => + new ConfigureSwaggerOptions( + provider.GetRequiredService(), + Options.Create(provider.GetRequiredService()))); + + return services; + } + + /// + /// 开发环境启用 Swagger UI(自动注册所有版本)。 + /// + public static IApplicationBuilder UseSharedSwagger(this IApplicationBuilder app) + { + var env = app.ApplicationServices.GetRequiredService(); + if (!env.IsDevelopment()) + { + return app; + } + + var provider = app.ApplicationServices.GetRequiredService(); + var settings = app.ApplicationServices.GetRequiredService(); + + app.UseSwagger(); + app.UseSwaggerUI(options => + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint( + $"/swagger/{description.GroupName}/swagger.json", + $"{settings.Title} {description.ApiVersion}"); + } + + options.DisplayRequestDuration(); + }); + + return app; + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj new file mode 100644 index 0000000..94b4aeb --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj @@ -0,0 +1,23 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj new file mode 100644 index 0000000..2617cb0 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + + + + + + + + From ddf584f212170fd618473c06d427351b733b7783 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 22 Nov 2025 21:27:38 +0800 Subject: [PATCH 02/56] =?UTF-8?q?chore:=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=9F=BA=E7=A1=80=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + 0_Document/01_项目概述.md | 188 ++ 0_Document/02_技术架构.md | 253 +++ 0_Document/03_数据库设计.md | 641 +++++++ 0_Document/04A_管理后台API.md | 93 + 0_Document/04B_小程序API.md | 108 ++ 0_Document/04_API接口设计.md | 885 +++++++++ 0_Document/05_部署运维.md | 976 ++++++++++ 0_Document/06_开发规范.md | 395 ++++ 0_Document/07_系统架构图.md | 321 ++++ 0_Document/08_AI编程规范.md | 1589 +++++++++++++++++ 0_Document/09_AI精简开发规范.md | 79 + 0_Document/README.md | 195 ++ Directory.Build.props | 10 + README.md | 171 +- TakeoutSaaS.sln | 255 +++ .../TakeoutSaaS.AdminApi/Controllers/.gitkeep | 1 + .../Properties/launchSettings.json | 12 + .../TakeoutSaaS.AdminApi.csproj | 21 + .../TakeoutSaaS.MiniApi/Controllers/.gitkeep | 1 + .../Properties/launchSettings.json | 12 + .../TakeoutSaaS.MiniApi.csproj | 21 + .../Properties/launchSettings.json | 12 + .../TakeoutSaaS.UserApi.csproj | 17 + .../TakeoutSaaS.Application/Services/.gitkeep | 1 + .../TakeoutSaaS.Application.csproj | 12 + .../Constants/ErrorCodes.cs | 19 + .../Entities/IAuditableEntity.cs | 11 + .../Exceptions/BusinessException.cs | 20 + .../Exceptions/ValidationException.cs | 22 + .../TakeoutSaaS.Shared.Abstractions.csproj | 8 + .../Tenancy/ITenantProvider.cs | 7 + .../TakeoutSaaS.Shared.Kernel.csproj | 11 + .../Api/BaseApiController.cs | 15 + .../TakeoutSaaS.Domain.csproj | 11 + src/Gateway/TakeoutSaaS.ApiGateway/Program.cs | 53 + .../Properties/launchSettings.json | 12 + .../TakeoutSaaS.ApiGateway.csproj | 11 + .../TakeoutSaaS.Infrastructure.csproj | 17 + .../TakeoutSaaS.Module.Authorization.csproj | 11 + .../TakeoutSaaS.Module.Delivery.csproj | 11 + .../TakeoutSaaS.Module.Dictionary.csproj | 11 + .../Abstractions/IWeChatAuthService.cs | 23 + .../TakeoutSaaS.Module.Identity.csproj | 11 + .../TakeoutSaaS.Module.Messaging.csproj | 14 + .../TakeoutSaaS.Module.Scheduler.csproj | 11 + .../TakeoutSaaS.Module.Sms.csproj | 11 + .../TakeoutSaaS.Module.Storage.csproj | 11 + .../TenantProvider.cs | 39 + 49 files changed, 6629 insertions(+), 15 deletions(-) create mode 100644 .gitignore create mode 100644 0_Document/01_项目概述.md create mode 100644 0_Document/02_技术架构.md create mode 100644 0_Document/03_数据库设计.md create mode 100644 0_Document/04A_管理后台API.md create mode 100644 0_Document/04B_小程序API.md create mode 100644 0_Document/04_API接口设计.md create mode 100644 0_Document/05_部署运维.md create mode 100644 0_Document/06_开发规范.md create mode 100644 0_Document/07_系统架构图.md create mode 100644 0_Document/08_AI编程规范.md create mode 100644 0_Document/09_AI精简开发规范.md create mode 100644 0_Document/README.md create mode 100644 Directory.Build.props create mode 100644 TakeoutSaaS.sln create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep create mode 100644 src/Api/TakeoutSaaS.AdminApi/Properties/launchSettings.json create mode 100644 src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj create mode 100644 src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep create mode 100644 src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json create mode 100644 src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj create mode 100644 src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json create mode 100644 src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj create mode 100644 src/Application/TakeoutSaaS.Application/Services/.gitkeep create mode 100644 src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/Program.cs create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj create mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3857e65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vs/ +bin/ +obj/ +**/bin/ +**/obj/ diff --git a/0_Document/01_项目概述.md b/0_Document/01_项目概述.md new file mode 100644 index 0000000..3d8920c --- /dev/null +++ b/0_Document/01_项目概述.md @@ -0,0 +1,188 @@ +# 外卖SaaS系统 - 项目概述 + +## 1. 项目简介 + +### 1.1 项目背景 +外卖SaaS系统是一个面向餐饮企业的多租户外卖管理平台,旨在为中小型餐饮企业提供完整的外卖业务解决方案。系统支持商家入驻、菜品管理、订单处理、配送管理等核心功能。 + +### 1.2 项目目标 +- 提供稳定、高效的外卖业务管理平台 +- 支持多租户架构,实现数据隔离和资源共享 +- 提供完善的商家管理和运营工具 +- 支持灵活的配送模式(自配送、第三方配送) +- 提供实时数据分析和报表功能 + +### 1.3 核心价值 +- **降低成本**:SaaS模式降低企业IT投入成本 +- **快速上线**:开箱即用,快速开展外卖业务 +- **灵活扩展**:支持业务增长和功能定制 +- **数据驱动**:提供数据分析,辅助经营决策 + +## 2. 业务模块 + +### 2.1 租户管理模块 +- 租户注册与认证 +- 租户信息管理 +- 套餐订阅管理 +- 权限与配额管理 + +### 2.2 商家管理模块 +- 商家入驻审核 +- 商家信息管理 +- 门店管理(支持多门店) +- 营业时间设置 +- 配送范围设置 + +### 2.3 菜品管理模块 +- 菜品分类管理 +- 菜品信息管理(名称、价格、图片、描述) +- 菜品规格管理(大份、小份等) +- 菜品库存管理 +- 菜品上下架管理 + +### 2.4 订单管理模块 +- 订单创建与支付 +- 订单状态流转(待支付、待接单、制作中、配送中、已完成、已取消) +- 订单查询与筛选 +- 订单退款处理 +- 订单统计分析 + +### 2.5 配送管理模块 +- 配送员管理 +- 配送任务分配 +- 配送路线规划 +- 配送状态跟踪 +- 配送费用计算 + +### 2.6 用户管理模块 +- 用户注册与登录 +- 用户信息管理 +- 收货地址管理 +- 用户订单历史 +- 用户评价管理 + +### 2.7 支付管理模块 +- 多支付方式支持(微信、支付宝、余额) +- 支付回调处理 +- 退款处理 +- 账单管理 + +### 2.8 营销管理模块 +- 优惠券管理 +- 满减活动 +- 会员积分 +- 推广活动 + +### 2.9 数据分析模块 +- 销售数据统计 +- 订单趋势分析 +- 用户行为分析 +- 商家经营报表 +- 平台运营大盘 + +### 2.10 系统管理模块 +- 系统配置管理 +- 日志管理 +- 权限管理 +- 消息通知管理 + +## 3. 用户角色 + +### 3.1 平台管理员(Web管理端) +- 管理所有租户和商家 +- 系统配置和维护 +- 数据监控和分析 +- 审核商家入驻 +- 平台运营管理 + +### 3.2 租户管理员(Web管理端) +- 管理租户下的所有商家 +- 查看租户数据报表 +- 管理租户套餐和权限 +- 租户配置管理 + +### 3.3 商家管理员(Web管理端) +- 管理门店信息 +- 管理菜品和订单 +- 查看经营数据 +- 管理配送(自配送或第三方配送对接) +- 营销活动管理 + +### 3.4 商家员工(Web管理端) +- 处理订单(接单/出餐/发货) +- 更新菜品状态 +- 订单打印与出餐看板 + +### 3.5 普通用户/消费者(小程序端 + Web用户端) +- 浏览商家和菜品 +- 下单和支付 +- 查看订单状态 +- 评价和反馈 +- 收货地址管理 +- 优惠券领取和使用 + +## 4. 系统特性 + +### 4.1 多租户架构 +- 数据隔离:每个租户数据完全隔离 +- 资源共享:共享基础设施,降低成本 +- 灵活配置:支持租户级别的个性化配置 + +### 4.2 高可用性 +- 服务高可用:支持集群部署 +- 数据高可用:数据库主从复制 +- 故障自动恢复 + +### 4.3 高性能 +- 缓存策略:Redis缓存热点数据 +- 数据库优化:索引优化、查询优化 +- 异步处理:消息队列处理耗时任务 + +### 4.4 安全性 +- 身份认证:JWT Token认证 +- 权限控制:基于角色的访问控制(RBAC) +- 数据加密:敏感数据加密存储 +- 接口防护:限流、防重放攻击 + +### 4.5 可扩展性 +- 微服务架构:支持服务独立扩展 +- 插件化设计:支持功能模块插拔 +- API开放:提供开放API接口 + +## 5. 技术选型 + +- **后端框架**:.NET 10 +- **ORM框架**:Entity Framework Core 10 + Dapper +- **数据库**:PostgreSQL 16+ +- **缓存**:Redis 7.0+ +- **消息队列**:RabbitMQ 3.12+ +- **API文档**:Swagger/OpenAPI +- **日志**:Serilog +- **认证授权**:JWT + OAuth2.0 + +## 6. 项目里程碑 + +### Phase 1:基础功能(1-2个月) +- 租户管理 +- 商家管理 +- 菜品管理 +- 订单管理(基础流程) + +### Phase 2:核心功能(2-3个月) +- 配送管理 +- 支付集成 +- 用户管理 +- 基础营销功能 + +### Phase 3:高级功能(3-4个月) +- 数据分析 +- 高级营销 +- 系统优化 +- 性能调优 + +### Phase 4:完善与上线(1个月) +- 测试与修复 +- 文档完善 +- 部署上线 +- 运维监控 + diff --git a/0_Document/02_技术架构.md b/0_Document/02_技术架构.md new file mode 100644 index 0000000..e7d1861 --- /dev/null +++ b/0_Document/02_技术架构.md @@ -0,0 +1,253 @@ +# 外卖SaaS系统 - 技术架构 + +## 1. 技术栈 + +### 1.1 后端技术栈 +- **.NET 10**:最新的.NET平台,提供高性能和现代化开发体验 +- **ASP.NET Core Web API**:构建RESTful API服务 +- **Entity Framework Core 10**:最新ORM框架,用于复杂查询和实体管理 +- **Dapper 2.1+**:轻量级ORM,用于高性能查询和批量操作 +- **PostgreSQL 16+**:主数据库,支持JSON、全文搜索等高级特性 +- **Redis 7.0+**:缓存和会话存储 +- **RabbitMQ 3.12+**:消息队列,处理异步任务 + +### 1.2 开发工具和框架 +- **AutoMapper**:对象映射 +- **FluentValidation**:数据验证 +- **Serilog**:结构化日志 +- **MediatR**:CQRS和中介者模式实现 +- **Hangfire**:后台任务调度 +- **Polly**:弹性和瞬态故障处理 +- **Swagger/Swashbuckle**:API文档生成 + +### 1.3 认证授权 +- **JWT (JSON Web Token)**:无状态身份认证 +- **IdentityServer/Duende IdentityServer**:OAuth2.0和OpenID Connect +- **ASP.NET Core Identity**:用户身份管理 + +### 1.4 测试框架 +- **xUnit**:单元测试框架 +- **Moq**:Mock框架 +- **FluentAssertions**:断言库 +- **Testcontainers**:集成测试容器化 + +### 1.5 DevOps工具 +- **Docker**:容器化部署 +- **Docker Compose**:本地开发环境 +- **GitHub Actions/GitLab CI**:CI/CD流水线 +- **Nginx**:反向代理和负载均衡 + +## 2. 系统架构 + +### 2.1 整体架构 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ +│ │ Web管理端 │ │ Web用户端 │ │ 小程序端(用户) │ │ +│ └──────────┘ └──────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ API网关层 │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Nginx / API Gateway (路由、限流、认证、日志) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 应用服务层 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │租户服务 │ │商家服务 │ │订单服务 │ │配送服务 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │用户服务 │ │支付服务 │ │营销服务 │ │通知服务 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 基础设施层 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │PostgreSQL │ │ Redis │ │ RabbitMQ │ │ MinIO │ │ +│ │ (主库) │ │ (缓存) │ │ (消息队列)│ │(对象存储) │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 分层架构 + +#### 2.2.1 表现层 (Presentation Layer) +- **TakeoutSaaS.AdminApi**:管理后台 Web API 项目(/api/admin/v1) + - Controllers:后台管理API控制器 + - Filters:过滤器(异常处理、日志、验证) + - Middleware:中间件(认证、租户识别、RBAC) + - Models:请求/响应DTO +- **TakeoutSaaS.MiniApi**:小程序/用户端 Web API 项目(/api/mini/v1) + - Controllers:用户端API控制器 + - Filters:过滤器(异常处理、限流、签名校验) + - Middleware:中间件(小程序登录态、租户识别、CORS) + - Models:请求/响应DTO + +#### 2.2.2 应用层 (Application Layer) +- **TakeoutSaaS.Application**:应用逻辑 + - Services:应用服务 + - DTOs:数据传输对象 + - Interfaces:服务接口 + - Validators:FluentValidation验证器 + - Mappings:AutoMapper配置 + - Commands/Queries:CQRS命令和查询 + +#### 2.2.3 领域层 (Domain Layer) +- **TakeoutSaaS.Domain**:领域模型 + - Entities:实体类 + - ValueObjects:值对象 + - Enums:枚举 + - Events:领域事件 + - Interfaces:仓储接口 + - Specifications:规约模式 + +#### 2.2.4 基础设施层 (Infrastructure Layer) +- **TakeoutSaaS.Infrastructure**:基础设施实现 + - Data:数据访问 + - EFCore:EF Core DbContext和配置 + - Dapper:Dapper查询实现 + - Repositories:仓储实现 + - Migrations:数据库迁移 + - Cache:Redis缓存实现 + - MessageQueue:RabbitMQ实现 + - ExternalServices:第三方服务集成 + +#### 2.2.5 共享层 (Shared Layer) +- **TakeoutSaaS.Shared**:共享组件 + - Constants:常量定义 + - Exceptions:自定义异常 + - Extensions:扩展方法 + - Helpers:辅助类 + - Results:统一返回结果 + +## 3. 核心设计模式 + +### 3.1 多租户模式 +- **数据隔离策略**:每个租户独立Schema +- **租户识别**:通过HTTP Header或JWT Token识别租户 +- **动态切换**:运行时动态切换数据库连接 + +### 3.2 CQRS模式 +- **命令(Command)**:处理写操作,修改数据 +- **查询(Query)**:处理读操作,不修改数据 +- **分离优势**:读写分离,优化性能 + +### 3.3 仓储模式 +- **抽象数据访问**:统一数据访问接口 +- **EF Core仓储**:复杂查询和事务处理 +- **Dapper仓储**:高性能查询和批量操作 + +### 3.4 工作单元模式 +- **事务管理**:统一管理数据库事务 +- **批量提交**:减少数据库往返次数 + +### 3.5 领域驱动设计(DDD) +- **聚合根**:定义实体边界 +- **值对象**:不可变对象 +- **领域事件**:解耦业务逻辑 + +## 4. 数据访问策略 + +### 4.1 EF Core使用场景 +- 复杂的实体关系查询 +- 需要变更跟踪的操作 +- 事务性操作 +- 数据库迁移管理 + +### 4.2 Dapper使用场景 +- 高性能查询(大数据量) +- 复杂SQL查询 +- 批量插入/更新 +- 报表统计查询 +- 存储过程调用 + +### 4.3 混合使用策略 +```csharp +// EF Core - 复杂查询和实体管理 +public async Task GetOrderWithDetailsAsync(Guid orderId) +{ + return await _dbContext.Orders + .Include(o => o.OrderItems) + .Include(o => o.Customer) + .FirstOrDefaultAsync(o => o.Id == orderId); +} + +// Dapper - 高性能统计查询 +public async Task GetOrderStatisticsAsync(DateTime startDate, DateTime endDate) +{ + var sql = @" + SELECT + COUNT(*) as TotalOrders, + SUM(total_amount) as TotalAmount, + AVG(total_amount) as AvgAmount + FROM orders + WHERE created_at BETWEEN @StartDate AND @EndDate"; + + return await _connection.QueryFirstOrDefaultAsync(sql, + new { StartDate = startDate, EndDate = endDate }); +} +``` + +## 5. 缓存策略 + +### 5.1 缓存层次 +- **L1缓存**:内存缓存(IMemoryCache)- 进程内缓存 +- **L2缓存**:Redis缓存 - 分布式缓存 + +### 5.2 缓存场景 +- 商家信息缓存(30分钟) +- 菜品信息缓存(15分钟) +- 用户会话缓存(2小时) +- 配置信息缓存(1小时) +- 热点数据缓存(动态过期) + +### 5.3 缓存更新策略 +- **Cache-Aside**:旁路缓存,先查缓存,未命中查数据库 +- **Write-Through**:写入时同步更新缓存 +- **Write-Behind**:异步更新缓存 + +## 6. 消息队列应用 + +### 6.1 异步任务 +- 订单状态变更通知 +- 短信/邮件发送 +- 数据统计计算 +- 日志持久化 + +### 6.2 事件驱动 +- 订单创建事件 +- 支付成功事件 +- 配送状态变更事件 + +## 7. 安全设计 + +### 7.1 认证机制 +- JWT Token认证 +- Refresh Token刷新 +- Token过期管理 + +### 7.2 授权机制 +- 基于角色的访问控制(RBAC) +- 基于策略的授权 +- 资源级权限控制 + +### 7.3 数据安全 +- 敏感数据加密(密码、支付信息) +- HTTPS传输加密 +- SQL注入防护 +- XSS防护 + +### 7.4 接口安全 +- 请求签名验证 +- 接口限流(Rate Limiting) +- 防重放攻击 +- CORS跨域配置 + diff --git a/0_Document/03_数据库设计.md b/0_Document/03_数据库设计.md new file mode 100644 index 0000000..93074df --- /dev/null +++ b/0_Document/03_数据库设计.md @@ -0,0 +1,641 @@ +# 外卖SaaS系统 - 数据库设计 + +## 1. 数据库设计原则 + +### 1.1 命名规范 +- **表名**:小写字母,下划线分隔,复数形式(如:`orders`, `order_items`) +- **字段名**:小写字母,下划线分隔(如:`created_at`, `total_amount`) +- **主键**:统一使用 `id`,类型为 UUID +- **外键**:`表名_id`(如:`order_id`, `merchant_id`) +- **索引**:`idx_表名_字段名`(如:`idx_orders_merchant_id`) + +### 1.2 通用字段 +所有表都包含以下字段: +- `id`:UUID,主键 +- `created_at`:TIMESTAMP,创建时间 +- `updated_at`:TIMESTAMP,更新时间 +- `deleted_at`:TIMESTAMP,软删除时间(可选) +- `tenant_id`:UUID,租户ID(多租户隔离) + +### 1.3 数据类型规范 +- **金额**:DECIMAL(18,2) +- **时间**:TIMESTAMP WITH TIME ZONE +- **布尔**:BOOLEAN +- **枚举**:VARCHAR 或 INTEGER +- **JSON数据**:JSONB + +## 2. 核心表结构 + +### 2.1 租户管理 + +#### tenants(租户表) +```sql +CREATE TABLE tenants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + code VARCHAR(50) UNIQUE NOT NULL, + contact_name VARCHAR(50), + contact_phone VARCHAR(20), + contact_email VARCHAR(100), + status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:冻结 3:过期 + subscription_plan VARCHAR(50), -- 订阅套餐 + subscription_start_date TIMESTAMP WITH TIME ZONE, + subscription_end_date TIMESTAMP WITH TIME ZONE, + max_merchants INTEGER DEFAULT 10, -- 最大商家数 + max_orders_per_day INTEGER DEFAULT 1000, -- 每日订单限额 + settings JSONB, -- 租户配置 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_tenants_code ON tenants(code); +CREATE INDEX idx_tenants_status ON tenants(status); +``` + +### 2.2 商家管理 + +#### merchants(商家表) +```sql +CREATE TABLE merchants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + name VARCHAR(100) NOT NULL, + logo_url VARCHAR(500), + description TEXT, + contact_phone VARCHAR(20), + contact_person VARCHAR(50), + business_license VARCHAR(100), -- 营业执照号 + status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:休息 3:停业 + rating DECIMAL(3,2) DEFAULT 0, -- 评分 + total_sales INTEGER DEFAULT 0, -- 总销量 + settings JSONB, -- 商家配置 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_merchants_tenant_id ON merchants(tenant_id); +CREATE INDEX idx_merchants_status ON merchants(status); +``` + +#### merchant_stores(门店表) +```sql +CREATE TABLE merchant_stores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + merchant_id UUID NOT NULL REFERENCES merchants(id), + name VARCHAR(100) NOT NULL, + address VARCHAR(500) NOT NULL, + latitude DECIMAL(10,7), -- 纬度 + longitude DECIMAL(10,7), -- 经度 + phone VARCHAR(20), + business_hours JSONB, -- 营业时间 {"monday": {"open": "09:00", "close": "22:00"}} + delivery_range INTEGER DEFAULT 3000, -- 配送范围(米) + min_order_amount DECIMAL(18,2) DEFAULT 0, -- 起送价 + delivery_fee DECIMAL(18,2) DEFAULT 0, -- 配送费 + status INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_merchant_stores_merchant_id ON merchant_stores(merchant_id); +CREATE INDEX idx_merchant_stores_location ON merchant_stores USING GIST(point(longitude, latitude)); +``` + +### 2.3 菜品管理 + +#### categories(菜品分类表) +```sql +CREATE TABLE categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + merchant_id UUID NOT NULL REFERENCES merchants(id), + name VARCHAR(50) NOT NULL, + sort_order INTEGER DEFAULT 0, + status INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_categories_merchant_id ON categories(merchant_id); +``` + +#### dishes(菜品表) +```sql +CREATE TABLE dishes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + merchant_id UUID NOT NULL REFERENCES merchants(id), + category_id UUID REFERENCES categories(id), + name VARCHAR(100) NOT NULL, + description TEXT, + image_url VARCHAR(500), + price DECIMAL(18,2) NOT NULL, + original_price DECIMAL(18,2), -- 原价 + unit VARCHAR(20) DEFAULT '份', -- 单位 + stock INTEGER, -- 库存(NULL表示不限) + sales_count INTEGER DEFAULT 0, -- 销量 + rating DECIMAL(3,2) DEFAULT 0, -- 评分 + sort_order INTEGER DEFAULT 0, + status INTEGER NOT NULL DEFAULT 1, -- 1:上架 2:下架 + tags JSONB, -- 标签 ["热销", "新品"] + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_dishes_merchant_id ON dishes(merchant_id); +CREATE INDEX idx_dishes_category_id ON dishes(category_id); +CREATE INDEX idx_dishes_status ON dishes(status); +``` + +#### dish_specs(菜品规格表) +```sql +CREATE TABLE dish_specs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + dish_id UUID NOT NULL REFERENCES dishes(id), + name VARCHAR(50) NOT NULL, -- 规格名称(如:大份、小份) + price DECIMAL(18,2) NOT NULL, + stock INTEGER, + status INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_dish_specs_dish_id ON dish_specs(dish_id); +``` + +### 2.4 用户管理 + +#### users(用户表) +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + phone VARCHAR(20) UNIQUE NOT NULL, + nickname VARCHAR(50), + avatar_url VARCHAR(500), + gender INTEGER, -- 0:未知 1:男 2:女 + birthday DATE, + balance DECIMAL(18,2) DEFAULT 0, -- 余额 + points INTEGER DEFAULT 0, -- 积分 + status INTEGER NOT NULL DEFAULT 1, + last_login_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_users_phone ON users(phone); +``` + +#### user_addresses(用户地址表) +```sql +CREATE TABLE user_addresses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + contact_name VARCHAR(50) NOT NULL, + contact_phone VARCHAR(20) NOT NULL, + province VARCHAR(50), + city VARCHAR(50), + district VARCHAR(50), + address VARCHAR(500) NOT NULL, + house_number VARCHAR(50), -- 门牌号 + latitude DECIMAL(10,7), + longitude DECIMAL(10,7), + is_default BOOLEAN DEFAULT FALSE, + label VARCHAR(20), -- 标签:家、公司等 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_user_addresses_user_id ON user_addresses(user_id); +``` + +### 2.5 订单管理 + +#### orders(订单表) +```sql +CREATE TABLE orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + order_no VARCHAR(50) UNIQUE NOT NULL, -- 订单号 + merchant_id UUID NOT NULL REFERENCES merchants(id), + store_id UUID NOT NULL REFERENCES merchant_stores(id), + user_id UUID NOT NULL REFERENCES users(id), + + -- 收货信息 + delivery_address VARCHAR(500) NOT NULL, + delivery_latitude DECIMAL(10,7), + delivery_longitude DECIMAL(10,7), + contact_name VARCHAR(50) NOT NULL, + contact_phone VARCHAR(20) NOT NULL, + + -- 金额信息 + dish_amount DECIMAL(18,2) NOT NULL, -- 菜品金额 + delivery_fee DECIMAL(18,2) DEFAULT 0, -- 配送费 + package_fee DECIMAL(18,2) DEFAULT 0, -- 打包费 + discount_amount DECIMAL(18,2) DEFAULT 0, -- 优惠金额 + total_amount DECIMAL(18,2) NOT NULL, -- 总金额 + actual_amount DECIMAL(18,2) NOT NULL, -- 实付金额 + + -- 订单状态 + status INTEGER NOT NULL DEFAULT 1, -- 1:待支付 2:待接单 3:制作中 4:待配送 5:配送中 6:已完成 7:已取消 + payment_status INTEGER DEFAULT 0, -- 0:未支付 1:已支付 2:已退款 + payment_method VARCHAR(20), -- 支付方式 + payment_time TIMESTAMP WITH TIME ZONE, + + -- 时间信息 + estimated_delivery_time TIMESTAMP WITH TIME ZONE, -- 预计送达时间 + accepted_at TIMESTAMP WITH TIME ZONE, -- 接单时间 + cooking_at TIMESTAMP WITH TIME ZONE, -- 开始制作时间 + delivered_at TIMESTAMP WITH TIME ZONE, -- 送达时间 + completed_at TIMESTAMP WITH TIME ZONE, -- 完成时间 + cancelled_at TIMESTAMP WITH TIME ZONE, -- 取消时间 + + remark TEXT, -- 备注 + cancel_reason TEXT, -- 取消原因 + + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_orders_tenant_id ON orders(tenant_id); +CREATE INDEX idx_orders_order_no ON orders(order_no); +CREATE INDEX idx_orders_merchant_id ON orders(merchant_id); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_orders_created_at ON orders(created_at); +``` + +#### order_items(订单明细表) +```sql +CREATE TABLE order_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + order_id UUID NOT NULL REFERENCES orders(id), + dish_id UUID NOT NULL REFERENCES dishes(id), + dish_name VARCHAR(100) NOT NULL, -- 冗余字段,防止菜品被删除 + dish_image_url VARCHAR(500), + spec_id UUID REFERENCES dish_specs(id), + spec_name VARCHAR(50), + price DECIMAL(18,2) NOT NULL, -- 单价 + quantity INTEGER NOT NULL, -- 数量 + amount DECIMAL(18,2) NOT NULL, -- 小计 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_order_items_order_id ON order_items(order_id); +CREATE INDEX idx_order_items_dish_id ON order_items(dish_id); +``` + +### 2.6 配送管理 + +#### delivery_drivers(配送员表) +```sql +CREATE TABLE delivery_drivers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + merchant_id UUID REFERENCES merchants(id), -- NULL表示平台配送员 + name VARCHAR(50) NOT NULL, + phone VARCHAR(20) UNIQUE NOT NULL, + id_card VARCHAR(18), -- 身份证号 + vehicle_type VARCHAR(20), -- 车辆类型:电动车、摩托车 + vehicle_number VARCHAR(20), -- 车牌号 + status INTEGER NOT NULL DEFAULT 1, -- 1:空闲 2:配送中 3:休息 4:离线 + current_latitude DECIMAL(10,7), -- 当前位置 + current_longitude DECIMAL(10,7), + rating DECIMAL(3,2) DEFAULT 0, + total_deliveries INTEGER DEFAULT 0, -- 总配送单数 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_delivery_drivers_merchant_id ON delivery_drivers(merchant_id); +CREATE INDEX idx_delivery_drivers_status ON delivery_drivers(status); +``` + +#### delivery_tasks(配送任务表) +```sql +CREATE TABLE delivery_tasks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + order_id UUID NOT NULL REFERENCES orders(id), + driver_id UUID REFERENCES delivery_drivers(id), + pickup_address VARCHAR(500) NOT NULL, -- 取餐地址 + pickup_latitude DECIMAL(10,7), + pickup_longitude DECIMAL(10,7), + delivery_address VARCHAR(500) NOT NULL, -- 送餐地址 + delivery_latitude DECIMAL(10,7), + delivery_longitude DECIMAL(10,7), + distance INTEGER, -- 配送距离(米) + estimated_time INTEGER, -- 预计时长(分钟) + status INTEGER NOT NULL DEFAULT 1, -- 1:待分配 2:待取餐 3:配送中 4:已送达 5:异常 + assigned_at TIMESTAMP WITH TIME ZONE, -- 分配时间 + picked_at TIMESTAMP WITH TIME ZONE, -- 取餐时间 + delivered_at TIMESTAMP WITH TIME ZONE, -- 送达时间 + delivery_fee DECIMAL(18,2) DEFAULT 0, -- 配送费 + remark TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_delivery_tasks_order_id ON delivery_tasks(order_id); +CREATE INDEX idx_delivery_tasks_driver_id ON delivery_tasks(driver_id); +CREATE INDEX idx_delivery_tasks_status ON delivery_tasks(status); +``` + +### 2.7 支付管理 + +#### payments(支付记录表) +```sql +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + order_id UUID NOT NULL REFERENCES orders(id), + user_id UUID NOT NULL REFERENCES users(id), + payment_no VARCHAR(50) UNIQUE NOT NULL, -- 支付单号 + payment_method VARCHAR(20) NOT NULL, -- 支付方式:wechat、alipay、balance + amount DECIMAL(18,2) NOT NULL, + status INTEGER NOT NULL DEFAULT 0, -- 0:待支付 1:支付中 2:成功 3:失败 4:已退款 + third_party_no VARCHAR(100), -- 第三方支付单号 + paid_at TIMESTAMP WITH TIME ZONE, + callback_data JSONB, -- 回调数据 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_payments_order_id ON payments(order_id); +CREATE INDEX idx_payments_payment_no ON payments(payment_no); +CREATE INDEX idx_payments_user_id ON payments(user_id); +``` + +#### refunds(退款记录表) +```sql +CREATE TABLE refunds ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + order_id UUID NOT NULL REFERENCES orders(id), + payment_id UUID NOT NULL REFERENCES payments(id), + refund_no VARCHAR(50) UNIQUE NOT NULL, + amount DECIMAL(18,2) NOT NULL, + reason TEXT, + status INTEGER NOT NULL DEFAULT 0, -- 0:待审核 1:退款中 2:成功 3:失败 + third_party_no VARCHAR(100), + refunded_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_refunds_order_id ON refunds(order_id); +CREATE INDEX idx_refunds_payment_id ON refunds(payment_id); +``` + +### 2.8 营销管理 + +#### coupons(优惠券表) +```sql +CREATE TABLE coupons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + merchant_id UUID REFERENCES merchants(id), -- NULL表示平台券 + name VARCHAR(100) NOT NULL, + type INTEGER NOT NULL, -- 1:满减券 2:折扣券 3:代金券 + discount_type INTEGER NOT NULL, -- 1:固定金额 2:百分比 + discount_value DECIMAL(18,2) NOT NULL, -- 优惠值 + min_order_amount DECIMAL(18,2) DEFAULT 0, -- 最低消费 + max_discount_amount DECIMAL(18,2), -- 最大优惠金额(折扣券用) + total_quantity INTEGER NOT NULL, -- 总数量 + received_quantity INTEGER DEFAULT 0, -- 已领取数量 + used_quantity INTEGER DEFAULT 0, -- 已使用数量 + valid_start_time TIMESTAMP WITH TIME ZONE NOT NULL, + valid_end_time TIMESTAMP WITH TIME ZONE NOT NULL, + status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:停用 + description TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_coupons_merchant_id ON coupons(merchant_id); +CREATE INDEX idx_coupons_status ON coupons(status); +``` + +#### user_coupons(用户优惠券表) +```sql +CREATE TABLE user_coupons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + coupon_id UUID NOT NULL REFERENCES coupons(id), + status INTEGER NOT NULL DEFAULT 1, -- 1:未使用 2:已使用 3:已过期 + used_order_id UUID REFERENCES orders(id), + received_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + used_at TIMESTAMP WITH TIME ZONE, + expired_at TIMESTAMP WITH TIME ZONE NOT NULL +); + +CREATE INDEX idx_user_coupons_user_id ON user_coupons(user_id); +CREATE INDEX idx_user_coupons_coupon_id ON user_coupons(coupon_id); +CREATE INDEX idx_user_coupons_status ON user_coupons(status); +``` + +### 2.9 评价管理 + +#### reviews(评价表) +```sql +CREATE TABLE reviews ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id), + order_id UUID NOT NULL REFERENCES orders(id), + user_id UUID NOT NULL REFERENCES users(id), + merchant_id UUID NOT NULL REFERENCES merchants(id), + rating INTEGER NOT NULL, -- 评分 1-5 + taste_rating INTEGER, -- 口味评分 + package_rating INTEGER, -- 包装评分 + delivery_rating INTEGER, -- 配送评分 + content TEXT, + images JSONB, -- 评价图片 + is_anonymous BOOLEAN DEFAULT FALSE, + reply_content TEXT, -- 商家回复 + reply_at TIMESTAMP WITH TIME ZONE, + status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:隐藏 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_reviews_order_id ON reviews(order_id); +CREATE INDEX idx_reviews_user_id ON reviews(user_id); +CREATE INDEX idx_reviews_merchant_id ON reviews(merchant_id); +``` + +### 2.10 系统管理 + +#### system_users(系统用户表) +```sql +CREATE TABLE system_users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id), -- NULL表示平台管理员 + merchant_id UUID REFERENCES merchants(id), -- NULL表示租户管理员 + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + real_name VARCHAR(50), + phone VARCHAR(20), + email VARCHAR(100), + role_id UUID REFERENCES roles(id), + status INTEGER NOT NULL DEFAULT 1, + last_login_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_system_users_username ON system_users(username); +CREATE INDEX idx_system_users_tenant_id ON system_users(tenant_id); +``` + +#### roles(角色表) +```sql +CREATE TABLE roles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id), + name VARCHAR(50) NOT NULL, + code VARCHAR(50) NOT NULL, + description TEXT, + permissions JSONB, -- 权限列表 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_roles_tenant_id ON roles(tenant_id); +``` + +#### operation_logs(操作日志表) +```sql +CREATE TABLE operation_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id), + user_id UUID, + user_type VARCHAR(20), -- system_user, merchant_user, customer + module VARCHAR(50), -- 模块 + action VARCHAR(50), -- 操作 + description TEXT, + ip_address VARCHAR(50), + user_agent TEXT, + request_data JSONB, + response_data JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_operation_logs_tenant_id ON operation_logs(tenant_id); +CREATE INDEX idx_operation_logs_user_id ON operation_logs(user_id); +CREATE INDEX idx_operation_logs_created_at ON operation_logs(created_at); +``` + +## 3. 数据库索引策略 + +### 3.1 主键索引 +- 所有表使用UUID作为主键,自动创建主键索引 + +### 3.2 外键索引 +- 所有外键字段创建索引,提升关联查询性能 + +### 3.3 业务索引 +- 订单号、支付单号等唯一业务字段创建唯一索引 +- 状态字段创建普通索引 +- 时间字段(created_at)创建索引,支持时间范围查询 + +### 3.4 复合索引 +```sql +-- 订单查询常用复合索引 +CREATE INDEX idx_orders_merchant_status_created ON orders(merchant_id, status, created_at DESC); + +-- 用户订单查询 +CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC); +``` + +### 3.5 地理位置索引 +```sql +-- 使用PostGIS扩展支持地理位置查询 +CREATE EXTENSION IF NOT EXISTS postgis; + +-- 门店位置索引 +CREATE INDEX idx_merchant_stores_location ON merchant_stores + USING GIST(ST_MakePoint(longitude, latitude)); +``` + +## 4. 数据库优化 + +### 4.1 分区策略 +```sql +-- 订单表按月分区 +CREATE TABLE orders_2024_01 PARTITION OF orders + FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); + +CREATE TABLE orders_2024_02 PARTITION OF orders + FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); +``` + +### 4.2 物化视图 +```sql +-- 商家统计物化视图 +CREATE MATERIALIZED VIEW merchant_statistics AS +SELECT + m.id as merchant_id, + m.name, + COUNT(DISTINCT o.id) as total_orders, + SUM(o.actual_amount) as total_revenue, + AVG(r.rating) as avg_rating +FROM merchants m +LEFT JOIN orders o ON m.id = o.merchant_id AND o.status = 6 +LEFT JOIN reviews r ON m.id = r.merchant_id +GROUP BY m.id, m.name; + +CREATE UNIQUE INDEX ON merchant_statistics(merchant_id); +``` + +### 4.3 查询优化建议 +- 避免SELECT *,只查询需要的字段 +- 使用EXPLAIN分析查询计划 +- 合理使用JOIN,避免过多关联 +- 大数据量查询使用分页 +- 使用prepared statement防止SQL注入 + +## 5. 数据备份策略 + +### 5.1 备份方案 +- **全量备份**:每天凌晨2点执行 +- **增量备份**:每4小时执行一次 +- **WAL归档**:实时归档,支持PITR + +### 5.2 备份脚本示例 +```bash +#!/bin/bash +# 全量备份 +pg_dump -h localhost -U postgres -d takeout_saas -F c -f /backup/full_$(date +%Y%m%d).dump + +# 保留最近30天的备份 +find /backup -name "full_*.dump" -mtime +30 -delete +``` + +## 6. 数据迁移 + +### 6.1 EF Core Migrations +```bash +# 添加迁移 +dotnet ef migrations add InitialCreate --project TakeoutSaaS.Infrastructure + +# 更新数据库 +dotnet ef database update --project TakeoutSaaS.Infrastructure +``` + +### 6.2 版本控制 +- 所有数据库变更通过Migration管理 +- Migration文件纳入版本控制 +- 生产环境变更需要审核 + diff --git a/0_Document/04A_管理后台API.md b/0_Document/04A_管理后台API.md new file mode 100644 index 0000000..9a2d7cf --- /dev/null +++ b/0_Document/04A_管理后台API.md @@ -0,0 +1,93 @@ +# 管理后台 API 设计(Admin API) + +- 项目:TakeoutSaaS.AdminApi +- 版本前缀:/api/admin/v1 +- 认证:JWT + RBAC(平台、租户、商家角色) +- 租户识别:X-Tenant-Id 头或 Token Claim + +## 1. 通用规范 +- Content-Type: application/json +- 成功响应 +{ + "success": true, + "code": 200, + "message": "OK", + "data": {} +} +- 失败响应 +{ + "success": false, + "code": 422, + "message": "业务异常" +} + +## 2. 认证与权限 +- POST /api/admin/v1/auth/login +- POST /api/admin/v1/auth/refresh +- GET /api/admin/v1/auth/profile +- 角色:PlatformAdmin、TenantAdmin、MerchantAdmin、Staff + +## 3. 租户与商家管理 +- 租户 + - GET /api/admin/v1/tenants + - POST /api/admin/v1/tenants + - PUT /api/admin/v1/tenants/{id} + - PATCH/api/admin/v1/tenants/{id}/status +- 商家 + - GET /api/admin/v1/merchants + - POST /api/admin/v1/merchants + - GET /api/admin/v1/merchants/{id} + - PUT /api/admin/v1/merchants/{id} + - DELETE /api/admin/v1/merchants/{id} +- 门店 + - GET /api/admin/v1/stores + - POST /api/admin/v1/stores + +## 4. 菜品管理 +- 分类 + - GET /api/admin/v1/categories + - POST /api/admin/v1/categories + - PUT /api/admin/v1/categories/{id} + - DELETE /api/admin/v1/categories/{id} +- 菜品 + - GET /api/admin/v1/dishes + - POST /api/admin/v1/dishes + - GET /api/admin/v1/dishes/{id} + - PUT /api/admin/v1/dishes/{id} + - PATCH/api/admin/v1/dishes/batch-status + +## 5. 订单与售后 +- 订单 + - GET /api/admin/v1/orders + - GET /api/admin/v1/orders/{id} + - POST /api/admin/v1/orders/{id}/accept + - POST /api/admin/v1/orders/{id}/cook + - POST /api/admin/v1/orders/{id}/deliver + - POST /api/admin/v1/orders/{id}/complete + - POST /api/admin/v1/orders/{id}/cancel +- 售后 + - GET /api/admin/v1/refunds + - POST /api/admin/v1/refunds/{id}/approve + - POST /api/admin/v1/refunds/{id}/reject + +## 6. 营销与用户运营 +- 优惠券 + - GET /api/admin/v1/coupons + - POST /api/admin/v1/coupons + - PUT /api/admin/v1/coupons/{id} + - PATCH/api/admin/v1/coupons/{id}/status +- 评价 + - GET /api/admin/v1/reviews + - POST /api/admin/v1/reviews/{id}/reply + +## 7. 统计报表 +- GET /api/admin/v1/statistics/merchant/overview?merchantId= +- GET /api/admin/v1/statistics/platform/overview + +## 8. 文件上传 +- POST /api/admin/v1/files/upload (multipart/form-data) + +## 9. WebSocket(可选) +- ws://{host}/ws/admin?token=xxx +- 主题:order.new、order.status、refund.updated + diff --git a/0_Document/04B_小程序API.md b/0_Document/04B_小程序API.md new file mode 100644 index 0000000..6a1e340 --- /dev/null +++ b/0_Document/04B_小程序API.md @@ -0,0 +1,108 @@ +# 小程序/用户端 API 设计(Mini API) + +- 项目:TakeoutSaaS.MiniApi +- 版本前缀:/api/mini/v1 +- 认证:JWT(小程序登录态)/ 第三方登录(微信/支付宝) +- 租户识别:X-Tenant-Id 头或域名/小程序场景参数 + +## 1. 通用规范 +- Content-Type: application/json +- 成功响应 +{ + "success": true, + "code": 200, + "message": "OK", + "data": {} +} + +## 2. 认证登录 +- 微信登录 + - POST /api/mini/v1/auth/wechat/login + - { code, encryptedData?, iv? } +- 刷新Token + - POST /api/mini/v1/auth/refresh +- 获取用户信息 + - GET /api/mini/v1/me + +## 3. 商家与门店 +- 获取推荐商家 + - GET /api/mini/v1/merchants/recommend?lat=&lng=&pageIndex=&pageSize= +- 商家详情(含门店与公告) + - GET /api/mini/v1/merchants/{id} +- 门店列表(按距离) + - GET /api/mini/v1/merchants/{id}/stores?lat=&lng= + +## 4. 菜品与分类 +- 分类列表 + - GET /api/mini/v1/categories?merchantId= +- 菜品列表 + - GET /api/mini/v1/dishes?merchantId=&categoryId=&keyword=&sort= +- 菜品详情 + - GET /api/mini/v1/dishes/{id} + +## 5. 购物车 +- 获取购物车 + - GET /api/mini/v1/cart?merchantId= +- 同步购物车(幂等) + - PUT /api/mini/v1/cart + - { merchantId, items:[{dishId,specId?,quantity}] } +- 清空购物车 + - DELETE /api/mini/v1/cart?merchantId= + +## 6. 地址簿 +- 地址列表 + - GET /api/mini/v1/addresses +- 新增地址 + - POST /api/mini/v1/addresses +- 更新地址 + - PUT /api/mini/v1/addresses/{id} +- 删除地址 + - DELETE /api/mini/v1/addresses/{id} +- 设为默认地址 + - POST /api/mini/v1/addresses/{id}/default + +## 7. 订单 +- 创建订单(下单) + - POST /api/mini/v1/orders + - { merchantId, storeId, items:[{dishId,specId?,quantity}], addressId, remark?, couponId? } +- 订单列表 + - GET /api/mini/v1/orders?status=&pageIndex=&pageSize= +- 订单详情 + - GET /api/mini/v1/orders/{id} +- 取消订单 + - POST /api/mini/v1/orders/{id}/cancel { reason } +- 再来一单 + - POST /api/mini/v1/orders/{id}/reorder + +## 8. 支付 +- 预下单(获取支付参数) + - POST /api/mini/v1/payments + - { orderId, method: wechat|alipay } +- 查询支付状态 + - GET /api/mini/v1/payments/{paymentNo} +- 第三方回调(回调专用) + - POST /api/mini/v1/payments/callback/wechat + - POST /api/mini/v1/payments/callback/alipay + +## 9. 优惠券 +- 可领取优惠券列表 + - GET /api/mini/v1/coupons/available?merchantId= +- 领取优惠券 + - POST /api/mini/v1/coupons/{id}/receive +- 我的优惠券 + - GET /api/mini/v1/user-coupons?status= + +## 10. 评价 +- 发表评价 + - POST /api/mini/v1/reviews { orderId, rating, content?, images?[] } +- 商家评价列表 + - GET /api/mini/v1/reviews?merchantId=&rating=&page= + +## 11. 文件上传 +- 上传评价图片/头像 + - POST /api/mini/v1/files/upload (multipart/form-data) + +## 12. WebSocket(可选) +- ws://{host}/ws/mini?token=xxx +- 主题:order.status, payment.success + diff --git a/0_Document/04_API接口设计.md b/0_Document/04_API接口设计.md new file mode 100644 index 0000000..1f3735d --- /dev/null +++ b/0_Document/04_API接口设计.md @@ -0,0 +1,885 @@ +# 外卖SaaS系统 - API接口设计 + +## 1. API设计规范 + +### 1.1 RESTful规范 +- 使用标准HTTP方法:GET、POST、PUT、DELETE、PATCH +- URL使用名词复数形式,如:`/api/orders` +- 使用HTTP状态码表示请求结果 +- 版本控制:`/api/v1/orders` + +### 1.2 请求规范 +- **Content-Type**:`application/json` +- **认证方式**:Bearer Token (JWT) +- **租户识别**:通过Header `X-Tenant-Id` 或从Token中解析 + +### 1.3 响应规范 +```json +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": {}, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +### 1.4 错误响应 +```json +{ + "success": false, + "code": 400, + "message": "参数错误", + "errors": [ + { + "field": "phone", + "message": "手机号格式不正确" + } + ], + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +### 1.5 HTTP状态码 +- **200 OK**:请求成功 +- **201 Created**:创建成功 +- **204 No Content**:删除成功 +- **400 Bad Request**:参数错误 +- **401 Unauthorized**:未认证 +- **403 Forbidden**:无权限 +- **404 Not Found**:资源不存在 +- **409 Conflict**:资源冲突 +- **422 Unprocessable Entity**:业务逻辑错误 +- **500 Internal Server Error**:服务器错误 + +### 1.6 分页规范 +```json +// 请求参数 +{ + "pageIndex": 1, + "pageSize": 20, + "sortBy": "createdAt", + "sortOrder": "desc" +} + +// 响应格式 +{ + "success": true, + "data": { + "items": [], + "totalCount": 100, + "pageIndex": 1, + "pageSize": 20, + "totalPages": 5 + } +} +``` + +## 2. 认证授权接口 + +### 2.1 用户登录 +```http +POST /api/v1/auth/login +Content-Type: application/json + +{ + "phone": "13800138000", + "password": "password123", + "loginType": "customer" // customer, merchant, system +} + +Response: +{ + "success": true, + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "eyJhbGciOiJIUzI1NiIs...", + "expiresIn": 7200, + "tokenType": "Bearer", + "userInfo": { + "id": "uuid", + "phone": "13800138000", + "nickname": "张三", + "avatar": "https://..." + } + } +} +``` + +### 2.2 刷新Token +```http +POST /api/v1/auth/refresh +Content-Type: application/json + +{ + "refreshToken": "eyJhbGciOiJIUzI1NiIs..." +} + +Response: +{ + "success": true, + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "expiresIn": 7200 + } +} +``` + +### 2.3 用户注册 +```http +POST /api/v1/auth/register +Content-Type: application/json + +{ + "phone": "13800138000", + "password": "password123", + "verificationCode": "123456", + "nickname": "张三" +} +``` + +### 2.4 发送验证码 +```http +POST /api/v1/auth/send-code +Content-Type: application/json + +{ + "phone": "13800138000", + "type": "register" // register, login, reset_password +} +``` + +## 3. 商家管理接口 + +### 3.1 获取商家列表 +```http +GET /api/v1/merchants?pageIndex=1&pageSize=20&keyword=&status=1 +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "items": [ + { + "id": "uuid", + "name": "美味餐厅", + "logo": "https://...", + "rating": 4.5, + "totalSales": 1000, + "status": 1, + "createdAt": "2024-01-01T12:00:00Z" + } + ], + "totalCount": 50, + "pageIndex": 1, + "pageSize": 20 + } +} +``` + +### 3.2 获取商家详情 +```http +GET /api/v1/merchants/{id} +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "id": "uuid", + "name": "美味餐厅", + "logo": "https://...", + "description": "专注美食20年", + "contactPhone": "400-123-4567", + "rating": 4.5, + "totalSales": 1000, + "status": 1, + "stores": [ + { + "id": "uuid", + "name": "总店", + "address": "北京市朝阳区...", + "phone": "010-12345678" + } + ] + } +} +``` + +### 3.3 创建商家 +```http +POST /api/v1/merchants +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "美味餐厅", + "logo": "https://...", + "description": "专注美食20年", + "contactPhone": "400-123-4567", + "contactPerson": "张三", + "businessLicense": "91110000..." +} +``` + +### 3.4 更新商家信息 +```http +PUT /api/v1/merchants/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "美味餐厅", + "logo": "https://...", + "description": "专注美食20年" +} +``` + +### 3.5 删除商家 +```http +DELETE /api/v1/merchants/{id} +Authorization: Bearer {token} +``` + +## 4. 菜品管理接口 + +### 4.1 获取菜品列表 +```http +GET /api/v1/dishes?merchantId={merchantId}&categoryId={categoryId}&keyword=&status=1&pageIndex=1&pageSize=20 +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "items": [ + { + "id": "uuid", + "name": "宫保鸡丁", + "description": "经典川菜", + "image": "https://...", + "price": 38.00, + "originalPrice": 48.00, + "salesCount": 500, + "rating": 4.8, + "status": 1, + "tags": ["热销", "招牌菜"] + } + ], + "totalCount": 100 + } +} +``` + +### 4.2 获取菜品详情 +```http +GET /api/v1/dishes/{id} +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "id": "uuid", + "name": "宫保鸡丁", + "description": "经典川菜,选用优质鸡肉...", + "image": "https://...", + "price": 38.00, + "originalPrice": 48.00, + "unit": "份", + "stock": 100, + "salesCount": 500, + "rating": 4.8, + "status": 1, + "tags": ["热销", "招牌菜"], + "specs": [ + { + "id": "uuid", + "name": "大份", + "price": 48.00, + "stock": 50 + }, + { + "id": "uuid", + "name": "小份", + "price": 28.00, + "stock": 50 + } + ] + } +} +``` + +### 4.3 创建菜品 +```http +POST /api/v1/dishes +Authorization: Bearer {token} +Content-Type: application/json + +{ + "merchantId": "uuid", + "categoryId": "uuid", + "name": "宫保鸡丁", + "description": "经典川菜", + "image": "https://...", + "price": 38.00, + "originalPrice": 48.00, + "unit": "份", + "stock": 100, + "tags": ["热销", "招牌菜"], + "specs": [ + { + "name": "大份", + "price": 48.00, + "stock": 50 + } + ] +} +``` + +### 4.4 更新菜品 +```http +PUT /api/v1/dishes/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "宫保鸡丁", + "price": 38.00, + "stock": 100, + "status": 1 +} +``` + +### 4.5 批量上下架 +```http +PATCH /api/v1/dishes/batch-status +Authorization: Bearer {token} +Content-Type: application/json + +{ + "dishIds": ["uuid1", "uuid2"], + "status": 1 // 1:上架 2:下架 +} +``` + +## 5. 订单管理接口 + +### 5.1 创建订单 +```http +POST /api/v1/orders +Authorization: Bearer {token} +Content-Type: application/json + +{ + "merchantId": "uuid", + "storeId": "uuid", + "items": [ + { + "dishId": "uuid", + "specId": "uuid", + "quantity": 2, + "price": 38.00 + } + ], + "deliveryAddress": { + "contactName": "张三", + "contactPhone": "13800138000", + "address": "北京市朝阳区...", + "latitude": 39.9042, + "longitude": 116.4074 + }, + "remark": "少辣", + "couponId": "uuid" +} + +Response: +{ + "success": true, + "data": { + "orderId": "uuid", + "orderNo": "202401010001", + "totalAmount": 76.00, + "deliveryFee": 5.00, + "discountAmount": 10.00, + "actualAmount": 71.00, + "paymentInfo": { + "paymentNo": "PAY202401010001", + "qrCode": "https://..." // 支付二维码 + } + } +} +``` + +### 5.2 获取订单列表 +```http +GET /api/v1/orders?status=&startDate=&endDate=&pageIndex=1&pageSize=20 +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "items": [ + { + "id": "uuid", + "orderNo": "202401010001", + "merchantName": "美味餐厅", + "totalAmount": 76.00, + "actualAmount": 71.00, + "status": 2, + "statusText": "待接单", + "createdAt": "2024-01-01T12:00:00Z", + "items": [ + { + "dishName": "宫保鸡丁", + "specName": "大份", + "quantity": 2, + "price": 38.00 + } + ] + } + ], + "totalCount": 50 + } +} +``` + +### 5.3 获取订单详情 +```http +GET /api/v1/orders/{id} +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "id": "uuid", + "orderNo": "202401010001", + "merchant": { + "id": "uuid", + "name": "美味餐厅", + "phone": "400-123-4567" + }, + "items": [ + { + "dishName": "宫保鸡丁", + "dishImage": "https://...", + "specName": "大份", + "quantity": 2, + "price": 38.00, + "amount": 76.00 + } + ], + "deliveryAddress": { + "contactName": "张三", + "contactPhone": "13800138000", + "address": "北京市朝阳区..." + }, + "dishAmount": 76.00, + "deliveryFee": 5.00, + "packageFee": 2.00, + "discountAmount": 10.00, + "totalAmount": 83.00, + "actualAmount": 73.00, + "status": 2, + "statusText": "待接单", + "paymentStatus": 1, + "paymentMethod": "wechat", + "estimatedDeliveryTime": "2024-01-01T13:00:00Z", + "createdAt": "2024-01-01T12:00:00Z", + "paidAt": "2024-01-01T12:05:00Z", + "remark": "少辣", + "timeline": [ + { + "status": "created", + "statusText": "订单创建", + "time": "2024-01-01T12:00:00Z" + }, + { + "status": "paid", + "statusText": "支付成功", + "time": "2024-01-01T12:05:00Z" + } + ] + } +} +``` + +### 5.4 商家接单 +```http +POST /api/v1/orders/{id}/accept +Authorization: Bearer {token} +Content-Type: application/json + +{ + "estimatedTime": 30 // 预计制作时长(分钟) +} +``` + +### 5.5 开始制作 +```http +POST /api/v1/orders/{id}/cooking +Authorization: Bearer {token} +``` + +### 5.6 订单完成 +```http +POST /api/v1/orders/{id}/complete +Authorization: Bearer {token} +``` + +### 5.7 取消订单 +```http +POST /api/v1/orders/{id}/cancel +Authorization: Bearer {token} +Content-Type: application/json + +{ + "reason": "用户取消", + "cancelBy": "customer" // customer, merchant, system +} +``` + +## 6. 支付接口 + +### 6.1 创建支付 +```http +POST /api/v1/payments +Authorization: Bearer {token} +Content-Type: application/json + +{ + "orderId": "uuid", + "paymentMethod": "wechat", // wechat, alipay, balance + "amount": 71.00 +} + +Response: +{ + "success": true, + "data": { + "paymentNo": "PAY202401010001", + "qrCode": "https://...", // 支付二维码 + "deepLink": "weixin://..." // 唤起支付的深度链接 + } +} +``` + +### 6.2 查询支付状态 +```http +GET /api/v1/payments/{paymentNo} +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "paymentNo": "PAY202401010001", + "status": 2, // 0:待支付 1:支付中 2:成功 3:失败 + "amount": 71.00, + "paidAt": "2024-01-01T12:05:00Z" + } +} +``` + +### 6.3 支付回调(第三方调用) +```http +POST /api/v1/payments/callback/wechat +Content-Type: application/json + +{ + "out_trade_no": "PAY202401010001", + "transaction_id": "4200001234567890", + "total_fee": 7100, + "result_code": "SUCCESS" +} +``` + +### 6.4 申请退款 +```http +POST /api/v1/refunds +Authorization: Bearer {token} +Content-Type: application/json + +{ + "orderId": "uuid", + "amount": 71.00, + "reason": "不想要了" +} +``` + +## 7. 配送管理接口 + +### 7.1 获取配送任务列表 +```http +GET /api/v1/delivery-tasks?status=&driverId=&pageIndex=1&pageSize=20 +Authorization: Bearer {token} +``` + +### 7.2 分配配送员 +```http +POST /api/v1/delivery-tasks/{id}/assign +Authorization: Bearer {token} +Content-Type: application/json + +{ + "driverId": "uuid" +} +``` + +### 7.3 配送员接单 +```http +POST /api/v1/delivery-tasks/{id}/accept +Authorization: Bearer {token} +``` + +### 7.4 确认取餐 +```http +POST /api/v1/delivery-tasks/{id}/pickup +Authorization: Bearer {token} +``` + +### 7.5 确认送达 +```http +POST /api/v1/delivery-tasks/{id}/deliver +Authorization: Bearer {token} +Content-Type: application/json + +{ + "deliveryCode": "123456" // 取餐码 +} +``` + +### 7.6 更新配送员位置 +```http +POST /api/v1/delivery-drivers/location +Authorization: Bearer {token} +Content-Type: application/json + +{ + "latitude": 39.9042, + "longitude": 116.4074 +} +``` + +## 8. 营销管理接口 + +### 8.1 获取优惠券列表 +```http +GET /api/v1/coupons?merchantId=&status=1&pageIndex=1&pageSize=20 +Authorization: Bearer {token} +``` + +### 8.2 领取优惠券 +```http +POST /api/v1/coupons/{id}/receive +Authorization: Bearer {token} +``` + +### 8.3 获取用户优惠券 +```http +GET /api/v1/user-coupons?status=1&pageIndex=1&pageSize=20 +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "items": [ + { + "id": "uuid", + "couponName": "满50减10", + "discountValue": 10.00, + "minOrderAmount": 50.00, + "status": 1, + "expiredAt": "2024-12-31T23:59:59Z" + } + ] + } +} +``` + +### 8.4 获取可用优惠券 +```http +GET /api/v1/user-coupons/available?merchantId={merchantId}&amount={amount} +Authorization: Bearer {token} +``` + +## 9. 评价管理接口 + +### 9.1 创建评价 +```http +POST /api/v1/reviews +Authorization: Bearer {token} +Content-Type: application/json + +{ + "orderId": "uuid", + "rating": 5, + "tasteRating": 5, + "packageRating": 5, + "deliveryRating": 5, + "content": "非常好吃", + "images": ["https://...", "https://..."], + "isAnonymous": false +} +``` + +### 9.2 获取商家评价列表 +```http +GET /api/v1/reviews?merchantId={merchantId}&rating=&pageIndex=1&pageSize=20 + +Response: +{ + "success": true, + "data": { + "items": [ + { + "id": "uuid", + "userName": "张三", + "userAvatar": "https://...", + "rating": 5, + "content": "非常好吃", + "images": ["https://..."], + "createdAt": "2024-01-01T12:00:00Z", + "replyContent": "感谢支持", + "replyAt": "2024-01-01T13:00:00Z" + } + ], + "totalCount": 100, + "statistics": { + "avgRating": 4.8, + "totalReviews": 100, + "rating5Count": 80, + "rating4Count": 15, + "rating3Count": 3, + "rating2Count": 1, + "rating1Count": 1 + } + } +} +``` + +### 9.3 商家回复评价 +```http +POST /api/v1/reviews/{id}/reply +Authorization: Bearer {token} +Content-Type: application/json + +{ + "replyContent": "感谢您的支持" +} +``` + +## 10. 数据统计接口 + +### 10.1 商家数据概览 +```http +GET /api/v1/statistics/merchant/overview?merchantId={merchantId}&startDate=&endDate= +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "totalOrders": 1000, + "totalRevenue": 50000.00, + "avgOrderAmount": 50.00, + "completionRate": 0.95, + "todayOrders": 50, + "todayRevenue": 2500.00, + "orderTrend": [ + { + "date": "2024-01-01", + "orders": 50, + "revenue": 2500.00 + } + ], + "topDishes": [ + { + "dishId": "uuid", + "dishName": "宫保鸡丁", + "salesCount": 200, + "revenue": 7600.00 + } + ] + } +} +``` + +### 10.2 平台数据大盘 +```http +GET /api/v1/statistics/platform/dashboard?startDate=&endDate= +Authorization: Bearer {token} + +Response: +{ + "success": true, + "data": { + "totalMerchants": 100, + "totalUsers": 10000, + "totalOrders": 50000, + "totalRevenue": 2500000.00, + "activeMerchants": 80, + "activeUsers": 5000, + "todayOrders": 500, + "todayRevenue": 25000.00 + } +} +``` + +## 11. 文件上传接口 + +### 11.1 上传图片 +```http +POST /api/v1/files/upload +Authorization: Bearer {token} +Content-Type: multipart/form-data + +file: +type: dish_image // dish_image, merchant_logo, user_avatar, review_image + +Response: +{ + "success": true, + "data": { + "url": "https://cdn.example.com/images/xxx.jpg", + "fileName": "xxx.jpg", + "fileSize": 102400 + } +} +``` + +## 12. WebSocket实时通知 + +### 12.1 连接WebSocket +```javascript +// 连接地址 +ws://api.example.com/ws?token={jwt_token} + +// 订阅主题 +{ + "action": "subscribe", + "topics": ["order.new", "order.status", "delivery.location"] +} + +// 接收消息 +{ + "topic": "order.new", + "data": { + "orderId": "uuid", + "orderNo": "202401010001", + "merchantId": "uuid" + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +### 12.2 消息主题 +- `order.new`:新订单通知 +- `order.status`:订单状态变更 +- `delivery.location`:配送员位置更新 +- `payment.success`:支付成功通知 + diff --git a/0_Document/05_部署运维.md b/0_Document/05_部署运维.md new file mode 100644 index 0000000..f593ee9 --- /dev/null +++ b/0_Document/05_部署运维.md @@ -0,0 +1,976 @@ +# 外卖SaaS系统 - 部署运维 + +## 1. 环境要求 + +### 1.1 开发环境 +- **.NET SDK**:10.0 或更高版本 +- **IDE**:Visual Studio 2022 / JetBrains Rider / VS Code +- **数据库**:PostgreSQL 16+ +- **缓存**:Redis 7.0+ +- **消息队列**:RabbitMQ 3.12+ +- **Git**:版本控制 +- **Docker Desktop**:容器化开发(可选) + +### 1.2 生产环境 +- **操作系统**:Linux (Ubuntu 22.04 LTS / CentOS 8+) +- **运行时**:.NET Runtime 10.0 +- **Web服务器**:Nginx 1.24+ +- **数据库**:PostgreSQL 16+ (主从复制) +- **缓存**:Redis 7.0+ (哨兵模式) +- **消息队列**:RabbitMQ 3.12+ (集群模式) +- **对象存储**:MinIO / 阿里云OSS / 腾讯云COS +- **监控**:Prometheus + Grafana +- **日志**:ELK Stack (Elasticsearch + Logstash + Kibana) + +### 1.3 硬件要求(生产环境) +- **应用服务器**:4核8GB内存(最低),推荐8核16GB +- **数据库服务器**:8核16GB内存,SSD存储 +- **Redis服务器**:4核8GB内存 +- **负载均衡器**:2核4GB内存 + +## 2. 本地开发环境搭建 + +### 2.1 安装.NET SDK +```bash +# Windows +# 从官网下载安装:https://dotnet.microsoft.com/download + +# Linux (Ubuntu) +wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +sudo apt-get update +sudo apt-get install -y dotnet-sdk-10.0 + +# 验证安装 +dotnet --version +``` + +### 2.2 安装PostgreSQL +```bash +# Ubuntu +sudo apt-get update +sudo apt-get install -y postgresql-16 postgresql-contrib-16 + +# 启动服务 +sudo systemctl start postgresql +sudo systemctl enable postgresql + +# 创建数据库 +sudo -u postgres psql +CREATE DATABASE takeout_saas; +CREATE USER takeout_user WITH PASSWORD 'your_password'; +GRANT ALL PRIVILEGES ON DATABASE takeout_saas TO takeout_user; +\q +``` + +### 2.3 安装Redis +```bash +# Ubuntu +sudo apt-get install -y redis-server + +# 启动服务 +sudo systemctl start redis-server +sudo systemctl enable redis-server + +# 测试连接 +redis-cli ping +``` + +### 2.4 安装RabbitMQ +```bash +# Ubuntu +sudo apt-get install -y rabbitmq-server + +# 启动服务 +sudo systemctl start rabbitmq-server +sudo systemctl enable rabbitmq-server + +# 启用管理插件 +sudo rabbitmq-plugins enable rabbitmq_management + +# 创建用户 +sudo rabbitmqctl add_user admin password +sudo rabbitmqctl set_user_tags admin administrator +sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*" + +# 访问管理界面:http://localhost:15672 +``` + +### 2.5 使用Docker Compose(推荐) +```yaml +# docker-compose.yml +version: '3.8' + +services: + postgres: + image: postgres:16 + container_name: takeout_postgres + environment: + POSTGRES_DB: takeout_saas + POSTGRES_USER: takeout_user + POSTGRES_PASSWORD: your_password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + container_name: takeout_redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + + rabbitmq: + image: rabbitmq:3.12-management + container_name: takeout_rabbitmq + environment: + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: password + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbitmq_data:/var/lib/rabbitmq + + minio: + image: minio/minio:latest + container_name: takeout_minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: password123 + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + +volumes: + postgres_data: + redis_data: + rabbitmq_data: + minio_data: +``` + +```bash +# 启动所有服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 停止服务 +docker-compose down +``` + +### 2.6 配置项目 +```bash +# 克隆项目 +git clone https://github.com/your-org/takeout-saas.git +cd takeout-saas + +# 还原依赖 +dotnet restore + +# 配置appsettings.Development.json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password" + }, + "Redis": { + "Configuration": "localhost:6379" + }, + "RabbitMQ": { + "Host": "localhost", + "Port": 5672, + "Username": "admin", + "Password": "password" + } +} + +# 执行数据库迁移 +cd src/TakeoutSaaS.Api +dotnet ef database update + +# 运行项目 +dotnet run +``` + +## 3. Docker部署 + +### 3.1 创建Dockerfile +```dockerfile +# Dockerfile +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY ["src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj", "src/TakeoutSaaS.Api/"] +COPY ["src/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj", "src/TakeoutSaaS.Application/"] +COPY ["src/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj", "src/TakeoutSaaS.Domain/"] +COPY ["src/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj", "src/TakeoutSaaS.Infrastructure/"] +COPY ["src/TakeoutSaaS.Shared/TakeoutSaaS.Shared.csproj", "src/TakeoutSaaS.Shared/"] +RUN dotnet restore "src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj" +COPY . . +WORKDIR "/src/src/TakeoutSaaS.Api" +RUN dotnet build "TakeoutSaaS.Api.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "TakeoutSaaS.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "TakeoutSaaS.Api.dll"] +``` + +### 3.2 构建镜像 +```bash +# 构建镜像 +docker build -t takeout-saas-api:latest . + +# 查看镜像 +docker images | grep takeout-saas + +# 运行容器 +docker run -d \ + --name takeout-api \ + -p 8080:80 \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e ConnectionStrings__DefaultConnection="Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password" \ + takeout-saas-api:latest +``` + +### 3.3 生产环境Docker Compose +```yaml +# docker-compose.prod.yml +version: '3.8' + +services: + api: + image: takeout-saas-api:latest + container_name: takeout_api + restart: always + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=${DB_PASSWORD}" + Redis__Configuration: "redis:6379" + RabbitMQ__Host: "rabbitmq" + ports: + - "8080:80" + depends_on: + - postgres + - redis + - rabbitmq + networks: + - takeout_network + + nginx: + image: nginx:latest + container_name: takeout_nginx + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/ssl:/etc/nginx/ssl + depends_on: + - api + networks: + - takeout_network + +networks: + takeout_network: + driver: bridge +``` + +## 4. Nginx配置 + +### 4.1 基础配置 +```nginx +# /etc/nginx/nginx.conf +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip压缩 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; + + # 限流配置 + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s; + limit_conn_zone $binary_remote_addr zone=conn_limit:10m; + + include /etc/nginx/conf.d/*.conf; +} +``` + +### 4.2 API服务配置 +```nginx +# /etc/nginx/conf.d/api.conf +upstream api_backend { + least_conn; + server api1:80 weight=1 max_fails=3 fail_timeout=30s; + server api2:80 weight=1 max_fails=3 fail_timeout=30s; + keepalive 32; +} + +server { + listen 80; + server_name api.example.com; + + # 重定向到HTTPS + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name api.example.com; + + # SSL证书配置 + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # 客户端请求体大小限制 + client_max_body_size 10M; + + # API接口 + location /api/ { + # 限流 + limit_req zone=api_limit burst=20 nodelay; + limit_conn conn_limit 10; + + proxy_pass http://api_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + proxy_busy_buffers_size 8k; + } + + # WebSocket + location /ws { + proxy_pass http://api_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # WebSocket超时 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # 健康检查 + location /health { + proxy_pass http://api_backend; + access_log off; + } + + # 静态文件缓存 + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + proxy_pass http://api_backend; + expires 30d; + add_header Cache-Control "public, immutable"; + } +} +``` + +## 5. 数据库部署 + +### 5.1 PostgreSQL主从复制 +```bash +# 主库配置 (postgresql.conf) +listen_addresses = '*' +wal_level = replica +max_wal_senders = 10 +wal_keep_size = 64MB +hot_standby = on + +# 主库配置 (pg_hba.conf) +host replication replicator 192.168.1.0/24 md5 + +# 创建复制用户 +CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'repl_password'; + +# 从库配置 +# 1. 停止从库 +sudo systemctl stop postgresql + +# 2. 清空从库数据目录 +rm -rf /var/lib/postgresql/16/main/* + +# 3. 从主库复制数据 +pg_basebackup -h master_ip -D /var/lib/postgresql/16/main -U replicator -P -v -R -X stream -C -S replica1 + +# 4. 启动从库 +sudo systemctl start postgresql + +# 5. 验证复制状态 +# 主库执行 +SELECT * FROM pg_stat_replication; +``` + +### 5.2 数据库备份脚本 +```bash +#!/bin/bash +# backup_db.sh + +BACKUP_DIR="/backup/postgres" +DATE=$(date +%Y%m%d_%H%M%S) +DB_NAME="takeout_saas" +DB_USER="takeout_user" +RETENTION_DAYS=30 + +# 创建备份目录 +mkdir -p $BACKUP_DIR + +# 全量备份 +pg_dump -h localhost -U $DB_USER -d $DB_NAME -F c -f $BACKUP_DIR/full_$DATE.dump + +# 压缩备份 +gzip $BACKUP_DIR/full_$DATE.dump + +# 删除过期备份 +find $BACKUP_DIR -name "full_*.dump.gz" -mtime +$RETENTION_DAYS -delete + +# 上传到对象存储(可选) +# aws s3 cp $BACKUP_DIR/full_$DATE.dump.gz s3://your-bucket/backups/ + +echo "Backup completed: full_$DATE.dump.gz" +``` + +### 5.3 定时备份(Crontab) +```bash +# 编辑crontab +crontab -e + +# 每天凌晨2点执行备份 +0 2 * * * /path/to/backup_db.sh >> /var/log/backup.log 2>&1 +``` + +## 6. Redis部署 + +### 6.1 Redis哨兵模式 +```bash +# redis.conf (主节点) +bind 0.0.0.0 +port 6379 +requirepass your_password +masterauth your_password + +# sentinel.conf +port 26379 +sentinel monitor mymaster 192.168.1.100 6379 2 +sentinel auth-pass mymaster your_password +sentinel down-after-milliseconds mymaster 5000 +sentinel parallel-syncs mymaster 1 +sentinel failover-timeout mymaster 10000 +``` + +### 6.2 Redis持久化配置 +```bash +# redis.conf +# RDB持久化 +save 900 1 +save 300 10 +save 60 10000 +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb + +# AOF持久化 +appendonly yes +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +``` + +## 7. CI/CD配置 + +### 7.1 GitHub Actions +```yaml +# .github/workflows/deploy.yml +name: Deploy to Production + +on: + push: + branches: [ main ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '10.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --no-build --verbosity normal + + - name: Publish + run: dotnet publish src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj -c Release -o ./publish + + - name: Build Docker image + run: | + docker build -t takeout-saas-api:${{ github.sha }} . + docker tag takeout-saas-api:${{ github.sha }} takeout-saas-api:latest + + - name: Push to Registry + run: | + echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + docker push takeout-saas-api:${{ github.sha }} + docker push takeout-saas-api:latest + + - name: Deploy to Server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + cd /opt/takeout-saas + docker-compose pull + docker-compose up -d + docker system prune -f +``` + +### 7.2 GitLab CI +```yaml +# .gitlab-ci.yml +stages: + - build + - test + - deploy + +variables: + DOCKER_IMAGE: registry.example.com/takeout-saas-api + +build: + stage: build + image: mcr.microsoft.com/dotnet/sdk:10.0 + script: + - dotnet restore + - dotnet build --configuration Release + artifacts: + paths: + - src/*/bin/Release/ + +test: + stage: test + image: mcr.microsoft.com/dotnet/sdk:10.0 + script: + - dotnet test --configuration Release + +deploy: + stage: deploy + image: docker:latest + services: + - docker:dind + script: + - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA . + - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest + - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA + - docker push $DOCKER_IMAGE:latest + only: + - main +``` + +## 8. 监控告警 + +### 8.1 Prometheus配置 +```yaml +# prometheus.yml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'takeout-api' + static_configs: + - targets: ['api:80'] + metrics_path: '/metrics' + + - job_name: 'postgres' + static_configs: + - targets: ['postgres-exporter:9187'] + + - job_name: 'redis' + static_configs: + - targets: ['redis-exporter:9121'] + + - job_name: 'node' + static_configs: + - targets: ['node-exporter:9100'] +``` + +### 8.2 应用监控指标 +```csharp +// Program.cs - 添加Prometheus监控 +builder.Services.AddPrometheusMetrics(); + +app.UseMetricServer(); // /metrics端点 +app.UseHttpMetrics(); // HTTP请求指标 + +// 自定义指标 +public class MetricsService +{ + private static readonly Counter OrderCreatedCounter = Metrics + .CreateCounter("orders_created_total", "Total orders created"); + + private static readonly Histogram OrderProcessingDuration = Metrics + .CreateHistogram("order_processing_duration_seconds", "Order processing duration"); + + public void RecordOrderCreated() + { + OrderCreatedCounter.Inc(); + } + + public IDisposable MeasureOrderProcessing() + { + return OrderProcessingDuration.NewTimer(); + } +} +``` + +### 8.3 Grafana仪表板 +```json +{ + "dashboard": { + "title": "外卖SaaS系统监控", + "panels": [ + { + "title": "API请求速率", + "targets": [ + { + "expr": "rate(http_requests_total[5m])" + } + ] + }, + { + "title": "订单创建数", + "targets": [ + { + "expr": "increase(orders_created_total[1h])" + } + ] + }, + { + "title": "数据库连接数", + "targets": [ + { + "expr": "pg_stat_activity_count" + } + ] + } + ] + } +} +``` + +### 8.4 告警规则 +```yaml +# alert.rules.yml +groups: + - name: takeout_alerts + interval: 30s + rules: + - alert: HighErrorRate + expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05 + for: 5m + labels: + severity: critical + annotations: + summary: "高错误率告警" + description: "API错误率超过5%" + + - alert: DatabaseDown + expr: up{job="postgres"} == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "数据库宕机" + description: "PostgreSQL数据库不可用" + + - alert: HighMemoryUsage + expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9 + for: 5m + labels: + severity: warning + annotations: + summary: "内存使用率过高" + description: "内存使用率超过90%" +``` + +## 9. 日志管理 + +### 9.1 Serilog配置 +```json +{ + "Serilog": { + "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Elasticsearch"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/log-.txt", + "rollingInterval": "Day", + "retainedFileCountLimit": 30 + } + }, + { + "Name": "Elasticsearch", + "Args": { + "nodeUris": "http://elasticsearch:9200", + "indexFormat": "takeout-logs-{0:yyyy.MM.dd}", + "autoRegisterTemplate": true + } + } + ], + "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"] + } +} +``` + +### 9.2 ELK Stack部署 +```yaml +# docker-compose.elk.yml +version: '3.8' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 + environment: + - discovery.type=single-node + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ports: + - "9200:9200" + volumes: + - es_data:/usr/share/elasticsearch/data + + logstash: + image: docker.elastic.co/logstash/logstash:8.11.0 + volumes: + - ./logstash/pipeline:/usr/share/logstash/pipeline + ports: + - "5044:5044" + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana:8.11.0 + ports: + - "5601:5601" + environment: + ELASTICSEARCH_HOSTS: http://elasticsearch:9200 + depends_on: + - elasticsearch + +volumes: + es_data: +``` + +## 10. 安全加固 + +### 10.1 防火墙配置 +```bash +# UFW防火墙 +sudo ufw enable +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw deny 5432/tcp # 禁止外部访问数据库 +sudo ufw deny 6379/tcp # 禁止外部访问Redis +``` + +### 10.2 SSL证书(Let's Encrypt) +```bash +# 安装Certbot +sudo apt-get install certbot python3-certbot-nginx + +# 获取证书 +sudo certbot --nginx -d api.example.com + +# 自动续期 +sudo certbot renew --dry-run + +# 添加定时任务 +0 3 * * * certbot renew --quiet +``` + +### 10.3 应用安全配置 +```csharp +// Program.cs +builder.Services.AddHsts(options => +{ + options.MaxAge = TimeSpan.FromDays(365); + options.IncludeSubDomains = true; + options.Preload = true; +}); + +builder.Services.AddHttpsRedirection(options => +{ + options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect; + options.HttpsPort = 443; +}); + +// 添加安全头 +app.Use(async (context, next) => +{ + context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + context.Response.Headers.Add("X-Frame-Options", "DENY"); + context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); + context.Response.Headers.Add("Referrer-Policy", "no-referrer"); + await next(); +}); +``` + +## 11. 性能优化 + +### 11.1 数据库连接池 +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=user;Password=pass;Pooling=true;MinPoolSize=5;MaxPoolSize=100;ConnectionLifetime=300" + } +} +``` + +### 11.2 Redis连接池 +```csharp +services.AddStackExchangeRedisCache(options => +{ + options.Configuration = configuration["Redis:Configuration"]; + options.InstanceName = "TakeoutSaaS:"; +}); +``` + +### 11.3 响应压缩 +```csharp +builder.Services.AddResponseCompression(options => +{ + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); +}); +``` + +## 12. 故障恢复 + +### 12.1 数据库恢复 +```bash +# 从备份恢复 +pg_restore -h localhost -U takeout_user -d takeout_saas -v /backup/full_20240101.dump + +# PITR恢复到指定时间点 +# 1. 停止数据库 +sudo systemctl stop postgresql + +# 2. 恢复基础备份 +rm -rf /var/lib/postgresql/16/main/* +tar -xzf /backup/base_backup.tar.gz -C /var/lib/postgresql/16/main/ + +# 3. 配置recovery.conf +restore_command = 'cp /backup/wal_archive/%f %p' +recovery_target_time = '2024-01-01 12:00:00' + +# 4. 启动数据库 +sudo systemctl start postgresql +``` + +### 12.2 应用回滚 +```bash +# Docker回滚到上一个版本 +docker-compose down +docker-compose up -d --force-recreate --no-deps api + +# 或使用特定版本 +docker pull takeout-saas-api:previous-version +docker-compose up -d +``` + diff --git a/0_Document/06_开发规范.md b/0_Document/06_开发规范.md new file mode 100644 index 0000000..f558dfb --- /dev/null +++ b/0_Document/06_开发规范.md @@ -0,0 +1,395 @@ +# 外卖SaaS系统 - 开发规范 + +## 1. 代码规范 + +### 1.1 命名规范 + +#### C#命名规范 +```csharp +// 类名:PascalCase +public class OrderService { } + +// 接口:I + PascalCase +public interface IOrderRepository { } + +// 方法:PascalCase +public async Task CreateOrderAsync() { } + +// 私有字段:_camelCase +private readonly IOrderRepository _orderRepository; + +// 公共属性:PascalCase +public string OrderNo { get; set; } + +// 局部变量:camelCase +var orderTotal = 100.00m; + +// 常量:PascalCase +public const int MaxOrderItems = 50; + +// 枚举:PascalCase +public enum OrderStatus +{ + Pending = 1, + Confirmed = 2, + Completed = 3 +} +``` + +#### 数据库命名规范 +```sql +-- 表名:小写,下划线分隔,复数 +orders +order_items +merchant_stores + +-- 字段名:小写,下划线分隔 +order_no +created_at +total_amount + +-- 索引:idx_表名_字段名 +idx_orders_merchant_id +idx_orders_created_at + +-- 外键:fk_表名_引用表名 +fk_orders_merchants +``` + +### 1.2 代码组织 + +#### 项目结构 +``` +TakeoutSaaS/ +├── src/ +│ ├── TakeoutSaaS.Api/ # Web API层 +│ │ ├── Controllers/ # 控制器 +│ │ ├── Filters/ # 过滤器 +│ │ ├── Middleware/ # 中间件 +│ │ ├── Models/ # DTO模型 +│ │ └── Program.cs +│ ├── TakeoutSaaS.Application/ # 应用层 +│ │ ├── Services/ # 应用服务 +│ │ ├── DTOs/ # 数据传输对象 +│ │ ├── Interfaces/ # 服务接口 +│ │ ├── Validators/ # 验证器 +│ │ ├── Mappings/ # 对象映射 +│ │ └── Commands/ # CQRS命令 +│ │ └── Queries/ # CQRS查询 +│ ├── TakeoutSaaS.Domain/ # 领域层 +│ │ ├── Entities/ # 实体 +│ │ ├── ValueObjects/ # 值对象 +│ │ ├── Enums/ # 枚举 +│ │ ├── Events/ # 领域事件 +│ │ └── Interfaces/ # 仓储接口 +│ ├── TakeoutSaaS.Infrastructure/ # 基础设施层 +│ │ ├── Data/ # 数据访问 +│ │ │ ├── EFCore/ # EF Core实现 +│ │ │ ├── Dapper/ # Dapper实现 +│ │ │ └── Repositories/ # 仓储实现 +│ │ ├── Cache/ # 缓存实现 +│ │ ├── MessageQueue/ # 消息队列 +│ │ └── ExternalServices/ # 外部服务 +│ └── TakeoutSaaS.Shared/ # 共享层 +│ ├── Constants/ # 常量 +│ ├── Exceptions/ # 异常 +│ ├── Extensions/ # 扩展方法 +│ └── Results/ # 统一返回结果 +├── tests/ +│ ├── TakeoutSaaS.UnitTests/ # 单元测试 +│ ├── TakeoutSaaS.IntegrationTests/ # 集成测试 +│ └── TakeoutSaaS.PerformanceTests/ # 性能测试 +└── docs/ # 文档 +``` + +### 1.3 代码注释 + +```csharp +/// +/// 订单服务接口 +/// +public interface IOrderService +{ + /// + /// 创建订单 + /// + /// 订单创建请求 + /// 订单信息 + /// 业务异常 + Task CreateOrderAsync(CreateOrderRequest request); +} + +// 复杂业务逻辑添加注释 +public async Task CalculateOrderAmount(Order order) +{ + // 1. 计算菜品总金额 + var dishAmount = order.Items.Sum(x => x.Price * x.Quantity); + + // 2. 计算配送费(距离 > 3km,每公里加收2元) + var deliveryFee = CalculateDeliveryFee(order.Distance); + + // 3. 应用优惠券折扣 + var discount = await ApplyCouponDiscountAsync(order.CouponId, dishAmount); + + // 4. 计算最终金额 + return dishAmount + deliveryFee - discount; +} +``` + +### 1.4 异常处理 + +```csharp +// 自定义业务异常 +public class BusinessException : Exception +{ + public int ErrorCode { get; } + + public BusinessException(int errorCode, string message) + : base(message) + { + ErrorCode = errorCode; + } +} + +// 全局异常处理中间件 +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "业务异常:{Message}", ex.Message); + await HandleBusinessExceptionAsync(context, ex); + } + catch (Exception ex) + { + _logger.LogError(ex, "系统异常:{Message}", ex.Message); + await HandleSystemExceptionAsync(context, ex); + } + } + + private static Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex) + { + context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; + return context.Response.WriteAsJsonAsync(new + { + success = false, + code = ex.ErrorCode, + message = ex.Message + }); + } +} + +// 使用示例 +public async Task GetOrderAsync(Guid orderId) +{ + var order = await _orderRepository.GetByIdAsync(orderId); + if (order == null) + { + throw new BusinessException(404, "订单不存在"); + } + return order; +} +``` + +## 2. Git工作流 + +### 2.1 分支管理 + +``` +main # 主分支,生产环境代码 +├── develop # 开发分支 +│ ├── feature/order-management # 功能分支 +│ ├── feature/payment-integration # 功能分支 +│ └── bugfix/order-calculation # 修复分支 +└── hotfix/critical-bug # 紧急修复分支 +``` + +### 2.2 分支命名规范 + +- **功能分支**:`feature/功能名称`(如:`feature/order-management`) +- **修复分支**:`bugfix/问题描述`(如:`bugfix/order-calculation`) +- **紧急修复**:`hotfix/问题描述`(如:`hotfix/payment-error`) +- **发布分支**:`release/版本号`(如:`release/v1.0.0`) + +### 2.3 提交信息规范 + +```bash +# 格式:(): + +# type类型: +# feat: 新功能 +# fix: 修复bug +# docs: 文档更新 +# style: 代码格式调整 +# refactor: 重构 +# perf: 性能优化 +# test: 测试相关 +# chore: 构建/工具相关 + +# 示例 +git commit -m "feat(order): 添加订单创建功能" +git commit -m "fix(payment): 修复支付回调处理错误" +git commit -m "docs(api): 更新API文档" +git commit -m "refactor(service): 重构订单服务" +``` + +### 2.4 工作流程 + +```bash +# 1. 从develop创建功能分支 +git checkout develop +git pull origin develop +git checkout -b feature/order-management + +# 2. 开发并提交 +git add . +git commit -m "feat(order): 添加订单创建功能" + +# 3. 推送到远程 +git push origin feature/order-management + +# 4. 创建Pull Request到develop分支 + +# 5. 代码审查通过后合并 + +# 6. 删除功能分支 +git branch -d feature/order-management +git push origin --delete feature/order-management +``` + +## 3. 代码审查 + +### 3.1 审查清单 + +- [ ] 代码符合命名规范 +- [ ] 代码逻辑清晰,易于理解 +- [ ] 适当的注释和文档 +- [ ] 异常处理完善 +- [ ] 单元测试覆盖 +- [ ] 性能考虑(N+1查询、大数据量处理) +- [ ] 安全性考虑(SQL注入、XSS、权限校验) +- [ ] 日志记录完善 +- [ ] 无硬编码配置 +- [ ] 符合SOLID原则 + +### 3.2 审查重点 + +```csharp +// ❌ 不好的实践 +public class OrderService +{ + public Order CreateOrder(CreateOrderRequest request) + { + // 直接在服务层操作DbContext + var order = new Order(); + _dbContext.Orders.Add(order); + _dbContext.SaveChanges(); + + // 硬编码配置 + var deliveryFee = 5.0m; + + // 没有异常处理 + // 没有日志记录 + + return order; + } +} + +// ✅ 好的实践 +public class OrderService : IOrderService +{ + private readonly IOrderRepository _orderRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IOptions _settings; + + public async Task CreateOrderAsync(CreateOrderRequest request) + { + try + { + // 参数验证 + if (request == null) + throw new ArgumentNullException(nameof(request)); + + _logger.LogInformation("创建订单:{@Request}", request); + + // 业务逻辑 + var order = new Order + { + // ... 初始化订单 + DeliveryFee = _settings.Value.DefaultDeliveryFee + }; + + // 使用仓储 + await _orderRepository.AddAsync(order); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation("订单创建成功:{OrderId}", order.Id); + + return _mapper.Map(order); + } + catch (Exception ex) + { + _logger.LogError(ex, "创建订单失败:{@Request}", request); + throw; + } + } +} +``` + + + +## 4. 单元测试规范 + +### 4.1 测试命名 +- 命名格式:`MethodName_Scenario_ExpectedResult` +- 测试覆盖率要求:核心业务逻辑 >= 80% + +### 4.2 测试示例 + +```csharp +[Fact] +public async Task CreateOrder_ValidRequest_ReturnsOrderDto() +{ + // Arrange + var request = new CreateOrderRequest { /* ... */ }; + + // Act + var result = await _orderService.CreateOrderAsync(request); + + // Assert + result.Should().NotBeNull(); + result.OrderNo.Should().NotBeNullOrEmpty(); +} +``` + +## 5. 性能优化规范 + +### 5.1 数据库查询优化 +- 避免N+1查询,使用Include预加载 +- 大数据量查询使用Dapper +- 合理使用索引 + +### 5.2 缓存策略 +- 商家信息:30分钟 +- 菜品信息:15分钟 +- 配置信息:1小时 +- 用户会话:2小时 + +## 6. 文档要求 + +### 6.1 代码文档 +- 所有公共API必须有XML文档注释 +- 复杂业务逻辑添加详细注释 +- README.md说明项目结构和运行方式 + +### 6.2 变更日志 +维护CHANGELOG.md记录版本变更 \ No newline at end of file diff --git a/0_Document/07_系统架构图.md b/0_Document/07_系统架构图.md new file mode 100644 index 0000000..3a5a350 --- /dev/null +++ b/0_Document/07_系统架构图.md @@ -0,0 +1,321 @@ +# 外卖SaaS系统 - 系统架构图 + +## 1. 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ +│ │ Web管理端 │ │ Web用户端 │ │ 小程序端(用户) │ │ +│ │ (React/Vue) │ │ (React/Vue) │ │ (微信/支付宝) │ │ +│ └──────────────┘ └──────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ API网关层 │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ Nginx / API Gateway │ │ +│ │ - 路由转发 │ │ +│ │ - 负载均衡 │ │ +│ │ - 限流熔断 │ │ +│ │ - SSL终止 │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 应用服务层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 租户服务 │ │ 商家服务 │ │ 菜品服务 │ │ +│ │ - 租户管理 │ │ - 商家管理 │ │ - 菜品管理 │ │ +│ │ - 权限管理 │ │ - 门店管理 │ │ - 分类管理 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 订单服务 │ │ 配送服务 │ │ 用户服务 │ │ +│ │ - 订单管理 │ │ - 配送员管理 │ │ - 用户管理 │ │ +│ │ - 订单流转 │ │ - 任务分配 │ │ - 地址管理 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 支付服务 │ │ 营销服务 │ │ 通知服务 │ │ +│ │ - 支付处理 │ │ - 优惠券 │ │ - 短信通知 │ │ +│ │ - 退款处理 │ │ - 活动管理 │ │ - 推送通知 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 基础设施层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ RabbitMQ │ │ +│ │ - 主数据库 │ │ - 缓存 │ │ - 消息队列 │ │ +│ │ - 主从复制 │ │ - 会话存储 │ │ - 异步任务 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ MinIO/OSS │ │ Elasticsearch│ │ Prometheus │ │ +│ │ - 对象存储 │ │ - 日志存储 │ │ - 监控告警 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## 2. 应用分层架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Presentation Layer │ +│ (表现层) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ TakeoutSaaS.Api │ │ +│ │ - Controllers (控制器) │ │ +│ │ - Filters (过滤器) │ │ +│ │ - Middleware (中间件) │ │ +│ │ - Models (DTO模型) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ (应用层) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ TakeoutSaaS.Application │ │ +│ │ - Services (应用服务) │ │ +│ │ - DTOs (数据传输对象) │ │ +│ │ - Interfaces (服务接口) │ │ +│ │ - Validators (验证器) │ │ +│ │ - Mappings (对象映射) │ │ +│ │ - Commands/Queries (CQRS) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ (领域层) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ TakeoutSaaS.Domain │ │ +│ │ - Entities (实体) │ │ +│ │ - ValueObjects (值对象) │ │ +│ │ - Enums (枚举) │ │ +│ │ - Events (领域事件) │ │ +│ │ - Interfaces (仓储接口) │ │ +│ │ - Specifications (规约) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Infrastructure Layer │ +│ (基础设施层) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ TakeoutSaaS.Infrastructure │ │ +│ │ - Data (数据访问) │ │ +│ │ - EFCore (EF Core实现) │ │ +│ │ - Dapper (Dapper实现) │ │ +│ │ - Repositories (仓储实现) │ │ +│ │ - Cache (缓存实现) │ │ +│ │ - MessageQueue (消息队列) │ │ +│ │ - ExternalServices (外部服务) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 3. 订单处理流程图 + +``` +用户下单 → 创建订单 → 支付 → 商家接单 → 制作 → 配送 → 完成 + │ │ │ │ │ │ │ + │ │ │ │ │ │ └─→ 订单完成 + │ │ │ │ │ └─→ 配送中 + │ │ │ │ └─→ 制作中 + │ │ │ └─→ 待制作 + │ │ └─→ 待接单 + │ └─→ 待支付 + └─→ 订单创建 + +取消流程: +用户取消 ──→ 退款处理 ──→ 订单取消 +商家拒单 ──→ 退款处理 ──→ 订单取消 +超时未支付 ──→ 自动取消 +``` + +## 4. 数据流转图 + +``` +┌──────────┐ +│ 客户端 │ +└────┬─────┘ + │ HTTP Request + ▼ +┌──────────────────┐ +│ API Gateway │ +│ (Nginx) │ +└────┬─────────────┘ + │ 路由转发 + ▼ +┌──────────────────┐ +│ Web API │ +│ - 认证授权 │ +│ - 参数验证 │ +└────┬─────────────┘ + │ 调用服务 + ▼ +┌──────────────────┐ +│ Application │ +│ Service │ +│ - 业务逻辑 │ +└────┬─────────────┘ + │ 数据访问 + ▼ +┌──────────────────┐ ┌──────────┐ +│ Repository │────→│ Cache │ +│ - EF Core │ │ (Redis) │ +│ - Dapper │ └──────────┘ +└────┬─────────────┘ + │ SQL查询 + ▼ +┌──────────────────┐ +│ PostgreSQL │ +│ Database │ +└──────────────────┘ +``` + +## 5. 多租户数据隔离架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 租户识别中间件 │ +│ - 从JWT Token解析租户ID │ +│ - 从HTTP Header获取租户ID │ +└─────────────────────┬───────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 租户上下文 │ +│ - 当前租户ID │ +│ - 租户配置信息 │ +└─────────────────────┬───────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 数据访问层 │ +│ - 自动添加租户ID过滤 │ +│ - 全局查询过滤器 │ +└─────────────────────┬───────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ 数据库 │ +│ 租户A数据 │ 租户B数据 │ 租户C数据 │ +│ (tenant_id = A) (tenant_id = B) (tenant_id = C)│ +└─────────────────────────────────────────────────┘ +``` + +## 6. 缓存架构 + +``` +┌──────────────┐ +│ Application │ +└──────┬───────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ Cache Aside Pattern │ +│ 1. 查询缓存 │ +│ 2. 缓存未命中,查询数据库 │ +│ 3. 写入缓存 │ +└──────┬───────────────────────────┘ + │ + ├─→ L1 Cache (Memory Cache) + │ - 进程内缓存 + │ - 热点数据 + │ + └─→ L2 Cache (Redis) + - 分布式缓存 + - 会话数据 + - 共享数据 +``` + +## 7. 消息队列架构 + +``` +┌──────────────┐ +│ Producer │ +│ (订单服务) │ +└──────┬───────┘ + │ 发布事件 + ▼ +┌──────────────────┐ +│ RabbitMQ │ +│ Exchange │ +└──────┬───────────┘ + │ + ├─→ Queue: order.created + │ └─→ Consumer: 通知服务 + │ + ├─→ Queue: order.paid + │ └─→ Consumer: 库存服务 + │ + └─→ Queue: order.completed + └─→ Consumer: 统计服务 +``` + +## 8. 部署架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 负载均衡器 (Nginx) │ +└─────────────┬───────────────────────────────────┘ + │ + ┌───────┴───────┐ + │ │ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│ API 1 │ │ API 2 │ +│ (容器) │ │ (容器) │ +└────┬─────┘ └────┬─────┘ + │ │ + └───────┬───────┘ + │ + ┌───────┴────────┬──────────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────────┐ ┌──────────┐ ┌──────────┐ +│PostgreSQL│ │ Redis │ │ RabbitMQ │ +│ 主从 │ │ 哨兵 │ │ 集群 │ +└──────────┘ └──────────┘ └──────────┘ +``` + +## 9. 监控架构 + +``` +┌──────────────────────────────────────────┐ +│ 应用程序 │ +│ - 业务指标 │ +│ - 性能指标 │ +│ - 日志输出 │ +└─────────┬────────────────────────────────┘ + │ + ┌─────┴─────┬──────────┐ + │ │ │ + ▼ ▼ ▼ +┌────────┐ ┌────────┐ ┌────────┐ +│Metrics │ │ Logs │ │Traces │ +│ │ │ │ │ │ +└───┬────┘ └───┬────┘ └───┬────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────────────────┐ +│ Prometheus │ +│ Elasticsearch │ +│ Jaeger │ +└─────────┬────────────────────┘ + │ + ▼ +┌──────────────────────────────┐ +│ Grafana │ +│ Kibana │ +│ - 可视化仪表板 │ +│ - 告警配置 │ +└──────────────────────────────┘ +``` diff --git a/0_Document/08_AI编程规范.md b/0_Document/08_AI编程规范.md new file mode 100644 index 0000000..c893af3 --- /dev/null +++ b/0_Document/08_AI编程规范.md @@ -0,0 +1,1589 @@ +# 外卖SaaS系统 - AI编程规范汇总 + +> 本文档专门为AI编程助手准备,汇总了所有编码规范和规则,确保代码质量和一致性。 + +## 1. 技术栈要求 + +### 1.1 核心技术 +- **.NET 10** + **ASP.NET Core Web API** +- **Entity Framework Core 10**(复杂查询和实体管理) +- **Dapper 2.1+**(高性能查询和批量操作) +- **PostgreSQL 16+**(主数据库) +- **Redis 7.0+**(缓存和会话) +- **RabbitMQ 3.12+**(消息队列) + +### 1.2 必用框架和库 +- **AutoMapper**:对象映射 +- **FluentValidation**:数据验证 +- **Serilog**:结构化日志 +- **MediatR**:CQRS和中介者模式 +- **Hangfire**:后台任务调度 +- **Polly**:弹性和瞬态故障处理 +- **Swagger/Swashbuckle**:API文档 + +### 1.3 测试框架 +- **xUnit**:单元测试 +- **Moq**:Mock框架 +- **FluentAssertions**:断言库 + +## 2. 命名规范(严格遵守) + +### 2.1 C#命名规范 +```csharp +// ✅ 类名:PascalCase +public class OrderService { } + +// ✅ 接口:I + PascalCase +public interface IOrderRepository { } + +// ✅ 方法:PascalCase,异步方法以Async结尾 +public async Task CreateOrderAsync() { } + +// ✅ 私有字段:_camelCase(下划线前缀) +private readonly IOrderRepository _orderRepository; + +// ✅ 公共属性:PascalCase +public string OrderNo { get; set; } + +// ✅ 局部变量:camelCase +var orderTotal = 100.00m; + +// ✅ 常量:PascalCase +public const int MaxOrderItems = 50; + +// ✅ 枚举:PascalCase,枚举值也是PascalCase +public enum OrderStatus +{ + Pending = 1, + Confirmed = 2, + Completed = 3 +} +``` + +### 2.2 数据库命名规范 +```sql +-- ✅ 表名:小写,下划线分隔,复数形式 +orders +order_items +merchant_stores + +-- ✅ 字段名:小写,下划线分隔 +order_no +created_at +total_amount + +-- ✅ 索引:idx_表名_字段名 +idx_orders_merchant_id +idx_orders_created_at + +-- ✅ 外键:fk_表名_引用表名 +fk_orders_merchants +``` + +## 3. 项目结构规范 + +### 3.1 分层架构(DDD + Clean Architecture) +``` +TakeoutSaaS/ +├── src/ +│ ├── Api/ # API层(表现层) +│ │ ├── Controllers/ # 控制器 +│ │ ├── Filters/ # 过滤器 +│ │ ├── Middleware/ # 中间件 +│ │ └── Models/ # DTO模型 +│ ├── Application/ # 应用层 +│ │ ├── Services/ # 应用服务 +│ │ ├── DTOs/ # 数据传输对象 +│ │ ├── Interfaces/ # 服务接口 +│ │ ├── Validators/ # FluentValidation验证器 +│ │ ├── Mappings/ # AutoMapper配置 +│ │ └── Commands/Queries/ # CQRS命令和查询 +│ ├── Domain/ # 领域层(核心业务) +│ │ ├── Entities/ # 实体 +│ │ ├── ValueObjects/ # 值对象 +│ │ ├── Enums/ # 枚举 +│ │ ├── Events/ # 领域事件 +│ │ └── Interfaces/ # 仓储接口 +│ ├── Infrastructure/ # 基础设施层 +│ │ ├── Data/ # 数据访问 +│ │ │ ├── EFCore/ # EF Core实现 +│ │ │ ├── Dapper/ # Dapper实现 +│ │ │ └── Repositories/ # 仓储实现 +│ │ ├── Cache/ # 缓存实现 +│ │ ├── MessageQueue/ # 消息队列 +│ │ └── ExternalServices/ # 外部服务 +│ ├── Core/ # 核心共享层 +│ │ ├── Constants/ # 常量 +│ │ ├── Exceptions/ # 异常 +│ │ ├── Extensions/ # 扩展方法 +│ │ └── Results/ # 统一返回结果 +│ └── Modules/ # 模块化(可选) +└── tests/ + ├── UnitTests/ # 单元测试 + ├── IntegrationTests/ # 集成测试 + └── PerformanceTests/ # 性能测试 +``` + +### 3.2 文件组织规则 +- 每个文件只包含一个公共类/接口 +- 文件名与类名保持一致 +- 相关的类放在同一个文件夹 +- 使用命名空间反映文件夹结构 + +## 4. 代码注释规范 + +### 4.1 XML文档注释(必须) +```csharp +/// +/// 订单服务接口 +/// +public interface IOrderService +{ + /// + /// 创建订单 + /// + /// 订单创建请求 + /// 订单信息 + /// 业务异常 + Task CreateOrderAsync(CreateOrderRequest request); +} +``` + +### 4.2 业务逻辑注释 +```csharp +// ✅ 复杂业务逻辑必须添加注释 +public async Task CalculateOrderAmount(Order order) +{ + // 1. 计算菜品总金额 + var dishAmount = order.Items.Sum(x => x.Price * x.Quantity); + + // 2. 计算配送费(距离 > 3km,每公里加收2元) + var deliveryFee = CalculateDeliveryFee(order.Distance); + + // 3. 应用优惠券折扣 + var discount = await ApplyCouponDiscountAsync(order.CouponId, dishAmount); + + // 4. 计算最终金额 + return dishAmount + deliveryFee - discount; +} +``` + +## 5. 异常处理规范 + +### 5.1 自定义异常 +```csharp +// ✅ 业务异常 +public class BusinessException : Exception +{ + public int ErrorCode { get; } + + public BusinessException(int errorCode, string message) + : base(message) + { + ErrorCode = errorCode; + } +} + +// ✅ 验证异常 +public class ValidationException : Exception +{ + public IDictionary Errors { get; } + + public ValidationException(IDictionary errors) + : base("一个或多个验证错误") + { + Errors = errors; + } +} +``` + +### 5.2 全局异常处理中间件(必须实现) +```csharp +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "业务异常:{Message}", ex.Message); + await HandleBusinessExceptionAsync(context, ex); + } + catch (ValidationException ex) + { + _logger.LogWarning(ex, "验证异常:{Errors}", ex.Errors); + await HandleValidationExceptionAsync(context, ex); + } + catch (Exception ex) + { + _logger.LogError(ex, "系统异常:{Message}", ex.Message); + await HandleSystemExceptionAsync(context, ex); + } + } + + private static Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex) + { + context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; + return context.Response.WriteAsJsonAsync(new + { + success = false, + code = ex.ErrorCode, + message = ex.Message + }); + } +} +``` + +### 5.3 异常使用示例 +```csharp +// ✅ 正确的异常抛出 +public async Task GetOrderAsync(Guid orderId) +{ + var order = await _orderRepository.GetByIdAsync(orderId); + if (order == null) + { + throw new BusinessException(404, "订单不存在"); + } + return order; +} + +// ❌ 错误:不要吞掉异常 +try +{ + // ... +} +catch (Exception) +{ + // 什么都不做 - 这是错误的! +} +``` + +## 6. 服务层编码规范 + +### 6.1 服务类结构(标准模板) +```csharp +// ✅ 好的服务实现 +public class OrderService : IOrderService +{ + private readonly IOrderRepository _orderRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly IOptions _settings; + + public OrderService( + IOrderRepository orderRepository, + IUnitOfWork unitOfWork, + ILogger logger, + IMapper mapper, + IOptions settings) + { + _orderRepository = orderRepository; + _unitOfWork = unitOfWork; + _logger = logger; + _mapper = mapper; + _settings = settings; + } + + public async Task CreateOrderAsync(CreateOrderRequest request) + { + try + { + // 1. 参数验证(FluentValidation会自动验证,这里是额外检查) + if (request == null) + throw new ArgumentNullException(nameof(request)); + + // 2. 记录日志 + _logger.LogInformation("创建订单:{@Request}", request); + + // 3. 业务逻辑 + var order = new Order + { + OrderNo = GenerateOrderNo(), + TotalAmount = request.TotalAmount, + DeliveryFee = _settings.Value.DefaultDeliveryFee, + CreatedAt = DateTime.UtcNow + }; + + // 4. 数据持久化 + await _orderRepository.AddAsync(order); + await _unitOfWork.SaveChangesAsync(); + + // 5. 记录成功日志 + _logger.LogInformation("订单创建成功:{OrderId}", order.Id); + + // 6. 返回DTO + return _mapper.Map(order); + } + catch (Exception ex) + { + _logger.LogError(ex, "创建订单失败:{@Request}", request); + throw; + } + } +} + +// ❌ 错误的服务实现 +public class BadOrderService +{ + // ❌ 直接注入DbContext而不是仓储 + private readonly AppDbContext _dbContext; + + // ❌ 没有日志 + // ❌ 硬编码配置 + public Order CreateOrder(CreateOrderRequest request) + { + var order = new Order(); + order.DeliveryFee = 5.0m; // ❌ 硬编码 + _dbContext.Orders.Add(order); + _dbContext.SaveChanges(); // ❌ 同步方法 + return order; // ❌ 返回实体而不是DTO + } +} +``` + +### 6.2 依赖注入规则 +```csharp +// ✅ 使用构造函数注入 +public class OrderService +{ + private readonly IOrderRepository _orderRepository; + + public OrderService(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } +} + +// ❌ 不要使用属性注入 +public class BadOrderService +{ + [Inject] + public IOrderRepository OrderRepository { get; set; } +} + +// ❌ 不要使用服务定位器模式 +public class BadOrderService +{ + public void DoSomething() + { + var repository = ServiceLocator.GetService(); + } +} +``` + +## 7. 数据访问规范 + +### 7.1 仓储模式(必须使用) +```csharp +// ✅ 仓储接口 +public interface IOrderRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Order order); + Task UpdateAsync(Order order); + Task DeleteAsync(Guid id); + Task ExistsAsync(Guid id); +} + +// ✅ EF Core仓储实现 +public class OrderRepository : IOrderRepository +{ + private readonly AppDbContext _context; + + public OrderRepository(AppDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid id) + { + return await _context.Orders + .Include(o => o.OrderItems) + .Include(o => o.Customer) + .FirstOrDefaultAsync(o => o.Id == id); + } + + public async Task AddAsync(Order order) + { + await _context.Orders.AddAsync(order); + return order; + } +} +``` + +### 7.2 EF Core vs Dapper 使用场景 +```csharp +// ✅ EF Core - 复杂查询和实体管理 +public async Task GetOrderWithDetailsAsync(Guid orderId) +{ + return await _dbContext.Orders + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.Dish) + .Include(o => o.Customer) + .Include(o => o.Merchant) + .FirstOrDefaultAsync(o => o.Id == orderId); +} + +// ✅ Dapper - 高性能统计查询 +public async Task GetOrderStatisticsAsync(DateTime startDate, DateTime endDate) +{ + var sql = @" + SELECT + COUNT(*) as TotalOrders, + SUM(total_amount) as TotalAmount, + AVG(total_amount) as AvgAmount, + MAX(total_amount) as MaxAmount, + MIN(total_amount) as MinAmount + FROM orders + WHERE created_at BETWEEN @StartDate AND @EndDate + AND status = @Status"; + + return await _connection.QueryFirstOrDefaultAsync(sql, + new { StartDate = startDate, EndDate = endDate, Status = OrderStatus.Completed }); +} + +// ✅ Dapper - 批量插入 +public async Task BulkInsertOrdersAsync(IEnumerable orders) +{ + var sql = @" + INSERT INTO orders (id, order_no, merchant_id, total_amount, created_at) + VALUES (@Id, @OrderNo, @MerchantId, @TotalAmount, @CreatedAt)"; + + return await _connection.ExecuteAsync(sql, orders); +} +``` + +### 7.3 工作单元模式(必须使用) +```csharp +// ✅ 工作单元接口 +public interface IUnitOfWork : IDisposable +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task BeginTransactionAsync(); + Task CommitTransactionAsync(); + Task RollbackTransactionAsync(); +} + +// ✅ 使用工作单元 +public async Task CreateOrderWithItemsAsync(CreateOrderRequest request) +{ + await _unitOfWork.BeginTransactionAsync(); + + try + { + // 1. 创建订单 + var order = new Order { /* ... */ }; + await _orderRepository.AddAsync(order); + + // 2. 创建订单项 + foreach (var item in request.Items) + { + var orderItem = new OrderItem { /* ... */ }; + await _orderItemRepository.AddAsync(orderItem); + } + + // 3. 提交事务 + await _unitOfWork.SaveChangesAsync(); + await _unitOfWork.CommitTransactionAsync(); + + return _mapper.Map(order); + } + catch + { + await _unitOfWork.RollbackTransactionAsync(); + throw; + } +} +``` + +## 8. CQRS模式规范 + +### 8.1 命令(Command)- 写操作 +```csharp +// ✅ 命令定义 +public class CreateOrderCommand : IRequest +{ + public Guid MerchantId { get; set; } + public Guid CustomerId { get; set; } + public List Items { get; set; } + public decimal TotalAmount { get; set; } +} + +// ✅ 命令处理器 +public class CreateOrderCommandHandler : IRequestHandler +{ + private readonly IOrderRepository _orderRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation("处理创建订单命令:{@Command}", request); + + var order = new Order + { + MerchantId = request.MerchantId, + CustomerId = request.CustomerId, + TotalAmount = request.TotalAmount + }; + + await _orderRepository.AddAsync(order); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return _mapper.Map(order); + } +} +``` + +### 8.2 查询(Query)- 读操作 +```csharp +// ✅ 查询定义 +public class GetOrderByIdQuery : IRequest +{ + public Guid OrderId { get; set; } +} + +// ✅ 查询处理器 +public class GetOrderByIdQueryHandler : IRequestHandler +{ + private readonly IOrderRepository _orderRepository; + private readonly IMapper _mapper; + + public async Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken) + { + var order = await _orderRepository.GetByIdAsync(request.OrderId); + + if (order == null) + throw new BusinessException(404, "订单不存在"); + + return _mapper.Map(order); + } +} +``` + +## 9. 验证规范(FluentValidation) + +### 9.1 验证器定义 +```csharp +// ✅ 使用FluentValidation +public class CreateOrderRequestValidator : AbstractValidator +{ + public CreateOrderRequestValidator() + { + RuleFor(x => x.MerchantId) + .NotEmpty().WithMessage("商家ID不能为空"); + + RuleFor(x => x.CustomerId) + .NotEmpty().WithMessage("客户ID不能为空"); + + RuleFor(x => x.Items) + .NotEmpty().WithMessage("订单项不能为空") + .Must(items => items.Count <= 50).WithMessage("订单项不能超过50个"); + + RuleFor(x => x.TotalAmount) + .GreaterThan(0).WithMessage("订单金额必须大于0"); + + RuleFor(x => x.DeliveryAddress) + .NotEmpty().WithMessage("配送地址不能为空") + .MaximumLength(200).WithMessage("配送地址不能超过200个字符"); + } +} +``` + +### 9.2 验证器注册 +```csharp +// ✅ 在Program.cs中注册 +builder.Services.AddValidatorsFromAssemblyContaining(); +builder.Services.AddFluentValidationAutoValidation(); +``` + +## 10. 缓存策略规范 + + +### 10.1 缓存时间策略 +```csharp +// ✅ 缓存时间常量 +public static class CacheTimeouts +{ + public static readonly TimeSpan MerchantInfo = TimeSpan.FromMinutes(30); + public static readonly TimeSpan DishInfo = TimeSpan.FromMinutes(15); + public static readonly TimeSpan UserSession = TimeSpan.FromHours(2); + public static readonly TimeSpan ConfigInfo = TimeSpan.FromHours(1); + public static readonly TimeSpan HotData = TimeSpan.FromMinutes(5); +} +``` + +### 10.2 缓存使用示例 +```csharp +// ✅ 使用分布式缓存(Redis) +public class MerchantService : IMerchantService +{ + private readonly IMerchantRepository _merchantRepository; + private readonly IDistributedCache _cache; + private readonly ILogger _logger; + + public async Task GetMerchantAsync(Guid merchantId) + { + var cacheKey = $"merchant:{merchantId}"; + + // 1. 尝试从缓存获取 + var cachedData = await _cache.GetStringAsync(cacheKey); + if (!string.IsNullOrEmpty(cachedData)) + { + _logger.LogDebug("从缓存获取商家信息:{MerchantId}", merchantId); + return JsonSerializer.Deserialize(cachedData); + } + + // 2. 缓存未命中,从数据库查询 + var merchant = await _merchantRepository.GetByIdAsync(merchantId); + if (merchant == null) + throw new BusinessException(404, "商家不存在"); + + var dto = _mapper.Map(merchant); + + // 3. 写入缓存 + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = CacheTimeouts.MerchantInfo + }; + await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), cacheOptions); + + _logger.LogDebug("商家信息已缓存:{MerchantId}", merchantId); + return dto; + } + + public async Task UpdateMerchantAsync(Guid merchantId, UpdateMerchantRequest request) + { + // 更新数据库 + var merchant = await _merchantRepository.GetByIdAsync(merchantId); + // ... 更新逻辑 + await _unitOfWork.SaveChangesAsync(); + + // ✅ 更新后清除缓存 + var cacheKey = $"merchant:{merchantId}"; + await _cache.RemoveAsync(cacheKey); + _logger.LogDebug("已清除商家缓存:{MerchantId}", merchantId); + } +} +``` + +## 11. 日志规范(Serilog) + +### 11.1 日志级别使用 +```csharp +// ✅ 正确的日志级别使用 +public class OrderService +{ + private readonly ILogger _logger; + + public async Task CreateOrderAsync(CreateOrderRequest request) + { + // Trace: 非常详细的调试信息(生产环境不记录) + _logger.LogTrace("进入CreateOrderAsync方法"); + + // Debug: 调试信息(生产环境不记录) + _logger.LogDebug("订单请求参数:{@Request}", request); + + // Information: 一般信息(重要业务流程) + _logger.LogInformation("开始创建订单,商家ID:{MerchantId},客户ID:{CustomerId}", + request.MerchantId, request.CustomerId); + + try + { + // ... 业务逻辑 + + // Information: 成功完成 + _logger.LogInformation("订单创建成功:{OrderId},订单号:{OrderNo}", + order.Id, order.OrderNo); + + return dto; + } + catch (BusinessException ex) + { + // Warning: 业务异常(预期内的错误) + _logger.LogWarning(ex, "创建订单失败(业务异常):{Message}", ex.Message); + throw; + } + catch (Exception ex) + { + // Error: 系统异常(非预期错误) + _logger.LogError(ex, "创建订单失败(系统异常):{@Request}", request); + throw; + } + } +} +``` + +### 11.2 结构化日志 +```csharp +// ✅ 使用结构化日志(推荐) +_logger.LogInformation("用户 {UserId} 创建了订单 {OrderId},金额 {Amount}", + userId, orderId, amount); + +// ✅ 记录对象(使用@符号) +_logger.LogInformation("订单详情:{@Order}", order); + +// ❌ 不要使用字符串拼接 +_logger.LogInformation("用户 " + userId + " 创建了订单 " + orderId); +``` + +### 11.3 敏感信息处理 +```csharp +// ✅ 不要记录敏感信息 +public class PaymentService +{ + public async Task ProcessPaymentAsync(PaymentRequest request) + { + // ❌ 错误:记录了密码、支付密码等敏感信息 + _logger.LogInformation("支付请求:{@Request}", request); + + // ✅ 正确:只记录非敏感信息 + _logger.LogInformation("处理支付,订单ID:{OrderId},金额:{Amount}", + request.OrderId, request.Amount); + } +} +``` + +## 12. API控制器规范 + +### 12.1 控制器结构(标准模板) +```csharp +/// +/// 订单管理API +/// +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class OrdersController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public OrdersController(IMediator mediator, ILogger logger) + { + _mediator = mediator; + _logger = logger; + } + + /// + /// 创建订单 + /// + /// 订单创建请求 + /// 订单信息 + /// 创建成功 + /// 请求参数错误 + /// 业务验证失败 + [HttpPost] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)] + public async Task CreateOrder([FromBody] CreateOrderRequest request) + { + _logger.LogInformation("API调用:创建订单"); + + var command = new CreateOrderCommand + { + MerchantId = request.MerchantId, + CustomerId = request.CustomerId, + Items = request.Items, + TotalAmount = request.TotalAmount + }; + + var result = await _mediator.Send(command); + + return Ok(ApiResponse.SuccessResult(result)); + } + + /// + /// 获取订单详情 + /// + /// 订单ID + /// 订单详情 + [HttpGet("{id}")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task GetOrder(Guid id) + { + var query = new GetOrderByIdQuery { OrderId = id }; + var result = await _mediator.Send(query); + return Ok(ApiResponse.SuccessResult(result)); + } + + /// + /// 获取订单列表 + /// + /// 查询参数 + /// 订单列表 + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task GetOrders([FromQuery] GetOrdersRequest request) + { + var query = new GetOrdersQuery + { + PageIndex = request.PageIndex, + PageSize = request.PageSize, + Status = request.Status + }; + + var result = await _mediator.Send(query); + return Ok(ApiResponse>.SuccessResult(result)); + } +} +``` + +### 12.2 统一返回结果(ApiResponse) +```csharp +// ✅ 统一返回结果(泛型) +public class ApiResponse +{ + public bool Success { get; set; } + public int Code { get; set; } = 200; + public string? Message { get; set; } + public T? Data { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + // 工厂方法:成功 + public static ApiResponse SuccessResult(T data, string? message = "操作成功") => new() + { + Success = true, + Code = 200, + Message = message, + Data = data + }; + + // 工厂方法:失败 + public static ApiResponse Failure(int code, string message) => new() + { + Success = false, + Code = code, + Message = message + }; +} + +// ✅ 分页结果 +public class PagedResult +{ + public List Items { get; set; } + public int TotalCount { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } + public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); + public bool HasPreviousPage => PageIndex > 1; + public bool HasNextPage => PageIndex < TotalPages; +} +``` + +### 12.3 非泛型便捷封装(推荐在仅返回消息时使用) +```csharp +public static class ApiResponse +{ + // ✅ 仅返回成功/消息(无数据载荷) + public static ApiResponse Success(string? message = "操作成功") + => new ApiResponse { Success = true, Code = 200, Message = message, Data = null }; + + // ✅ 错误(配合统一错误码) + public static ApiResponse Failure(int code, string message) + => new ApiResponse { Success = false, Code = code, Message = message, Data = null }; +} +``` + +### 12.4 统一错误码(ErrorCodes) +```csharp +public static class ErrorCodes +{ + public const int BadRequest = 400; + public const int Unauthorized = 401; + public const int Forbidden = 403; + public const int NotFound = 404; + public const int Conflict = 409; + public const int ValidationFailed = 422; // 业务/验证不通过 + public const int InternalServerError = 500; + + // 业务自定义区间(10000+) + public const int BusinessError = 10001; +} +``` + +### 12.5 错误响应的ProblemDetails映射(全局异常处理中间件) +```csharp +public class BusinessException : Exception +{ + public int ErrorCode { get; } + public BusinessException(int errorCode, string message) : base(message) => ErrorCode = errorCode; +} + +public class ValidationException : Exception +{ + public IDictionary Errors { get; } + public ValidationException(IDictionary errors) : base("一个或多个验证错误") => Errors = errors; +} + +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { _next = next; _logger = logger; } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (BusinessException ex) + { + await WriteProblemDetailsAsync(context, StatusCodes.Status422UnprocessableEntity, "业务异常", ex.Message, ex.ErrorCode); + } + catch (ValidationException ex) + { + await WriteProblemDetailsAsync(context, StatusCodes.Status422UnprocessableEntity, "验证异常", ex.Message, ErrorCodes.ValidationFailed, ex.Errors); + } + catch (Exception ex) + { + _logger.LogError(ex, "系统异常"); + await WriteProblemDetailsAsync(context, StatusCodes.Status500InternalServerError, "系统异常", "服务器发生错误,请稍后重试", ErrorCodes.InternalServerError); + } + } + + private static Task WriteProblemDetailsAsync(HttpContext context, int status, string title, string detail, int code, object? errors = null) + { + var problem = new ProblemDetails + { + Status = status, + Title = title, + Detail = detail, + Instance = context.Request.Path, + Type = $"https://httpstatuses.com/{status}" + }; + problem.Extensions["code"] = code; + if (errors != null) problem.Extensions["errors"] = errors; + + context.Response.StatusCode = status; + context.Response.ContentType = "application/problem+json"; + return context.Response.WriteAsJsonAsync(problem); + } +} + +// Program.cs 中注册 +app.UseMiddleware(); +``` + + +## 13. 实体和DTO规范 + +### 13.1 实体类(Domain Entity) +```csharp +// ✅ 领域实体 +public class Order : BaseEntity +{ + public Guid Id { get; private set; } + public string OrderNo { get; private set; } + public Guid MerchantId { get; private set; } + public Guid CustomerId { get; private set; } + public decimal TotalAmount { get; private set; } + public OrderStatus Status { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime? UpdatedAt { get; private set; } + + // 导航属性 + public virtual Merchant Merchant { get; private set; } + public virtual Customer Customer { get; private set; } + public virtual ICollection OrderItems { get; private set; } + + // 私有构造函数(用于EF Core) + private Order() { } + + // 工厂方法 + public static Order Create(Guid merchantId, Guid customerId, decimal totalAmount) + { + return new Order + { + Id = Guid.NewGuid(), + OrderNo = GenerateOrderNo(), + MerchantId = merchantId, + CustomerId = customerId, + TotalAmount = totalAmount, + Status = OrderStatus.Pending, + CreatedAt = DateTime.UtcNow + }; + } + + // 业务方法 + public void Confirm() + { + if (Status != OrderStatus.Pending) + throw new BusinessException(400, "只有待确认的订单才能确认"); + + Status = OrderStatus.Confirmed; + UpdatedAt = DateTime.UtcNow; + } + + public void Cancel(string reason) + { + if (Status == OrderStatus.Completed) + throw new BusinessException(400, "已完成的订单不能取消"); + + Status = OrderStatus.Cancelled; + UpdatedAt = DateTime.UtcNow; + } + + private static string GenerateOrderNo() + { + return $"ORD{DateTime.UtcNow:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}"; + } +} +``` + +### 13.2 DTO(数据传输对象) +```csharp +// ✅ 请求DTO +public class CreateOrderRequest +{ + public Guid MerchantId { get; set; } + public Guid CustomerId { get; set; } + public List Items { get; set; } + public decimal TotalAmount { get; set; } + public string DeliveryAddress { get; set; } + public string ContactPhone { get; set; } + public string Remark { get; set; } +} + +// ✅ 响应DTO +public class OrderDto +{ + public Guid Id { get; set; } + public string OrderNo { get; set; } + public Guid MerchantId { get; set; } + public string MerchantName { get; set; } + public Guid CustomerId { get; set; } + public string CustomerName { get; set; } + public decimal TotalAmount { get; set; } + public OrderStatus Status { get; set; } + public string StatusText { get; set; } + public List Items { get; set; } + public DateTime CreatedAt { get; set; } +} +``` + +### 13.3 AutoMapper配置 +```csharp +// ✅ AutoMapper Profile +public class OrderMappingProfile : Profile +{ + public OrderMappingProfile() + { + CreateMap() + .ForMember(dest => dest.MerchantName, opt => opt.MapFrom(src => src.Merchant.Name)) + .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)) + .ForMember(dest => dest.StatusText, opt => opt.MapFrom(src => src.Status.ToString())); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.OrderNo, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()); + } +} +``` + + +## 14. Git工作流规范 + +### 14.1 分支管理 +``` +main # 主分支,生产环境代码 +├── develop # 开发分支 +│ ├── feature/order-management # 功能分支 +│ ├── feature/payment-integration # 功能分支 +│ └── bugfix/order-calculation # 修复分支 +└── hotfix/critical-bug # 紧急修复分支 +``` + +### 14.2 分支命名规范 +- **功能分支**:`feature/功能名称`(如:`feature/order-management`) +- **修复分支**:`bugfix/问题描述`(如:`bugfix/order-calculation`) +- **紧急修复**:`hotfix/问题描述`(如:`hotfix/payment-error`) +- **发布分支**:`release/版本号`(如:`release/v1.0.0`) + +### 14.3 提交信息规范(严格遵守) +```bash +# 格式:(): + +# type类型(必须使用以下之一): +# feat: 新功能 +# fix: 修复bug +# docs: 文档更新 +# style: 代码格式调整(不影响代码运行) +# refactor: 重构(既不是新功能也不是修复bug) +# perf: 性能优化 +# test: 测试相关 +# chore: 构建/工具相关 + +# 示例(必须遵循): +git commit -m "feat(order): 添加订单创建功能" +git commit -m "fix(payment): 修复支付回调处理错误" +git commit -m "docs(api): 更新API文档" +git commit -m "refactor(service): 重构订单服务" +git commit -m "perf(query): 优化订单查询性能" +``` + +## 15. 单元测试规范 + +### 15.1 测试命名规范 +```csharp +// ✅ 测试命名格式:MethodName_Scenario_ExpectedResult +[Fact] +public async Task CreateOrder_ValidRequest_ReturnsOrderDto() +{ + // Arrange(准备) + var request = new CreateOrderRequest + { + MerchantId = Guid.NewGuid(), + CustomerId = Guid.NewGuid(), + Items = new List + { + new OrderItemDto { DishId = Guid.NewGuid(), Quantity = 2, Price = 50.00m } + }, + TotalAmount = 100.00m + }; + + // Act(执行) + var result = await _orderService.CreateOrderAsync(request); + + // Assert(断言) + result.Should().NotBeNull(); + result.OrderNo.Should().NotBeNullOrEmpty(); + result.TotalAmount.Should().Be(100.00m); +} + +[Fact] +public async Task CreateOrder_InvalidMerchantId_ThrowsBusinessException() +{ + // Arrange + var request = new CreateOrderRequest + { + MerchantId = Guid.Empty, // 无效的商家ID + CustomerId = Guid.NewGuid(), + TotalAmount = 100.00m + }; + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _orderService.CreateOrderAsync(request)); +} +``` + +### 15.2 Mock使用 +```csharp +// ✅ 使用Moq进行Mock +public class OrderServiceTests +{ + private readonly Mock _orderRepositoryMock; + private readonly Mock _unitOfWorkMock; + private readonly Mock> _loggerMock; + private readonly Mock _mapperMock; + private readonly OrderService _orderService; + + public OrderServiceTests() + { + _orderRepositoryMock = new Mock(); + _unitOfWorkMock = new Mock(); + _loggerMock = new Mock>(); + _mapperMock = new Mock(); + + _orderService = new OrderService( + _orderRepositoryMock.Object, + _unitOfWorkMock.Object, + _loggerMock.Object, + _mapperMock.Object, + Options.Create(new OrderSettings()) + ); + } + + [Fact] + public async Task GetOrder_ExistingId_ReturnsOrder() + { + // Arrange + var orderId = Guid.NewGuid(); + var order = new Order { Id = orderId, OrderNo = "ORD001" }; + var orderDto = new OrderDto { Id = orderId, OrderNo = "ORD001" }; + + _orderRepositoryMock + .Setup(x => x.GetByIdAsync(orderId)) + .ReturnsAsync(order); + + _mapperMock + .Setup(x => x.Map(order)) + .Returns(orderDto); + + // Act + var result = await _orderService.GetOrderAsync(orderId); + + // Assert + result.Should().NotBeNull(); + result.Id.Should().Be(orderId); + _orderRepositoryMock.Verify(x => x.GetByIdAsync(orderId), Times.Once); + } +} +``` + +### 15.3 测试覆盖率要求 +- **核心业务逻辑**:>= 80% +- **服务层**:>= 70% +- **仓储层**:>= 60% +- **控制器层**:>= 50% + +## 16. 性能优化规范 + +### 16.1 数据库查询优化 +```csharp +// ❌ 错误:N+1查询问题 +public async Task> GetOrdersAsync() +{ + var orders = await _context.Orders.ToListAsync(); + + foreach (var order in orders) + { + // 每次循环都会查询数据库! + order.Customer = await _context.Customers.FindAsync(order.CustomerId); + order.Merchant = await _context.Merchants.FindAsync(order.MerchantId); + } + + return _mapper.Map>(orders); +} + +// ✅ 正确:使用Include预加载 +public async Task> GetOrdersAsync() +{ + var orders = await _context.Orders + .Include(o => o.Customer) + .Include(o => o.Merchant) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.Dish) + .ToListAsync(); + + return _mapper.Map>(orders); +} + +// ✅ 更好:大数据量使用Dapper +public async Task> GetOrdersAsync() +{ + var sql = @" + SELECT + o.id, o.order_no, o.total_amount, + c.id as customer_id, c.name as customer_name, + m.id as merchant_id, m.name as merchant_name + FROM orders o + INNER JOIN customers c ON o.customer_id = c.id + INNER JOIN merchants m ON o.merchant_id = m.id + WHERE o.status = @Status + ORDER BY o.created_at DESC + LIMIT @Limit OFFSET @Offset"; + + return await _connection.QueryAsync(sql, new { Status = 1, Limit = 100, Offset = 0 }); +} +``` + +### 16.2 异步编程规范 +```csharp +// ✅ 正确:使用async/await +public async Task CreateOrderAsync(CreateOrderRequest request) +{ + var order = new Order { /* ... */ }; + await _orderRepository.AddAsync(order); + await _unitOfWork.SaveChangesAsync(); + return _mapper.Map(order); +} + +// ❌ 错误:不要使用.Result或.Wait() +public OrderDto CreateOrder(CreateOrderRequest request) +{ + var order = new Order { /* ... */ }; + _orderRepository.AddAsync(order).Wait(); // 可能导致死锁! + _unitOfWork.SaveChangesAsync().Result; // 可能导致死锁! + return _mapper.Map(order); +} + +// ❌ 错误:不要混用同步和异步 +public async Task CreateOrderAsync(CreateOrderRequest request) +{ + var order = new Order { /* ... */ }; + _orderRepository.AddAsync(order).Wait(); // 错误! + await _unitOfWork.SaveChangesAsync(); + return _mapper.Map(order); +} +``` + +### 16.3 批量操作优化 +```csharp +// ❌ 错误:逐条插入 +public async Task ImportOrdersAsync(List orders) +{ + foreach (var order in orders) + { + await _context.Orders.AddAsync(order); + await _context.SaveChangesAsync(); // 每次都保存,性能差! + } +} + +// ✅ 正确:批量插入 +public async Task ImportOrdersAsync(List orders) +{ + await _context.Orders.AddRangeAsync(orders); + await _context.SaveChangesAsync(); // 一次性保存 +} + +// ✅ 更好:使用Dapper批量插入(大数据量) +public async Task ImportOrdersAsync(List orders) +{ + var sql = @" + INSERT INTO orders (id, order_no, merchant_id, total_amount, created_at) + VALUES (@Id, @OrderNo, @MerchantId, @TotalAmount, @CreatedAt)"; + + await _connection.ExecuteAsync(sql, orders); +} +``` + +## 17. 安全规范 + +### 17.1 SQL注入防护 +```csharp +// ❌ 错误:字符串拼接SQL(SQL注入风险) +public async Task GetOrderByNoAsync(string orderNo) +{ + var sql = $"SELECT * FROM orders WHERE order_no = '{orderNo}'"; // 危险! + return await _connection.QueryFirstOrDefaultAsync(sql); +} + +// ✅ 正确:使用参数化查询 +public async Task GetOrderByNoAsync(string orderNo) +{ + var sql = "SELECT * FROM orders WHERE order_no = @OrderNo"; + return await _connection.QueryFirstOrDefaultAsync(sql, new { OrderNo = orderNo }); +} +``` + +### 17.2 敏感数据加密 +```csharp +// ✅ 密码加密存储 +public class UserService +{ + private readonly IPasswordHasher _passwordHasher; + + public async Task CreateUserAsync(string username, string password) + { + var user = new User { Username = username }; + + // ✅ 使用密码哈希 + user.PasswordHash = _passwordHasher.HashPassword(user, password); + + await _userRepository.AddAsync(user); + await _unitOfWork.SaveChangesAsync(); + + return user; + } + + public async Task ValidatePasswordAsync(User user, string password) + { + var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); + return result == PasswordVerificationResult.Success; + } +} +``` + +### 17.3 授权验证 +```csharp +// ✅ 使用授权特性 +[Authorize(Roles = "Admin")] +public class AdminController : ControllerBase +{ + [HttpGet("users")] + public async Task GetUsers() + { + // 只有Admin角色可以访问 + } +} + +// ✅ 基于策略的授权 +[Authorize(Policy = "MerchantOwner")] +public class MerchantController : ControllerBase +{ + [HttpPut("{id}")] + public async Task UpdateMerchant(Guid id, UpdateMerchantRequest request) + { + // 只有商家所有者可以更新 + } +} + +// ✅ 在服务层也要验证权限 +public async Task UpdateMerchantAsync(Guid merchantId, Guid userId, UpdateMerchantRequest request) +{ + var merchant = await _merchantRepository.GetByIdAsync(merchantId); + + // 验证用户是否有权限 + if (merchant.OwnerId != userId) + { + throw new BusinessException(403, "无权限操作"); + } + + // ... 更新逻辑 +} +``` + +## 18. 代码审查清单 + +### 18.1 必查项目 +- [ ] 代码符合命名规范(PascalCase、camelCase、_camelCase) +- [ ] 所有公共API有XML文档注释 +- [ ] 复杂业务逻辑有注释说明 +- [ ] 异常处理完善(try-catch、自定义异常) +- [ ] 使用异步方法(async/await) +- [ ] 使用依赖注入(构造函数注入) +- [ ] 使用仓储模式(不直接操作DbContext) +- [ ] 使用工作单元模式(事务管理) +- [ ] 日志记录完善(Information、Warning、Error) +- [ ] 参数验证(FluentValidation) +- [ ] 返回DTO而不是实体 +- [ ] 无硬编码配置(使用IOptions) +- [ ] 无SQL注入风险(参数化查询) +- [ ] 敏感数据加密 +- [ ] 权限验证完善 + +### 18.2 性能检查 +- [ ] 避免N+1查询(使用Include) +- [ ] 大数据量使用Dapper +- [ ] 合理使用缓存 +- [ ] 批量操作使用批量方法 +- [ ] 异步方法不使用.Result或.Wait() + +### 18.3 安全检查 +- [ ] 无SQL注入风险 +- [ ] 密码已加密 +- [ ] 授权验证完善 +- [ ] 敏感信息不记录日志 +- [ ] HTTPS传输 + +## 19. 禁止事项(严格禁止) + +### 19.1 绝对禁止 +```csharp +// ❌ 禁止:直接在控制器或服务中使用DbContext +public class OrderController +{ + private readonly AppDbContext _context; // 禁止! +} + +// ❌ 禁止:硬编码配置 +var deliveryFee = 5.0m; // 禁止!应该从配置读取 + +// ❌ 禁止:返回实体类 +public Order GetOrder(Guid id) // 禁止!应该返回DTO +{ + return _context.Orders.Find(id); +} + +// ❌ 禁止:字符串拼接SQL +var sql = $"SELECT * FROM orders WHERE id = '{id}'"; // 禁止!SQL注入风险 + +// ❌ 禁止:吞掉异常 +try +{ + // ... +} +catch (Exception) +{ + // 什么都不做 - 禁止! +} + +// ❌ 禁止:使用.Result或.Wait() +var result = _service.GetOrderAsync(id).Result; // 禁止!可能死锁 + +// ❌ 禁止:记录敏感信息 +_logger.LogInformation("用户密码:{Password}", password); // 禁止! + +// ❌ 禁止:不使用异步方法 +public Order CreateOrder(CreateOrderRequest request) // 禁止!应该使用async +{ + _context.Orders.Add(order); + _context.SaveChanges(); // 应该使用SaveChangesAsync +} +``` + +## 20. 最佳实践总结 + +### 20.1 核心原则 +1. **SOLID原则**:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置 +2. **DRY原则**:不要重复自己(Don't Repeat Yourself) +3. **KISS原则**:保持简单(Keep It Simple, Stupid) +4. **YAGNI原则**:你不会需要它(You Aren't Gonna Need It) + +### 20.2 编码习惯 +- ✅ 使用有意义的变量名 +- ✅ 方法保持简短(不超过50行) +- ✅ 类保持单一职责 +- ✅ 优先使用组合而不是继承 +- ✅ 编写可测试的代码 +- ✅ 先写测试再写代码(TDD) +- ✅ 持续重构,保持代码整洁 + +### 20.3 团队协作 +- ✅ 遵循统一的代码规范 +- ✅ 代码审查必须通过 +- ✅ 提交前运行测试 +- ✅ 提交信息清晰明确 +- ✅ 及时更新文档 +- ✅ 主动分享知识 + +--- + +## 附录:快速参考 + +### A. 常用命名模式 +- 类:`OrderService`、`MerchantRepository` +- 接口:`IOrderService`、`IMerchantRepository` +- 方法:`CreateOrderAsync`、`GetOrderByIdAsync` +- 字段:`_orderRepository`、`_logger` +- 属性:`OrderNo`、`TotalAmount` +- 变量:`orderTotal`、`merchantId` + +### B. 常用文件夹结构 +``` +Controllers/ +Services/ +Repositories/ +DTOs/ +Entities/ +Validators/ +Mappings/ +Exceptions/ +Constants/ +Extensions/ +``` + +### C. 必须使用的NuGet包 +- Microsoft.EntityFrameworkCore +- Dapper +- AutoMapper.Extensions.Microsoft.DependencyInjection +- FluentValidation.AspNetCore +- Serilog.AspNetCore +- MediatR +- Swashbuckle.AspNetCore +- xUnit +- Moq +- FluentAssertions + +--- + +**文档版本**:v1.0 +**最后更新**:2025-11-22 +**适用项目**:外卖SaaS系统 +**目标读者**:AI编程助手、开发人员 diff --git a/0_Document/09_AI精简开发规范.md b/0_Document/09_AI精简开发规范.md new file mode 100644 index 0000000..7447db8 --- /dev/null +++ b/0_Document/09_AI精简开发规范.md @@ -0,0 +1,79 @@ +# 编程规范_FOR_AI(TakeoutSaaS) + +说明:本规范为AI编程助手与开发者共同遵循的统一编码规范,结合 0_Document 下文档约定(特别是 06_开发规范.md、02_技术架构.md、12.* 规范)执行。超出本文件内容的详细条目请以文档中心为准。 + +## 1. 技术栈 +- .NET 10 + ASP.NET Core Web API +- EF Core 10(复杂关系/事务)+ Dapper(统计/批量)+ PostgreSQL 16+ +- Redis、RabbitMQ、Swagger、MediatR、Serilog、FluentValidation、AutoMapper、Hangfire、Polly + +## 2. 命名与风格 +- 类/方法/属性:PascalCase;接口:I前缀;私有字段:_camelCase;变量:camelCase;常量:PascalCase +- 每文件仅1个公共类型,文件名与类型名一致 +- 命名空间与目录结构一致 + +## 3. 分层与结构 +- 物理结构:Api(AdminApi/MiniApi/UserApi)+ Application + Domain + Infrastructure + Core(Shared.*) + Modules + Gateway +- 不允许在Controller/Service中直接操作DbContext,必须通过仓储/应用服务 +- 返回DTO,禁止直接返回实体 + +## 4. 注释与文档 +- 所有公共API、接口、复杂逻辑必须有XML注释 +- 控制器、服务方法提供简要说明与异常声明 + +## 5. 异常与错误码 +- 使用 BusinessException(含ErrorCode)/ ValidationException;禁止吞异常 +- 全局异常中间件输出 ProblemDetails(扩展code与errors) +- 错误码:400/401/403/404/409/422/500 + 业务10001+ + +## 6. 异步与日志 +- 全面使用 async/await,禁止 .Result/.Wait() +- 使用 Serilog 记录结构化日志,避免记录敏感数据 + +## 7. 依赖注入 +- 统一使用构造函数注入,禁止服务定位器 +- 业务逻辑在应用层,仓储在基础设施层 + +## 8. 数据访问 +- EF Core 10 负责关系/事务/迁移;Dapper 负责统计和大批量 +- 使用工作单元与仓储模式;避免N+1;只读查询使用AsNoTracking +- 参数化查询,禁止字符串拼接SQL + +## 9. 多租户 +- 通过 Header:X-Tenant-Id 或 Token Claim: tenant_id 解析租户 +- EF Core 全局过滤(tenant_id);写入数据时自动填充租户 + +## 10. 安全 +- HTTPS、Security Headers、CORS按端配置 +- 授权:AdminApi 使用JWT+RBAC;MiniApi 小程序登录态+JWT +- 严禁日志打印密码/支付信息等敏感数据 + +## 11. API 设计 +- RESTful,统一 /api/{area}/v{version} +- 统一返回:ApiResponse;分页返回使用 PagedResult +- Swagger 按版本与端分组,开启鉴权按钮 + +## 12. 模块化 +- 独立模块抽象:Identity、Authorization、Tenancy、Dictionary、Storage、Sms、Messaging、Scheduler、Delivery +- 公共横切能力抽到 Shared.* 复用 + +## 13. 测试 +- xUnit + Moq + FluentAssertions;命名:Method_Scenario_Expected +- 核心业务覆盖率≥80% + +## 14. Git 提交 +- 使用 Conventional Commits:feat/fix/docs/style/refactor/perf/test/chore + +## 15. 性能 +- 投影查询、编译查询、批量操作(ExecuteUpdate/ExecuteDelete) +- 缓存优先:Cache-Aside;更新后清缓存 + +## 16. 禁止事项 +- 直接使用DbContext(绕过仓储/工作单元) +- 硬编码配置(使用IOptions) +- 返回实体类 +- SQL拼接注入风险 +- 吞异常或静默失败 +- 同步阻塞异步 + +以上规范将随着文档中心的演进不断完善;AI编程助手生成的代码必须符合本规范,并默认使用这些约束。 diff --git a/0_Document/README.md b/0_Document/README.md new file mode 100644 index 0000000..aa08578 --- /dev/null +++ b/0_Document/README.md @@ -0,0 +1,195 @@ +# 外卖SaaS系统 - 文档中心 + +欢迎查阅外卖SaaS系统的完整文档。本文档中心包含了项目的所有技术文档和开发指南。 + +## 📚 文档目录 + +### 1. [项目概述](01_项目概述.md) +- 项目简介与背景 +- 核心业务模块介绍 +- 用户角色说明 +- 系统特性 +- 技术选型 +- 项目里程碑 + +**适合人群**:项目经理、产品经理、新加入的开发人员 + +--- + +### 2. [技术架构](02_技术架构.md) +- 技术栈详解 +- 系统架构设计 +- 分层架构说明 +- 核心设计模式 +- 数据访问策略(EF Core + Dapper) +- 缓存策略 +- 消息队列应用 +- 安全设计 + +**适合人群**:架构师、技术负责人、高级开发人员 + +--- + +### 3. [数据库设计](03_数据库设计.md) +- 数据库设计原则 +- 命名规范 +- 核心表结构 + - 租户管理 + - 商家管理 + - 菜品管理 + - 订单管理 + - 配送管理 + - 支付管理 + - 营销管理 + - 系统管理 +- 索引策略 +- 数据库优化 +- 备份策略 + +**适合人群**:数据库管理员、后端开发人员 + +--- + +### 4A. [管理后台 API 设计](04A_管理后台API.md) +- 角色与权限(平台/租户/商家) +- 租户与商家管理 +- 菜品与分类管理 +- 订单流转与售后 +- 优惠券与评价管理 +- 统计报表与文件上传 + +### 4B. [小程序/用户端 API 设计](04B_小程序API.md) +- 小程序登录与用户信息 +- 商家与门店浏览 +- 菜品与分类列表 +- 购物车同步 +- 订单创建/查询/取消 +- 支付对接(微信/支付宝) +- 优惠券领取与使用、评价发布 + +**适合人群**:前端开发人员(小程序/Web用户端)、后端开发人员、接口对接人员 + +--- + +### 5. [部署运维](05_部署运维.md) +- 环境要求 +- 本地开发环境搭建 +- Docker部署 +- Nginx配置 +- 数据库部署(主从复制) +- Redis部署(哨兵模式) +- CI/CD配置 +- 监控告警(Prometheus + Grafana) +- 日志管理(ELK Stack) +- 安全加固 +- 性能优化 +- 故障恢复 + +**适合人群**:运维工程师、DevOps工程师、系统管理员 + +--- + +### 6. [开发规范](06_开发规范.md) +- 代码规范 + - 命名规范 + - 代码组织 + - 代码注释 + - 异常处理 +- Git工作流 + - 分支管理 + - 提交信息规范 +- 代码审查标准 +- 单元测试规范 +- 性能优化规范 +- 安全规范 +- 日志规范 +- 配置管理 +- API设计规范 + +**适合人群**:所有开发人员 + +--- + +### 7. [系统架构图](07_系统架构图.md) +- 整体架构图 +- 应用分层架构 +- 订单处理流程图 +- 数据流转图 +- 多租户数据隔离架构 +- 缓存架构 +- 消息队列架构 +- 部署架构 +- 监控架构 + +**适合人群**:架构师、技术负责人、所有开发人员 + +--- + +## 🚀 快速导航 + +### 我是新人,从哪里开始? +1. 先阅读 [项目概述](01_项目概述.md) 了解项目背景和业务 +2. 查看 [系统架构图](07_系统架构图.md) 理解系统整体架构 +3. 阅读 [开发规范](06_开发规范.md) 了解开发要求 +4. 参考 [部署运维](05_部署运维.md) 搭建本地开发环境 + +### 我要开发新功能 +1. 查看 [数据库设计](03_数据库设计.md) 了解数据模型 +2. 参考 [API接口设计](04_API接口设计.md) 设计接口 +3. 遵循 [开发规范](06_开发规范.md) 编写代码 +4. 参考 [技术架构](02_技术架构.md) 选择合适的技术方案 + +### 我要部署系统 +1. 阅读 [部署运维](05_部署运维.md) 了解部署流程 +2. 参考 [系统架构图](07_系统架构图.md) 理解部署架构 +3. 按照文档配置监控和日志系统 + +### 我要对接API +1. 查看 [API接口设计](04_API接口设计.md) 了解接口规范 +2. 参考接口文档进行开发和测试 + +--- + +## 📖 文档更新记录 + +### v1.0.0 (2024-01-01) +- ✅ 完成项目概述文档 +- ✅ 完成技术架构文档 +- ✅ 完成数据库设计文档 +- ✅ 完成API接口设计文档 +- ✅ 完成部署运维文档 +- ✅ 完成开发规范文档 +- ✅ 完成系统架构图文档 + +--- + +## 💡 文档贡献 + +如果您发现文档有任何问题或需要改进的地方,欢迎: +1. 提交 Issue 反馈问题 +2. 提交 Pull Request 改进文档 +3. 联系项目负责人 + +--- + +## 📞 联系方式 + +- 项目地址:https://github.com/your-org/takeout-saas +- 问题反馈:https://github.com/your-org/takeout-saas/issues +- 邮箱:dev@example.com + +--- + +## 📝 文档规范 + +本文档使用 Markdown 格式编写,遵循以下规范: +- 使用清晰的标题层级 +- 代码示例使用语法高亮 +- 重要内容使用加粗或引用 +- 保持文档简洁易读 +- 及时更新文档内容 + +--- + +**最后更新时间**:2024-01-01 +**文档版本**:v1.0.0 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..59c4f40 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + net10.0 + enable + enable + latest + false + + + diff --git a/README.md b/README.md index 4bd4606..d82e93a 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,173 @@ -## 项目名称 -> 请介绍一下你的项目吧 +# 外卖SaaS系统 (TakeoutSaaS) +## 项目简介 +外卖SaaS系统是一个基于.NET 10的多租户外卖管理平台,为中小型餐饮企业提供完整的外卖业务解决方案。系统采用现代化的技术栈,支持商家管理、菜品管理、订单处理、配送管理、支付集成等核心功能。 + +### 核心特性 + +- 🏢 **多租户架构**:支持多租户数据隔离,SaaS模式运营 +- 🍔 **商家管理**:完善的商家入驻、门店管理、菜品管理功能 +- 📦 **订单管理**:订单全生命周期管理,实时状态跟踪 +🚚 配送管理:配送任务、路线规划、第三方配送对接 +- 💰 **支付集成**:支持微信、支付宝等多种支付方式 +- 🎁 **营销功能**:优惠券、满减活动、会员积分 +- 📊 **数据分析**:实时数据统计、经营报表、趋势分析 +- 🔒 **安全可靠**:JWT认证、权限控制、数据加密 + +## 技术栈 + +### 后端技术 +- **.NET 10**:最新的.NET平台 +- **ASP.NET Core Web API**:RESTful API服务 +- **Entity Framework Core 10**:最新ORM框架 +- **Dapper 2.1+**:高性能数据访问 +- **PostgreSQL 16+**:主数据库 +- **Redis 7.0+**:分布式缓存 +- **RabbitMQ 3.12+**:消息队列 + +### 开发框架 +- **AutoMapper**:对象映射 +- **FluentValidation**:数据验证 +- **Serilog**:结构化日志 +- **MediatR**:CQRS模式 +- **Hangfire**:后台任务 +- **Swagger**:API文档 ## 运行条件 -> 列出运行该项目所必须的条件和相关依赖 -* 条件一 -* 条件二 -* 条件三 +### 开发环境要求 +* .NET SDK 10.0 或更高版本 +* PostgreSQL 16+ +* Redis 7.0+ +* RabbitMQ 3.12+(可选) +* Docker Desktop(推荐,用于容器化开发) +### 推荐IDE +* Visual Studio 2022 +* JetBrains Rider +* Visual Studio Code -## 运行说明 -> 说明如何运行和使用你的项目,建议给出具体的步骤说明 -* 操作一 -* 操作二 -* 操作三 +## 快速开始 +### 1. 克隆项目 +```bash +git clone https://github.com/your-org/takeout-saas.git +cd takeout-saas +``` +### 2. 使用Docker Compose启动依赖服务(推荐) +```bash +# 启动PostgreSQL、Redis、RabbitMQ等服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps +``` + +### 3. 配置数据库连接 +编辑 `src/TakeoutSaaS.Api/appsettings.Development.json` + +### 4. 执行数据库迁移 +```bash +cd src/TakeoutSaaS.Api +dotnet ef database update +``` + +### 5. 运行项目 +```bash +dotnet run +``` + +访问 API 文档: +- 管理后台 AdminApi Swagger:http://localhost:5001/swagger +- 小程序/用户端 MiniApi Swagger:http://localhost:5002/swagger + +## 项目结构 + +``` +TakeoutSaaS/ +├── 0_Document/ # 项目文档 +│ ├── 01_项目概述.md +│ ├── 02_技术架构.md +│ ├── 03_数据库设计.md +│ ├── 04A_管理后台API.md +│ ├── 04B_小程序API.md +│ ├── 05_部署运维.md +│ └── 06_开发规范.md +├── src/ +│ ├── TakeoutSaaS.AdminApi/ # 管理后台 Web API +│ ├── TakeoutSaaS.MiniApi/ # 小程序/用户端 Web API +│ ├── TakeoutSaaS.Application/ # 应用层 +│ ├── TakeoutSaaS.Domain/ # 领域层 +│ ├── TakeoutSaaS.Infrastructure/ # 基础设施层 +│ └── TakeoutSaaS.Shared/ # 共享层 +├── tests/ +│ ├── TakeoutSaaS.UnitTests/ # 单元测试 +│ └── TakeoutSaaS.IntegrationTests/ # 集成测试 +├── docker-compose.yml # Docker编排文件 +└── README.md +``` ## 测试说明 -> 如果有测试相关内容需要说明,请填写在这里 +### 运行单元测试 +```bash +dotnet test tests/TakeoutSaaS.UnitTests +``` +### 运行集成测试 +```bash +dotnet test tests/TakeoutSaaS.IntegrationTests +``` -## 技术架构 -> 使用的技术框架或系统架构图等相关说明,请填写在这里 +## 部署说明 +### Docker部署 +```bash +# 构建镜像 +docker build -t takeout-saas-api:latest . + +# 运行容器 +docker run -d -p 8080:80 --name takeout-api takeout-saas-api:latest +``` + +详细部署文档请参考:[部署运维文档](0_Document/05_部署运维.md) + +## 文档 + +- [项目概述](0_Document/01_项目概述.md) - 系统介绍和业务说明 +- [技术架构](0_Document/02_技术架构.md) - 技术栈和架构设计 +- [数据库设计](0_Document/03_数据库设计.md) - 数据模型和表结构 +- [API接口设计](0_Document/04_API接口设计.md) - RESTful API规范 +- [部署运维](0_Document/05_部署运维.md) - 部署和运维指南 +- [开发规范](0_Document/06_开发规范.md) - 代码规范和最佳实践 + +## 开发规范 + +请遵循项目的[开发规范](0_Document/06_开发规范.md) + +## 贡献指南 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +## 许可证 + +本项目采用 MIT 许可证 + +## 联系方式 + +- 项目地址:https://github.com/your-org/takeout-saas +- 问题反馈:https://github.com/your-org/takeout-saas/issues ## 协作者 -> 高效的协作会激发无尽的创造力,将他们的名字记录在这里吧 + +感谢所有为本项目做出贡献的开发者! + +--- + +⭐ 如果这个项目对你有帮助,请给我们一个星标! diff --git a/TakeoutSaaS.sln b/TakeoutSaaS.sln new file mode 100644 index 0000000..86c3d6d --- /dev/null +++ b/TakeoutSaaS.sln @@ -0,0 +1,255 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{81034408-37C8-1011-444E-4C15C2FADA8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.AdminApi", "src\Api\TakeoutSaaS.AdminApi\TakeoutSaaS.AdminApi.csproj", "{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8D626EA8-CB54-BC41-363A-217881BEBA6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Web", "src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj", "{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Abstractions", "src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj", "{0DA03B31-E718-4424-A1F0-9989E79FFE81}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{22BAF98C-8415-17C4-B26A-D537657BC863}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Application", "src\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj", "{27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{8B290487-4C16-E85E-E807-F579CBE9FC4D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Domain", "src\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj", "{464913F5-70F2-4661-B3AF-B1C87FFFA4EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{9048EB7F-3875-A59E-E36B-5BD4C6F2A282}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Infrastructure", "src\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj", "{80B45C7D-9423-400A-8279-40D95BFEBC9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Identity", "src\Modules\TakeoutSaaS.Module.Identity\TakeoutSaaS.Module.Identity.csproj", "{582EDD19-3C2F-4693-9595-CC367318CD19}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Authorization", "src\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj", "{6CB8487D-5C74-487C-9D84-E57838BDA015}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Tenancy", "src\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj", "{5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.MiniApi", "src\Api\TakeoutSaaS.MiniApi\TakeoutSaaS.MiniApi.csproj", "{12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.UserApi", "src\Api\TakeoutSaaS.UserApi\TakeoutSaaS.UserApi.csproj", "{1C0BCC51-AF18-44F3-A1E6-A693F74276B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gateway", "Gateway", "{6306A8FB-679E-111F-6585-8F70E0EE6013}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.ApiGateway", "src\Gateway\TakeoutSaaS.ApiGateway\TakeoutSaaS.ApiGateway.csproj", "{A2620200-D487-49A7-ABAF-9B84951F81DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Kernel", "src\Core\TakeoutSaaS.Shared.Kernel\TakeoutSaaS.Shared.Kernel.csproj", "{BBC99B58-ECA8-42C3-9070-9AA058D778D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Storage", "src\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj", "{05058F44-6FB7-43AF-8648-8BF538E283EF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x64.ActiveCfg = Debug|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x64.Build.0 = Debug|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x86.ActiveCfg = Debug|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x86.Build.0 = Debug|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|Any CPU.Build.0 = Release|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x64.ActiveCfg = Release|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x64.Build.0 = Release|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x86.ActiveCfg = Release|Any CPU + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x86.Build.0 = Release|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|x64.Build.0 = Debug|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|x86.ActiveCfg = Debug|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|x86.Build.0 = Debug|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Release|Any CPU.Build.0 = Release|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Release|x64.ActiveCfg = Release|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Release|x64.Build.0 = Release|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Release|x86.ActiveCfg = Release|Any CPU + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Release|x86.Build.0 = Release|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Debug|x64.ActiveCfg = Debug|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Debug|x64.Build.0 = Debug|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Debug|x86.ActiveCfg = Debug|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Debug|x86.Build.0 = Debug|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Release|Any CPU.Build.0 = Release|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Release|x64.ActiveCfg = Release|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Release|x64.Build.0 = Release|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Release|x86.ActiveCfg = Release|Any CPU + {0DA03B31-E718-4424-A1F0-9989E79FFE81}.Release|x86.Build.0 = Release|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Debug|x64.ActiveCfg = Debug|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Debug|x64.Build.0 = Debug|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Debug|x86.ActiveCfg = Debug|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Debug|x86.Build.0 = Debug|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Release|Any CPU.Build.0 = Release|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Release|x64.ActiveCfg = Release|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Release|x64.Build.0 = Release|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Release|x86.ActiveCfg = Release|Any CPU + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00}.Release|x86.Build.0 = Release|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Debug|x64.Build.0 = Debug|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Debug|x86.Build.0 = Debug|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Release|Any CPU.Build.0 = Release|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Release|x64.ActiveCfg = Release|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Release|x64.Build.0 = Release|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Release|x86.ActiveCfg = Release|Any CPU + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC}.Release|x86.Build.0 = Release|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Debug|x64.ActiveCfg = Debug|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Debug|x64.Build.0 = Debug|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Debug|x86.ActiveCfg = Debug|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Debug|x86.Build.0 = Debug|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|Any CPU.Build.0 = Release|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x64.ActiveCfg = Release|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x64.Build.0 = Release|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x86.ActiveCfg = Release|Any CPU + {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x86.Build.0 = Release|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x64.ActiveCfg = Debug|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x64.Build.0 = Debug|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x86.ActiveCfg = Debug|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x86.Build.0 = Debug|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|Any CPU.Build.0 = Release|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x64.ActiveCfg = Release|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x64.Build.0 = Release|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x86.ActiveCfg = Release|Any CPU + {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x86.Build.0 = Release|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|x64.ActiveCfg = Debug|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|x64.Build.0 = Debug|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|x86.ActiveCfg = Debug|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|x86.Build.0 = Debug|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|Any CPU.Build.0 = Release|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x64.ActiveCfg = Release|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x64.Build.0 = Release|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x86.ActiveCfg = Release|Any CPU + {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x86.Build.0 = Release|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x64.Build.0 = Debug|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x86.Build.0 = Debug|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|Any CPU.Build.0 = Release|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x64.ActiveCfg = Release|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x64.Build.0 = Release|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x86.ActiveCfg = Release|Any CPU + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x86.Build.0 = Release|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x64.ActiveCfg = Debug|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x64.Build.0 = Debug|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x86.ActiveCfg = Debug|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x86.Build.0 = Debug|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|Any CPU.Build.0 = Release|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x64.ActiveCfg = Release|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x64.Build.0 = Release|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x86.ActiveCfg = Release|Any CPU + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x86.Build.0 = Release|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x64.Build.0 = Debug|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x86.Build.0 = Debug|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|Any CPU.Build.0 = Release|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x64.ActiveCfg = Release|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x64.Build.0 = Release|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x86.ActiveCfg = Release|Any CPU + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x86.Build.0 = Release|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x64.Build.0 = Debug|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x86.Build.0 = Debug|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|Any CPU.Build.0 = Release|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x64.ActiveCfg = Release|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x64.Build.0 = Release|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x86.ActiveCfg = Release|Any CPU + {A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x86.Build.0 = Release|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|x64.Build.0 = Debug|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|x86.Build.0 = Debug|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Release|Any CPU.Build.0 = Release|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Release|x64.ActiveCfg = Release|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Release|x64.Build.0 = Release|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Release|x86.ActiveCfg = Release|Any CPU + {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Release|x86.Build.0 = Release|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Debug|x64.Build.0 = Debug|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Debug|x86.Build.0 = Debug|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Release|Any CPU.Build.0 = Release|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Release|x64.ActiveCfg = Release|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Release|x64.Build.0 = Release|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Release|x86.ActiveCfg = Release|Any CPU + {05058F44-6FB7-43AF-8648-8BF538E283EF}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {81034408-37C8-1011-444E-4C15C2FADA8E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954} = {81034408-37C8-1011-444E-4C15C2FADA8E} + {8D626EA8-CB54-BC41-363A-217881BEBA6E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {022FCF39-EC48-46EA-AC08-FA2EAD1548B7} = {8D626EA8-CB54-BC41-363A-217881BEBA6E} + {0DA03B31-E718-4424-A1F0-9989E79FFE81} = {8D626EA8-CB54-BC41-363A-217881BEBA6E} + {22BAF98C-8415-17C4-B26A-D537657BC863} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {27FA49F3-FC1A-44F7-B2A9-3833AC3A2E00} = {22BAF98C-8415-17C4-B26A-D537657BC863} + {8B290487-4C16-E85E-E807-F579CBE9FC4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {464913F5-70F2-4661-B3AF-B1C87FFFA4EC} = {8B290487-4C16-E85E-E807-F579CBE9FC4D} + {9048EB7F-3875-A59E-E36B-5BD4C6F2A282} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {80B45C7D-9423-400A-8279-40D95BFEBC9D} = {9048EB7F-3875-A59E-E36B-5BD4C6F2A282} + {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {582EDD19-3C2F-4693-9595-CC367318CD19} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {6CB8487D-5C74-487C-9D84-E57838BDA015} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D} = {81034408-37C8-1011-444E-4C15C2FADA8E} + {1C0BCC51-AF18-44F3-A1E6-A693F74276B5} = {81034408-37C8-1011-444E-4C15C2FADA8E} + {6306A8FB-679E-111F-6585-8F70E0EE6013} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A2620200-D487-49A7-ABAF-9B84951F81DD} = {6306A8FB-679E-111F-6585-8F70E0EE6013} + {BBC99B58-ECA8-42C3-9070-9AA058D778D3} = {8D626EA8-CB54-BC41-363A-217881BEBA6E} + {05058F44-6FB7-43AF-8648-8BF538E283EF} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + EndGlobalSection +EndGlobal diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep b/src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Api/TakeoutSaaS.AdminApi/Properties/launchSettings.json b/src/Api/TakeoutSaaS.AdminApi/Properties/launchSettings.json new file mode 100644 index 0000000..efda216 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TakeoutSaaS.AdminApi": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:2676;http://localhost:2680" + } + } +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj new file mode 100644 index 0000000..9a71725 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -0,0 +1,21 @@ + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep b/src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json b/src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json new file mode 100644 index 0000000..c959907 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TakeoutSaaS.MiniApi": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:2678;http://localhost:2681" + } + } +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj new file mode 100644 index 0000000..6be5000 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj @@ -0,0 +1,21 @@ + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json b/src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json new file mode 100644 index 0000000..8439ade --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TakeoutSaaS.UserApi": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:2679;http://localhost:2682" + } + } +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj new file mode 100644 index 0000000..ca338f6 --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + true + + + + + + + + + + + diff --git a/src/Application/TakeoutSaaS.Application/Services/.gitkeep b/src/Application/TakeoutSaaS.Application/Services/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Services/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj new file mode 100644 index 0000000..88dea96 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj @@ -0,0 +1,12 @@ + + + net10.0 + enable + enable + + + + + + + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs new file mode 100644 index 0000000..2d7ed97 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs @@ -0,0 +1,19 @@ +namespace TakeoutSaaS.Shared.Abstractions.Constants; + +/// +/// 统一错误码常量。 +/// +public static class ErrorCodes +{ + public const int BadRequest = 400; + public const int Unauthorized = 401; + public const int Forbidden = 403; + public const int NotFound = 404; + public const int Conflict = 409; + public const int ValidationFailed = 422; + public const int InternalServerError = 500; + + // 业务自定义区间(10000+) + public const int BusinessError = 10001; +} + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs new file mode 100644 index 0000000..fe612a2 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs @@ -0,0 +1,11 @@ +namespace TakeoutSaaS.Shared.Abstractions.Entities; + +/// +/// 审计字段接口 +/// +public interface IAuditableEntity +{ + DateTime CreatedAt { get; set; } + DateTime? UpdatedAt { get; set; } +} + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs new file mode 100644 index 0000000..60d793a --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs @@ -0,0 +1,20 @@ +using System; + +namespace TakeoutSaaS.Shared.Abstractions.Exceptions; + +/// +/// 业务异常(用于可预期的业务校验错误)。 +/// +public class BusinessException : Exception +{ + /// + /// 业务错误码。 + /// + public int ErrorCode { get; } + + public BusinessException(int errorCode, string message) : base(message) + { + ErrorCode = errorCode; + } +} + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs new file mode 100644 index 0000000..a4c48d0 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace TakeoutSaaS.Shared.Abstractions.Exceptions; + +/// +/// 验证异常(用于聚合验证错误信息)。 +/// +public class ValidationException : Exception +{ + /// + /// 字段/属性的错误集合。 + /// + public IDictionary Errors { get; } + + public ValidationException(IDictionary errors) + : base("一个或多个验证错误") + { + Errors = errors; + } +} + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj new file mode 100644 index 0000000..05f0387 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj @@ -0,0 +1,8 @@ + + + net10.0 + enable + enable + + + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs new file mode 100644 index 0000000..4af8592 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs @@ -0,0 +1,7 @@ +namespace TakeoutSaaS.Shared.Abstractions.Tenancy; + +public interface ITenantProvider +{ + Guid GetCurrentTenantId(); +} + diff --git a/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj b/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj new file mode 100644 index 0000000..c52f050 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs b/src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs new file mode 100644 index 0000000..af1240e --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TakeoutSaaS.Shared.Web.Api; + +/// +/// API 基类控制器: +/// - 统一应用 [ApiController] 和默认响应类型 +/// - 作为所有 API 控制器的基类,便于复用过滤器/中间件特性 +/// +[ApiController] +[Produces("application/json")] +public abstract class BaseApiController : ControllerBase +{ +} + diff --git a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs new file mode 100644 index 0000000..3ec985c --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddReverseProxy() + .LoadFromMemory(new() + { + Clusters = + { + ["admin"] = new() + { + Destinations = { ["d1"] = new() { Address = "http://localhost:5001/" } } + }, + ["mini"] = new() + { + Destinations = { ["d1"] = new() { Address = "http://localhost:5002/" } } + }, + ["user"] = new() + { + Destinations = { ["d1"] = new() { Address = "http://localhost:5003/" } } + } + }, + Routes = + { + new() + { + RouteId = "admin-route", + ClusterId = "admin", + Match = new() { Path = "/api/admin/{**catch-all}" } + }, + new() + { + RouteId = "mini-route", + ClusterId = "mini", + Match = new() { Path = "/api/mini/{**catch-all}" } + }, + new() + { + RouteId = "user-route", + ClusterId = "user", + Match = new() { Path = "/api/user/{**catch-all}" } + } + } + }); + +var app = builder.Build(); + +app.MapReverseProxy(); + +app.Run(); + diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json b/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json new file mode 100644 index 0000000..3956499 --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TakeoutSaaS.ApiGateway": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:2677;http://localhost:2683" + } + } +} \ No newline at end of file diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj new file mode 100644 index 0000000..fda4d4d --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj new file mode 100644 index 0000000..10cd70f --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj b/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj b/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs b/src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs new file mode 100644 index 0000000..f14abfe --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs @@ -0,0 +1,23 @@ +namespace TakeoutSaaS.Module.Identity.Abstractions; + +/// +/// 微信登录服务抽象(code2Session) +/// +public interface IWeChatAuthService +{ + /// + /// 使用小程序登录 code 换取 openid/unionid/session_key + /// + Task Code2SessionAsync(string code, CancellationToken cancellationToken = default); +} + +/// +/// 微信会话信息 +/// +public sealed class WeChatSessionInfo +{ + public string OpenId { get; init; } = string.Empty; + public string? UnionId { get; init; } + public string SessionKey { get; init; } = string.Empty; +} + diff --git a/src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj b/src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj new file mode 100644 index 0000000..4e9d749 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj @@ -0,0 +1,14 @@ + + + net10.0 + enable + enable + + + + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj new file mode 100644 index 0000000..b407eac --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs new file mode 100644 index 0000000..41e74d1 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs @@ -0,0 +1,39 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Module.Tenancy; + +/// +/// 默认租户提供者:优先从Header: X-Tenant-Id,其次从Token Claim: tenant_id +/// +public sealed class TenantProvider : ITenantProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public TenantProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public Guid GetCurrentTenantId() + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) return Guid.Empty; + + // 1. Header 优先 + if (httpContext.Request.Headers.TryGetValue("X-Tenant-Id", out var values)) + { + if (Guid.TryParse(values.FirstOrDefault(), out var headerTenant)) + return headerTenant; + } + + // 2. Token Claim + var claim = httpContext.User?.FindFirst("tenant_id"); + if (claim != null && Guid.TryParse(claim.Value, out var claimTenant)) + return claimTenant; + + return Guid.Empty; // 未识别到则返回空(上层可按需处理) + } +} + From 1169e1f2202b8d8668b2b4b1f2e2647e1737b3da Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sun, 23 Nov 2025 01:25:20 +0800 Subject: [PATCH 03/56] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=BC=80=E5=8F=91=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 35 +++++ .vscode/settings.json | 3 + .vscode/tasks.json | 41 ++++++ 0_Document/10_TODO.md | 10 +- TakeoutSaaS.sln | 15 --- .../Controllers/AuthController.cs | 77 +++++++++++ src/Api/TakeoutSaaS.AdminApi/Program.cs | 10 ++ .../TakeoutSaaS.AdminApi.csproj | 2 - .../Controllers/AuthController.cs | 51 +++++++ .../Controllers/MeController.cs | 51 +++++++ src/Api/TakeoutSaaS.MiniApi/Program.cs | 2 + .../TakeoutSaaS.MiniApi.csproj | 1 - .../Abstractions/IAdminAuthService.cs | 16 +++ .../Identity/Abstractions/IJwtTokenService.cs | 13 ++ .../Abstractions/ILoginRateLimiter.cs | 13 ++ .../Identity/Abstractions/IMiniAuthService.cs | 16 +++ .../Abstractions/IRefreshTokenStore.cs | 16 +++ .../Abstractions/IWeChatAuthService.cs | 13 +- .../Identity/Contracts/AdminLoginRequest.cs | 17 +++ .../Identity/Contracts/CurrentUserProfile.cs | 18 +++ .../Identity/Contracts/RefreshTokenRequest.cs | 13 ++ .../Identity/Contracts/TokenResponse.cs | 16 +++ .../Identity/Contracts/WeChatLoginRequest.cs | 23 ++++ .../IdentityServiceCollectionExtensions.cs | 28 ++++ .../Identity/Models/RefreshTokenDescriptor.cs | 12 ++ .../Identity/Services/AdminAuthService.cs | 87 ++++++++++++ .../Identity/Services/MiniAuthService.cs | 124 ++++++++++++++++++ .../TakeoutSaaS.Application/Services/.gitkeep | 1 - .../TakeoutSaaS.Application.csproj | 5 +- .../Security/ClaimsPrincipalExtensions.cs | 28 ++++ .../Identity/Entities/IdentityUser.cs | 54 ++++++++ .../Identity/Entities/MiniUser.cs | 39 ++++++ .../Repositories/IIdentityUserRepository.cs | 22 ++++ .../Repositories/IMiniUserRepository.cs | 18 +++ src/Gateway/TakeoutSaaS.ApiGateway/Program.cs | 88 +++++++------ .../Extensions/JwtAuthenticationExtensions.cs | 55 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 99 ++++++++++++++ .../Identity/Options/AdminSeedOptions.cs | 30 +++++ .../Identity/Options/JwtOptions.cs | 25 ++++ .../Identity/Options/LoginRateLimitOptions.cs | 15 +++ .../Options/RefreshTokenStoreOptions.cs | 9 ++ .../Identity/Options/WeChatMiniOptions.cs | 15 +++ .../Persistence/EfIdentityUserRepository.cs | 27 ++++ .../Persistence/EfMiniUserRepository.cs | 54 ++++++++ .../Persistence/IdentityDataSeeder.cs | 94 +++++++++++++ .../Identity/Persistence/IdentityDbContext.cs | 70 ++++++++++ .../Identity/Services/JwtTokenService.cs | 93 +++++++++++++ .../Services/RedisLoginRateLimiter.cs | 52 ++++++++ .../Services/RedisRefreshTokenStore.cs | 63 +++++++++ .../Identity/Services/WeChatAuthService.cs | 79 +++++++++++ .../TakeoutSaaS.Infrastructure.csproj | 8 +- .../PermissionAuthorizeAttribute.cs | 37 ++++++ ...uthorizationServiceCollectionExtensions.cs | 21 +++ .../PermissionAuthorizationHandler.cs | 41 ++++++ .../PermissionAuthorizationPolicyProvider.cs | 69 ++++++++++ .../Policies/PermissionRequirement.cs | 18 +++ .../TakeoutSaaS.Module.Authorization.csproj | 5 +- .../TakeoutSaaS.Module.Identity.csproj | 11 -- 58 files changed, 1886 insertions(+), 82 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs create mode 100644 src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs create mode 100644 src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IJwtTokenService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/ILoginRateLimiter.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs rename src/{Modules/TakeoutSaaS.Module.Identity => Application/TakeoutSaaS.Application/Identity}/Abstractions/IWeChatAuthService.cs (64%) create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/AdminLoginRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/RefreshTokenRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Services/.gitkeep create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/JwtAuthenticationExtensions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Authorization/Extensions/AuthorizationServiceCollectionExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..933c59b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // 使用 IntelliSense 找出 C# 调试存在哪些属性 + // 将悬停用于现有属性的说明 + // 有关详细信息,请访问 https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md。 + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // 如果已更改目标框架,请确保更新程序路径。 + "program": "${workspaceFolder}/src/Api/TakeoutSaaS.AdminApi/bin/Debug/net10.0/TakeoutSaaS.AdminApi.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Api/TakeoutSaaS.AdminApi", + "stopAtEntry": false, + // 启用在启动 ASP.NET Core 时启动 Web 浏览器。有关详细信息: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..02be578 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "chatgpt.openOnStartup": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..a48b929 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/TakeoutSaaS.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/TakeoutSaaS.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/TakeoutSaaS.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/0_Document/10_TODO.md b/0_Document/10_TODO.md index 7157dbf..9190463 100644 --- a/0_Document/10_TODO.md +++ b/0_Document/10_TODO.md @@ -1,4 +1,4 @@ -# TODO Roadmap +# TODO Roadmap 说明:本清单覆盖当前阶段的骨架搭建与核心基础能力(不含部署与CI/CD,留到项目跑通后再做)。 @@ -11,10 +11,10 @@ - [x] 安全中间件:Security Headers、CORS 策略(按端区分) ## B. 认证与权限 -- [ ] JWT 颁发与刷新(AdminApi、MiniApi) -- [ ] RBAC 权限模型(角色/权限/策略)与特性授权(AdminApi) -- [ ] 小程序登录(微信 code2Session)并绑定用户账户(MiniApi) -- [ ] 登录防刷限流(MiniApi) +- [x] JWT 颁发与刷新(AdminApi、MiniApi) +- [x] RBAC 权限模型(角色/权限/策略)与特性授权(AdminApi) +- [x] 小程序登录(微信 code2Session)并绑定用户账户(MiniApi) +- [x] 登录防刷限流(MiniApi) ## C. 多租户与参数字典 - [ ] 多租户中间件:从 Header/域名解析租户(Shared.Web + Tenancy) diff --git a/TakeoutSaaS.sln b/TakeoutSaaS.sln index 86c3d6d..d924b98 100644 --- a/TakeoutSaaS.sln +++ b/TakeoutSaaS.sln @@ -29,8 +29,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Infrastructure" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Identity", "src\Modules\TakeoutSaaS.Module.Identity\TakeoutSaaS.Module.Identity.csproj", "{582EDD19-3C2F-4693-9595-CC367318CD19}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Authorization", "src\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj", "{6CB8487D-5C74-487C-9D84-E57838BDA015}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Tenancy", "src\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj", "{5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}" @@ -129,18 +127,6 @@ Global {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x64.Build.0 = Release|Any CPU {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x86.ActiveCfg = Release|Any CPU {80B45C7D-9423-400A-8279-40D95BFEBC9D}.Release|x86.Build.0 = Release|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x64.ActiveCfg = Debug|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x64.Build.0 = Debug|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x86.ActiveCfg = Debug|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Debug|x86.Build.0 = Debug|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|Any CPU.Build.0 = Release|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x64.ActiveCfg = Release|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x64.Build.0 = Release|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x86.ActiveCfg = Release|Any CPU - {582EDD19-3C2F-4693-9595-CC367318CD19}.Release|x86.Build.0 = Release|Any CPU {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CB8487D-5C74-487C-9D84-E57838BDA015}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -242,7 +228,6 @@ Global {9048EB7F-3875-A59E-E36B-5BD4C6F2A282} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {80B45C7D-9423-400A-8279-40D95BFEBC9D} = {9048EB7F-3875-A59E-E36B-5BD4C6F2A282} {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {582EDD19-3C2F-4693-9595-CC367318CD19} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {6CB8487D-5C74-487C-9D84-E57838BDA015} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D} = {81034408-37C8-1011-444E-4C15C2FADA8E} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs new file mode 100644 index 0000000..04bedfc --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; +using TakeoutSaaS.Shared.Web.Security; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 管理后台认证接口 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/auth")] +public sealed class AuthController : BaseApiController +{ + private readonly IAdminAuthService _authService; + + /// + /// + /// + /// + public AuthController(IAdminAuthService authService) + { + _authService = authService; + } + + /// + /// 登录获取 Token + /// + [HttpPost("login")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) + { + var response = await _authService.LoginAsync(request, cancellationToken); + return Ok(ApiResponse.Ok(response)); + } + + /// + /// 刷新 Token + /// + [HttpPost("refresh")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + { + var response = await _authService.RefreshTokenAsync(request, cancellationToken); + return Ok(ApiResponse.Ok(response)); + } + + /// + /// 获取当前用户信息 + /// + [HttpGet("profile")] + [PermissionAuthorize("identity:profile:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> GetProfile(CancellationToken cancellationToken) + { + var userId = User.GetUserId(); + if (userId == Guid.Empty) + { + return Unauthorized(ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识")); + } + + var profile = await _authService.GetProfileAsync(userId, cancellationToken); + return Ok(ApiResponse.Ok(profile)); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 7e650e0..5359f9f 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -6,6 +6,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; +using TakeoutSaaS.Application.Identity.Extensions; +using TakeoutSaaS.Infrastructure.Identity.Extensions; +using TakeoutSaaS.Module.Authorization.Extensions; using TakeoutSaaS.Module.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Extensions; @@ -28,6 +31,11 @@ builder.Services.AddSharedSwagger(options => options.Description = "管理后台 API 文档"; options.EnableAuthorization = true; }); +builder.Services.AddIdentityApplication(); +builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true); +builder.Services.AddJwtAuthentication(builder.Configuration); +builder.Services.AddAuthorization(); +builder.Services.AddPermissionAuthorization(); var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin"); builder.Services.AddCors(options => @@ -44,6 +52,8 @@ var app = builder.Build(); app.UseCors("AdminApiCors"); app.UseSharedWebCore(); +app.UseAuthentication(); +app.UseAuthorization(); app.UseSharedSwagger(); app.MapControllers(); diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index 9a71725..d4c9b45 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -13,9 +13,7 @@ - - diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs new file mode 100644 index 0000000..2c80505 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs @@ -0,0 +1,51 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.MiniApi.Controllers; + +/// +/// 小程序登录认证 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/mini/v{version:apiVersion}/auth")] +public sealed class AuthController : BaseApiController +{ + private readonly IMiniAuthService _authService; + + public AuthController(IMiniAuthService authService) + { + _authService = authService; + } + + /// + /// 微信登录 + /// + [HttpPost("wechat/login")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken) + { + var response = await _authService.LoginWithWeChatAsync(request, cancellationToken); + return Ok(ApiResponse.Ok(response)); + } + + /// + /// 刷新 Token + /// + [HttpPost("refresh")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + { + var response = await _authService.RefreshTokenAsync(request, cancellationToken); + return Ok(ApiResponse.Ok(response)); + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs new file mode 100644 index 0000000..8ccdc14 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; +using TakeoutSaaS.Shared.Web.Security; + +namespace TakeoutSaaS.MiniApi.Controllers; + +/// +/// 当前用户信息 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/mini/v{version:apiVersion}/me")] +public sealed class MeController : BaseApiController +{ + private readonly IMiniAuthService _authService; + + /// + /// + /// + /// + public MeController(IMiniAuthService authService) + { + _authService = authService; + } + + /// + /// 获取用户档案 + /// + [HttpGet] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> Get(CancellationToken cancellationToken) + { + var userId = User.GetUserId(); + if (userId == Guid.Empty) + { + return Unauthorized(ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识")); + } + + var profile = await _authService.GetProfileAsync(userId, cancellationToken); + return Ok(ApiResponse.Ok(profile)); + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index d6621d5..ccdb0fd 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; +using TakeoutSaaS.Application.Identity.Extensions; +using TakeoutSaaS.Infrastructure.Identity.Extensions; using TakeoutSaaS.Module.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Extensions; diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index 6be5000..4a352b2 100644 --- a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj +++ b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs new file mode 100644 index 0000000..f60dffb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Abstractions; + +/// +/// 管理后台认证服务。 +/// +public interface IAdminAuthService +{ + Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); + Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); + Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IJwtTokenService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IJwtTokenService.cs new file mode 100644 index 0000000..4235181 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IJwtTokenService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Abstractions; + +/// +/// JWT 令牌服务契约。 +/// +public interface IJwtTokenService +{ + Task CreateTokensAsync(CurrentUserProfile profile, bool isNewUser = false, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/ILoginRateLimiter.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/ILoginRateLimiter.cs new file mode 100644 index 0000000..f4a7c5c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/ILoginRateLimiter.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace TakeoutSaaS.Application.Identity.Abstractions; + +/// +/// 登录限流器。 +/// +public interface ILoginRateLimiter +{ + Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default); + Task ResetAsync(string key, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs new file mode 100644 index 0000000..11efdb4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Abstractions; + +/// +/// 小程序认证服务。 +/// +public interface IMiniAuthService +{ + Task LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default); + Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); + Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs new file mode 100644 index 0000000..d966ca2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Identity.Models; + +namespace TakeoutSaaS.Application.Identity.Abstractions; + +/// +/// 刷新令牌存储。 +/// +public interface IRefreshTokenStore +{ + Task IssueAsync(Guid userId, DateTime expiresAt, CancellationToken cancellationToken = default); + Task GetAsync(string refreshToken, CancellationToken cancellationToken = default); + Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default); +} diff --git a/src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs similarity index 64% rename from src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs rename to src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs index f14abfe..417c8b9 100644 --- a/src/Modules/TakeoutSaaS.Module.Identity/Abstractions/IWeChatAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs @@ -1,18 +1,18 @@ -namespace TakeoutSaaS.Module.Identity.Abstractions; +using System.Threading; +using System.Threading.Tasks; + +namespace TakeoutSaaS.Application.Identity.Abstractions; /// -/// 微信登录服务抽象(code2Session) +/// 微信 code2Session 服务契约。 /// public interface IWeChatAuthService { - /// - /// 使用小程序登录 code 换取 openid/unionid/session_key - /// Task Code2SessionAsync(string code, CancellationToken cancellationToken = default); } /// -/// 微信会话信息 +/// 微信会话信息。 /// public sealed class WeChatSessionInfo { @@ -20,4 +20,3 @@ public sealed class WeChatSessionInfo public string? UnionId { get; init; } public string SessionKey { get; init; } = string.Empty; } - diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/AdminLoginRequest.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/AdminLoginRequest.cs new file mode 100644 index 0000000..fbbe58d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/AdminLoginRequest.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 管理后台登录请求。 +/// +public sealed class AdminLoginRequest +{ + [Required] + [MaxLength(64)] + public string Account { get; set; } = string.Empty; + + [Required] + [MaxLength(128)] + public string Password { get; set; } = string.Empty; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs new file mode 100644 index 0000000..be4eb44 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs @@ -0,0 +1,18 @@ +using System; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 登录用户档案。 +/// +public sealed class CurrentUserProfile +{ + public Guid UserId { get; init; } + public string Account { get; init; } = string.Empty; + public string DisplayName { get; init; } = string.Empty; + public Guid TenantId { get; init; } + public Guid? MerchantId { get; init; } + public string[] Roles { get; init; } = Array.Empty(); + public string[] Permissions { get; init; } = Array.Empty(); + public string? Avatar { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RefreshTokenRequest.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RefreshTokenRequest.cs new file mode 100644 index 0000000..67b3c53 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RefreshTokenRequest.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 刷新令牌请求。 +/// +public sealed class RefreshTokenRequest +{ + [Required] + [MaxLength(256)] + public string RefreshToken { get; set; } = string.Empty; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs new file mode 100644 index 0000000..57b3ded --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs @@ -0,0 +1,16 @@ +using System; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// Access/Refresh 令牌响应。 +/// +public class TokenResponse +{ + public string AccessToken { get; init; } = string.Empty; + public DateTime AccessTokenExpiresAt { get; init; } + public string RefreshToken { get; init; } = string.Empty; + public DateTime RefreshTokenExpiresAt { get; init; } + public CurrentUserProfile? User { get; init; } + public bool IsNewUser { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs new file mode 100644 index 0000000..27152e6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 微信小程序登录请求。 +/// +public sealed class WeChatLoginRequest +{ + [Required] + [MaxLength(128)] + public string Code { get; set; } = string.Empty; + + [MaxLength(64)] + public string? Nickname { get; set; } + + [MaxLength(256)] + public string? Avatar { get; set; } + + public string? EncryptedData { get; set; } + + public string? Iv { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs new file mode 100644 index 0000000..c5df667 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Services; + +namespace TakeoutSaaS.Application.Identity.Extensions; + +/// +/// 应用层身份认证服务注入 +/// +public static class IdentityServiceCollectionExtensions +{ + /// + /// 注册身份认证相关应用服务 + /// + /// 服务集合 + /// 是否注册小程序认证服务 + public static IServiceCollection AddIdentityApplication(this IServiceCollection services, bool enableMiniSupport = false) + { + services.AddScoped(); + + if (enableMiniSupport) + { + services.AddScoped(); + } + + return services; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs b/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs new file mode 100644 index 0000000..f508c3e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs @@ -0,0 +1,12 @@ +using System; + +namespace TakeoutSaaS.Application.Identity.Models; + +/// +/// 刷新令牌描述。 +/// +public sealed record class RefreshTokenDescriptor( + string Token, + Guid UserId, + DateTime ExpiresAt, + bool Revoked); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs new file mode 100644 index 0000000..477e53f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Application.Identity.Services; + +/// +/// 管理后台认证服务实现。 +/// +public sealed class AdminAuthService : IAdminAuthService +{ + private readonly IIdentityUserRepository _userRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IJwtTokenService _jwtTokenService; + private readonly IRefreshTokenStore _refreshTokenStore; + + public AdminAuthService( + IIdentityUserRepository userRepository, + IPasswordHasher passwordHasher, + IJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore) + { + _userRepository = userRepository; + _passwordHasher = passwordHasher; + _jwtTokenService = jwtTokenService; + _refreshTokenStore = refreshTokenStore; + } + + public async Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default) + { + var user = await _userRepository.FindByAccountAsync(request.Account, cancellationToken) + ?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); + + var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password); + if (result == PasswordVerificationResult.Failed) + { + throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); + } + + var profile = BuildProfile(user); + return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); + } + + public async Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) + { + var descriptor = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked) + { + throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期"); + } + + var user = await _userRepository.FindByIdAsync(descriptor.UserId, cancellationToken) + ?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在"); + + await _refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); + var profile = BuildProfile(user); + return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); + } + + public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default) + { + var user = await _userRepository.FindByIdAsync(userId, cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); + + return BuildProfile(user); + } + + private static CurrentUserProfile BuildProfile(IdentityUser user) + => new() + { + UserId = user.Id, + Account = user.Account, + DisplayName = user.DisplayName, + TenantId = user.TenantId, + MerchantId = user.MerchantId, + Roles = user.Roles, + Permissions = user.Permissions, + Avatar = user.Avatar + }; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs new file mode 100644 index 0000000..0d27c61 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs @@ -0,0 +1,124 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Services; + +/// +/// 小程序认证服务实现。 +/// +public sealed class MiniAuthService : IMiniAuthService +{ + private readonly IWeChatAuthService _weChatAuthService; + private readonly IMiniUserRepository _miniUserRepository; + private readonly IJwtTokenService _jwtTokenService; + private readonly IRefreshTokenStore _refreshTokenStore; + private readonly ILoginRateLimiter _rateLimiter; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ITenantProvider _tenantProvider; + + public MiniAuthService( + IWeChatAuthService weChatAuthService, + IMiniUserRepository miniUserRepository, + IJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore, + ILoginRateLimiter rateLimiter, + IHttpContextAccessor httpContextAccessor, + ITenantProvider tenantProvider) + { + _weChatAuthService = weChatAuthService; + _miniUserRepository = miniUserRepository; + _jwtTokenService = jwtTokenService; + _refreshTokenStore = refreshTokenStore; + _rateLimiter = rateLimiter; + _httpContextAccessor = httpContextAccessor; + _tenantProvider = tenantProvider; + } + + public async Task LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default) + { + var throttleKey = BuildThrottleKey(); + await _rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken); + + var session = await _weChatAuthService.Code2SessionAsync(request.Code, cancellationToken); + if (string.IsNullOrWhiteSpace(session.OpenId)) + { + throw new BusinessException(ErrorCodes.Unauthorized, "获取微信用户信息失败"); + } + + var tenantId = _tenantProvider.GetCurrentTenantId(); + if (tenantId == Guid.Empty) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); + } + var (user, isNew) = await GetOrBindMiniUserAsync(session.OpenId, session.UnionId, request.Nickname, request.Avatar, tenantId, cancellationToken); + + await _rateLimiter.ResetAsync(throttleKey, cancellationToken); + var profile = BuildProfile(user); + return await _jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken); + } + + public async Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) + { + var descriptor = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked) + { + throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期"); + } + + var user = await _miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken) + ?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在"); + + await _refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); + var profile = BuildProfile(user); + return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); + } + + public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default) + { + var user = await _miniUserRepository.FindByIdAsync(userId, cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); + + return BuildProfile(user); + } + + private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken) + { + var existing = await _miniUserRepository.FindByOpenIdAsync(openId, cancellationToken); + if (existing != null) + { + return (existing, false); + } + + var created = await _miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken); + return (created, true); + } + + private static CurrentUserProfile BuildProfile(MiniUser user) + => new() + { + UserId = user.Id, + Account = user.OpenId, + DisplayName = user.Nickname, + TenantId = user.TenantId, + MerchantId = null, + Roles = Array.Empty(), + Permissions = Array.Empty(), + Avatar = user.Avatar + }; + + private string BuildThrottleKey() + { + var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback; + return $"mini-login:{ip}"; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Services/.gitkeep b/src/Application/TakeoutSaaS.Application/Services/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/Application/TakeoutSaaS.Application/Services/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index 88dea96..233eb4d 100644 --- a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj +++ b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj @@ -4,9 +4,12 @@ enable enable + + + + - diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..8502109 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Security.Claims; + +namespace TakeoutSaaS.Shared.Web.Security; + +/// +/// ClaimsPrincipal 便捷扩展 +/// +public static class ClaimsPrincipalExtensions +{ + /// + /// 获取当前用户 Id(不存在时返回 Guid.Empty) + /// + public static Guid GetUserId(this ClaimsPrincipal? principal) + { + if (principal == null) + { + return Guid.Empty; + } + + var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier) + ?? principal.FindFirstValue("sub"); + + return Guid.TryParse(identifier, out var userId) + ? userId + : Guid.Empty; + } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs new file mode 100644 index 0000000..123208a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -0,0 +1,54 @@ +using System; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 后台账号实体(平台/商户/员工)。 +/// +public sealed class IdentityUser +{ + /// + /// 用户 ID。 + /// + public Guid Id { get; set; } + + /// + /// 登录账号。 + /// + public string Account { get; set; } = string.Empty; + + /// + /// 展示名称。 + /// + public string DisplayName { get; set; } = string.Empty; + + /// + /// 密码哈希。 + /// + public string PasswordHash { get; set; } = string.Empty; + + /// + /// 所属租户。 + /// + public Guid TenantId { get; set; } + + /// + /// 所属商户(平台管理员为空)。 + /// + public Guid? MerchantId { get; set; } + + /// + /// 角色集合。 + /// + public string[] Roles { get; set; } = Array.Empty(); + + /// + /// 权限集合。 + /// + public string[] Permissions { get; set; } = Array.Empty(); + + /// + /// 头像地址。 + /// + public string? Avatar { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs new file mode 100644 index 0000000..3c4fdf8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs @@ -0,0 +1,39 @@ +using System; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 小程序用户。 +/// +public sealed class MiniUser +{ + /// + /// 用户 ID。 + /// + public Guid Id { get; set; } + + /// + /// 微信 OpenId。 + /// + public string OpenId { get; set; } = string.Empty; + + /// + /// 微信 UnionId,可为空。 + /// + public string? UnionId { get; set; } + + /// + /// 昵称。 + /// + public string Nickname { get; set; } = string.Empty; + + /// + /// 头像地址。 + /// + public string? Avatar { get; set; } + + /// + /// 所属租户。 + /// + public Guid TenantId { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs new file mode 100644 index 0000000..2d5eae1 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 后台用户仓储契约。 +/// +public interface IIdentityUserRepository +{ + /// + /// 根据账号获取后台用户。 + /// + Task FindByAccountAsync(string account, CancellationToken cancellationToken = default); + + /// + /// 根据 ID 获取后台用户。 + /// + Task FindByIdAsync(Guid userId, CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs new file mode 100644 index 0000000..db3e810 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 小程序用户仓储契约。 +/// +public interface IMiniUserRepository +{ + Task FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default); + + Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default); + + Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default); +} diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs index 3ec985c..1c832fa 100644 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs @@ -1,49 +1,63 @@ +using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Yarp.ReverseProxy.Configuration; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddReverseProxy() - .LoadFromMemory(new() +var routes = new[] +{ + new RouteConfig { - Clusters = + RouteId = "admin-route", + ClusterId = "admin", + Match = new() { Path = "/api/admin/{**catch-all}" } + }, + new RouteConfig + { + RouteId = "mini-route", + ClusterId = "mini", + Match = new() { Path = "/api/mini/{**catch-all}" } + }, + new RouteConfig + { + RouteId = "user-route", + ClusterId = "user", + Match = new() { Path = "/api/user/{**catch-all}" } + } +}; + +var clusters = new[] +{ + new ClusterConfig + { + ClusterId = "admin", + Destinations = new Dictionary { - ["admin"] = new() - { - Destinations = { ["d1"] = new() { Address = "http://localhost:5001/" } } - }, - ["mini"] = new() - { - Destinations = { ["d1"] = new() { Address = "http://localhost:5002/" } } - }, - ["user"] = new() - { - Destinations = { ["d1"] = new() { Address = "http://localhost:5003/" } } - } - }, - Routes = - { - new() - { - RouteId = "admin-route", - ClusterId = "admin", - Match = new() { Path = "/api/admin/{**catch-all}" } - }, - new() - { - RouteId = "mini-route", - ClusterId = "mini", - Match = new() { Path = "/api/mini/{**catch-all}" } - }, - new() - { - RouteId = "user-route", - ClusterId = "user", - Match = new() { Path = "/api/user/{**catch-all}" } - } + ["d1"] = new() { Address = "http://localhost:5001/" } } - }); + }, + new ClusterConfig + { + ClusterId = "mini", + Destinations = new Dictionary + { + ["d1"] = new() { Address = "http://localhost:5002/" } + } + }, + new ClusterConfig + { + ClusterId = "user", + Destinations = new Dictionary + { + ["d1"] = new() { Address = "http://localhost:5003/" } + } + } +}; + +builder.Services.AddReverseProxy() + .LoadFromMemory(routes, clusters); var app = builder.Build(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/JwtAuthenticationExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/JwtAuthenticationExtensions.cs new file mode 100644 index 0000000..79c475c --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/JwtAuthenticationExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using TakeoutSaaS.Infrastructure.Identity.Options; + +namespace TakeoutSaaS.Infrastructure.Identity.Extensions; + +/// +/// JWT 认证扩展 +/// +public static class JwtAuthenticationExtensions +{ + /// + /// 配置 JWT Bearer 认证 + /// + public static IServiceCollection AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration) + { + var jwtOptions = configuration.GetSection("Identity:Jwt").Get() + ?? throw new InvalidOperationException("缺少 Identity:Jwt 配置"); + + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear(); + + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtOptions.Issuer, + ValidateAudience = true, + ValidAudience = jwtOptions.Audience, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Secret)), + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(1), + NameClaimType = ClaimTypes.NameIdentifier, + RoleClaimType = ClaimTypes.Role + }; + }); + + return services; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6b588c6 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Infrastructure.Identity.Options; +using TakeoutSaaS.Infrastructure.Identity.Persistence; +using TakeoutSaaS.Infrastructure.Identity.Services; +using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; + +namespace TakeoutSaaS.Infrastructure.Identity.Extensions; + +/// +/// 身份认证基础设施注入 +/// +public static class ServiceCollectionExtensions +{ + /// + /// 注册身份认证基础设施(数据库、Redis、JWT、限流等) + /// + /// 服务集合 + /// 配置源 + /// 是否启用小程序相关依赖(如微信登录) + /// 是否启用后台账号初始化 + public static IServiceCollection AddIdentityInfrastructure( + this IServiceCollection services, + IConfiguration configuration, + bool enableMiniFeatures = false, + bool enableAdminSeed = false) + { + var dbConnection = configuration.GetConnectionString("IdentityDatabase"); + if (string.IsNullOrWhiteSpace(dbConnection)) + { + throw new InvalidOperationException("缺少 IdentityDatabase 连接字符串配置"); + } + + services.AddDbContext(options => options.UseNpgsql(dbConnection)); + + var redisConnection = configuration.GetConnectionString("Redis"); + if (string.IsNullOrWhiteSpace(redisConnection)) + { + throw new InvalidOperationException("缺少 Redis 连接字符串配置"); + } + + services.AddStackExchangeRedisCache(options => + { + options.Configuration = redisConnection; + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped, PasswordHasher>(); + + services.AddOptions() + .Bind(configuration.GetSection("Identity:Jwt")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddOptions() + .Bind(configuration.GetSection("Identity:LoginRateLimit")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddOptions() + .Bind(configuration.GetSection("Identity:RefreshTokenStore")); + + if (enableMiniFeatures) + { + services.AddOptions() + .Bind(configuration.GetSection("Identity:WeChatMini")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://api.weixin.qq.com/"); + client.Timeout = TimeSpan.FromSeconds(10); + }); + } + + if (enableAdminSeed) + { + services.AddOptions() + .Bind(configuration.GetSection("Identity:AdminSeed")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddHostedService(); + } + + return services; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs new file mode 100644 index 0000000..d1dd243 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.Identity.Options; + +/// +/// 管理后台初始账号配置。 +/// +public sealed class AdminSeedOptions +{ + public List Users { get; set; } = new(); +} + +public sealed class SeedUserOptions +{ + [Required] + public string Account { get; set; } = string.Empty; + + [Required] + public string Password { get; set; } = string.Empty; + + [Required] + public string DisplayName { get; set; } = string.Empty; + + public Guid TenantId { get; set; } + public Guid? MerchantId { get; set; } + public string[] Roles { get; set; } = Array.Empty(); + public string[] Permissions { get; set; } = Array.Empty(); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs new file mode 100644 index 0000000..28e1052 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.Identity.Options; + +/// +/// JWT 配置。 +/// +public sealed class JwtOptions +{ + [Required] + public string Issuer { get; set; } = string.Empty; + + [Required] + public string Audience { get; set; } = string.Empty; + + [Required] + [MinLength(32)] + public string Secret { get; set; } = string.Empty; + + [Range(5, 1440)] + public int AccessTokenExpirationMinutes { get; set; } = 60; + + [Range(60, 1440 * 14)] + public int RefreshTokenExpirationMinutes { get; set; } = 60 * 24 * 7; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs new file mode 100644 index 0000000..a5fb4f1 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.Identity.Options; + +/// +/// 登录限流配置。 +/// +public sealed class LoginRateLimitOptions +{ + [Range(1, 3600)] + public int WindowSeconds { get; set; } = 60; + + [Range(1, 100)] + public int MaxAttempts { get; set; } = 5; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs new file mode 100644 index 0000000..ab69c3d --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs @@ -0,0 +1,9 @@ +namespace TakeoutSaaS.Infrastructure.Identity.Options; + +/// +/// 刷新令牌存储配置。 +/// +public sealed class RefreshTokenStoreOptions +{ + public string Prefix { get; set; } = "identity:refresh:"; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs new file mode 100644 index 0000000..e30d274 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.Identity.Options; + +/// +/// 微信小程序配置。 +/// +public sealed class WeChatMiniOptions +{ + [Required] + public string AppId { get; set; } = string.Empty; + + [Required] + public string Secret { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs new file mode 100644 index 0000000..e90127c --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF Core 后台用户仓储实现。 +/// +public sealed class EfIdentityUserRepository : IIdentityUserRepository +{ + private readonly IdentityDbContext _dbContext; + + public EfIdentityUserRepository(IdentityDbContext dbContext) + { + _dbContext = dbContext; + } + + public Task FindByAccountAsync(string account, CancellationToken cancellationToken = default) + => _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); + + public Task FindByIdAsync(Guid userId, CancellationToken cancellationToken = default) + => _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs new file mode 100644 index 0000000..d5c372b --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF Core 小程序用户仓储实现。 +/// +public sealed class EfMiniUserRepository : IMiniUserRepository +{ + private readonly IdentityDbContext _dbContext; + + public EfMiniUserRepository(IdentityDbContext dbContext) + { + _dbContext = dbContext; + } + + public Task FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default) + => _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); + + public Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default) + => _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + + public async Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default) + { + var user = await _dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); + if (user == null) + { + user = new MiniUser + { + Id = Guid.NewGuid(), + OpenId = openId, + UnionId = unionId, + Nickname = nickname ?? "小程序用户", + Avatar = avatar, + TenantId = tenantId + }; + _dbContext.MiniUsers.Add(user); + } + else + { + user.UnionId = unionId ?? user.UnionId; + user.Nickname = nickname ?? user.Nickname; + user.Avatar = avatar ?? user.Avatar; + } + + await _dbContext.SaveChangesAsync(cancellationToken); + return user; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs new file mode 100644 index 0000000..80327ac --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Infrastructure.Identity.Options; +using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// 后台账号初始化种子任务 +/// +public sealed class IdentityDataSeeder : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public IdentityDataSeeder(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>().Value; + var passwordHasher = scope.ServiceProvider.GetRequiredService>(); + + await context.Database.MigrateAsync(cancellationToken); + + if (options.Users == null || options.Users.Count == 0) + { + _logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化"); + return; + } + + foreach (var userOptions in options.Users) + { + var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken); + var roles = NormalizeValues(userOptions.Roles); + var permissions = NormalizeValues(userOptions.Permissions); + + if (user == null) + { + user = new DomainIdentityUser + { + Id = Guid.NewGuid(), + Account = userOptions.Account, + DisplayName = userOptions.DisplayName, + TenantId = userOptions.TenantId, + MerchantId = userOptions.MerchantId, + Avatar = null, + Roles = roles, + Permissions = permissions, + }; + user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); + context.IdentityUsers.Add(user); + _logger.LogInformation("已创建后台账号 {Account}", user.Account); + } + else + { + user.DisplayName = userOptions.DisplayName; + user.TenantId = userOptions.TenantId; + user.MerchantId = userOptions.MerchantId; + user.Roles = roles; + user.Permissions = permissions; + user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); + _logger.LogInformation("已更新后台账号 {Account}", user.Account); + } + } + + await context.SaveChangesAsync(cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private static string[] NormalizeValues(string[]? values) + => values == null + ? Array.Empty() + : values + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Select(v => v.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs new file mode 100644 index 0000000..6168f7e --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// 身份认证 DbContext。 +/// +public sealed class IdentityDbContext : DbContext +{ + public IdentityDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet IdentityUsers => Set(); + public DbSet MiniUsers => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + ConfigureIdentityUser(modelBuilder.Entity()); + ConfigureMiniUser(modelBuilder.Entity()); + } + + private static void ConfigureIdentityUser(EntityTypeBuilder builder) + { + builder.ToTable("identity_users"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Account).HasMaxLength(64).IsRequired(); + builder.Property(x => x.DisplayName).HasMaxLength(64).IsRequired(); + builder.Property(x => x.PasswordHash).HasMaxLength(256).IsRequired(); + builder.Property(x => x.Avatar).HasMaxLength(256); + + var converter = new ValueConverter( + v => string.Join(',', v ?? Array.Empty()), + v => string.IsNullOrWhiteSpace(v) ? Array.Empty() : v.Split(',', StringSplitOptions.RemoveEmptyEntries)); + + var comparer = new ValueComparer( + (l, r) => l!.SequenceEqual(r!), + v => v.Aggregate(0, (current, item) => HashCode.Combine(current, item.GetHashCode())), + v => v.ToArray()); + + builder.Property(x => x.Roles) + .HasConversion(converter) + .Metadata.SetValueComparer(comparer); + + builder.Property(x => x.Permissions) + .HasConversion(converter) + .Metadata.SetValueComparer(comparer); + + builder.HasIndex(x => x.Account).IsUnique(); + } + + private static void ConfigureMiniUser(EntityTypeBuilder builder) + { + builder.ToTable("mini_users"); + builder.HasKey(x => x.Id); + builder.Property(x => x.OpenId).HasMaxLength(128).IsRequired(); + builder.Property(x => x.UnionId).HasMaxLength(128); + builder.Property(x => x.Nickname).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Avatar).HasMaxLength(256); + + builder.HasIndex(x => x.OpenId).IsUnique(); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs new file mode 100644 index 0000000..fa728ac --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Infrastructure.Identity.Options; + +namespace TakeoutSaaS.Infrastructure.Identity.Services; + +/// +/// JWT 令牌生成器。 +/// +public sealed class JwtTokenService : IJwtTokenService +{ + private readonly JwtSecurityTokenHandler _tokenHandler = new(); + private readonly IRefreshTokenStore _refreshTokenStore; + private readonly JwtOptions _options; + + public JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions options) + { + _refreshTokenStore = refreshTokenStore; + _options = options.Value; + } + + public async Task CreateTokensAsync(CurrentUserProfile profile, bool isNewUser = false, CancellationToken cancellationToken = default) + { + var now = DateTime.UtcNow; + var accessExpires = now.AddMinutes(_options.AccessTokenExpirationMinutes); + var refreshExpires = now.AddMinutes(_options.RefreshTokenExpirationMinutes); + + var claims = BuildClaims(profile); + var signingCredentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Secret)), + SecurityAlgorithms.HmacSha256); + + var jwt = new JwtSecurityToken( + issuer: _options.Issuer, + audience: _options.Audience, + claims: claims, + notBefore: now, + expires: accessExpires, + signingCredentials: signingCredentials); + + var accessToken = _tokenHandler.WriteToken(jwt); + var refreshDescriptor = await _refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken); + + return new TokenResponse + { + AccessToken = accessToken, + AccessTokenExpiresAt = accessExpires, + RefreshToken = refreshDescriptor.Token, + RefreshTokenExpiresAt = refreshDescriptor.ExpiresAt, + User = profile, + IsNewUser = isNewUser + }; + } + + private static IEnumerable BuildClaims(CurrentUserProfile profile) + { + var userId = profile.UserId.ToString(); + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId), + new(ClaimTypes.NameIdentifier, userId), + new(JwtRegisteredClaimNames.UniqueName, profile.Account), + new("tenant_id", profile.TenantId.ToString()), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + if (profile.MerchantId.HasValue) + { + claims.Add(new Claim("merchant_id", profile.MerchantId.Value.ToString())); + } + + foreach (var role in profile.Roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + foreach (var permission in profile.Permissions) + { + claims.Add(new Claim("permission", permission)); + } + + return claims; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs new file mode 100644 index 0000000..9c7e339 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Infrastructure.Identity.Options; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Infrastructure.Identity.Services; + +/// +/// Redis 登录限流实现。 +/// +public sealed class RedisLoginRateLimiter : ILoginRateLimiter +{ + private readonly IDistributedCache _cache; + private readonly LoginRateLimitOptions _options; + + public RedisLoginRateLimiter(IDistributedCache cache, IOptions options) + { + _cache = cache; + _options = options.Value; + } + + public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default) + { + var cacheKey = BuildKey(key); + var current = await _cache.GetStringAsync(cacheKey, cancellationToken); + var count = string.IsNullOrWhiteSpace(current) ? 0 : int.Parse(current); + if (count >= _options.MaxAttempts) + { + throw new BusinessException(ErrorCodes.Forbidden, "尝试次数过多,请稍后再试"); + } + + count++; + await _cache.SetStringAsync( + cacheKey, + count.ToString(), + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_options.WindowSeconds) + }, + cancellationToken); + } + + public Task ResetAsync(string key, CancellationToken cancellationToken = default) + => _cache.RemoveAsync(BuildKey(key), cancellationToken); + + private static string BuildKey(string key) => $"identity:login:{key}"; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs new file mode 100644 index 0000000..aabd967 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs @@ -0,0 +1,63 @@ +using System; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Models; +using TakeoutSaaS.Infrastructure.Identity.Options; + +namespace TakeoutSaaS.Infrastructure.Identity.Services; + +/// +/// Redis 刷新令牌存储。 +/// +public sealed class RedisRefreshTokenStore : IRefreshTokenStore +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + private readonly IDistributedCache _cache; + private readonly RefreshTokenStoreOptions _options; + + public RedisRefreshTokenStore(IDistributedCache cache, IOptions options) + { + _cache = cache; + _options = options.Value; + } + + public async Task IssueAsync(Guid userId, DateTime expiresAt, CancellationToken cancellationToken = default) + { + var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48)); + var descriptor = new RefreshTokenDescriptor(token, userId, expiresAt, false); + + var key = BuildKey(token); + var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt }; + await _cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken); + + return descriptor; + } + + public async Task GetAsync(string refreshToken, CancellationToken cancellationToken = default) + { + var json = await _cache.GetStringAsync(BuildKey(refreshToken), cancellationToken); + return string.IsNullOrWhiteSpace(json) + ? null + : JsonSerializer.Deserialize(json, JsonOptions); + } + + public async Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default) + { + var descriptor = await GetAsync(refreshToken, cancellationToken); + if (descriptor == null) + { + return; + } + + var updated = descriptor with { Revoked = true }; + var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt }; + await _cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken); + } + + private string BuildKey(string token) => $"{_options.Prefix}{token}"; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs new file mode 100644 index 0000000..6fa0efb --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs @@ -0,0 +1,79 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Infrastructure.Identity.Options; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Infrastructure.Identity.Services; + +/// +/// 微信 code2Session 实现 +/// +public sealed class WeChatAuthService : IWeChatAuthService +{ + private readonly HttpClient _httpClient; + private readonly WeChatMiniOptions _options; + + public WeChatAuthService(HttpClient httpClient, IOptions options) + { + _httpClient = httpClient; + _options = options.Value; + } + + public async Task Code2SessionAsync(string code, CancellationToken cancellationToken = default) + { + var requestUri = $"sns/jscode2session?appid={Uri.EscapeDataString(_options.AppId)}&secret={Uri.EscapeDataString(_options.Secret)}&js_code={Uri.EscapeDataString(code)}&grant_type=authorization_code"; + using var response = await _httpClient.GetAsync(requestUri, cancellationToken); + response.EnsureSuccessStatusCode(); + + var payload = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + if (payload == null) + { + throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:响应为空"); + } + + if (payload.ErrorCode.HasValue && payload.ErrorCode.Value != 0) + { + var message = string.IsNullOrWhiteSpace(payload.ErrorMessage) + ? $"微信登录失败,错误码:{payload.ErrorCode}" + : payload.ErrorMessage; + throw new BusinessException(ErrorCodes.Unauthorized, message); + } + + if (string.IsNullOrWhiteSpace(payload.OpenId) || string.IsNullOrWhiteSpace(payload.SessionKey)) + { + throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:返回数据无效"); + } + + return new WeChatSessionInfo + { + OpenId = payload.OpenId, + UnionId = payload.UnionId, + SessionKey = payload.SessionKey + }; + } + + private sealed class WeChatSessionResponse + { + [JsonPropertyName("openid")] + public string? OpenId { get; set; } + + [JsonPropertyName("unionid")] + public string? UnionId { get; set; } + + [JsonPropertyName("session_key")] + public string? SessionKey { get; set; } + + [JsonPropertyName("errcode")] + public int? ErrorCode { get; set; } + + [JsonPropertyName("errmsg")] + public string? ErrorMessage { get; set; } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index 10cd70f..92fa65a 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj @@ -6,12 +6,18 @@ + + + + + + + - diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs new file mode 100644 index 0000000..b55f602 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using TakeoutSaaS.Module.Authorization.Policies; + +namespace TakeoutSaaS.Module.Authorization.Attributes; + +/// +/// 权限校验特性 +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +public sealed class PermissionAuthorizeAttribute : AuthorizeAttribute +{ + public PermissionAuthorizeAttribute(params string[] permissions) + { + ArgumentNullException.ThrowIfNull(permissions); + var normalized = permissions + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (normalized.Length == 0) + { + throw new ArgumentException("至少需要一个权限标识", nameof(permissions)); + } + + Permissions = normalized; + Policy = PermissionAuthorizationPolicyProvider.BuildPolicyName(normalized); + } + + /// + /// 所需权限集合 + /// + public IReadOnlyCollection Permissions { get; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Extensions/AuthorizationServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Extensions/AuthorizationServiceCollectionExtensions.cs new file mode 100644 index 0000000..0e368a0 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Extensions/AuthorizationServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Module.Authorization.Policies; + +namespace TakeoutSaaS.Module.Authorization.Extensions; + +/// +/// 权限授权注入扩展 +/// +public static class AuthorizationServiceCollectionExtensions +{ + /// + /// 启用自定义权限策略提供者与处理器 + /// + public static IServiceCollection AddPermissionAuthorization(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + return services; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs new file mode 100644 index 0000000..b8df4fe --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace TakeoutSaaS.Module.Authorization.Policies; + +/// +/// 权限校验处理器 +/// +public sealed class PermissionAuthorizationHandler : AuthorizationHandler +{ + public const string PermissionClaimType = "permission"; + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) + { + if (context.User?.Identity?.IsAuthenticated != true) + { + return Task.CompletedTask; + } + + var userPermissions = context.User + .FindAll(PermissionClaimType) + .Select(claim => claim.Value) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => value.Trim()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (userPermissions.Count == 0) + { + return Task.CompletedTask; + } + + if (requirement.Permissions.Any(userPermissions.Contains)) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs new file mode 100644 index 0000000..daaa3f0 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace TakeoutSaaS.Module.Authorization.Policies; + +/// +/// 权限策略提供者(按需动态构建策略) +/// +public sealed class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider +{ + public const string PolicyPrefix = "PERMISSION:"; + private readonly AuthorizationOptions _options; + + public PermissionAuthorizationPolicyProvider(IOptions options) + : base(options) + { + _options = options.Value; + } + + public override Task GetPolicyAsync(string policyName) + { + if (policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase)) + { + var existingPolicy = _options.GetPolicy(policyName); + if (existingPolicy != null) + { + return Task.FromResult(existingPolicy); + } + + var permissions = ParsePermissions(policyName); + if (permissions.Length == 0) + { + return Task.FromResult(null); + } + + var policy = new AuthorizationPolicyBuilder() + .AddRequirements(new PermissionRequirement(permissions)) + .Build(); + + _options.AddPolicy(policyName, policy); + return Task.FromResult(policy); + } + + return base.GetPolicyAsync(policyName); + } + + /// + /// 根据权限集合构建策略名称 + /// + public static string BuildPolicyName(IEnumerable permissions) + => $"{PolicyPrefix}{string.Join('|', NormalizePermissions(permissions))}"; + + private static string[] ParsePermissions(string policyName) + { + var raw = policyName[PolicyPrefix.Length..]; + return NormalizePermissions(raw.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + + private static string[] NormalizePermissions(IEnumerable permissions) + => permissions + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); +} diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs new file mode 100644 index 0000000..2ed0421 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; + +namespace TakeoutSaaS.Module.Authorization.Policies; + +/// +/// 权限要求 +/// +public sealed class PermissionRequirement : IAuthorizationRequirement +{ + public PermissionRequirement(IReadOnlyCollection permissions) + { + Permissions = permissions ?? throw new ArgumentNullException(nameof(permissions)); + } + + public IReadOnlyCollection Permissions { get; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj b/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj index b407eac..9dfdb25 100644 --- a/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj +++ b/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj @@ -4,8 +4,11 @@ enable enable + + + + - diff --git a/src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj b/src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj deleted file mode 100644 index b407eac..0000000 --- a/src/Modules/TakeoutSaaS.Module.Identity/TakeoutSaaS.Module.Identity.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - net10.0 - enable - enable - - - - - - From ccadacaa9de37630b2a69201d6ce1ebb84ef0d3f Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sun, 23 Nov 2025 09:52:54 +0800 Subject: [PATCH 04/56] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AuthController.cs | 15 +-- .../Controllers/HealthController.cs | 5 +- .../Controllers/AuthController.cs | 15 +-- .../Controllers/HealthController.cs | 5 +- .../Controllers/MeController.cs | 7 +- src/Api/TakeoutSaaS.MiniApi/Program.cs | 10 +- .../Controllers/HealthController.cs | 5 +- src/Api/TakeoutSaaS.UserApi/Program.cs | 8 +- .../Identity/Contracts/CurrentUserProfile.cs | 33 +++++- .../Identity/Contracts/TokenResponse.cs | 25 +++- .../Identity/Models/RefreshTokenDescriptor.cs | 10 +- .../Identity/Services/AdminAuthService.cs | 71 ++++++----- .../Identity/Services/MiniAuthService.cs | 110 +++++++++++------- .../Entities/IAuditableEntity.cs | 9 +- .../Exceptions/BusinessException.cs | 2 - .../Results/ApiResponse.cs | 5 +- .../Tenancy/ITenantProvider.cs | 7 ++ .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Filters/ApiResponseResultFilter.cs | 104 +++++++++++++++++ .../Middleware/SecurityHeadersMiddleware.cs | 11 +- .../Swagger/SwaggerExtensions.cs | 3 +- .../Repositories/IMiniUserRepository.cs | 25 +++- .../Identity/Options/AdminSeedOptions.cs | 32 ++++- .../Identity/Options/JwtOptions.cs | 17 ++- .../Identity/Options/LoginRateLimitOptions.cs | 8 +- .../Options/RefreshTokenStoreOptions.cs | 5 +- .../Identity/Options/WeChatMiniOptions.cs | 8 +- .../Persistence/IdentityDataSeeder.cs | 33 ++---- .../Identity/Persistence/IdentityDbContext.cs | 11 +- .../Identity/Services/JwtTokenService.cs | 36 +++--- .../PermissionAuthorizeAttribute.cs | 5 +- .../PermissionAuthorizationPolicyProvider.cs | 21 +--- .../Policies/PermissionRequirement.cs | 16 +-- 33 files changed, 457 insertions(+), 221 deletions(-) create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index 04bedfc..164d0c3 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -39,10 +39,10 @@ public sealed class AuthController : BaseApiController [HttpPost("login")] [AllowAnonymous] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task>> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) + public async Task> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) { var response = await _authService.LoginAsync(request, cancellationToken); - return Ok(ApiResponse.Ok(response)); + return ApiResponse.Ok(response); } /// @@ -51,10 +51,10 @@ public sealed class AuthController : BaseApiController [HttpPost("refresh")] [AllowAnonymous] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + public async Task> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) { var response = await _authService.RefreshTokenAsync(request, cancellationToken); - return Ok(ApiResponse.Ok(response)); + return ApiResponse.Ok(response); } /// @@ -63,15 +63,16 @@ public sealed class AuthController : BaseApiController [HttpGet("profile")] [PermissionAuthorize("identity:profile:read")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task>> GetProfile(CancellationToken cancellationToken) + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] + public async Task> GetProfile(CancellationToken cancellationToken) { var userId = User.GetUserId(); if (userId == Guid.Empty) { - return Unauthorized(ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识")); + return ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); } var profile = await _authService.GetProfileAsync(userId, cancellationToken); - return Ok(ApiResponse.Ok(profile)); + return ApiResponse.Ok(profile); } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs index 68ca72d..4db5f17 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -20,9 +21,9 @@ public class HealthController : BaseApiController [HttpGet] [AllowAnonymous] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public IActionResult Get() + public ApiResponse Get() { var payload = new { status = "OK", service = "AdminApi", time = DateTime.UtcNow }; - return Ok(ApiResponse.Ok(payload)); + return ApiResponse.Ok(payload); } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs index 2c80505..07961a3 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; @@ -20,6 +17,10 @@ public sealed class AuthController : BaseApiController { private readonly IMiniAuthService _authService; + /// + /// 小程序登录认证 + /// + /// public AuthController(IMiniAuthService authService) { _authService = authService; @@ -31,10 +32,10 @@ public sealed class AuthController : BaseApiController [HttpPost("wechat/login")] [AllowAnonymous] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task>> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken) + public async Task> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken) { var response = await _authService.LoginWithWeChatAsync(request, cancellationToken); - return Ok(ApiResponse.Ok(response)); + return ApiResponse.Ok(response); } /// @@ -43,9 +44,9 @@ public sealed class AuthController : BaseApiController [HttpPost("refresh")] [AllowAnonymous] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + public async Task> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) { var response = await _authService.RefreshTokenAsync(request, cancellationToken); - return Ok(ApiResponse.Ok(response)); + return ApiResponse.Ok(response); } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs index e915adf..c2673f8 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -20,9 +21,9 @@ public class HealthController : BaseApiController /// 健康状态 [HttpGet] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public IActionResult Get() + public ApiResponse Get() { var payload = new { status = "OK", service = "MiniApi", time = DateTime.UtcNow }; - return Ok(ApiResponse.Ok(payload)); + return ApiResponse.Ok(payload); } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs index 8ccdc14..795116f 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs @@ -37,15 +37,16 @@ public sealed class MeController : BaseApiController /// [HttpGet] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task>> Get(CancellationToken cancellationToken) + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] + public async Task> Get(CancellationToken cancellationToken) { var userId = User.GetUserId(); if (userId == Guid.Empty) { - return Unauthorized(ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识")); + return ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); } var profile = await _authService.GetProfileAsync(userId, cancellationToken); - return Ok(ApiResponse.Ok(profile)); + return ApiResponse.Ok(profile); } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index ccdb0fd..fac60c8 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -1,13 +1,5 @@ -using System; -using System.Linq; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Serilog; -using TakeoutSaaS.Application.Identity.Extensions; -using TakeoutSaaS.Infrastructure.Identity.Extensions; using TakeoutSaaS.Module.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Extensions; @@ -15,7 +7,7 @@ using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); -builder.Host.UseSerilog((context, _, configuration) => +builder.Host.UseSerilog((_, _, configuration) => { configuration .Enrich.FromLogContext() diff --git a/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs index 1648b83..e1fa6d5 100644 --- a/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs +++ b/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -20,9 +21,9 @@ public class HealthController : BaseApiController /// 健康状态 [HttpGet] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public IActionResult Get() + public ApiResponse Get() { var payload = new { status = "OK", service = "UserApi", time = DateTime.UtcNow }; - return Ok(ApiResponse.Ok(payload)); + return ApiResponse.Ok(payload); } } diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index fc30e4f..d43a488 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -1,10 +1,4 @@ -using System; -using System.Linq; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Serilog; using TakeoutSaaS.Module.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -13,7 +7,7 @@ using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); -builder.Host.UseSerilog((context, _, configuration) => +builder.Host.UseSerilog((_, _, configuration) => { configuration .Enrich.FromLogContext() diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs index be4eb44..e93e2bc 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs @@ -1,5 +1,3 @@ -using System; - namespace TakeoutSaaS.Application.Identity.Contracts; /// @@ -7,12 +5,43 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed class CurrentUserProfile { + /// + /// 用户 ID。 + /// public Guid UserId { get; init; } + + /// + /// 登录账号。 + /// public string Account { get; init; } = string.Empty; + + /// + /// 展示名称。 + /// public string DisplayName { get; init; } = string.Empty; + + /// + /// 所属租户 ID。 + /// public Guid TenantId { get; init; } + + /// + /// 所属商户 ID(平台管理员为空)。 + /// public Guid? MerchantId { get; init; } + + /// + /// 角色集合。 + /// public string[] Roles { get; init; } = Array.Empty(); + + /// + /// 权限集合。 + /// public string[] Permissions { get; init; } = Array.Empty(); + + /// + /// 头像地址(可选)。 + /// public string? Avatar { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs index 57b3ded..716364e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/TokenResponse.cs @@ -1,5 +1,3 @@ -using System; - namespace TakeoutSaaS.Application.Identity.Contracts; /// @@ -7,10 +5,33 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public class TokenResponse { + /// + /// 访问令牌(JWT)。 + /// public string AccessToken { get; init; } = string.Empty; + + /// + /// 访问令牌过期时间(UTC)。 + /// public DateTime AccessTokenExpiresAt { get; init; } + + /// + /// 刷新令牌。 + /// public string RefreshToken { get; init; } = string.Empty; + + /// + /// 刷新令牌过期时间(UTC)。 + /// public DateTime RefreshTokenExpiresAt { get; init; } + + /// + /// 当前用户档案(可选,首次登录时可能为空)。 + /// public CurrentUserProfile? User { get; init; } + + /// + /// 是否为新用户(首次登录)。 + /// public bool IsNewUser { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs b/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs index f508c3e..68e07e6 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs @@ -1,11 +1,13 @@ -using System; - namespace TakeoutSaaS.Application.Identity.Models; /// -/// 刷新令牌描述。 +/// 刷新令牌描述:存储刷新令牌的元数据信息。 /// -public sealed record class RefreshTokenDescriptor( +/// 刷新令牌值 +/// 关联的用户 ID +/// 过期时间(UTC) +/// 是否已撤销 +public sealed record RefreshTokenDescriptor( string Token, Guid UserId, DateTime ExpiresAt, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 477e53f..ee0fbeb 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; @@ -14,59 +11,75 @@ namespace TakeoutSaaS.Application.Identity.Services; /// /// 管理后台认证服务实现。 /// -public sealed class AdminAuthService : IAdminAuthService +public sealed class AdminAuthService( + IIdentityUserRepository userRepository, + IPasswordHasher passwordHasher, + IJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore) : IAdminAuthService { - private readonly IIdentityUserRepository _userRepository; - private readonly IPasswordHasher _passwordHasher; - private readonly IJwtTokenService _jwtTokenService; - private readonly IRefreshTokenStore _refreshTokenStore; - - public AdminAuthService( - IIdentityUserRepository userRepository, - IPasswordHasher passwordHasher, - IJwtTokenService jwtTokenService, - IRefreshTokenStore refreshTokenStore) - { - _userRepository = userRepository; - _passwordHasher = passwordHasher; - _jwtTokenService = jwtTokenService; - _refreshTokenStore = refreshTokenStore; - } - + /// + /// 管理后台登录:验证账号密码并生成令牌。 + /// + /// 登录请求 + /// 取消令牌 + /// 令牌响应 + /// 账号或密码错误时抛出 public async Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default) { - var user = await _userRepository.FindByAccountAsync(request.Account, cancellationToken) + // 1. 根据账号查找用户 + var user = await userRepository.FindByAccountAsync(request.Account, cancellationToken) ?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); - var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password); + // 2. 验证密码(使用 ASP.NET Core Identity 的密码哈希器) + var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password); if (result == PasswordVerificationResult.Failed) { throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); } + // 3. 构建用户档案并生成令牌 var profile = BuildProfile(user); - return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); + return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } + /// + /// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。 + /// + /// 刷新令牌请求 + /// 取消令牌 + /// 新的令牌响应 + /// 刷新令牌无效、已过期或用户不存在时抛出 public async Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) { - var descriptor = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + // 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销) + var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked) { throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期"); } - var user = await _userRepository.FindByIdAsync(descriptor.UserId, cancellationToken) + // 2. 根据用户 ID 查找用户 + var user = await userRepository.FindByIdAsync(descriptor.UserId, cancellationToken) ?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在"); - await _refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); + // 3. 撤销旧刷新令牌(防止重复使用) + await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); + + // 4. 生成新的令牌对 var profile = BuildProfile(user); - return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); + return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } + /// + /// 获取用户档案。 + /// + /// 用户 ID + /// 取消令牌 + /// 用户档案 + /// 用户不存在时抛出 public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default) { - var user = await _userRepository.FindByIdAsync(userId, cancellationToken) + var user = await userRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); return BuildProfile(user); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs index 0d27c61..25efbf9 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs @@ -1,7 +1,4 @@ -using System; using System.Net; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; @@ -16,90 +13,117 @@ namespace TakeoutSaaS.Application.Identity.Services; /// /// 小程序认证服务实现。 /// -public sealed class MiniAuthService : IMiniAuthService +public sealed class MiniAuthService( + IWeChatAuthService weChatAuthService, + IMiniUserRepository miniUserRepository, + IJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore, + ILoginRateLimiter rateLimiter, + IHttpContextAccessor httpContextAccessor, + ITenantProvider tenantProvider) : IMiniAuthService { - private readonly IWeChatAuthService _weChatAuthService; - private readonly IMiniUserRepository _miniUserRepository; - private readonly IJwtTokenService _jwtTokenService; - private readonly IRefreshTokenStore _refreshTokenStore; - private readonly ILoginRateLimiter _rateLimiter; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ITenantProvider _tenantProvider; - - public MiniAuthService( - IWeChatAuthService weChatAuthService, - IMiniUserRepository miniUserRepository, - IJwtTokenService jwtTokenService, - IRefreshTokenStore refreshTokenStore, - ILoginRateLimiter rateLimiter, - IHttpContextAccessor httpContextAccessor, - ITenantProvider tenantProvider) - { - _weChatAuthService = weChatAuthService; - _miniUserRepository = miniUserRepository; - _jwtTokenService = jwtTokenService; - _refreshTokenStore = refreshTokenStore; - _rateLimiter = rateLimiter; - _httpContextAccessor = httpContextAccessor; - _tenantProvider = tenantProvider; - } - + /// + /// 微信小程序登录:通过微信 code 获取用户信息并生成令牌。 + /// + /// 微信登录请求 + /// 取消令牌 + /// 令牌响应 + /// 获取微信用户信息失败、缺少租户标识时抛出 public async Task LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default) { + // 1. 限流检查(基于 IP 地址) var throttleKey = BuildThrottleKey(); - await _rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken); + await rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken); - var session = await _weChatAuthService.Code2SessionAsync(request.Code, cancellationToken); + // 2. 通过微信 code 获取 session(OpenId、UnionId、SessionKey) + var session = await weChatAuthService.Code2SessionAsync(request.Code, cancellationToken); if (string.IsNullOrWhiteSpace(session.OpenId)) { throw new BusinessException(ErrorCodes.Unauthorized, "获取微信用户信息失败"); } - var tenantId = _tenantProvider.GetCurrentTenantId(); + // 3. 获取当前租户 ID(多租户支持) + var tenantId = tenantProvider.GetCurrentTenantId(); if (tenantId == Guid.Empty) { throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); } + + // 4. 获取或创建小程序用户(如果 OpenId 已存在则返回现有用户,否则创建新用户) var (user, isNew) = await GetOrBindMiniUserAsync(session.OpenId, session.UnionId, request.Nickname, request.Avatar, tenantId, cancellationToken); - await _rateLimiter.ResetAsync(throttleKey, cancellationToken); + // 5. 登录成功后重置限流计数 + await rateLimiter.ResetAsync(throttleKey, cancellationToken); + + // 6. 构建用户档案并生成令牌 var profile = BuildProfile(user); - return await _jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken); + return await jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken); } + /// + /// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。 + /// + /// 刷新令牌请求 + /// 取消令牌 + /// 新的令牌响应 + /// 刷新令牌无效、已过期或用户不存在时抛出 public async Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) { - var descriptor = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + // 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销) + var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked) { throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期"); } - var user = await _miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken) + // 2. 根据用户 ID 查找用户 + var user = await miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken) ?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在"); - await _refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); + // 3. 撤销旧刷新令牌(防止重复使用) + await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); + + // 4. 生成新的令牌对 var profile = BuildProfile(user); - return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); + return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } + /// + /// 获取用户档案。 + /// + /// 用户 ID + /// 取消令牌 + /// 用户档案 + /// 用户不存在时抛出 public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default) { - var user = await _miniUserRepository.FindByIdAsync(userId, cancellationToken) + var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); return BuildProfile(user); } + /// + /// 获取或绑定小程序用户:如果 OpenId 已存在则返回现有用户,否则创建新用户。 + /// + /// 微信 OpenId + /// 微信 UnionId(可选) + /// 昵称 + /// 头像地址(可选) + /// 租户 ID + /// 取消令牌 + /// 用户实体和是否为新用户的元组 private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken) { - var existing = await _miniUserRepository.FindByOpenIdAsync(openId, cancellationToken); + // 检查用户是否已存在 + var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken); if (existing != null) { return (existing, false); } - var created = await _miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken); + // 创建新用户 + var created = await miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken); return (created, true); } @@ -118,7 +142,7 @@ public sealed class MiniAuthService : IMiniAuthService private string BuildThrottleKey() { - var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback; + var ip = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback; return $"mini-login:{ip}"; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs index fe612a2..aa6a7cd 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs @@ -1,11 +1,18 @@ namespace TakeoutSaaS.Shared.Abstractions.Entities; /// -/// 审计字段接口 +/// 审计字段接口:提供创建时间和更新时间字段。 /// public interface IAuditableEntity { + /// + /// 创建时间(UTC)。 + /// DateTime CreatedAt { get; set; } + + /// + /// 更新时间(UTC),未更新时为 null。 + /// DateTime? UpdatedAt { get; set; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs index 60d793a..c270130 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs @@ -1,5 +1,3 @@ -using System; - namespace TakeoutSaaS.Shared.Abstractions.Exceptions; /// diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs index 6669468..50401a7 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using TakeoutSaaS.Shared.Abstractions.Diagnostics; @@ -8,7 +7,7 @@ namespace TakeoutSaaS.Shared.Abstractions.Results; /// 统一的 API 返回结果包装。 /// /// 数据载荷类型 -public sealed record class ApiResponse +public sealed record ApiResponse { /// /// 是否成功。 @@ -97,7 +96,7 @@ public sealed record class ApiResponse { if (!string.IsNullOrWhiteSpace(TraceContext.TraceId)) { - return TraceContext.TraceId!; + return TraceContext.TraceId; } return Activity.Current?.Id ?? Guid.NewGuid().ToString("N"); diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs index 4af8592..8782625 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs @@ -1,7 +1,14 @@ namespace TakeoutSaaS.Shared.Abstractions.Tenancy; +/// +/// 租户提供者接口:用于获取当前请求的租户标识。 +/// public interface ITenantProvider { + /// + /// 获取当前请求的租户 ID。 + /// + /// 租户 ID,如果未设置则返回 Guid.Empty Guid GetCurrentTenantId(); } diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs index 781364d..edc70d8 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs @@ -22,6 +22,7 @@ public static class ServiceCollectionExtensions .AddControllers(options => { options.Filters.Add(); + options.Filters.Add(); }) .AddNewtonsoftJson(); diff --git a/src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs b/src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs new file mode 100644 index 0000000..4877e1e --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Shared.Web.Filters; + +/// +/// ApiResponse 结果过滤器:自动将 ApiResponse 转换为对应的 HTTP 状态码。 +/// 使用此过滤器后,控制器可以直接返回 ApiResponse<T>,无需再包一层 Ok() 或 Unauthorized()。 +/// +public sealed class ApiResponseResultFilter : IAsyncResultFilter +{ + public Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 只处理 ObjectResult 类型的结果 + if (context.Result is not ObjectResult objectResult) + { + return next(); + } + + var value = objectResult.Value; + if (value == null) + { + return next(); + } + + // 检查是否是 ApiResponse 类型 + var valueType = value.GetType(); + if (!IsApiResponseType(valueType)) + { + return next(); + } + + // 使用反射获取 Success 和 Code 属性 + // 注意:由于已通过 IsApiResponseType 检查,属性名是固定的 + const string successPropertyName = "Success"; + const string codePropertyName = "Code"; + var successProperty = valueType.GetProperty(successPropertyName); + var codeProperty = valueType.GetProperty(codePropertyName); + + if (successProperty == null || codeProperty == null) + { + return next(); + } + + var success = (bool)(successProperty.GetValue(value) ?? false); + var code = (int)(codeProperty.GetValue(value) ?? 200); + + // 根据 Success 和 Code 设置 HTTP 状态码 + var statusCode = success ? MapSuccessCode(code) : MapErrorCode(code); + + // 更新 ObjectResult 的状态码 + objectResult.StatusCode = statusCode; + + return next(); + } + + private static bool IsApiResponseType(Type type) + { + // 检查是否是 ApiResponse 类型 + if (type.IsGenericType) + { + var genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(ApiResponse<>); + } + + return false; + } + + private static int MapSuccessCode(int code) + { + // 成功情况下,通常返回 200 + // 但也可以根据业务码返回其他成功状态码(如 201 Created) + return code switch + { + 200 => StatusCodes.Status200OK, + 201 => StatusCodes.Status201Created, + 204 => StatusCodes.Status204NoContent, + _ => StatusCodes.Status200OK + }; + } + + private static int MapErrorCode(int code) + { + // 根据业务错误码映射到 HTTP 状态码 + return code switch + { + ErrorCodes.BadRequest => StatusCodes.Status400BadRequest, + ErrorCodes.Unauthorized => StatusCodes.Status401Unauthorized, + ErrorCodes.Forbidden => StatusCodes.Status403Forbidden, + ErrorCodes.NotFound => StatusCodes.Status404NotFound, + ErrorCodes.Conflict => StatusCodes.Status409Conflict, + ErrorCodes.ValidationFailed => StatusCodes.Status422UnprocessableEntity, + ErrorCodes.InternalServerError => StatusCodes.Status500InternalServerError, + // 业务错误码(10000+)统一返回 422 + >= 10000 => StatusCodes.Status422UnprocessableEntity, + // 默认返回 400 + _ => StatusCodes.Status400BadRequest + }; + } +} + diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs index 7bb1b0c..febf3dc 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs @@ -5,15 +5,8 @@ namespace TakeoutSaaS.Shared.Web.Middleware; /// /// 安全响应头中间件 /// -public sealed class SecurityHeadersMiddleware +public sealed class SecurityHeadersMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - - public SecurityHeadersMiddleware(RequestDelegate next) - { - _next = next; - } - public async Task InvokeAsync(HttpContext context) { var headers = context.Response.Headers; @@ -21,7 +14,7 @@ public sealed class SecurityHeadersMiddleware headers["X-Frame-Options"] = "DENY"; headers["X-XSS-Protection"] = "1; mode=block"; headers["Referrer-Policy"] = "no-referrer"; - await _next(context); + await next(context); } } diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs index c192adb..8b1fb25 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +18,7 @@ public static class SwaggerExtensions public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action? configure = null) { services.AddSwaggerGen(); - services.AddSingleton(provider => + services.AddSingleton(_ => { var settings = new SwaggerDocumentSettings(); configure?.Invoke(settings); diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs index db3e810..4688ea6 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using TakeoutSaaS.Domain.Identity.Entities; namespace TakeoutSaaS.Domain.Identity.Repositories; @@ -10,9 +7,31 @@ namespace TakeoutSaaS.Domain.Identity.Repositories; /// public interface IMiniUserRepository { + /// + /// 根据微信 OpenId 查找小程序用户。 + /// + /// 微信 OpenId + /// 取消令牌 + /// 小程序用户,如果不存在则返回 null Task FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default); + /// + /// 根据用户 ID 查找小程序用户。 + /// + /// 用户 ID + /// 取消令牌 + /// 小程序用户,如果不存在则返回 null Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default); + /// + /// 创建或更新小程序用户(如果 OpenId 已存在则更新,否则创建)。 + /// + /// 微信 OpenId + /// 微信 UnionId(可选) + /// 昵称 + /// 头像地址(可选) + /// 租户 ID + /// 取消令牌 + /// 创建或更新后的小程序用户 Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs index d1dd243..575f1b0 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace TakeoutSaaS.Infrastructure.Identity.Options; @@ -9,22 +7,52 @@ namespace TakeoutSaaS.Infrastructure.Identity.Options; /// public sealed class AdminSeedOptions { + /// + /// 初始用户列表。 + /// public List Users { get; set; } = new(); } +/// +/// 种子用户配置:用于初始化管理后台账号。 +/// public sealed class SeedUserOptions { + /// + /// 登录账号。 + /// [Required] public string Account { get; set; } = string.Empty; + /// + /// 登录密码(明文,将在初始化时进行哈希处理)。 + /// [Required] public string Password { get; set; } = string.Empty; + /// + /// 展示名称。 + /// [Required] public string DisplayName { get; set; } = string.Empty; + /// + /// 所属租户 ID。 + /// public Guid TenantId { get; set; } + + /// + /// 所属商户 ID(平台管理员为空)。 + /// public Guid? MerchantId { get; set; } + + /// + /// 角色集合。 + /// public string[] Roles { get; set; } = Array.Empty(); + + /// + /// 权限集合。 + /// public string[] Permissions { get; set; } = Array.Empty(); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs index 28e1052..18aed1d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/JwtOptions.cs @@ -3,23 +3,38 @@ using System.ComponentModel.DataAnnotations; namespace TakeoutSaaS.Infrastructure.Identity.Options; /// -/// JWT 配置。 +/// JWT 配置选项。 /// public sealed class JwtOptions { + /// + /// 令牌颁发者(Issuer)。 + /// [Required] public string Issuer { get; set; } = string.Empty; + /// + /// 令牌受众(Audience)。 + /// [Required] public string Audience { get; set; } = string.Empty; + /// + /// JWT 签名密钥(至少 32 个字符)。 + /// [Required] [MinLength(32)] public string Secret { get; set; } = string.Empty; + /// + /// 访问令牌过期时间(分钟),范围:5-1440。 + /// [Range(5, 1440)] public int AccessTokenExpirationMinutes { get; set; } = 60; + /// + /// 刷新令牌过期时间(分钟),范围:60-20160(14天)。 + /// [Range(60, 1440 * 14)] public int RefreshTokenExpirationMinutes { get; set; } = 60 * 24 * 7; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs index a5fb4f1..d9f7a42 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/LoginRateLimitOptions.cs @@ -3,13 +3,19 @@ using System.ComponentModel.DataAnnotations; namespace TakeoutSaaS.Infrastructure.Identity.Options; /// -/// 登录限流配置。 +/// 登录限流配置选项。 /// public sealed class LoginRateLimitOptions { + /// + /// 时间窗口(秒),范围:1-3600。 + /// [Range(1, 3600)] public int WindowSeconds { get; set; } = 60; + /// + /// 时间窗口内允许的最大尝试次数,范围:1-100。 + /// [Range(1, 100)] public int MaxAttempts { get; set; } = 5; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs index ab69c3d..fcbf6e8 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/RefreshTokenStoreOptions.cs @@ -1,9 +1,12 @@ namespace TakeoutSaaS.Infrastructure.Identity.Options; /// -/// 刷新令牌存储配置。 +/// 刷新令牌存储配置选项。 /// public sealed class RefreshTokenStoreOptions { + /// + /// Redis 键前缀,用于存储刷新令牌。 + /// public string Prefix { get; set; } = "identity:refresh:"; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs index e30d274..0dee3be 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs @@ -3,13 +3,19 @@ using System.ComponentModel.DataAnnotations; namespace TakeoutSaaS.Infrastructure.Identity.Options; /// -/// 微信小程序配置。 +/// 微信小程序配置选项。 /// public sealed class WeChatMiniOptions { + /// + /// 微信小程序 AppId。 + /// [Required] public string AppId { get; set; } = string.Empty; + /// + /// 微信小程序 AppSecret。 + /// [Required] public string Secret { get; set; } = string.Empty; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index 80327ac..57d88b6 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -1,14 +1,9 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Infrastructure.Identity.Options; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; @@ -17,29 +12,20 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// /// 后台账号初始化种子任务 /// -public sealed class IdentityDataSeeder : IHostedService +public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger logger) : IHostedService { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public IdentityDataSeeder(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - public async Task StartAsync(CancellationToken cancellationToken) { - using var scope = _serviceProvider.CreateScope(); + using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService>().Value; var passwordHasher = scope.ServiceProvider.GetRequiredService>(); await context.Database.MigrateAsync(cancellationToken); - if (options.Users == null || options.Users.Count == 0) + if (options.Users is null or { Count: 0 }) { - _logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化"); + logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化"); return; } @@ -64,7 +50,7 @@ public sealed class IdentityDataSeeder : IHostedService }; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); context.IdentityUsers.Add(user); - _logger.LogInformation("已创建后台账号 {Account}", user.Account); + logger.LogInformation("已创建后台账号 {Account}", user.Account); } else { @@ -74,7 +60,7 @@ public sealed class IdentityDataSeeder : IHostedService user.Roles = roles; user.Permissions = permissions; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); - _logger.LogInformation("已更新后台账号 {Account}", user.Account); + logger.LogInformation("已更新后台账号 {Account}", user.Account); } } @@ -85,10 +71,9 @@ public sealed class IdentityDataSeeder : IHostedService private static string[] NormalizeValues(string[]? values) => values == null - ? Array.Empty() - : values + ? [] + : [.. values .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => v.Trim()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + .Distinct(StringComparer.OrdinalIgnoreCase)]; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 6168f7e..9887201 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -11,12 +10,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// /// 身份认证 DbContext。 /// -public sealed class IdentityDbContext : DbContext +public sealed class IdentityDbContext(DbContextOptions options) : DbContext(options) { - public IdentityDbContext(DbContextOptions options) - : base(options) - { - } public DbSet IdentityUsers => Set(); public DbSet MiniUsers => Set(); @@ -37,11 +32,11 @@ public sealed class IdentityDbContext : DbContext builder.Property(x => x.Avatar).HasMaxLength(256); var converter = new ValueConverter( - v => string.Join(',', v ?? Array.Empty()), + v => string.Join(',', v), v => string.IsNullOrWhiteSpace(v) ? Array.Empty() : v.Split(',', StringSplitOptions.RemoveEmptyEntries)); var comparer = new ValueComparer( - (l, r) => l!.SequenceEqual(r!), + (l, r) => (l == null && r == null) || (l != null && r != null && Enumerable.SequenceEqual(l, r)), v => v.Aggregate(0, (current, item) => HashCode.Combine(current, item.GetHashCode())), v => v.ToArray()); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs index fa728ac..cf9c8e9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using TakeoutSaaS.Application.Identity.Abstractions; @@ -16,29 +12,33 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services; /// /// JWT 令牌生成器。 /// -public sealed class JwtTokenService : IJwtTokenService +public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions options) : IJwtTokenService { private readonly JwtSecurityTokenHandler _tokenHandler = new(); - private readonly IRefreshTokenStore _refreshTokenStore; - private readonly JwtOptions _options; - - public JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions options) - { - _refreshTokenStore = refreshTokenStore; - _options = options.Value; - } + private readonly JwtOptions _options = options.Value; + /// + /// 创建访问令牌和刷新令牌对。 + /// + /// 用户档案 + /// 是否为新用户(首次登录) + /// 取消令牌 + /// 令牌响应 public async Task CreateTokensAsync(CurrentUserProfile profile, bool isNewUser = false, CancellationToken cancellationToken = default) { var now = DateTime.UtcNow; var accessExpires = now.AddMinutes(_options.AccessTokenExpirationMinutes); var refreshExpires = now.AddMinutes(_options.RefreshTokenExpirationMinutes); + // 1. 构建 JWT Claims(包含用户 ID、账号、租户 ID、商户 ID、角色、权限等) var claims = BuildClaims(profile); + + // 2. 创建签名凭据(使用 HMAC SHA256 算法) var signingCredentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Secret)), SecurityAlgorithms.HmacSha256); + // 3. 创建 JWT 安全令牌 var jwt = new JwtSecurityToken( issuer: _options.Issuer, audience: _options.Audience, @@ -47,8 +47,11 @@ public sealed class JwtTokenService : IJwtTokenService expires: accessExpires, signingCredentials: signingCredentials); + // 4. 序列化 JWT 为字符串 var accessToken = _tokenHandler.WriteToken(jwt); - var refreshDescriptor = await _refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken); + + // 5. 生成刷新令牌并存储到 Redis + var refreshDescriptor = await refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken); return new TokenResponse { @@ -61,6 +64,11 @@ public sealed class JwtTokenService : IJwtTokenService }; } + /// + /// 构建 JWT Claims:将用户档案转换为 Claims 集合。 + /// + /// 用户档案 + /// Claims 集合 private static IEnumerable BuildClaims(CurrentUserProfile profile) { var userId = profile.UserId.ToString(); diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs index b55f602..78a16e9 100644 --- a/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Authorization; using TakeoutSaaS.Module.Authorization.Policies; @@ -9,7 +6,7 @@ namespace TakeoutSaaS.Module.Authorization.Attributes; /// /// 权限校验特性 /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public sealed class PermissionAuthorizeAttribute : AuthorizeAttribute { public PermissionAuthorizeAttribute(params string[] permissions) diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs index daaa3f0..4f2e610 100644 --- a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; @@ -10,16 +6,10 @@ namespace TakeoutSaaS.Module.Authorization.Policies; /// /// 权限策略提供者(按需动态构建策略) /// -public sealed class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider +public sealed class PermissionAuthorizationPolicyProvider(IOptions options) : DefaultAuthorizationPolicyProvider(options) { public const string PolicyPrefix = "PERMISSION:"; - private readonly AuthorizationOptions _options; - - public PermissionAuthorizationPolicyProvider(IOptions options) - : base(options) - { - _options = options.Value; - } + private readonly AuthorizationOptions _options = options.Value; public override Task GetPolicyAsync(string policyName) { @@ -28,7 +18,7 @@ public sealed class PermissionAuthorizationPolicyProvider : DefaultAuthorization var existingPolicy = _options.GetPolicy(policyName); if (existingPolicy != null) { - return Task.FromResult(existingPolicy); + return Task.FromResult(existingPolicy); } var permissions = ParsePermissions(policyName); @@ -61,9 +51,8 @@ public sealed class PermissionAuthorizationPolicyProvider : DefaultAuthorization } private static string[] NormalizePermissions(IEnumerable permissions) - => permissions + => [.. permissions .Where(p => !string.IsNullOrWhiteSpace(p)) .Select(p => p.Trim()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + .Distinct(StringComparer.OrdinalIgnoreCase)]; } diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs index 2ed0421..b188ec0 100644 --- a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionRequirement.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; namespace TakeoutSaaS.Module.Authorization.Policies; /// -/// 权限要求 +/// 权限要求:用于授权策略中定义所需的权限集合。 /// -public sealed class PermissionRequirement : IAuthorizationRequirement +public sealed class PermissionRequirement(IReadOnlyCollection permissions) : IAuthorizationRequirement { - public PermissionRequirement(IReadOnlyCollection permissions) - { - Permissions = permissions ?? throw new ArgumentNullException(nameof(permissions)); - } - - public IReadOnlyCollection Permissions { get; } + /// + /// 所需的权限集合。 + /// + public IReadOnlyCollection Permissions { get; } = permissions ?? throw new ArgumentNullException(nameof(permissions)); } From cd52131c34fed728f2d481cbdfd4daefcd1abac3 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sun, 23 Nov 2025 09:55:11 +0800 Subject: [PATCH 05/56] =?UTF-8?q?chore:=20=E6=9B=B4=E6=94=B9=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {0_Document => Document}/01_项目概述.md | 0 {0_Document => Document}/02_技术架构.md | 0 {0_Document => Document}/03_数据库设计.md | 0 {0_Document => Document}/04A_管理后台API.md | 0 {0_Document => Document}/04B_小程序API.md | 0 {0_Document => Document}/04_API接口设计.md | 0 {0_Document => Document}/05_部署运维.md | 0 {0_Document => Document}/06_开发规范.md | 0 {0_Document => Document}/07_系统架构图.md | 0 {0_Document => Document}/08_AI编程规范.md | 0 {0_Document => Document}/09_AI精简开发规范.md | 0 {0_Document => Document}/10_TODO.md | 0 {0_Document => Document}/README.md | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename {0_Document => Document}/01_项目概述.md (100%) rename {0_Document => Document}/02_技术架构.md (100%) rename {0_Document => Document}/03_数据库设计.md (100%) rename {0_Document => Document}/04A_管理后台API.md (100%) rename {0_Document => Document}/04B_小程序API.md (100%) rename {0_Document => Document}/04_API接口设计.md (100%) rename {0_Document => Document}/05_部署运维.md (100%) rename {0_Document => Document}/06_开发规范.md (100%) rename {0_Document => Document}/07_系统架构图.md (100%) rename {0_Document => Document}/08_AI编程规范.md (100%) rename {0_Document => Document}/09_AI精简开发规范.md (100%) rename {0_Document => Document}/10_TODO.md (100%) rename {0_Document => Document}/README.md (100%) diff --git a/0_Document/01_项目概述.md b/Document/01_项目概述.md similarity index 100% rename from 0_Document/01_项目概述.md rename to Document/01_项目概述.md diff --git a/0_Document/02_技术架构.md b/Document/02_技术架构.md similarity index 100% rename from 0_Document/02_技术架构.md rename to Document/02_技术架构.md diff --git a/0_Document/03_数据库设计.md b/Document/03_数据库设计.md similarity index 100% rename from 0_Document/03_数据库设计.md rename to Document/03_数据库设计.md diff --git a/0_Document/04A_管理后台API.md b/Document/04A_管理后台API.md similarity index 100% rename from 0_Document/04A_管理后台API.md rename to Document/04A_管理后台API.md diff --git a/0_Document/04B_小程序API.md b/Document/04B_小程序API.md similarity index 100% rename from 0_Document/04B_小程序API.md rename to Document/04B_小程序API.md diff --git a/0_Document/04_API接口设计.md b/Document/04_API接口设计.md similarity index 100% rename from 0_Document/04_API接口设计.md rename to Document/04_API接口设计.md diff --git a/0_Document/05_部署运维.md b/Document/05_部署运维.md similarity index 100% rename from 0_Document/05_部署运维.md rename to Document/05_部署运维.md diff --git a/0_Document/06_开发规范.md b/Document/06_开发规范.md similarity index 100% rename from 0_Document/06_开发规范.md rename to Document/06_开发规范.md diff --git a/0_Document/07_系统架构图.md b/Document/07_系统架构图.md similarity index 100% rename from 0_Document/07_系统架构图.md rename to Document/07_系统架构图.md diff --git a/0_Document/08_AI编程规范.md b/Document/08_AI编程规范.md similarity index 100% rename from 0_Document/08_AI编程规范.md rename to Document/08_AI编程规范.md diff --git a/0_Document/09_AI精简开发规范.md b/Document/09_AI精简开发规范.md similarity index 100% rename from 0_Document/09_AI精简开发规范.md rename to Document/09_AI精简开发规范.md diff --git a/0_Document/10_TODO.md b/Document/10_TODO.md similarity index 100% rename from 0_Document/10_TODO.md rename to Document/10_TODO.md diff --git a/0_Document/README.md b/Document/README.md similarity index 100% rename from 0_Document/README.md rename to Document/README.md From 429d4fb747d3c535caf2785b2976ad19cfdcde11 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sun, 23 Nov 2025 12:47:29 +0800 Subject: [PATCH 06/56] =?UTF-8?q?chore:=20=E6=8F=90=E4=BA=A4=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/10_TODO.md | 6 +- .../Controllers/DictionaryController.cs | 126 +++++++ src/Api/TakeoutSaaS.AdminApi/Program.cs | 9 +- .../TakeoutSaaS.AdminApi.csproj | 1 + src/Api/TakeoutSaaS.MiniApi/Program.cs | 7 +- src/Api/TakeoutSaaS.UserApi/Program.cs | 7 +- .../Abstractions/IDictionaryAppService.cs | 26 ++ .../Abstractions/IDictionaryCache.cs | 27 ++ .../Contracts/CreateDictionaryGroupRequest.cs | 34 ++ .../Contracts/CreateDictionaryItemRequest.cs | 48 +++ .../Contracts/DictionaryBatchQueryRequest.cs | 15 + .../Contracts/DictionaryGroupQuery.cs | 19 + .../Contracts/UpdateDictionaryGroupRequest.cs | 26 ++ .../Contracts/UpdateDictionaryItemRequest.cs | 36 ++ .../DictionaryServiceCollectionExtensions.cs | 20 + .../Dictionary/Models/DictionaryGroupDto.cs | 23 ++ .../Dictionary/Models/DictionaryItemDto.cs | 23 ++ .../Services/DictionaryAppService.cs | 344 ++++++++++++++++++ .../Entities/IMultiTenantEntity.cs | 12 + .../Tenancy/ITenantContextAccessor.cs | 12 + .../Tenancy/ITenantProvider.cs | 6 +- .../Tenancy/TenantConstants.cs | 12 + .../Tenancy/TenantContext.cs | 45 +++ .../Security/TenantHttpContextExtensions.cs | 31 ++ .../Dictionary/Entities/DictionaryGroup.cs | 61 ++++ .../Dictionary/Entities/DictionaryItem.cs | 69 ++++ .../Dictionary/Enums/DictionaryScope.cs | 17 + .../Repositories/IDictionaryRepository.cs | 68 ++++ .../Identity/Entities/IdentityUser.cs | 6 +- .../Identity/Entities/MiniUser.cs | 8 +- .../Persistence/TenantAwareDbContext.cs | 89 +++++ .../DictionaryServiceCollectionExtensions.cs | 44 +++ .../Options/DictionaryCacheOptions.cs | 12 + .../Persistence/DictionaryDbContext.cs | 63 ++++ .../Repositories/EfDictionaryRepository.cs | 105 ++++++ .../Services/DistributedDictionaryCache.cs | 56 +++ .../Persistence/IdentityDataSeeder.cs | 16 + .../Identity/Persistence/IdentityDbContext.cs | 11 +- .../Identity/Services/JwtTokenService.cs | 12 +- .../Extensions/DictionaryModuleExtensions.cs | 22 ++ .../TakeoutSaaS.Module.Dictionary.csproj | 3 +- .../TenantServiceCollectionExtensions.cs | 34 ++ .../TenantContextAccessor.cs | 34 ++ .../TenantProvider.cs | 35 +- .../TenantResolutionMiddleware.cs | 191 ++++++++++ .../TenantResolutionOptions.cs | 56 +++ 46 files changed, 1864 insertions(+), 63 deletions(-) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryGroupRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryItemRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Dictionary/Enums/DictionaryScope.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Options/DictionaryCacheOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Dictionary/Extensions/DictionaryModuleExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs diff --git a/Document/10_TODO.md b/Document/10_TODO.md index 9190463..ddb4c41 100644 --- a/Document/10_TODO.md +++ b/Document/10_TODO.md @@ -17,9 +17,9 @@ - [x] 登录防刷限流(MiniApi) ## C. 多租户与参数字典 -- [ ] 多租户中间件:从 Header/域名解析租户(Shared.Web + Tenancy) -- [ ] EF Core 全局查询过滤(tenant_id) -- [ ] 参数字典模块(系统参数/业务参数)CRUD 与缓存(Dictionary 模块) +- [x] 多租户中间件:从 Header/域名解析租户(Shared.Web + Tenancy) +- [x] EF Core 全局查询过滤(tenant_id) +- [x] 参数字典模块(系统参数/业务参数)CRUD 与缓存(Dictionary 模块) ## D. 数据访问与多数据源 - [ ] EF Core 10 基础上下文、实体基类、审计字段 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs new file mode 100644 index 0000000..7232ed2 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Dictionary.Abstractions; +using TakeoutSaaS.Application.Dictionary.Contracts; +using TakeoutSaaS.Application.Dictionary.Models; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 参数字典管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/dictionaries")] +public sealed class DictionaryController : BaseApiController +{ + private readonly IDictionaryAppService _dictionaryAppService; + + /// + /// 初始化字典控制器。 + /// + /// 字典服务 + public DictionaryController(IDictionaryAppService dictionaryAppService) + { + _dictionaryAppService = dictionaryAppService; + } + + /// + /// 查询字典分组。 + /// + [HttpGet] + [PermissionAuthorize("dictionary:group:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetGroups([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken) + { + var groups = await _dictionaryAppService.SearchGroupsAsync(query, cancellationToken); + return ApiResponse>.Ok(groups); + } + + /// + /// 创建字典分组。 + /// + [HttpPost] + [PermissionAuthorize("dictionary:group:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> CreateGroup([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken) + { + var group = await _dictionaryAppService.CreateGroupAsync(request, cancellationToken); + return ApiResponse.Ok(group); + } + + /// + /// 更新字典分组。 + /// + [HttpPut("{groupId:guid}")] + [PermissionAuthorize("dictionary:group:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> UpdateGroup(Guid groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken) + { + var group = await _dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken); + return ApiResponse.Ok(group); + } + + /// + /// 删除字典分组。 + /// + [HttpDelete("{groupId:guid}")] + [PermissionAuthorize("dictionary:group:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> DeleteGroup(Guid groupId, CancellationToken cancellationToken) + { + await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken); + return ApiResponse.Success(); + } + + /// + /// 创建字典项。 + /// + [HttpPost("{groupId:guid}/items")] + [PermissionAuthorize("dictionary:item:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> CreateItem(Guid groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken) + { + request.GroupId = groupId; + var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken); + return ApiResponse.Ok(item); + } + + /// + /// 更新字典项。 + /// + [HttpPut("items/{itemId:guid}")] + [PermissionAuthorize("dictionary:item:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> UpdateItem(Guid itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken) + { + var item = await _dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken); + return ApiResponse.Ok(item); + } + + /// + /// 删除字典项。 + /// + [HttpDelete("items/{itemId:guid}")] + [PermissionAuthorize("dictionary:item:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> DeleteItem(Guid itemId, CancellationToken cancellationToken) + { + await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken); + return ApiResponse.Success(); + } + + /// + /// 批量获取字典项(命中缓存)。 + /// + [HttpPost("batch")] + [ProducesResponseType(typeof(ApiResponse>>), StatusCodes.Status200OK)] + public async Task>>> BatchGet([FromBody] DictionaryBatchQueryRequest request, CancellationToken cancellationToken) + { + var dictionaries = await _dictionaryAppService.GetCachedItemsAsync(request, cancellationToken); + return ApiResponse>>.Ok(dictionaries); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 5359f9f..c506c0c 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -9,8 +9,8 @@ using Serilog; using TakeoutSaaS.Application.Identity.Extensions; using TakeoutSaaS.Infrastructure.Identity.Extensions; using TakeoutSaaS.Module.Authorization.Extensions; -using TakeoutSaaS.Module.Tenancy; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Module.Dictionary.Extensions; +using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -36,6 +36,8 @@ builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSee builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddAuthorization(); builder.Services.AddPermissionAuthorization(); +builder.Services.AddTenantResolution(builder.Configuration); +builder.Services.AddDictionaryModule(builder.Configuration); var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin"); builder.Services.AddCors(options => @@ -46,11 +48,10 @@ builder.Services.AddCors(options => }); }); -builder.Services.AddScoped(); - var app = builder.Build(); app.UseCors("AdminApiCors"); +app.UseTenantResolution(); app.UseSharedWebCore(); app.UseAuthentication(); app.UseAuthorization(); diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index d4c9b45..c1f701a 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index fac60c8..fd0ebd4 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Serilog; -using TakeoutSaaS.Module.Tenancy; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -22,6 +21,7 @@ builder.Services.AddSharedSwagger(options => options.Description = "小程序 API 文档"; options.EnableAuthorization = true; }); +builder.Services.AddTenantResolution(builder.Configuration); var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini"); builder.Services.AddCors(options => @@ -32,11 +32,10 @@ builder.Services.AddCors(options => }); }); -builder.Services.AddScoped(); - var app = builder.Build(); app.UseCors("MiniApiCors"); +app.UseTenantResolution(); app.UseSharedWebCore(); app.UseSharedSwagger(); diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index d43a488..ddfa3af 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Serilog; -using TakeoutSaaS.Module.Tenancy; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -22,6 +21,7 @@ builder.Services.AddSharedSwagger(options => options.Description = "C 端用户 API 文档"; options.EnableAuthorization = true; }); +builder.Services.AddTenantResolution(builder.Configuration); var userOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:User"); builder.Services.AddCors(options => @@ -32,11 +32,10 @@ builder.Services.AddCors(options => }); }); -builder.Services.AddScoped(); - var app = builder.Build(); app.UseCors("UserApiCors"); +app.UseTenantResolution(); app.UseSharedWebCore(); app.UseSharedSwagger(); diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs new file mode 100644 index 0000000..dc51fe3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs @@ -0,0 +1,26 @@ +using TakeoutSaaS.Application.Dictionary.Contracts; +using TakeoutSaaS.Application.Dictionary.Models; + +namespace TakeoutSaaS.Application.Dictionary.Abstractions; + +/// +/// 参数字典应用服务接口。 +/// +public interface IDictionaryAppService +{ + Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default); + + Task UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default); + + Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default); + + Task> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default); + + Task CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default); + + Task UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default); + + Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default); + + Task>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs new file mode 100644 index 0000000..4bbf169 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Dictionary.Models; + +namespace TakeoutSaaS.Application.Dictionary.Abstractions; + +/// +/// 字典缓存读写接口。 +/// +public interface IDictionaryCache +{ + /// + /// 获取缓存。 + /// + Task?> GetAsync(Guid tenantId, string code, CancellationToken cancellationToken = default); + + /// + /// 写入缓存。 + /// + Task SetAsync(Guid tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default); + + /// + /// 移除缓存。 + /// + Task RemoveAsync(Guid tenantId, string code, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs new file mode 100644 index 0000000..10454ff --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Domain.Dictionary.Enums; + +namespace TakeoutSaaS.Application.Dictionary.Contracts; + +/// +/// 创建字典分组请求。 +/// +public sealed class CreateDictionaryGroupRequest +{ + /// + /// 分组编码。 + /// + [Required, MaxLength(64)] + public string Code { get; set; } = string.Empty; + + /// + /// 分组名称。 + /// + [Required, MaxLength(128)] + public string Name { get; set; } = string.Empty; + + /// + /// 作用域:系统/业务。 + /// + [Required] + public DictionaryScope Scope { get; set; } = DictionaryScope.Business; + + /// + /// 描述信息。 + /// + [MaxLength(512)] + public string? Description { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs new file mode 100644 index 0000000..553401a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Dictionary.Contracts; + +/// +/// 创建字典项请求。 +/// +public sealed class CreateDictionaryItemRequest +{ + /// + /// 所属分组 ID。 + /// + [Required] + public Guid GroupId { get; set; } + + /// + /// 字典项键。 + /// + [Required, MaxLength(64)] + public string Key { get; set; } = string.Empty; + + /// + /// 字典项值。 + /// + [Required, MaxLength(256)] + public string Value { get; set; } = string.Empty; + + /// + /// 是否默认项。 + /// + public bool IsDefault { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 描述信息。 + /// + [MaxLength(512)] + public string? Description { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs new file mode 100644 index 0000000..cc5e2c6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Dictionary.Contracts; + +/// +/// 批量查询字典项请求。 +/// +public sealed class DictionaryBatchQueryRequest +{ + /// + /// 分组编码集合。 + /// + [Required] + public IReadOnlyCollection Codes { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs new file mode 100644 index 0000000..861b082 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Domain.Dictionary.Enums; + +namespace TakeoutSaaS.Application.Dictionary.Contracts; + +/// +/// 字典分组查询参数。 +/// +public sealed class DictionaryGroupQuery +{ + /// + /// 作用域过滤。 + /// + public DictionaryScope? Scope { get; set; } + + /// + /// 是否包含字典项。 + /// + public bool IncludeItems { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryGroupRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryGroupRequest.cs new file mode 100644 index 0000000..4ed0fdc --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryGroupRequest.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Dictionary.Contracts; + +/// +/// 更新字典分组请求。 +/// +public sealed class UpdateDictionaryGroupRequest +{ + /// + /// 分组名称。 + /// + [Required, MaxLength(128)] + public string Name { get; set; } = string.Empty; + + /// + /// 描述信息。 + /// + [MaxLength(512)] + public string? Description { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryItemRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryItemRequest.cs new file mode 100644 index 0000000..f2c9871 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/UpdateDictionaryItemRequest.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Dictionary.Contracts; + +/// +/// 更新字典项请求。 +/// +public sealed class UpdateDictionaryItemRequest +{ + /// + /// 字典项值。 + /// + [Required, MaxLength(256)] + public string Value { get; set; } = string.Empty; + + /// + /// 是否默认项。 + /// + public bool IsDefault { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 描述信息。 + /// + [MaxLength(512)] + public string? Description { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs new file mode 100644 index 0000000..5b03aa0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Dictionary.Abstractions; +using TakeoutSaaS.Application.Dictionary.Services; + +namespace TakeoutSaaS.Application.Dictionary.Extensions; + +/// +/// 字典应用服务注册扩展。 +/// +public static class DictionaryServiceCollectionExtensions +{ + /// + /// 注册字典模块应用层组件。 + /// + public static IServiceCollection AddDictionaryApplication(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs new file mode 100644 index 0000000..528167f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs @@ -0,0 +1,23 @@ +using TakeoutSaaS.Domain.Dictionary.Enums; + +namespace TakeoutSaaS.Application.Dictionary.Models; + +/// +/// 字典分组 DTO。 +/// +public sealed class DictionaryGroupDto +{ + public Guid Id { get; init; } + + public string Code { get; init; } = string.Empty; + + public string Name { get; init; } = string.Empty; + + public DictionaryScope Scope { get; init; } + + public string? Description { get; init; } + + public bool IsEnabled { get; init; } + + public IReadOnlyList Items { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs new file mode 100644 index 0000000..89faaf7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs @@ -0,0 +1,23 @@ +namespace TakeoutSaaS.Application.Dictionary.Models; + +/// +/// 字典项 DTO。 +/// +public sealed class DictionaryItemDto +{ + public Guid Id { get; init; } + + public Guid GroupId { get; init; } + + public string Key { get; init; } = string.Empty; + + public string Value { get; init; } = string.Empty; + + public bool IsDefault { get; init; } + + public bool IsEnabled { get; init; } + + public int SortOrder { get; init; } + + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs new file mode 100644 index 0000000..93d92c2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs @@ -0,0 +1,344 @@ +using System.Linq; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.Dictionary.Abstractions; +using TakeoutSaaS.Application.Dictionary.Contracts; +using TakeoutSaaS.Application.Dictionary.Models; +using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.Dictionary.Enums; +using TakeoutSaaS.Domain.Dictionary.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Dictionary.Services; + +/// +/// 参数字典应用服务实现。 +/// +public sealed class DictionaryAppService : IDictionaryAppService +{ + private readonly IDictionaryRepository _repository; + private readonly IDictionaryCache _cache; + private readonly ITenantProvider _tenantProvider; + private readonly ILogger _logger; + + public DictionaryAppService( + IDictionaryRepository repository, + IDictionaryCache cache, + ITenantProvider tenantProvider, + ILogger logger) + { + _repository = repository; + _cache = cache; + _tenantProvider = tenantProvider; + _logger = logger; + } + + public async Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default) + { + var normalizedCode = NormalizeCode(request.Code); + var targetTenant = ResolveTargetTenant(request.Scope); + + var existing = await _repository.FindGroupByCodeAsync(normalizedCode, cancellationToken); + if (existing != null) + { + throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在"); + } + + var group = new DictionaryGroup + { + Id = Guid.NewGuid(), + TenantId = targetTenant, + Code = normalizedCode, + Name = request.Name.Trim(), + Scope = request.Scope, + Description = request.Description?.Trim(), + IsEnabled = true + }; + + await _repository.AddGroupAsync(group, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope); + return MapGroup(group, includeItems: false); + } + + public async Task UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default) + { + var group = await RequireGroupAsync(groupId, cancellationToken); + EnsureScopePermission(group.Scope); + + group.Name = request.Name.Trim(); + group.Description = request.Description?.Trim(); + group.IsEnabled = request.IsEnabled; + + await _repository.SaveChangesAsync(cancellationToken); + await InvalidateCacheAsync(group, cancellationToken); + _logger.LogInformation("更新字典分组:{GroupId}", group.Id); + return MapGroup(group, includeItems: false); + } + + public async Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default) + { + var group = await RequireGroupAsync(groupId, cancellationToken); + EnsureScopePermission(group.Scope); + + await _repository.RemoveGroupAsync(group, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + await InvalidateCacheAsync(group, cancellationToken); + _logger.LogInformation("删除字典分组:{GroupId}", group.Id); + } + + public async Task> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var scope = ResolveScopeForQuery(request.Scope, tenantId); + EnsureScopePermission(scope); + + var groups = await _repository.SearchGroupsAsync(scope, cancellationToken); + var includeItems = request.IncludeItems; + var result = new List(groups.Count); + + foreach (var group in groups) + { + IReadOnlyList items = Array.Empty(); + if (includeItems) + { + var itemEntities = await _repository.GetItemsByGroupIdAsync(group.Id, cancellationToken); + items = itemEntities.Select(MapItem).ToList(); + } + + result.Add(MapGroup(group, includeItems, items)); + } + + return result; + } + + public async Task CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default) + { + var group = await RequireGroupAsync(request.GroupId, cancellationToken); + EnsureScopePermission(group.Scope); + + var item = new DictionaryItem + { + Id = Guid.NewGuid(), + TenantId = group.TenantId, + GroupId = group.Id, + Key = request.Key.Trim(), + Value = request.Value.Trim(), + Description = request.Description?.Trim(), + SortOrder = request.SortOrder, + IsDefault = request.IsDefault, + IsEnabled = request.IsEnabled + }; + + await _repository.AddItemAsync(item, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + await InvalidateCacheAsync(group, cancellationToken); + _logger.LogInformation("新增字典项:{ItemId}", item.Id); + return MapItem(item); + } + + public async Task UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default) + { + var item = await RequireItemAsync(itemId, cancellationToken); + var group = await RequireGroupAsync(item.GroupId, cancellationToken); + EnsureScopePermission(group.Scope); + + item.Value = request.Value.Trim(); + item.Description = request.Description?.Trim(); + item.SortOrder = request.SortOrder; + item.IsDefault = request.IsDefault; + item.IsEnabled = request.IsEnabled; + + await _repository.SaveChangesAsync(cancellationToken); + await InvalidateCacheAsync(group, cancellationToken); + _logger.LogInformation("更新字典项:{ItemId}", item.Id); + return MapItem(item); + } + + public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default) + { + var item = await RequireItemAsync(itemId, cancellationToken); + var group = await RequireGroupAsync(item.GroupId, cancellationToken); + EnsureScopePermission(group.Scope); + + await _repository.RemoveItemAsync(item, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + await InvalidateCacheAsync(group, cancellationToken); + _logger.LogInformation("删除字典项:{ItemId}", item.Id); + } + + public async Task>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default) + { + var normalizedCodes = request.Codes + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(NormalizeCode) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (normalizedCodes.Length == 0) + { + return new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + var tenantId = _tenantProvider.GetCurrentTenantId(); + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var code in normalizedCodes) + { + var systemItems = await GetOrLoadCacheAsync(Guid.Empty, code, cancellationToken); + if (tenantId == Guid.Empty) + { + result[code] = systemItems; + continue; + } + + var tenantItems = await GetOrLoadCacheAsync(tenantId, code, cancellationToken); + result[code] = MergeItems(systemItems, tenantItems); + } + + return result; + } + + private async Task RequireGroupAsync(Guid groupId, CancellationToken cancellationToken) + { + var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken); + if (group == null) + { + throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在"); + } + + return group; + } + + private async Task RequireItemAsync(Guid itemId, CancellationToken cancellationToken) + { + var item = await _repository.FindItemByIdAsync(itemId, cancellationToken); + if (item == null) + { + throw new BusinessException(ErrorCodes.NotFound, "字典项不存在"); + } + + return item; + } + + private Guid ResolveTargetTenant(DictionaryScope scope) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + if (scope == DictionaryScope.System) + { + EnsurePlatformTenant(tenantId); + return Guid.Empty; + } + + if (tenantId == Guid.Empty) + { + throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户"); + } + + return tenantId; + } + + private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant(); + + private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, Guid tenantId) + { + if (requestedScope.HasValue) + { + return requestedScope.Value; + } + + return tenantId == Guid.Empty ? DictionaryScope.System : DictionaryScope.Business; + } + + private void EnsureScopePermission(DictionaryScope scope) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + if (scope == DictionaryScope.System && tenantId != Guid.Empty) + { + throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); + } + } + + private void EnsurePlatformTenant(Guid tenantId) + { + if (tenantId != Guid.Empty) + { + throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); + } + } + + private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken) + { + await _cache.RemoveAsync(group.TenantId, group.Code, cancellationToken); + if (group.Scope == DictionaryScope.Business) + { + return; + } + + // 系统参数更新需要逐租户重新合并,由调用方在下一次请求时重新加载 + } + + private async Task> GetOrLoadCacheAsync(Guid tenantId, string code, CancellationToken cancellationToken) + { + var cached = await _cache.GetAsync(tenantId, code, cancellationToken); + if (cached != null) + { + return cached; + } + + var entities = await _repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken); + var items = entities + .Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true)) + .Select(MapItem) + .OrderBy(item => item.SortOrder) + .ToList(); + + await _cache.SetAsync(tenantId, code, items, cancellationToken); + return items; + } + + private static IReadOnlyList MergeItems(IReadOnlyList systemItems, IReadOnlyList tenantItems) + { + if (tenantItems.Count == 0) + { + return systemItems; + } + + if (systemItems.Count == 0) + { + return tenantItems; + } + + return systemItems.Concat(tenantItems) + .OrderBy(item => item.SortOrder) + .ToList(); + } + + private static DictionaryGroupDto MapGroup(DictionaryGroup group, bool includeItems, IReadOnlyList? items = null) + { + return new DictionaryGroupDto + { + Id = group.Id, + Code = group.Code, + Name = group.Name, + Scope = group.Scope, + Description = group.Description, + IsEnabled = group.IsEnabled, + Items = includeItems ? items ?? group.Items.Select(MapItem).ToList() : Array.Empty() + }; + } + + private static DictionaryItemDto MapItem(DictionaryItem item) + => new() + { + Id = item.Id, + GroupId = item.GroupId, + Key = item.Key, + Value = item.Value, + IsDefault = item.IsDefault, + IsEnabled = item.IsEnabled, + SortOrder = item.SortOrder, + Description = item.Description + }; +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs new file mode 100644 index 0000000..5ea8f7d --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Shared.Abstractions.Entities; + +/// +/// 多租户实体约定:所有持久化实体须包含租户标识字段。 +/// +public interface IMultiTenantEntity +{ + /// + /// 所属租户 ID。 + /// + Guid TenantId { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs new file mode 100644 index 0000000..c05f027 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Shared.Abstractions.Tenancy; + +/// +/// 租户上下文访问器:用于在请求生命周期内读写当前租户上下文。 +/// +public interface ITenantContextAccessor +{ + /// + /// 获取或设置当前租户上下文。 + /// + TenantContext? Current { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs index 8782625..41b999f 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs @@ -1,14 +1,12 @@ namespace TakeoutSaaS.Shared.Abstractions.Tenancy; /// -/// 租户提供者接口:用于获取当前请求的租户标识。 +/// 租户提供者:用于在各层读取当前请求绑定的租户 ID。 /// public interface ITenantProvider { /// - /// 获取当前请求的租户 ID。 + /// 获取当前租户 ID,未解析时返回 Guid.Empty。 /// - /// 租户 ID,如果未设置则返回 Guid.Empty Guid GetCurrentTenantId(); } - diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs new file mode 100644 index 0000000..d5638fa --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Shared.Abstractions.Tenancy; + +/// +/// 多租户相关通用常量。 +/// +public static class TenantConstants +{ + /// + /// HttpContext.Items 中租户上下文的键名。 + /// + public const string HttpContextItemKey = "__tenant_context"; +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs new file mode 100644 index 0000000..a4686a4 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs @@ -0,0 +1,45 @@ +namespace TakeoutSaaS.Shared.Abstractions.Tenancy; + +/// +/// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。 +/// +public sealed class TenantContext +{ + /// + /// 未解析到租户时的默认上下文。 + /// + public static TenantContext Empty { get; } = new(Guid.Empty, null, "unresolved"); + + /// + /// 初始化租户上下文。 + /// + /// 租户 ID + /// 租户编码(可选) + /// 解析来源 + public TenantContext(Guid tenantId, string? tenantCode, string source) + { + TenantId = tenantId; + TenantCode = tenantCode; + Source = source; + } + + /// + /// 当前租户 ID,未解析时为 Guid.Empty。 + /// + public Guid TenantId { get; } + + /// + /// 当前租户编码(例如子域名或业务编码),可为空。 + /// + public string? TenantCode { get; } + + /// + /// 租户解析来源(Header、Host、Token 等)。 + /// + public string Source { get; } + + /// + /// 是否已成功解析到租户。 + /// + public bool IsResolved => TenantId != Guid.Empty; +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs new file mode 100644 index 0000000..12832c9 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Shared.Web.Security; + +/// +/// HttpContext 租户扩展方法。 +/// +public static class TenantHttpContextExtensions +{ + /// + /// 获取 HttpContext.Items 中缓存的租户上下文。 + /// + /// 当前 HttpContext + /// 租户上下文,若不存在则返回 null + public static TenantContext? GetTenantContext(this HttpContext? context) + { + if (context == null) + { + return null; + } + + if (context.Items.TryGetValue(TenantConstants.HttpContextItemKey, out var value) && + value is TenantContext tenantContext) + { + return tenantContext; + } + + return null; + } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs new file mode 100644 index 0000000..4bba213 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using TakeoutSaaS.Domain.Dictionary.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Dictionary.Entities; + +/// +/// 参数字典分组(系统参数/业务参数)。 +/// +public sealed class DictionaryGroup : IMultiTenantEntity, IAuditableEntity +{ + /// + /// 分组 ID。 + /// + public Guid Id { get; set; } + + /// + /// 所属租户(系统参数为 Guid.Empty)。 + /// + public Guid TenantId { get; set; } + + /// + /// 分组编码(唯一)。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 分组名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 分组作用域:系统/业务。 + /// + public DictionaryScope Scope { get; set; } = DictionaryScope.Business; + + /// + /// 描述信息。 + /// + public string? Description { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 更新时间(UTC)。 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 字典项集合。 + /// + public ICollection Items { get; set; } = new List(); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs new file mode 100644 index 0000000..0058e23 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs @@ -0,0 +1,69 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Dictionary.Entities; + +/// +/// 参数字典项。 +/// +public sealed class DictionaryItem : IMultiTenantEntity, IAuditableEntity +{ + /// + /// 字典项 ID。 + /// + public Guid Id { get; set; } + + /// + /// 所属租户。 + /// + public Guid TenantId { get; set; } + + /// + /// 关联分组 ID。 + /// + public Guid GroupId { get; set; } + + /// + /// 字典项键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 字典项值。 + /// + public string Value { get; set; } = string.Empty; + + /// + /// 是否默认项。 + /// + public bool IsDefault { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 排序值,越小越靠前。 + /// + public int SortOrder { get; set; } + + /// + /// 描述信息。 + /// + public string? Description { get; set; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 更新时间(UTC)。 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 导航属性:所属分组。 + /// + public DictionaryGroup? Group { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Enums/DictionaryScope.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Enums/DictionaryScope.cs new file mode 100644 index 0000000..5e27e54 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Enums/DictionaryScope.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Dictionary.Enums; + +/// +/// 参数字典作用域。 +/// +public enum DictionaryScope +{ + /// + /// 系统级参数,所有租户共享。 + /// + System = 1, + + /// + /// 业务级参数,仅当前租户可见。 + /// + Business = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs new file mode 100644 index 0000000..ee338f2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.Dictionary.Enums; + +namespace TakeoutSaaS.Domain.Dictionary.Repositories; + +/// +/// 参数字典仓储契约。 +/// +public interface IDictionaryRepository +{ + /// + /// 依据 ID 获取分组。 + /// + Task FindGroupByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// 依据编码获取分组。 + /// + Task FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default); + + /// + /// 搜索分组,可按作用域过滤。 + /// + Task> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default); + + /// + /// 新增分组。 + /// + Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default); + + /// + /// 删除分组。 + /// + Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default); + + /// + /// 依据 ID 获取字典项。 + /// + Task FindItemByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// 获取某分组下的所有字典项。 + /// + Task> GetItemsByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default); + + /// + /// 按分组编码集合获取字典项(可包含系统参数)。 + /// + Task> GetItemsByCodesAsync(IEnumerable codes, Guid tenantId, bool includeSystem, CancellationToken cancellationToken = default); + + /// + /// 新增字典项。 + /// + Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default); + + /// + /// 删除字典项。 + /// + Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default); + + /// + /// 持久化更改。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs index 123208a..b47df34 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -1,11 +1,11 @@ -using System; +using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; /// -/// 后台账号实体(平台/商户/员工)。 +/// 管理后台账户实体(平台、租户或商户员工)。 /// -public sealed class IdentityUser +public sealed class IdentityUser : IMultiTenantEntity { /// /// 用户 ID。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs index 3c4fdf8..953e979 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs @@ -1,11 +1,11 @@ -using System; +using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; /// -/// 小程序用户。 +/// 小程序用户实体。 /// -public sealed class MiniUser +public sealed class MiniUser : IMultiTenantEntity { /// /// 用户 ID。 @@ -18,7 +18,7 @@ public sealed class MiniUser public string OpenId { get; set; } = string.Empty; /// - /// 微信 UnionId,可为空。 + /// 微信 UnionId,可能为空。 /// public string? UnionId { get; set; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs new file mode 100644 index 0000000..10bd6d5 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs @@ -0,0 +1,89 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Shared.Abstractions.Entities; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// 多租户感知 DbContext:自动应用租户过滤并填充租户字段。 +/// +public abstract class TenantAwareDbContext : DbContext +{ + private readonly ITenantProvider _tenantProvider; + + protected TenantAwareDbContext(DbContextOptions options, ITenantProvider tenantProvider) : base(options) + { + _tenantProvider = tenantProvider; + } + + /// + /// 当前请求租户 ID。 + /// + protected Guid CurrentTenantId => _tenantProvider.GetCurrentTenantId(); + + /// + /// 应用租户过滤器至所有实现 的实体。 + /// + protected void ApplyTenantQueryFilters(ModelBuilder modelBuilder) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (!typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType)) + { + continue; + } + + var methodInfo = typeof(TenantAwareDbContext) + .GetMethod(nameof(SetTenantFilter), BindingFlags.Instance | BindingFlags.NonPublic)! + .MakeGenericMethod(entityType.ClrType); + + methodInfo.Invoke(this, new object[] { modelBuilder }); + } + } + + private void SetTenantFilter(ModelBuilder modelBuilder) + where TEntity : class, IMultiTenantEntity + { + modelBuilder.Entity().HasQueryFilter(entity => entity.TenantId == CurrentTenantId); + } + + public override int SaveChanges() + { + ApplyTenantMetadata(); + return base.SaveChanges(); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + ApplyTenantMetadata(); + return base.SaveChangesAsync(cancellationToken); + } + + private void ApplyTenantMetadata() + { + var tenantId = CurrentTenantId; + + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added && entry.Entity.TenantId == Guid.Empty && tenantId != Guid.Empty) + { + entry.Entity.TenantId = tenantId; + } + } + + var utcNow = DateTime.UtcNow; + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedAt = utcNow; + entry.Entity.UpdatedAt = null; + } + else if (entry.State == EntityState.Modified) + { + entry.Entity.UpdatedAt = utcNow; + } + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs new file mode 100644 index 0000000..ad33dab --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Dictionary.Abstractions; +using TakeoutSaaS.Domain.Dictionary.Repositories; +using TakeoutSaaS.Infrastructure.Dictionary.Options; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; +using TakeoutSaaS.Infrastructure.Dictionary.Repositories; +using TakeoutSaaS.Infrastructure.Dictionary.Services; + +namespace TakeoutSaaS.Infrastructure.Dictionary.Extensions; + +/// +/// 字典基础设施注册扩展。 +/// +public static class DictionaryServiceCollectionExtensions +{ + /// + /// 注册字典模块基础设施。 + /// + public static IServiceCollection AddDictionaryInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("AppDatabase"); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException("缺少 AppDatabase 连接字符串配置"); + } + + services.AddDbContext(options => + { + options.UseNpgsql(connectionString); + }); + + services.AddScoped(); + services.AddScoped(); + + services.AddOptions() + .Bind(configuration.GetSection("Dictionary:Cache")) + .ValidateDataAnnotations(); + + return services; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Options/DictionaryCacheOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Options/DictionaryCacheOptions.cs new file mode 100644 index 0000000..c5df7a0 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Options/DictionaryCacheOptions.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Infrastructure.Dictionary.Options; + +/// +/// 字典缓存配置。 +/// +public sealed class DictionaryCacheOptions +{ + /// + /// 缓存滑动过期时间。 + /// + public TimeSpan SlidingExpiration { get; set; } = TimeSpan.FromMinutes(30); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs new file mode 100644 index 0000000..90351a8 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +/// +/// 参数字典 DbContext。 +/// +public sealed class DictionaryDbContext : TenantAwareDbContext +{ + public DictionaryDbContext(DbContextOptions options, ITenantProvider tenantProvider) + : base(options, tenantProvider) + { + } + + public DbSet DictionaryGroups => Set(); + public DbSet DictionaryItems => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + ConfigureGroup(modelBuilder.Entity()); + ConfigureItem(modelBuilder.Entity()); + ApplyTenantQueryFilters(modelBuilder); + } + + private static void ConfigureGroup(EntityTypeBuilder builder) + { + builder.ToTable("dictionary_groups"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Scope).HasConversion().IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.IsEnabled).HasDefaultValue(true); + builder.Property(x => x.CreatedAt).IsRequired(); + builder.Property(x => x.UpdatedAt); + + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigureItem(EntityTypeBuilder builder) + { + builder.ToTable("dictionary_items"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Key).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Value).HasMaxLength(256).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.SortOrder).HasDefaultValue(100); + builder.Property(x => x.IsEnabled).HasDefaultValue(true); + builder.Property(x => x.CreatedAt).IsRequired(); + builder.Property(x => x.UpdatedAt); + + builder.HasOne(x => x.Group) + .WithMany(g => g.Items) + .HasForeignKey(x => x.GroupId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasIndex(x => new { x.GroupId, x.Key }).IsUnique(); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs new file mode 100644 index 0000000..3bb86c1 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs @@ -0,0 +1,105 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; +using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.Dictionary.Enums; +using TakeoutSaaS.Domain.Dictionary.Repositories; + +namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories; + +/// +/// EF Core 字典仓储实现。 +/// +public sealed class EfDictionaryRepository : IDictionaryRepository +{ + private readonly DictionaryDbContext _context; + + public EfDictionaryRepository(DictionaryDbContext context) + { + _context = context; + } + + public Task FindGroupByIdAsync(Guid id, CancellationToken cancellationToken = default) + => _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); + + public Task FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default) + => _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken); + + public async Task> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default) + { + var query = _context.DictionaryGroups.AsNoTracking(); + if (scope.HasValue) + { + query = query.Where(group => group.Scope == scope.Value); + } + + return await query + .OrderBy(group => group.Code) + .ToListAsync(cancellationToken); + } + + public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) + { + _context.DictionaryGroups.Add(group); + return Task.CompletedTask; + } + + public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) + { + _context.DictionaryGroups.Remove(group); + return Task.CompletedTask; + } + + public Task FindItemByIdAsync(Guid id, CancellationToken cancellationToken = default) + => _context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); + + public async Task> GetItemsByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default) + { + return await _context.DictionaryItems + .AsNoTracking() + .Where(item => item.GroupId == groupId) + .OrderBy(item => item.SortOrder) + .ToListAsync(cancellationToken); + } + + public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) + { + _context.DictionaryItems.Add(item); + return Task.CompletedTask; + } + + public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) + { + _context.DictionaryItems.Remove(item); + return Task.CompletedTask; + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => _context.SaveChangesAsync(cancellationToken); + + public async Task> GetItemsByCodesAsync(IEnumerable codes, Guid tenantId, bool includeSystem, CancellationToken cancellationToken = default) + { + var normalizedCodes = codes + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => code.Trim().ToLowerInvariant()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (normalizedCodes.Length == 0) + { + return Array.Empty(); + } + + var query = _context.DictionaryItems + .AsNoTracking() + .IgnoreQueryFilters() + .Include(item => item.Group) + .Where(item => normalizedCodes.Contains(item.Group!.Code)); + + query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == Guid.Empty)); + + return await query + .OrderBy(item => item.SortOrder) + .ToListAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs new file mode 100644 index 0000000..9024674 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Application.Dictionary.Abstractions; +using TakeoutSaaS.Application.Dictionary.Models; +using TakeoutSaaS.Infrastructure.Dictionary.Options; + +namespace TakeoutSaaS.Infrastructure.Dictionary.Services; + +/// +/// 基于 IDistributedCache 的字典缓存实现。 +/// +public sealed class DistributedDictionaryCache : IDictionaryCache +{ + private readonly IDistributedCache _cache; + private readonly DictionaryCacheOptions _options; + private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); + + public DistributedDictionaryCache(IDistributedCache cache, IOptions options) + { + _cache = cache; + _options = options.Value; + } + + public async Task?> GetAsync(Guid tenantId, string code, CancellationToken cancellationToken = default) + { + var cacheKey = BuildKey(tenantId, code); + var payload = await _cache.GetAsync(cacheKey, cancellationToken); + if (payload == null || payload.Length == 0) + { + return null; + } + + return JsonSerializer.Deserialize>(payload, _serializerOptions); + } + + public Task SetAsync(Guid tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default) + { + var cacheKey = BuildKey(tenantId, code); + var payload = JsonSerializer.SerializeToUtf8Bytes(items, _serializerOptions); + var options = new DistributedCacheEntryOptions + { + SlidingExpiration = _options.SlidingExpiration + }; + return _cache.SetAsync(cacheKey, payload, options, cancellationToken); + } + + public Task RemoveAsync(Guid tenantId, string code, CancellationToken cancellationToken = default) + { + var cacheKey = BuildKey(tenantId, code); + return _cache.RemoveAsync(cacheKey, cancellationToken); + } + + private static string BuildKey(Guid tenantId, string code) + => $"dictionary:{tenantId.ToString().ToLowerInvariant()}:{code.ToLowerInvariant()}"; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index 57d88b6..f640da0 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Infrastructure.Identity.Options; +using TakeoutSaaS.Shared.Abstractions.Tenancy; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -20,6 +21,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger var context = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService>().Value; var passwordHasher = scope.ServiceProvider.GetRequiredService>(); + var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); await context.Database.MigrateAsync(cancellationToken); @@ -31,6 +33,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger foreach (var userOptions in options.Users) { + using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId); var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken); var roles = NormalizeValues(userOptions.Roles); var permissions = NormalizeValues(userOptions.Permissions); @@ -76,4 +79,17 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => v.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase)]; + + private static IDisposable EnterTenantScope(ITenantContextAccessor accessor, Guid tenantId) + { + var previous = accessor.Current; + accessor.Current = new TenantContext(tenantId, null, "admin-seed"); + return new Scope(() => accessor.Current = previous); + } + + private sealed class Scope(Action disposeAction) : IDisposable + { + private readonly Action _disposeAction = disposeAction; + public void Dispose() => _disposeAction(); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 9887201..d9b5b52 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -4,14 +4,20 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// -/// 身份认证 DbContext。 +/// 身份认证 DbContext,带多租户过滤。 /// -public sealed class IdentityDbContext(DbContextOptions options) : DbContext(options) +public sealed class IdentityDbContext : TenantAwareDbContext { + public IdentityDbContext(DbContextOptions options, ITenantProvider tenantProvider) + : base(options, tenantProvider) + { + } public DbSet IdentityUsers => Set(); public DbSet MiniUsers => Set(); @@ -20,6 +26,7 @@ public sealed class IdentityDbContext(DbContextOptions option { ConfigureIdentityUser(modelBuilder.Entity()); ConfigureMiniUser(modelBuilder.Entity()); + ApplyTenantQueryFilters(modelBuilder); } private static void ConfigureIdentityUser(EntityTypeBuilder builder) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs index cf9c8e9..78d3f0e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs @@ -69,7 +69,7 @@ public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptio /// /// 用户档案 /// Claims 集合 - private static IEnumerable BuildClaims(CurrentUserProfile profile) + private static List BuildClaims(CurrentUserProfile profile) { var userId = profile.UserId.ToString(); var claims = new List @@ -86,15 +86,9 @@ public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptio claims.Add(new Claim("merchant_id", profile.MerchantId.Value.ToString())); } - foreach (var role in profile.Roles) - { - claims.Add(new Claim(ClaimTypes.Role, role)); - } + claims.AddRange(profile.Roles.Select(role => new Claim(ClaimTypes.Role, role))); - foreach (var permission in profile.Permissions) - { - claims.Add(new Claim("permission", permission)); - } + claims.AddRange(profile.Permissions.Select(permission => new Claim("permission", permission))); return claims; } diff --git a/src/Modules/TakeoutSaaS.Module.Dictionary/Extensions/DictionaryModuleExtensions.cs b/src/Modules/TakeoutSaaS.Module.Dictionary/Extensions/DictionaryModuleExtensions.cs new file mode 100644 index 0000000..97334aa --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Dictionary/Extensions/DictionaryModuleExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Dictionary.Extensions; +using TakeoutSaaS.Infrastructure.Dictionary.Extensions; + +namespace TakeoutSaaS.Module.Dictionary.Extensions; + +/// +/// 字典模块服务扩展。 +/// +public static class DictionaryModuleExtensions +{ + /// + /// 注册字典模块应用层与基础设施。 + /// + public static IServiceCollection AddDictionaryModule(this IServiceCollection services, IConfiguration configuration) + { + services.AddDictionaryApplication(); + services.AddDictionaryInfrastructure(configuration); + return services; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj index b407eac..b03dcec 100644 --- a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj +++ b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj @@ -6,6 +6,7 @@ + + - diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs new file mode 100644 index 0000000..72afde3 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Module.Tenancy.Extensions; + +/// +/// 多租户服务注册及中间件扩展。 +/// +public static class TenantServiceCollectionExtensions +{ + /// + /// 注册租户上下文、解析中间件及默认租户提供者。 + /// + public static IServiceCollection AddTenantResolution(this IServiceCollection services, IConfiguration configuration) + { + services.TryAddSingleton(); + services.TryAddScoped(); + + services.AddOptions() + .Bind(configuration.GetSection("Tenancy")) + .ValidateDataAnnotations(); + + return services; + } + + /// + /// 使用多租户解析中间件。 + /// + public static IApplicationBuilder UseTenantResolution(this IApplicationBuilder app) + => app.UseMiddleware(); +} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs new file mode 100644 index 0000000..2a7f7ab --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs @@ -0,0 +1,34 @@ +using System.Threading; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Module.Tenancy; + +/// +/// 基于 的租户上下文访问器,实现请求级别隔离。 +/// +public sealed class TenantContextAccessor : ITenantContextAccessor +{ + private static readonly AsyncLocal Holder = new(); + + /// + public TenantContext? Current + { + get => Holder.Value?.Context; + set + { + if (Holder.Value != null) + { + Holder.Value.Context = value; + } + else if (value != null) + { + Holder.Value = new TenantContextHolder { Context = value }; + } + } + } + + private sealed class TenantContextHolder + { + public TenantContext? Context { get; set; } + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs index 41e74d1..1adf1d1 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs @@ -1,39 +1,24 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Module.Tenancy; /// -/// 默认租户提供者:优先从Header: X-Tenant-Id,其次从Token Claim: tenant_id +/// 默认租户提供者:基于租户上下文访问器暴露当前租户 ID。 /// public sealed class TenantProvider : ITenantProvider { - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ITenantContextAccessor _tenantContextAccessor; - public TenantProvider(IHttpContextAccessor httpContextAccessor) + /// + /// 初始化租户提供者。 + /// + /// 租户上下文访问器 + public TenantProvider(ITenantContextAccessor tenantContextAccessor) { - _httpContextAccessor = httpContextAccessor; + _tenantContextAccessor = tenantContextAccessor; } + /// public Guid GetCurrentTenantId() - { - var httpContext = _httpContextAccessor.HttpContext; - if (httpContext == null) return Guid.Empty; - - // 1. Header 优先 - if (httpContext.Request.Headers.TryGetValue("X-Tenant-Id", out var values)) - { - if (Guid.TryParse(values.FirstOrDefault(), out var headerTenant)) - return headerTenant; - } - - // 2. Token Claim - var claim = httpContext.User?.FindFirst("tenant_id"); - if (claim != null && Guid.TryParse(claim.Value, out var claimTenant)) - return claimTenant; - - return Guid.Empty; // 未识别到则返回空(上层可按需处理) - } + => _tenantContextAccessor.Current?.TenantId ?? Guid.Empty; } - diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs new file mode 100644 index 0000000..e7a2c2f --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs @@ -0,0 +1,191 @@ +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Module.Tenancy; + +/// +/// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。 +/// +public sealed class TenantResolutionMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly ITenantContextAccessor _tenantContextAccessor; + private readonly IOptionsMonitor _optionsMonitor; + + /// + /// 初始化中间件。 + /// + public TenantResolutionMiddleware( + RequestDelegate next, + ILogger logger, + ITenantContextAccessor tenantContextAccessor, + IOptionsMonitor optionsMonitor) + { + _next = next; + _logger = logger; + _tenantContextAccessor = tenantContextAccessor; + _optionsMonitor = optionsMonitor; + } + + /// + /// 解析租户并将上下文注入请求。 + /// + public async Task InvokeAsync(HttpContext context) + { + var options = _optionsMonitor.CurrentValue ?? new TenantResolutionOptions(); + if (ShouldSkip(context.Request.Path, options)) + { + await _next(context); + return; + } + + var tenantContext = ResolveTenant(context, options); + _tenantContextAccessor.Current = tenantContext; + context.Items[TenantConstants.HttpContextItemKey] = tenantContext; + + if (!tenantContext.IsResolved) + { + _logger.LogDebug("未能解析租户:{Path}", context.Request.Path); + + if (options.ThrowIfUnresolved) + { + var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识"); + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted); + _tenantContextAccessor.Current = null; + context.Items.Remove(TenantConstants.HttpContextItemKey); + return; + } + } + + try + { + await _next(context); + } + finally + { + _tenantContextAccessor.Current = null; + context.Items.Remove(TenantConstants.HttpContextItemKey); + } + } + + private static bool ShouldSkip(PathString path, TenantResolutionOptions options) + { + if (!path.HasValue) + { + return false; + } + + var value = path.Value ?? string.Empty; + if (options.IgnoredPaths.Contains(value)) + { + return true; + } + + return options.IgnoredPaths.Any(ignore => + { + if (string.IsNullOrWhiteSpace(ignore)) + { + return false; + } + + var ignorePath = new PathString(ignore); + return path.StartsWithSegments(ignorePath); + }); + } + + private TenantContext ResolveTenant(HttpContext context, TenantResolutionOptions options) + { + var request = context.Request; + + // 1. Header 中的租户 ID + if (!string.IsNullOrWhiteSpace(options.TenantIdHeaderName) && + request.Headers.TryGetValue(options.TenantIdHeaderName, out var tenantHeader) && + Guid.TryParse(tenantHeader.FirstOrDefault(), out var headerTenantId)) + { + return new TenantContext(headerTenantId, null, $"header:{options.TenantIdHeaderName}"); + } + + // 2. Header 中的租户编码 + if (!string.IsNullOrWhiteSpace(options.TenantCodeHeaderName) && + request.Headers.TryGetValue(options.TenantCodeHeaderName, out var codeHeader)) + { + var code = codeHeader.FirstOrDefault(); + if (TryResolveByCode(code, options, out var tenantFromCode)) + { + return new TenantContext(tenantFromCode, code, $"header:{options.TenantCodeHeaderName}"); + } + } + + // 3. Host 映射/子域名解析 + var host = request.Host.Host; + if (!string.IsNullOrWhiteSpace(host)) + { + if (options.DomainTenantMap.TryGetValue(host, out var tenantFromHost)) + { + return new TenantContext(tenantFromHost, null, $"host:{host}"); + } + + var codeFromHost = ResolveCodeFromHost(host, options.RootDomain); + if (TryResolveByCode(codeFromHost, options, out var tenantFromSubdomain)) + { + return new TenantContext(tenantFromSubdomain, codeFromHost, $"host:{host}"); + } + } + + // 4. Token Claim + var claim = context.User?.FindFirst("tenant_id"); + if (claim != null && Guid.TryParse(claim.Value, out var claimTenant)) + { + return new TenantContext(claimTenant, null, "claim:tenant_id"); + } + + return TenantContext.Empty; + } + + private static bool TryResolveByCode(string? code, TenantResolutionOptions options, out Guid tenantId) + { + tenantId = Guid.Empty; + if (string.IsNullOrWhiteSpace(code)) + { + return false; + } + + return options.CodeTenantMap.TryGetValue(code, out tenantId); + } + + private static string? ResolveCodeFromHost(string host, string? rootDomain) + { + if (string.IsNullOrWhiteSpace(rootDomain)) + { + return null; + } + + var normalizedRoot = rootDomain.TrimStart('.'); + if (!host.EndsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var suffixLength = normalizedRoot.Length; + if (host.Length <= suffixLength) + { + return null; + } + + var withoutRoot = host[..(host.Length - suffixLength)]; + if (withoutRoot.EndsWith('.')) + { + withoutRoot = withoutRoot[..^1]; + } + + var segments = withoutRoot.Split('.', StringSplitOptions.RemoveEmptyEntries); + return segments.Length == 0 ? null : segments[0]; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs new file mode 100644 index 0000000..8e0c5ff --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs @@ -0,0 +1,56 @@ +using System.Collections.ObjectModel; + +namespace TakeoutSaaS.Module.Tenancy; + +/// +/// 多租户解析配置项。 +/// +public sealed class TenantResolutionOptions +{ + /// + /// 通过 Header 解析租户 ID 时使用的头名称,默认 X-Tenant-Id。 + /// + public string TenantIdHeaderName { get; set; } = "X-Tenant-Id"; + + /// + /// 通过 Header 解析租户编码时使用的头名称,默认 X-Tenant-Code。 + /// + public string TenantCodeHeaderName { get; set; } = "X-Tenant-Code"; + + /// + /// 明确指定 host 与租户 ID 对应关系的映射表(精确匹配)。 + /// + public IDictionary DomainTenantMap { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// 租户编码到租户 ID 的映射表,用于 header 或子域名解析。 + /// + public IDictionary CodeTenantMap { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// 根域(不含子域),用于形如 {tenant}.rootDomain 的场景,例如 admin.takeoutsaas.com。 + /// + public string? RootDomain { get; set; } + + /// + /// 需要跳过租户解析的路径集合(如健康检查),默认仅包含 /health。 + /// + public ISet IgnoredPaths { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { "/health" }; + + /// + /// 若为 true,当无法解析租户时立即返回 400;否则交由上层自行判定。 + /// + public bool ThrowIfUnresolved { get; set; } + + /// + /// 对外只读视图,便于审计日志输出。 + /// + public IReadOnlyDictionary DomainMappings => new ReadOnlyDictionary(DomainTenantMap); + + /// + /// 对外只读的编码映射。 + /// + public IReadOnlyDictionary CodeMappings => new ReadOnlyDictionary(CodeTenantMap); +} From ae273e510a210837f24d5daef89de92e8633fa38 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sun, 23 Nov 2025 18:53:12 +0800 Subject: [PATCH 07/56] feat: finalize core modules and gateway --- Document/10_TODO.md | 30 +- Document/11_下一步TODO.md | 49 +++ .../Controllers/FilesController.cs | 52 ++++ src/Api/TakeoutSaaS.AdminApi/Program.cs | 15 + .../TakeoutSaaS.AdminApi.csproj | 4 + .../appsettings.Development.json | 142 +++++++++ .../Controllers/FilesController.cs | 52 ++++ src/Api/TakeoutSaaS.MiniApi/Program.cs | 12 + .../TakeoutSaaS.MiniApi.csproj | 3 + .../appsettings.Development.json | 133 +++++++++ .../appsettings.Development.json | 52 ++++ .../Messaging/Abstractions/IEventPublisher.cs | 15 + .../Messaging/EventRoutingKeys.cs | 17 ++ .../Messaging/Events/OrderCreatedEvent.cs | 32 ++ .../Messaging/Events/PaymentSucceededEvent.cs | 32 ++ .../MessagingServiceCollectionExtensions.cs | 20 ++ .../Messaging/Services/EventPublisher.cs | 16 + .../Abstractions/IVerificationCodeService.cs | 21 ++ .../Contracts/SendVerificationCodeRequest.cs | 34 +++ .../Contracts/SendVerificationCodeResponse.cs | 19 ++ .../VerifyVerificationCodeRequest.cs | 32 ++ .../SmsServiceCollectionExtensions.cs | 27 ++ .../Sms/Options/VerificationCodeOptions.cs | 33 +++ .../Sms/Services/VerificationCodeService.cs | 148 ++++++++++ .../Abstractions/IFileStorageService.cs | 21 ++ .../Storage/Contracts/DirectUploadRequest.cs | 46 +++ .../Storage/Contracts/DirectUploadResponse.cs | 35 +++ .../Storage/Contracts/FileUploadResponse.cs | 22 ++ .../Storage/Contracts/UploadFileRequest.cs | 59 ++++ .../Storage/Enums/UploadFileType.cs | 32 ++ .../StorageServiceCollectionExtensions.cs | 20 ++ .../Extensions/UploadFileTypeParser.cs | 46 +++ .../Storage/Services/FileStorageService.cs | 278 ++++++++++++++++++ .../TakeoutSaaS.Application.csproj | 4 + .../Constants/DatabaseConstants.cs | 17 ++ .../Data/DatabaseConnectionRole.cs | 17 ++ .../Data/IDapperExecutor.cs | 47 +++ .../Entities/AuditableEntityBase.cs | 37 +++ .../Entities/EntityBase.cs | 12 + .../Entities/IAuditableEntity.cs | 25 +- .../Entities/ISoftDeleteEntity.cs | 12 + .../Entities/MultiTenantEntityBase.cs | 12 + .../Security/ICurrentUserAccessor.cs | 17 ++ .../Extensions/ServiceCollectionExtensions.cs | 3 + .../HttpContextCurrentUserAccessor.cs | 42 +++ .../Dictionary/Entities/DictionaryGroup.cs | 24 +- .../Dictionary/Entities/DictionaryItem.cs | 22 +- .../Identity/Entities/IdentityUser.cs | 14 +- .../Identity/Entities/MiniUser.cs | 12 +- src/Gateway/TakeoutSaaS.ApiGateway/Program.cs | 153 ++++++---- .../appsettings.Development.json | 38 +++ .../DatabaseServiceCollectionExtensions.cs | 86 ++++++ .../Options/DatabaseDataSourceOptions.cs | 38 +++ .../Common/Options/DatabaseOptions.cs | 33 +++ .../Common/Persistence/AppDbContext.cs | 180 ++++++++++++ .../Common/Persistence/DapperExecutor.cs | 80 +++++ .../Persistence/DatabaseConnectionDetails.cs | 10 + .../Persistence/DatabaseConnectionFactory.cs | 121 ++++++++ .../Persistence/IDatabaseConnectionFactory.cs | 17 ++ .../Persistence/TenantAwareDbContext.cs | 59 ++-- .../DictionaryServiceCollectionExtensions.cs | 31 +- .../Persistence/DictionaryDbContext.cs | 45 ++- .../Extensions/ServiceCollectionExtensions.cs | 40 ++- .../Identity/Persistence/IdentityDbContext.cs | 46 ++- .../TakeoutSaaS.Infrastructure.csproj | 2 +- .../Abstractions/IMessagePublisher.cs | 15 + .../Abstractions/IMessageSubscriber.cs | 16 + .../MessagingServiceCollectionExtensions.cs | 32 ++ .../Options/RabbitMqOptions.cs | 55 ++++ .../Serialization/JsonMessageSerializer.cs | 22 ++ .../Services/RabbitMqConnectionFactory.cs | 30 ++ .../Services/RabbitMqMessagePublisher.cs | 66 +++++ .../Services/RabbitMqMessageSubscriber.cs | 92 ++++++ .../TakeoutSaaS.Module.Messaging.csproj | 7 +- .../Abstractions/IRecurringJobRegistrar.cs | 15 + .../SchedulerServiceCollectionExtensions.cs | 68 +++++ .../RecurringJobHostedService.cs | 21 ++ .../Jobs/CouponExpireJob.cs | 18 ++ .../Jobs/LogCleanupJob.cs | 18 ++ .../Jobs/OrderTimeoutJob.cs | 18 ++ .../Options/SchedulerOptions.cs | 31 ++ .../Services/RecurringJobRegistrar.cs | 37 +++ .../TakeoutSaaS.Module.Scheduler.csproj | 10 +- .../Abstractions/ISmsSender.cs | 21 ++ .../Abstractions/ISmsSenderResolver.cs | 12 + .../SmsServiceCollectionExtensions.cs | 33 +++ .../Models/SmsSendRequest.cs | 44 +++ .../Models/SmsSendResult.cs | 22 ++ .../Options/AliyunSmsOptions.cs | 36 +++ .../Options/SmsOptions.cs | 43 +++ .../Options/TencentSmsOptions.cs | 42 +++ .../Services/AliyunSmsSender.cs | 35 +++ .../Services/SmsSenderResolver.cs | 28 ++ .../Services/TencentSmsSender.cs | 136 +++++++++ .../TakeoutSaaS.Module.Sms/SmsProviderKind.cs | 17 ++ .../TakeoutSaaS.Module.Sms.csproj | 10 +- .../Abstractions/IObjectStorageProvider.cs | 36 +++ .../Abstractions/IStorageProviderResolver.cs | 14 + .../StorageServiceCollectionExtensions.cs | 34 +++ .../Models/StorageDirectUploadRequest.cs | 42 +++ .../Models/StorageDirectUploadResult.cs | 35 +++ .../Models/StorageUploadRequest.cs | 75 +++++ .../Models/StorageUploadResult.cs | 32 ++ .../Options/AliyunOssOptions.cs | 45 +++ .../Options/QiniuKodoOptions.cs | 50 ++++ .../Options/StorageOptions.cs | 44 +++ .../Options/StorageSecurityOptions.cs | 48 +++ .../Options/TencentCosOptions.cs | 54 ++++ .../Providers/AliyunOssStorageProvider.cs | 160 ++++++++++ .../Providers/QiniuKodoStorageProvider.cs | 53 ++++ .../Providers/S3StorageProviderBase.cs | 193 ++++++++++++ .../Providers/TencentCosStorageProvider.cs | 46 +++ .../Services/StorageProviderResolver.cs | 30 ++ .../StorageProviderKind.cs | 22 ++ .../TakeoutSaaS.Module.Storage.csproj | 10 +- 115 files changed, 4695 insertions(+), 223 deletions(-) create mode 100644 Document/11_下一步TODO.md create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json create mode 100644 src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs create mode 100644 src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json create mode 100644 src/Api/TakeoutSaaS.UserApi/appsettings.Development.json create mode 100644 src/Application/TakeoutSaaS.Application/Messaging/Abstractions/IEventPublisher.cs create mode 100644 src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs create mode 100644 src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs create mode 100644 src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs create mode 100644 src/Application/TakeoutSaaS.Application/Messaging/Extensions/MessagingServiceCollectionExtensions.cs create mode 100644 src/Application/TakeoutSaaS.Application/Messaging/Services/EventPublisher.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs create mode 100644 src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Abstractions/IFileStorageService.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadResponse.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Contracts/FileUploadResponse.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Enums/UploadFileType.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Extensions/StorageServiceCollectionExtensions.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Extensions/UploadFileTypeParser.cs create mode 100644 src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseDataSourceOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DapperExecutor.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionDetails.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/IDatabaseConnectionFactory.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessagePublisher.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessageSubscriber.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Extensions/MessagingServiceCollectionExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Options/RabbitMqOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Serialization/JsonMessageSerializer.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Abstractions/IRecurringJobRegistrar.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/HostedServices/RecurringJobHostedService.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/CouponExpireJob.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/LogCleanupJob.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/OrderTimeoutJob.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Options/SchedulerOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSender.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSenderResolver.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Extensions/SmsServiceCollectionExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendRequest.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendResult.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Options/AliyunSmsOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Options/SmsOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Options/TencentSmsOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Services/AliyunSmsSender.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Services/SmsSenderResolver.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/Services/TencentSmsSender.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Sms/SmsProviderKind.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IObjectStorageProvider.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IStorageProviderResolver.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Extensions/StorageServiceCollectionExtensions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadResult.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadResult.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Options/AliyunOssOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Options/QiniuKodoOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Options/StorageOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Options/StorageSecurityOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Options/TencentCosOptions.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Providers/AliyunOssStorageProvider.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Providers/QiniuKodoStorageProvider.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Providers/TencentCosStorageProvider.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/Services/StorageProviderResolver.cs create mode 100644 src/Modules/TakeoutSaaS.Module.Storage/StorageProviderKind.cs diff --git a/Document/10_TODO.md b/Document/10_TODO.md index ddb4c41..31f7d26 100644 --- a/Document/10_TODO.md +++ b/Document/10_TODO.md @@ -22,24 +22,24 @@ - [x] 参数字典模块(系统参数/业务参数)CRUD 与缓存(Dictionary 模块) ## D. 数据访问与多数据源 -- [ ] EF Core 10 基础上下文、实体基类、审计字段 -- [ ] 读写分离/多数据源配置(主写、从读;或按租户切库预留) -- [ ] Dapper 基础设施封装(统计/报表类查询) +- [x] EF Core 10 基础上下文、实体基类、审计字段 +- [x] 读写分离/多数据源配置(主写、从读) +- [x] Dapper 基础设施封装(统计/报表类查询) ## E. 文件与存储 -- [ ] 存储模块抽象(本地/MinIO/云厂商适配) -- [ ] 上传接口(AdminApi、MiniApi)与签名直传预留 -- [ ] 图片/文件访问安全策略(防盗链、过期签名) +- [x] 存储模块抽象(腾讯云COS/七牛云/阿里云OSS) +- [x] 上传接口(AdminApi、MiniApi)与签名直传预留 +- [x] 图片/文件访问安全策略(防盗链、过期签名) ## F. 短信与消息队列 -- [ ] 短信模块(阿里云/腾讯云 适配占位)与验证码发送 -- [ ] MQ 模块(RabbitMQ)Publisher/Subscriber 抽象 -- [ ] 业务事件定义(订单创建/支付成功等)与事件发布入口 +- [x] 短信模块(阿里云/腾讯云 适配占位)与验证码发送 +- [x] MQ 模块(RabbitMQ)Publisher/Subscriber 抽象 +- [x] 业务事件定义(订单创建/支付成功等)与事件发布入口 ## G. 调度与定时任务 -- [ ] 调度模块(Quartz/Hangfire 二选一,默认 Hangfire) -- [ ] 基础任务:订单超时取消、优惠券过期处理、日志清理 -- [ ] 调度面板(后续 AdminUI 对接) +- [x] 调度模块(Quartz/Hangfire 二选一,默认 Hangfire) +- [x] 基础任务:订单超时取消、优惠券过期处理、日志清理 +- [x] 调度面板(后续 AdminUI 对接) ## H. 第三方配送对接(仅第三方) - [ ] 配送适配抽象(达达/闪送/顺丰同城等) @@ -47,9 +47,9 @@ - [ ] AdminApi 后台运力单查询与补单 ## I. 网关与横切能力 -- [ ] YARP 路由拆分(/api/admin、/api/mini、/api/user) -- [ ] 网关级限流与请求日志 -- [ ] 透传鉴权/租户标识与统一错误页 +- [x] YARP 路由拆分(/api/admin、/api/mini、/api/user) +- [x] 网关级限流与请求日志 +- [x] 透传鉴权/租户标识与统一错误页 ## J. 测试与质量 - [ ] 单元测试工程骨架(xUnit + FluentAssertions) diff --git a/Document/11_下一步TODO.md b/Document/11_下一步TODO.md new file mode 100644 index 0000000..d812c4d --- /dev/null +++ b/Document/11_下一步TODO.md @@ -0,0 +1,49 @@ +# 下一步 TODO(骨架完成后) + +说明:当前骨架已覆盖认证、权限、多租户、存储、短信、MQ、调度、网关等基础能力。下面的清单用于进入“可运行/可上线”的补全与质量阶段,可按优先级推进。 + +## 1. 配置与基础设施落地(高优) +- 补充真实配置:数据库/Redis/RabbitMQ/对象存储/SMS/WeChat Mini/身份密钥,并分环境管理(Development/Staging/Production)。 +- 准备基础设施:PostgreSQL 主从、Redis(哨兵/集群)、RabbitMQ、COS/OSS、Hangfire 存储库;完善 docker-compose 与部署说明。 +- 网关与服务域名规划:为 admin/mini/user/gateway 配置实际域名、TLS 证书与 CORS 列表。 +- Hangfire Dashboard 鉴权:开启并加上 Admin 角色校验或网关白名单。 + +## 2. 数据与迁移(高优) +- 建立 EF Core Migration 基线并生成数据库(App/Identity/Dictionary/Hangfire)。 +- 设计并落地核心业务表(商户/门店/商品/订单/支付/配送等),补齐 Domain 与 Infrastructure 仓储。 +- 数据初始化/种子:系统参数、默认租户、管理员、基础字典。 + +## 3. 质量与测试(高优) +- 单元测试骨架:xUnit + FluentAssertions(Dictionary、Identity、Storage、Sms、Messaging、Scheduler)。 +- 集成测试基座:WebApplicationFactory + Testcontainers(Postgres/Redis/RabbitMQ/MinIO 可选)。 +- 静态分析:添加 .editorconfig/.globalconfig,启用可空警告、风格规则,接入 Roslyn 分析器。 + +## 4. 安全与合规 +- 完善鉴权:网关透传与后端校验的租户/用户/权限;Swagger 鉴权示例。 +- 输入校验与防刷:全局限流策略(按 IP/租户),登录与验证码防刷策略参数化。 +- 日志与审计:敏感字段脱敏,登录/权限/管理操作审计日志模型与落库。 +- 配置机密:使用 Secret Store/环境变量/KMS 管理密钥,禁止明文提交。 + +## 5. 可观测性与运维 +- 日志链路:统一 TraceId 透传(网关→服务),配置 Serilog 输出(Console/File/ELK)与留存策略。 +- 指标/监控:Prometheus exporter、健康检查探针(/health)、告警规则草案。 +- 备份恢复:PostgreSQL 全量/增量备份脚本,恢复演练记录。 + +## 6. 业务功能补全 +- 订单/商品/商户等领域建模与应用服务接口实现,结合 MQ 事件发布(订单创建、支付成功等)。 +- 配送对接抽象实现(达达/闪送/顺丰同城)占位,提供下单/取消/查询接口与回调验签。 +- 小程序端接口补齐:商品浏览、下单、支付、评价、上传图片直传联调。 + +## 7. 前台/后台 UI 对接 +- Admin UI:接入 Swagger 导出的 OpenAPI,生成或手写管理端界面;接入 Hangfire Dashboard/MQ 监控只读访问。 +- MiniApp:小程序登录流程与错误码文档完善,联调上传、下单、支付链路。 + +## 8. CI/CD 与发布 +- 建立流水线:构建/测试/扫描(SAST)、镜像推送、数据库迁移步骤。 +- 多环境部署策略:Dev/Staging/Prod 配置隔离,蓝绿或滚动发布方案草拟。 +- 版本与变更管理:约定版本号/发布说明模板。 + +## 9. 文档补全 +- 更新接口文档(新增业务 API、错误码、回调规范)、模块依赖关系图。 +- 运维手册:启动参数、环境变量列表、端口/域名映射、常见故障排查。 +- 安全与合规清单:数据分类分级、审计、留存周期。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs new file mode 100644 index 0000000..f53d344 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs @@ -0,0 +1,52 @@ +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Storage.Abstractions; +using TakeoutSaaS.Application.Storage.Contracts; +using TakeoutSaaS.Application.Storage.Extensions; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 管理后台文件上传。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/files")] +public sealed class FilesController(IFileStorageService fileStorageService) : BaseApiController +{ + private readonly IFileStorageService _fileStorageService = fileStorageService; + + /// + /// 上传图片或文件。 + /// + [HttpPost("upload")] + [RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + public async Task> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken) + { + if (file == null || file.Length == 0) + { + return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空"); + } + + if (!UploadFileTypeParser.TryParse(type, out var uploadType)) + { + return ApiResponse.Error(ErrorCodes.BadRequest, "上传类型不合法"); + } + + var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault(); + await using var stream = file.OpenReadStream(); + + var result = await _fileStorageService.UploadAsync( + new UploadFileRequest(uploadType, stream, file.FileName, file.ContentType ?? string.Empty, file.Length, origin), + cancellationToken); + + return ApiResponse.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index c506c0c..91a6c72 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -7,9 +7,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using TakeoutSaaS.Application.Identity.Extensions; +using TakeoutSaaS.Application.Messaging.Extensions; +using TakeoutSaaS.Application.Sms.Extensions; +using TakeoutSaaS.Application.Storage.Extensions; using TakeoutSaaS.Infrastructure.Identity.Extensions; using TakeoutSaaS.Module.Authorization.Extensions; using TakeoutSaaS.Module.Dictionary.Extensions; +using TakeoutSaaS.Module.Messaging.Extensions; +using TakeoutSaaS.Module.Scheduler.Extensions; +using TakeoutSaaS.Module.Sms.Extensions; +using TakeoutSaaS.Module.Storage.Extensions; using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -38,6 +45,13 @@ builder.Services.AddAuthorization(); builder.Services.AddPermissionAuthorization(); builder.Services.AddTenantResolution(builder.Configuration); builder.Services.AddDictionaryModule(builder.Configuration); +builder.Services.AddStorageModule(builder.Configuration); +builder.Services.AddStorageApplication(); +builder.Services.AddSmsModule(builder.Configuration); +builder.Services.AddSmsApplication(builder.Configuration); +builder.Services.AddMessagingModule(builder.Configuration); +builder.Services.AddMessagingApplication(); +builder.Services.AddSchedulerModule(builder.Configuration); var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin"); builder.Services.AddCors(options => @@ -56,6 +70,7 @@ app.UseSharedWebCore(); app.UseAuthentication(); app.UseAuthorization(); app.UseSharedSwagger(); +app.UseSchedulerDashboard(builder.Configuration); app.MapControllers(); app.Run(); diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index c1f701a..8a896da 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json new file mode 100644 index 0000000..cbdcf16 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -0,0 +1,142 @@ +{ + "Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "IdentityDatabase": { + "Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + } + } + }, + "Redis": "localhost:6379,abortConnect=false", + "Identity": { + "Jwt": { + "Issuer": "takeout-saas", + "Audience": "takeout-saas-clients", + "Secret": "ReplaceWithA32CharLongSecretKey_____", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationMinutes": 10080 + }, + "LoginRateLimit": { + "WindowSeconds": 60, + "MaxAttempts": 5 + }, + "RefreshTokenStore": { + "Prefix": "identity:refresh:" + }, + "AdminSeed": { + "Users": [] + } + }, + "Dictionary": { + "Cache": { + "SlidingExpiration": "00:30:00" + } + }, + "Tenancy": { + "TenantIdHeaderName": "X-Tenant-Id", + "TenantCodeHeaderName": "X-Tenant-Code", + "IgnoredPaths": [ "/health" ], + "RootDomain": "" + }, + "Storage": { + "Provider": "TencentCos", + "CdnBaseUrl": "https://cdn.example.com", + "TencentCos": { + "SecretId": "COS_SECRET_ID", + "SecretKey": "COS_SECRET_KEY", + "Region": "ap-guangzhou", + "Bucket": "takeout-bucket-123456", + "Endpoint": "", + "CdnBaseUrl": "https://cdn.example.com", + "UseHttps": true, + "ForcePathStyle": false + }, + "QiniuKodo": { + "AccessKey": "QINIU_ACCESS_KEY", + "SecretKey": "QINIU_SECRET_KEY", + "Bucket": "takeout-files", + "DownloadDomain": "", + "Endpoint": "", + "UseHttps": true, + "SignedUrlExpirationMinutes": 30 + }, + "AliyunOss": { + "AccessKeyId": "OSS_ACCESS_KEY_ID", + "AccessKeySecret": "OSS_ACCESS_KEY_SECRET", + "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", + "Bucket": "takeout-files", + "CdnBaseUrl": "", + "UseHttps": true + }, + "Security": { + "MaxFileSizeBytes": 10485760, + "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], + "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "DefaultUrlExpirationMinutes": 30, + "EnableRefererValidation": true, + "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AntiLeechTokenSecret": "ReplaceWithARandomToken" + } + }, + "Sms": { + "Provider": "Tencent", + "DefaultSignName": "外卖SaaS", + "UseMock": true, + "Tencent": { + "SecretId": "TENCENT_SMS_SECRET_ID", + "SecretKey": "TENCENT_SMS_SECRET_KEY", + "SdkAppId": "1400000000", + "SignName": "外卖SaaS", + "Region": "ap-guangzhou", + "Endpoint": "https://sms.tencentcloudapi.com" + }, + "Aliyun": { + "AccessKeyId": "ALIYUN_SMS_AK", + "AccessKeySecret": "ALIYUN_SMS_SK", + "Endpoint": "dysmsapi.aliyuncs.com", + "SignName": "外卖SaaS", + "Region": "cn-hangzhou" + }, + "SceneTemplates": { + "login": "LOGIN_TEMPLATE_ID", + "register": "REGISTER_TEMPLATE_ID", + "reset": "RESET_TEMPLATE_ID" + }, + "VerificationCode": { + "CodeLength": 6, + "ExpireMinutes": 5, + "CooldownSeconds": 60, + "CachePrefix": "sms:code" + } + }, + "RabbitMQ": { + "Host": "localhost", + "Port": 5672, + "Username": "admin", + "Password": "password", + "VirtualHost": "/", + "Exchange": "takeout.events", + "ExchangeType": "topic", + "PrefetchCount": 20 + }, + "Scheduler": { + "ConnectionString": "Host=localhost;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "WorkerCount": 5, + "DashboardEnabled": false, + "DashboardPath": "/hangfire" + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs new file mode 100644 index 0000000..a795c9e --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs @@ -0,0 +1,52 @@ +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Storage.Abstractions; +using TakeoutSaaS.Application.Storage.Contracts; +using TakeoutSaaS.Application.Storage.Extensions; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.MiniApi.Controllers; + +/// +/// 小程序文件上传。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/mini/v{version:apiVersion}/files")] +public sealed class FilesController(IFileStorageService fileStorageService) : BaseApiController +{ + private readonly IFileStorageService _fileStorageService = fileStorageService; + + /// + /// 上传图片或文件。 + /// + [HttpPost("upload")] + [RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + public async Task> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken) + { + if (file == null || file.Length == 0) + { + return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空"); + } + + if (!UploadFileTypeParser.TryParse(type, out var uploadType)) + { + return ApiResponse.Error(ErrorCodes.BadRequest, "上传类型不合法"); + } + + var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault(); + await using var stream = file.OpenReadStream(); + + var result = await _fileStorageService.UploadAsync( + new UploadFileRequest(uploadType, stream, file.FileName, file.ContentType ?? string.Empty, file.Length, origin), + cancellationToken); + + return ApiResponse.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index fd0ebd4..5dd9962 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -1,5 +1,11 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Serilog; +using TakeoutSaaS.Application.Messaging.Extensions; +using TakeoutSaaS.Application.Sms.Extensions; +using TakeoutSaaS.Application.Storage.Extensions; +using TakeoutSaaS.Module.Messaging.Extensions; +using TakeoutSaaS.Module.Sms.Extensions; +using TakeoutSaaS.Module.Storage.Extensions; using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -22,6 +28,12 @@ builder.Services.AddSharedSwagger(options => options.EnableAuthorization = true; }); builder.Services.AddTenantResolution(builder.Configuration); +builder.Services.AddStorageModule(builder.Configuration); +builder.Services.AddStorageApplication(); +builder.Services.AddSmsModule(builder.Configuration); +builder.Services.AddSmsApplication(builder.Configuration); +builder.Services.AddMessagingModule(builder.Configuration); +builder.Services.AddMessagingApplication(); var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini"); builder.Services.AddCors(options => diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index 4a352b2..2269e2e 100644 --- a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj +++ b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj @@ -13,6 +13,9 @@ + + + diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json new file mode 100644 index 0000000..1549e0a --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json @@ -0,0 +1,133 @@ +{ + "Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "IdentityDatabase": { + "Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + } + } + }, + "Redis": "localhost:6379,abortConnect=false", + "Identity": { + "Jwt": { + "Issuer": "takeout-saas", + "Audience": "takeout-saas-clients", + "Secret": "ReplaceWithA32CharLongSecretKey_____", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationMinutes": 10080 + }, + "LoginRateLimit": { + "WindowSeconds": 60, + "MaxAttempts": 5 + }, + "RefreshTokenStore": { + "Prefix": "identity:refresh:" + } + }, + "Dictionary": { + "Cache": { + "SlidingExpiration": "00:30:00" + } + }, + "Tenancy": { + "TenantIdHeaderName": "X-Tenant-Id", + "TenantCodeHeaderName": "X-Tenant-Code", + "IgnoredPaths": [ "/health" ], + "RootDomain": "" + }, + "Storage": { + "Provider": "TencentCos", + "CdnBaseUrl": "https://cdn.example.com", + "TencentCos": { + "SecretId": "COS_SECRET_ID", + "SecretKey": "COS_SECRET_KEY", + "Region": "ap-guangzhou", + "Bucket": "takeout-bucket-123456", + "Endpoint": "", + "CdnBaseUrl": "https://cdn.example.com", + "UseHttps": true, + "ForcePathStyle": false + }, + "QiniuKodo": { + "AccessKey": "QINIU_ACCESS_KEY", + "SecretKey": "QINIU_SECRET_KEY", + "Bucket": "takeout-files", + "DownloadDomain": "", + "Endpoint": "", + "UseHttps": true, + "SignedUrlExpirationMinutes": 30 + }, + "AliyunOss": { + "AccessKeyId": "OSS_ACCESS_KEY_ID", + "AccessKeySecret": "OSS_ACCESS_KEY_SECRET", + "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", + "Bucket": "takeout-files", + "CdnBaseUrl": "", + "UseHttps": true + }, + "Security": { + "MaxFileSizeBytes": 10485760, + "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], + "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "DefaultUrlExpirationMinutes": 30, + "EnableRefererValidation": true, + "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AntiLeechTokenSecret": "ReplaceWithARandomToken" + } + }, + "Sms": { + "Provider": "Tencent", + "DefaultSignName": "外卖SaaS", + "UseMock": true, + "Tencent": { + "SecretId": "TENCENT_SMS_SECRET_ID", + "SecretKey": "TENCENT_SMS_SECRET_KEY", + "SdkAppId": "1400000000", + "SignName": "外卖SaaS", + "Region": "ap-guangzhou", + "Endpoint": "https://sms.tencentcloudapi.com" + }, + "Aliyun": { + "AccessKeyId": "ALIYUN_SMS_AK", + "AccessKeySecret": "ALIYUN_SMS_SK", + "Endpoint": "dysmsapi.aliyuncs.com", + "SignName": "外卖SaaS", + "Region": "cn-hangzhou" + }, + "SceneTemplates": { + "login": "LOGIN_TEMPLATE_ID", + "register": "REGISTER_TEMPLATE_ID", + "reset": "RESET_TEMPLATE_ID" + }, + "VerificationCode": { + "CodeLength": 6, + "ExpireMinutes": 5, + "CooldownSeconds": 60, + "CachePrefix": "sms:code" + } + }, + "RabbitMQ": { + "Host": "localhost", + "Port": 5672, + "Username": "admin", + "Password": "password", + "VirtualHost": "/", + "Exchange": "takeout.events", + "ExchangeType": "topic", + "PrefetchCount": 20 + } +} diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json new file mode 100644 index 0000000..bf4d9de --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json @@ -0,0 +1,52 @@ +{ + "Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "IdentityDatabase": { + "Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + } + } + }, + "Redis": "localhost:6379,abortConnect=false", + "Identity": { + "Jwt": { + "Issuer": "takeout-saas", + "Audience": "takeout-saas-clients", + "Secret": "ReplaceWithA32CharLongSecretKey_____", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationMinutes": 10080 + }, + "LoginRateLimit": { + "WindowSeconds": 60, + "MaxAttempts": 5 + }, + "RefreshTokenStore": { + "Prefix": "identity:refresh:" + } + }, + "Dictionary": { + "Cache": { + "SlidingExpiration": "00:30:00" + } + }, + "Tenancy": { + "TenantIdHeaderName": "X-Tenant-Id", + "TenantCodeHeaderName": "X-Tenant-Code", + "IgnoredPaths": [ "/health" ], + "RootDomain": "" + } +} diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Abstractions/IEventPublisher.cs b/src/Application/TakeoutSaaS.Application/Messaging/Abstractions/IEventPublisher.cs new file mode 100644 index 0000000..b471f0a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Messaging/Abstractions/IEventPublisher.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace TakeoutSaaS.Application.Messaging.Abstractions; + +/// +/// 领域事件发布抽象。 +/// +public interface IEventPublisher +{ + /// + /// 发布领域事件。 + /// + Task PublishAsync(string routingKey, TEvent @event, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs b/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs new file mode 100644 index 0000000..c161ef3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Messaging/EventRoutingKeys.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Application.Messaging; + +/// +/// 事件路由键常量。 +/// +public static class EventRoutingKeys +{ + /// + /// 订单创建事件路由键。 + /// + public const string OrderCreated = "orders.created"; + + /// + /// 支付成功事件路由键。 + /// + public const string PaymentSucceeded = "payments.succeeded"; +} diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs new file mode 100644 index 0000000..be61584 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Application.Messaging.Events; + +/// +/// 订单创建事件。 +/// +public sealed class OrderCreatedEvent +{ + /// + /// 订单标识。 + /// + public Guid OrderId { get; init; } + + /// + /// 订单编号。 + /// + public string OrderNo { get; init; } = string.Empty; + + /// + /// 实付金额。 + /// + public decimal Amount { get; init; } + + /// + /// 所属租户。 + /// + public Guid TenantId { get; init; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs new file mode 100644 index 0000000..f62a88e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Application.Messaging.Events; + +/// +/// 支付成功事件。 +/// +public sealed class PaymentSucceededEvent +{ + /// + /// 订单标识。 + /// + public Guid OrderId { get; init; } + + /// + /// 支付流水号。 + /// + public string PaymentNo { get; init; } = string.Empty; + + /// + /// 支付金额。 + /// + public decimal Amount { get; init; } + + /// + /// 所属租户。 + /// + public Guid TenantId { get; init; } + + /// + /// 支付时间(UTC)。 + /// + public DateTime PaidAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Extensions/MessagingServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Messaging/Extensions/MessagingServiceCollectionExtensions.cs new file mode 100644 index 0000000..82e9614 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Messaging/Extensions/MessagingServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Messaging.Abstractions; +using TakeoutSaaS.Application.Messaging.Services; + +namespace TakeoutSaaS.Application.Messaging.Extensions; + +/// +/// 消息模块应用层注册。 +/// +public static class MessagingServiceCollectionExtensions +{ + /// + /// 注册事件发布器。 + /// + public static IServiceCollection AddMessagingApplication(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Services/EventPublisher.cs b/src/Application/TakeoutSaaS.Application/Messaging/Services/EventPublisher.cs new file mode 100644 index 0000000..60b04d3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Messaging/Services/EventPublisher.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Messaging.Abstractions; +using TakeoutSaaS.Module.Messaging.Abstractions; + +namespace TakeoutSaaS.Application.Messaging.Services; + +/// +/// 事件发布适配器,封装应用层到 MQ 的发布。 +/// +public sealed class EventPublisher(IMessagePublisher messagePublisher) : IEventPublisher +{ + /// + public Task PublishAsync(string routingKey, TEvent @event, CancellationToken cancellationToken = default) + => messagePublisher.PublishAsync(routingKey, @event, cancellationToken); +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs b/src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs new file mode 100644 index 0000000..514b843 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Sms.Contracts; + +namespace TakeoutSaaS.Application.Sms.Abstractions; + +/// +/// 短信验证码服务抽象。 +/// +public interface IVerificationCodeService +{ + /// + /// 发送验证码。 + /// + Task SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default); + + /// + /// 校验验证码。 + /// + Task VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs new file mode 100644 index 0000000..16e659d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Module.Sms; + +namespace TakeoutSaaS.Application.Sms.Contracts; + +/// +/// 发送验证码请求。 +/// +public sealed class SendVerificationCodeRequest +{ + /// + /// 创建发送请求。 + /// + public SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null) + { + PhoneNumber = phoneNumber; + Scene = scene; + Provider = provider; + } + + /// + /// 手机号(支持 +86 前缀或纯 11 位)。 + /// + public string PhoneNumber { get; } + + /// + /// 业务场景(如 login/register/reset)。 + /// + public string Scene { get; } + + /// + /// 指定服务商,未指定则使用默认配置。 + /// + public SmsProviderKind? Provider { get; } +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs new file mode 100644 index 0000000..5b6cf77 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs @@ -0,0 +1,19 @@ +using System; + +namespace TakeoutSaaS.Application.Sms.Contracts; + +/// +/// 发送验证码响应。 +/// +public sealed class SendVerificationCodeResponse +{ + /// + /// 过期时间。 + /// + public DateTimeOffset ExpiresAt { get; set; } + + /// + /// 请求标识。 + /// + public string? RequestId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs new file mode 100644 index 0000000..57df45e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Application.Sms.Contracts; + +/// +/// 校验验证码请求。 +/// +public sealed class VerifyVerificationCodeRequest +{ + /// + /// 创建校验请求。 + /// + public VerifyVerificationCodeRequest(string phoneNumber, string scene, string code) + { + PhoneNumber = phoneNumber; + Scene = scene; + Code = code; + } + + /// + /// 手机号。 + /// + public string PhoneNumber { get; } + + /// + /// 业务场景。 + /// + public string Scene { get; } + + /// + /// 填写的验证码。 + /// + public string Code { get; } +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs new file mode 100644 index 0000000..5a4d7c7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Sms.Abstractions; +using TakeoutSaaS.Application.Sms.Options; +using TakeoutSaaS.Application.Sms.Services; + +namespace TakeoutSaaS.Application.Sms.Extensions; + +/// +/// 短信应用服务注册扩展。 +/// +public static class SmsServiceCollectionExtensions +{ + /// + /// 注册短信验证码应用服务。 + /// + public static IServiceCollection AddSmsApplication(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection("Sms:VerificationCode")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddScoped(); + return services; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs b/src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs new file mode 100644 index 0000000..fd49271 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.Sms.Options; + +/// +/// 验证码发送配置。 +/// +public sealed class VerificationCodeOptions +{ + /// + /// 验证码位数,默认 6。 + /// + [Range(4, 10)] + public int CodeLength { get; set; } = 6; + + /// + /// 过期时间(分钟)。 + /// + [Range(1, 60)] + public int ExpireMinutes { get; set; } = 5; + + /// + /// 发送冷却时间(秒),用于防止频繁请求。 + /// + [Range(10, 300)] + public int CooldownSeconds { get; set; } = 60; + + /// + /// 缓存前缀。 + /// + [Required] + public string CachePrefix { get; set; } = "sms:code"; +} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs b/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs new file mode 100644 index 0000000..042410a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs @@ -0,0 +1,148 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Application.Sms.Abstractions; +using TakeoutSaaS.Application.Sms.Contracts; +using TakeoutSaaS.Application.Sms.Options; +using TakeoutSaaS.Module.Sms.Abstractions; +using TakeoutSaaS.Module.Sms.Models; +using TakeoutSaaS.Module.Sms.Options; +using TakeoutSaaS.Module.Sms; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Sms.Services; + +/// +/// 短信验证码服务实现。 +/// +public sealed class VerificationCodeService( + ISmsSenderResolver senderResolver, + IOptionsMonitor smsOptionsMonitor, + IOptionsMonitor codeOptionsMonitor, + ITenantProvider tenantProvider, + IDistributedCache cache, + ILogger logger) : IVerificationCodeService +{ + /// + public async Task SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(request.PhoneNumber)) + { + throw new BusinessException(ErrorCodes.BadRequest, "手机号不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Scene)) + { + throw new BusinessException(ErrorCodes.BadRequest, "场景不能为空"); + } + + var smsOptions = smsOptionsMonitor.CurrentValue; + var codeOptions = codeOptionsMonitor.CurrentValue; + var templateCode = ResolveTemplate(request.Scene, smsOptions); + var phone = NormalizePhoneNumber(request.PhoneNumber); + var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N"); + var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; + var cooldownKey = $"{cacheKey}:cooldown"; + + await EnsureCooldownAsync(cooldownKey, codeOptions.CooldownSeconds, cancellationToken).ConfigureAwait(false); + + var code = GenerateCode(codeOptions.CodeLength); + var variables = new Dictionary { { "code", code } }; + var sender = senderResolver.Resolve(request.Provider); + + var smsRequest = new SmsSendRequest(phone, templateCode, variables, smsOptions.DefaultSignName); + var smsResult = await sender.SendAsync(smsRequest, cancellationToken).ConfigureAwait(false); + if (!smsResult.Success) + { + throw new BusinessException(ErrorCodes.InternalServerError, $"短信发送失败:{smsResult.Message}"); + } + + var expiresAt = DateTimeOffset.UtcNow.AddMinutes(codeOptions.ExpireMinutes); + await cache.SetStringAsync(cacheKey, code, new DistributedCacheEntryOptions + { + AbsoluteExpiration = expiresAt + }, cancellationToken).ConfigureAwait(false); + + await cache.SetStringAsync(cooldownKey, "1", new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(codeOptions.CooldownSeconds) + }, cancellationToken).ConfigureAwait(false); + + logger.LogInformation("发送验证码成功,Phone:{Phone} Scene:{Scene} Tenant:{Tenant}", phone, request.Scene, tenantKey); + return new SendVerificationCodeResponse + { + ExpiresAt = expiresAt, + RequestId = smsResult.RequestId + }; + } + + /// + public async Task VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(request.Code)) + { + return false; + } + + var codeOptions = codeOptionsMonitor.CurrentValue; + var phone = NormalizePhoneNumber(request.PhoneNumber); + var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N"); + var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; + + var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(cachedCode)) + { + return false; + } + + var success = string.Equals(cachedCode, request.Code, StringComparison.Ordinal); + if (success) + { + await cache.RemoveAsync(cacheKey, cancellationToken).ConfigureAwait(false); + } + + return success; + } + + private static string ResolveTemplate(string scene, SmsOptions options) + { + if (options.SceneTemplates.TryGetValue(scene, out var template) && !string.IsNullOrWhiteSpace(template)) + { + return template; + } + + throw new BusinessException(ErrorCodes.BadRequest, $"未配置场景 {scene} 的短信模板"); + } + + private static string NormalizePhoneNumber(string phone) + { + var trimmed = phone.Trim(); + return trimmed.StartsWith("+", StringComparison.Ordinal) ? trimmed : $"+86{trimmed}"; + } + + private static string GenerateCode(int length) + { + var buffer = new byte[length]; + RandomNumberGenerator.Fill(buffer); + var builder = new StringBuilder(length); + foreach (var b in buffer) + { + builder.Append((b % 10).ToString()); + } + + return builder.ToString()[..length]; + } + + private async Task EnsureCooldownAsync(string cooldownKey, int cooldownSeconds, CancellationToken cancellationToken) + { + var existing = await cache.GetStringAsync(cooldownKey, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(existing)) + { + throw new BusinessException(ErrorCodes.BadRequest, "请求过于频繁,请稍后再试"); + } + } +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Abstractions/IFileStorageService.cs b/src/Application/TakeoutSaaS.Application/Storage/Abstractions/IFileStorageService.cs new file mode 100644 index 0000000..f164c5c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Abstractions/IFileStorageService.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Application.Storage.Contracts; + +namespace TakeoutSaaS.Application.Storage.Abstractions; + +/// +/// 文件存储应用服务抽象。 +/// +public interface IFileStorageService +{ + /// + /// 通过服务端中转上传文件。 + /// + Task UploadAsync(UploadFileRequest request, CancellationToken cancellationToken = default); + + /// + /// 生成前端直传凭证(预签名上传)。 + /// + Task CreateDirectUploadAsync(DirectUploadRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs new file mode 100644 index 0000000..de9a69f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs @@ -0,0 +1,46 @@ +using TakeoutSaaS.Application.Storage.Enums; + +namespace TakeoutSaaS.Application.Storage.Contracts; + +/// +/// 直传凭证请求模型。 +/// +public sealed class DirectUploadRequest +{ + /// + /// 创建直传请求。 + /// + public DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin) + { + FileType = fileType; + FileName = fileName; + ContentType = contentType; + ContentLength = contentLength; + RequestOrigin = requestOrigin; + } + + /// + /// 文件类型。 + /// + public UploadFileType FileType { get; } + + /// + /// 文件名。 + /// + public string FileName { get; } + + /// + /// 内容类型。 + /// + public string ContentType { get; } + + /// + /// 文件长度。 + /// + public long ContentLength { get; } + + /// + /// 请求来源(Origin/Referer)。 + /// + public string? RequestOrigin { get; } +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadResponse.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadResponse.cs new file mode 100644 index 0000000..4989657 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadResponse.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace TakeoutSaaS.Application.Storage.Contracts; + +/// +/// 直传凭证响应模型。 +/// +public sealed class DirectUploadResponse +{ + /// + /// 预签名上传地址。 + /// + public string UploadUrl { get; set; } = string.Empty; + + /// + /// 表单直传所需字段(PUT 直传为空)。 + /// + public IReadOnlyDictionary FormFields { get; set; } = new Dictionary(); + + /// + /// 预签名过期时间。 + /// + public DateTimeOffset ExpiresAt { get; set; } + + /// + /// 对象键。 + /// + public string ObjectKey { get; set; } = string.Empty; + + /// + /// 直传完成后的访问链接(包含签名)。 + /// + public string? DownloadUrl { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/FileUploadResponse.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/FileUploadResponse.cs new file mode 100644 index 0000000..3f12168 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/FileUploadResponse.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Application.Storage.Contracts; + +/// +/// 上传完成后的返回模型。 +/// +public sealed class FileUploadResponse +{ + /// + /// 访问 URL(已包含签名)。 + /// + public string Url { get; set; } = string.Empty; + + /// + /// 文件名。 + /// + public string FileName { get; set; } = string.Empty; + + /// + /// 文件大小。 + /// + public long FileSize { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs new file mode 100644 index 0000000..d60bb7b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs @@ -0,0 +1,59 @@ +using System.IO; +using TakeoutSaaS.Application.Storage.Enums; + +namespace TakeoutSaaS.Application.Storage.Contracts; + +/// +/// 上传文件请求模型。 +/// +public sealed class UploadFileRequest +{ + /// + /// 创建上传文件请求。 + /// + public UploadFileRequest( + UploadFileType fileType, + Stream content, + string fileName, + string contentType, + long contentLength, + string? requestOrigin) + { + FileType = fileType; + Content = content; + FileName = fileName; + ContentType = contentType; + ContentLength = contentLength; + RequestOrigin = requestOrigin; + } + + /// + /// 文件分类。 + /// + public UploadFileType FileType { get; } + + /// + /// 文件流。 + /// + public Stream Content { get; } + + /// + /// 原始文件名。 + /// + public string FileName { get; } + + /// + /// 内容类型。 + /// + public string ContentType { get; } + + /// + /// 文件大小。 + /// + public long ContentLength { get; } + + /// + /// 请求来源(Origin/Referer)。 + /// + public string? RequestOrigin { get; } +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Enums/UploadFileType.cs b/src/Application/TakeoutSaaS.Application/Storage/Enums/UploadFileType.cs new file mode 100644 index 0000000..f6c5228 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Enums/UploadFileType.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Application.Storage.Enums; + +/// +/// 上传文件类型,映射业务场景。 +/// +public enum UploadFileType +{ + /// + /// 菜品图片。 + /// + DishImage = 1, + + /// + /// 商户 Logo。 + /// + MerchantLogo = 2, + + /// + /// 用户头像。 + /// + UserAvatar = 3, + + /// + /// 评价图片。 + /// + ReviewImage = 4, + + /// + /// 其他通用文件。 + /// + Other = 9 +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Extensions/StorageServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Storage/Extensions/StorageServiceCollectionExtensions.cs new file mode 100644 index 0000000..1301804 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Extensions/StorageServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.Storage.Abstractions; +using TakeoutSaaS.Application.Storage.Services; + +namespace TakeoutSaaS.Application.Storage.Extensions; + +/// +/// 存储应用服务注册扩展。 +/// +public static class StorageServiceCollectionExtensions +{ + /// + /// 注册文件存储应用服务。 + /// + public static IServiceCollection AddStorageApplication(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Extensions/UploadFileTypeParser.cs b/src/Application/TakeoutSaaS.Application/Storage/Extensions/UploadFileTypeParser.cs new file mode 100644 index 0000000..dd712ba --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Extensions/UploadFileTypeParser.cs @@ -0,0 +1,46 @@ +using System; +using TakeoutSaaS.Application.Storage.Enums; + +namespace TakeoutSaaS.Application.Storage.Extensions; + +/// +/// 上传类型解析与辅助方法。 +/// +public static class UploadFileTypeParser +{ + /// + /// 将字符串解析为上传类型。 + /// + public static bool TryParse(string? value, out UploadFileType type) + { + type = UploadFileType.Other; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var normalized = value.Trim().ToLowerInvariant(); + type = normalized switch + { + "dish_image" => UploadFileType.DishImage, + "merchant_logo" => UploadFileType.MerchantLogo, + "user_avatar" => UploadFileType.UserAvatar, + "review_image" => UploadFileType.ReviewImage, + _ => UploadFileType.Other + }; + + return type != UploadFileType.Other || normalized == "other"; + } + + /// + /// 将上传类型转换为路径片段。 + /// + public static string ToFolderName(this UploadFileType type) => type switch + { + UploadFileType.DishImage => "dishes", + UploadFileType.MerchantLogo => "merchants", + UploadFileType.UserAvatar => "users", + UploadFileType.ReviewImage => "reviews", + _ => "files" + }; +} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs b/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs new file mode 100644 index 0000000..05a02f2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Application.Storage.Abstractions; +using TakeoutSaaS.Application.Storage.Contracts; +using TakeoutSaaS.Application.Storage.Enums; +using TakeoutSaaS.Application.Storage.Extensions; +using TakeoutSaaS.Module.Storage.Abstractions; +using TakeoutSaaS.Module.Storage.Models; +using TakeoutSaaS.Module.Storage.Options; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Storage.Services; + +/// +/// 文件存储应用服务,实现上传与直传凭证生成。 +/// +public sealed class FileStorageService( + IStorageProviderResolver providerResolver, + IOptionsMonitor optionsMonitor, + ITenantProvider tenantProvider, + ICurrentUserAccessor currentUserAccessor, + ILogger logger) : IFileStorageService +{ + /// + public async Task UploadAsync(UploadFileRequest request, CancellationToken cancellationToken = default) + { + if (request is null) + { + throw new BusinessException(ErrorCodes.BadRequest, "上传请求不能为空"); + } + + var options = optionsMonitor.CurrentValue; + var security = options.Security; + ValidateOrigin(request.RequestOrigin, security); + ValidateFileSize(request.ContentLength, security); + + var extension = NormalizeExtension(request.FileName); + ValidateExtension(request.FileType, extension, security); + var contentType = NormalizeContentType(request.ContentType, extension); + ResetStream(request.Content); + + var objectKey = BuildObjectKey(request.FileType, extension); + var metadata = BuildMetadata(request.FileType); + var expires = TimeSpan.FromMinutes(Math.Max(1, security.DefaultUrlExpirationMinutes)); + var provider = providerResolver.Resolve(); + + var uploadResult = await provider.UploadAsync( + new StorageUploadRequest(objectKey, request.Content, contentType, request.ContentLength, true, expires, metadata), + cancellationToken).ConfigureAwait(false); + + var finalUrl = AppendAntiLeechToken(uploadResult.SignedUrl ?? uploadResult.Url, objectKey, expires, security); + logger.LogInformation("文件上传成功:{ObjectKey} ({Size} bytes)", objectKey, request.ContentLength); + + return new FileUploadResponse + { + Url = finalUrl, + FileName = Path.GetFileName(uploadResult.ObjectKey), + FileSize = uploadResult.FileSize + }; + } + + /// + public async Task CreateDirectUploadAsync(DirectUploadRequest request, CancellationToken cancellationToken = default) + { + if (request is null) + { + throw new BusinessException(ErrorCodes.BadRequest, "直传请求不能为空"); + } + + var options = optionsMonitor.CurrentValue; + var security = options.Security; + ValidateOrigin(request.RequestOrigin, security); + ValidateFileSize(request.ContentLength, security); + + var extension = NormalizeExtension(request.FileName); + ValidateExtension(request.FileType, extension, security); + var contentType = NormalizeContentType(request.ContentType, extension); + + var objectKey = BuildObjectKey(request.FileType, extension); + var provider = providerResolver.Resolve(); + var expires = TimeSpan.FromMinutes(Math.Max(1, security.DefaultUrlExpirationMinutes)); + + var directResult = await provider.CreateDirectUploadAsync( + new StorageDirectUploadRequest(objectKey, contentType, request.ContentLength, expires), + cancellationToken).ConfigureAwait(false); + + var finalDownloadUrl = directResult.SignedDownloadUrl != null + ? AppendAntiLeechToken(directResult.SignedDownloadUrl, objectKey, expires, security) + : null; + + return new DirectUploadResponse + { + UploadUrl = directResult.UploadUrl, + FormFields = directResult.FormFields, + ExpiresAt = directResult.ExpiresAt, + ObjectKey = directResult.ObjectKey, + DownloadUrl = finalDownloadUrl + }; + } + + /// + /// 校验文件大小。 + /// + private static void ValidateFileSize(long size, StorageSecurityOptions security) + { + if (size <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "文件内容为空"); + } + + if (size > security.MaxFileSizeBytes) + { + throw new BusinessException(ErrorCodes.BadRequest, $"文件过大,最大允许 {security.MaxFileSizeBytes / 1024 / 1024}MB"); + } + } + + /// + /// 校验文件后缀是否符合配置。 + /// + private static void ValidateExtension(UploadFileType type, string extension, StorageSecurityOptions security) + { + var allowedImages = security.AllowedImageExtensions ?? Array.Empty(); + var allowedFiles = security.AllowedFileExtensions ?? Array.Empty(); + + if (type is UploadFileType.DishImage or UploadFileType.MerchantLogo or UploadFileType.UserAvatar or UploadFileType.ReviewImage) + { + if (!allowedImages.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + throw new BusinessException(ErrorCodes.BadRequest, $"不支持的图片格式:{extension}"); + } + } + else if (!allowedFiles.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + throw new BusinessException(ErrorCodes.BadRequest, $"不支持的文件格式:{extension}"); + } + } + + /// + /// 统一化文件后缀(小写,默认 .bin)。 + /// + private static string NormalizeExtension(string fileName) + { + var extension = Path.GetExtension(fileName); + if (string.IsNullOrWhiteSpace(extension)) + { + return ".bin"; + } + + return extension.ToLowerInvariant(); + } + + /// + /// 根据内容类型或后缀推断 Content-Type。 + /// + private static string NormalizeContentType(string contentType, string extension) + { + if (!string.IsNullOrWhiteSpace(contentType)) + { + return contentType; + } + + return extension switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".pdf" => "application/pdf", + _ => "application/octet-stream" + }; + } + + /// + /// 校验请求来源是否在白名单内。 + /// + private void ValidateOrigin(string? origin, StorageSecurityOptions security) + { + if (!security.EnableRefererValidation || security.AllowedReferers.Length == 0) + { + return; + } + + if (string.IsNullOrWhiteSpace(origin)) + { + throw new BusinessException(ErrorCodes.Forbidden, "未授权的访问来源"); + } + + var isAllowed = security.AllowedReferers.Any(allowed => + !string.IsNullOrWhiteSpace(allowed) && + origin.StartsWith(allowed, StringComparison.OrdinalIgnoreCase)); + + if (!isAllowed) + { + throw new BusinessException(ErrorCodes.Forbidden, "访问来源未在白名单中"); + } + } + + /// + /// 生成对象存储的键路径。 + /// + private string BuildObjectKey(UploadFileType type, string extension) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var tenantSegment = tenantId == Guid.Empty ? "platform" : tenantId.ToString("N"); + var folder = type.ToFolderName(); + var now = DateTime.UtcNow; + var fileName = $"{Guid.NewGuid():N}{extension}"; + + return $"{tenantSegment}/{folder}/{now:yyyy/MM/dd}/{fileName}"; + } + + /// + /// 组装对象元数据,便于追踪租户与用户。 + /// + private IDictionary BuildMetadata(UploadFileType type) + { + var metadata = new Dictionary + { + ["x-meta-upload-type"] = type.ToString(), + ["x-meta-tenant-id"] = tenantProvider.GetCurrentTenantId().ToString() + }; + + if (currentUserAccessor.IsAuthenticated) + { + metadata["x-meta-user-id"] = currentUserAccessor.UserId.ToString(); + } + + return metadata; + } + + /// + /// 重置文件流的读取位置。 + /// + private static void ResetStream(Stream stream) + { + if (stream.CanSeek) + { + stream.Position = 0; + } + } + + /// + /// 为访问链接追加防盗链签名(可配合 CDN Token 验证)。 + /// + private static string AppendAntiLeechToken(string url, string objectKey, TimeSpan expires, StorageSecurityOptions security) + { + if (string.IsNullOrWhiteSpace(security.AntiLeechTokenSecret)) + { + return url; + } + + // 若链接已包含云厂商签名参数,则避免追加自定义参数导致验签失败。 + if (url.Contains("X-Amz-Signature", StringComparison.OrdinalIgnoreCase) || + url.Contains("q-sign-algorithm", StringComparison.OrdinalIgnoreCase) || + url.Contains("Signature=", StringComparison.OrdinalIgnoreCase)) + { + return url; + } + + var expireAt = DateTimeOffset.UtcNow.Add(expires).ToUnixTimeSeconds(); + var payload = $"{objectKey}:{expireAt}:{security.AntiLeechTokenSecret}"; + var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(payload)); + var token = Convert.ToHexString(hashBytes).ToLowerInvariant(); + var separator = url.Contains('?', StringComparison.Ordinal) ? "&" : "?"; + return $"{url}{separator}ts={expireAt}&token={token}"; + } +} diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index 233eb4d..15da3e6 100644 --- a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj +++ b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj @@ -7,8 +7,12 @@ + + + + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs new file mode 100644 index 0000000..6fe8a35 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Shared.Abstractions.Constants; + +/// +/// 数据源名称常量,统一配置键与使用。 +/// +public static class DatabaseConstants +{ + /// + /// 默认业务库(AppDatabase)。 + /// + public const string AppDataSource = "AppDatabase"; + + /// + /// 身份认证库(IdentityDatabase)。 + /// + public const string IdentityDataSource = "IdentityDatabase"; +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs new file mode 100644 index 0000000..175f9f6 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Shared.Abstractions.Data; + +/// +/// 数据库连接角色,用于区分主写与从读连接。 +/// +public enum DatabaseConnectionRole +{ + /// + /// 主写连接,用于写入或强一致读。 + /// + Write = 1, + + /// + /// 从读连接,用于只读查询或报表。 + /// + Read = 2 +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs new file mode 100644 index 0000000..3783a93 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs @@ -0,0 +1,47 @@ +using System.Data; + +namespace TakeoutSaaS.Shared.Abstractions.Data; + +/// +/// Dapper 查询/命令执行器抽象,封装连接获取与读写路由。 +/// +public interface IDapperExecutor +{ + /// + /// 使用指定数据源与读写角色执行异步查询,并返回结果。 + /// + /// 查询结果类型。 + /// 逻辑数据源名称。 + /// 连接角色(读/写)。 + /// 查询委托,提供已打开的连接和取消标记。 + /// 取消标记。 + /// 查询结果。 + Task QueryAsync( + string dataSourceName, + DatabaseConnectionRole role, + Func> query, + CancellationToken cancellationToken = default); + + /// + /// 使用指定数据源与读写角色执行异步命令。 + /// + /// 逻辑数据源名称。 + /// 连接角色(读/写)。 + /// 命令委托,提供已打开的连接和取消标记。 + /// 取消标记。 + Task ExecuteAsync( + string dataSourceName, + DatabaseConnectionRole role, + Func command, + CancellationToken cancellationToken = default); + + /// + /// 获取指定数据源及角色的默认命令超时时间(秒)。 + /// + /// 逻辑数据源名称。 + /// 连接角色,默认读取从库。 + /// 命令超时时间(秒)。 + int GetDefaultCommandTimeoutSeconds( + string dataSourceName, + DatabaseConnectionRole role = DatabaseConnectionRole.Read); +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs new file mode 100644 index 0000000..3aaedf9 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs @@ -0,0 +1,37 @@ +namespace TakeoutSaaS.Shared.Abstractions.Entities; + +/// +/// 审计实体基类:提供创建、更新时间以及软删除时间。 +/// +public abstract class AuditableEntityBase : EntityBase, IAuditableEntity +{ + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 最近一次更新时间(UTC),从未更新时为 null。 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 软删除时间(UTC),未删除时为 null。 + /// + public DateTime? DeletedAt { get; set; } + + /// + /// 创建人用户标识,匿名或系统操作时为 null。 + /// + public Guid? CreatedBy { get; set; } + + /// + /// 最后更新人用户标识,匿名或系统操作时为 null。 + /// + public Guid? UpdatedBy { get; set; } + + /// + /// 删除人用户标识(软删除),未删除时为 null。 + /// + public Guid? DeletedBy { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs new file mode 100644 index 0000000..1cd539e --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Shared.Abstractions.Entities; + +/// +/// 实体基类,统一提供主键标识。 +/// +public abstract class EntityBase +{ + /// + /// 实体唯一标识。 + /// + public Guid Id { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs index aa6a7cd..7168803 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs @@ -1,9 +1,9 @@ namespace TakeoutSaaS.Shared.Abstractions.Entities; /// -/// 审计字段接口:提供创建时间和更新时间字段。 +/// 审计字段接口:提供创建、更新、删除时间与操作者标识。 /// -public interface IAuditableEntity +public interface IAuditableEntity : ISoftDeleteEntity { /// /// 创建时间(UTC)。 @@ -14,5 +14,24 @@ public interface IAuditableEntity /// 更新时间(UTC),未更新时为 null。 /// DateTime? UpdatedAt { get; set; } -} + /// + /// 删除时间(UTC),未删除时为 null。 + /// + new DateTime? DeletedAt { get; set; } + + /// + /// 创建人用户标识,匿名或系统操作时为 null。 + /// + Guid? CreatedBy { get; set; } + + /// + /// 最后更新人用户标识,匿名或系统操作时为 null。 + /// + Guid? UpdatedBy { get; set; } + + /// + /// 删除人用户标识(软删除),未删除时为 null。 + /// + Guid? DeletedBy { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs new file mode 100644 index 0000000..4bc3fba --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Shared.Abstractions.Entities; + +/// +/// 软删除实体约定:提供可空的删除时间戳以支持全局过滤。 +/// +public interface ISoftDeleteEntity +{ + /// + /// 删除时间(UTC),未删除时为 null。 + /// + DateTime? DeletedAt { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs new file mode 100644 index 0000000..59bf1f8 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Shared.Abstractions.Entities; + +/// +/// 多租户审计实体基类:提供租户标识、审计字段与软删除标记。 +/// +public abstract class MultiTenantEntityBase : AuditableEntityBase, IMultiTenantEntity +{ + /// + /// 所属租户 ID。 + /// + public Guid TenantId { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs new file mode 100644 index 0000000..9b7e7fb --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Shared.Abstractions.Security; + +/// +/// 当前用户访问器:提供与当前请求相关的用户标识信息。 +/// +public interface ICurrentUserAccessor +{ + /// + /// 当前用户 ID,未登录时为 Guid.Empty。 + /// + Guid UserId { get; } + + /// + /// 是否已登录。 + /// + bool IsAuthenticated { get; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs index edc70d8..dd5f1e3 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Web.Filters; +using TakeoutSaaS.Shared.Web.Security; namespace TakeoutSaaS.Shared.Web.Extensions; @@ -17,6 +19,7 @@ public static class ServiceCollectionExtensions { services.AddHttpContextAccessor(); services.AddEndpointsApiExplorer(); + services.AddScoped(); services .AddControllers(options => diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs new file mode 100644 index 0000000..d73fbbd --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs @@ -0,0 +1,42 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Shared.Abstractions.Security; + +namespace TakeoutSaaS.Shared.Web.Security; + +/// +/// 基于 HttpContext 的当前用户访问器。 +/// +public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// 初始化访问器。 + /// + public HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + public Guid UserId + { + get + { + var principal = _httpContextAccessor.HttpContext?.User; + if (principal == null || !principal.Identity?.IsAuthenticated == true) + { + return Guid.Empty; + } + + var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier) + ?? principal.FindFirstValue("sub"); + + return Guid.TryParse(identifier, out var id) ? id : Guid.Empty; + } + } + + /// + public bool IsAuthenticated => UserId != Guid.Empty; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs index 4bba213..68694b0 100644 --- a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryGroup.cs @@ -5,20 +5,10 @@ using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Dictionary.Entities; /// -/// 参数字典分组(系统参数/业务参数)。 +/// 参数字典分组(系统参数、业务参数)。 /// -public sealed class DictionaryGroup : IMultiTenantEntity, IAuditableEntity +public sealed class DictionaryGroup : MultiTenantEntityBase { - /// - /// 分组 ID。 - /// - public Guid Id { get; set; } - - /// - /// 所属租户(系统参数为 Guid.Empty)。 - /// - public Guid TenantId { get; set; } - /// /// 分组编码(唯一)。 /// @@ -44,16 +34,6 @@ public sealed class DictionaryGroup : IMultiTenantEntity, IAuditableEntity /// public bool IsEnabled { get; set; } = true; - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; set; } - - /// - /// 更新时间(UTC)。 - /// - public DateTime? UpdatedAt { get; set; } - /// /// 字典项集合。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs index 0058e23..4d47912 100644 --- a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs @@ -5,18 +5,8 @@ namespace TakeoutSaaS.Domain.Dictionary.Entities; /// /// 参数字典项。 /// -public sealed class DictionaryItem : IMultiTenantEntity, IAuditableEntity +public sealed class DictionaryItem : MultiTenantEntityBase { - /// - /// 字典项 ID。 - /// - public Guid Id { get; set; } - - /// - /// 所属租户。 - /// - public Guid TenantId { get; set; } - /// /// 关联分组 ID。 /// @@ -52,16 +42,6 @@ public sealed class DictionaryItem : IMultiTenantEntity, IAuditableEntity /// public string? Description { get; set; } - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; set; } - - /// - /// 更新时间(UTC)。 - /// - public DateTime? UpdatedAt { get; set; } - /// /// 导航属性:所属分组。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs index b47df34..6ccb304 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -3,15 +3,10 @@ using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; /// -/// 管理后台账户实体(平台、租户或商户员工)。 +/// 管理后台账户实体(平台管理员、租户管理员或商户员工)。 /// -public sealed class IdentityUser : IMultiTenantEntity +public sealed class IdentityUser : MultiTenantEntityBase { - /// - /// 用户 ID。 - /// - public Guid Id { get; set; } - /// /// 登录账号。 /// @@ -27,11 +22,6 @@ public sealed class IdentityUser : IMultiTenantEntity /// public string PasswordHash { get; set; } = string.Empty; - /// - /// 所属租户。 - /// - public Guid TenantId { get; set; } - /// /// 所属商户(平台管理员为空)。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs index 953e979..bc2ce45 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MiniUser.cs @@ -5,13 +5,8 @@ namespace TakeoutSaaS.Domain.Identity.Entities; /// /// 小程序用户实体。 /// -public sealed class MiniUser : IMultiTenantEntity +public sealed class MiniUser : MultiTenantEntityBase { - /// - /// 用户 ID。 - /// - public Guid Id { get; set; } - /// /// 微信 OpenId。 /// @@ -31,9 +26,4 @@ public sealed class MiniUser : IMultiTenantEntity /// 头像地址。 /// public string? Avatar { get; set; } - - /// - /// 所属租户。 - /// - public Guid TenantId { get; set; } } diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs index 1c832fa..f7467d5 100644 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs @@ -1,67 +1,110 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Yarp.ReverseProxy.Configuration; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Threading.RateLimiting; var builder = WebApplication.CreateBuilder(args); -var routes = new[] -{ - new RouteConfig - { - RouteId = "admin-route", - ClusterId = "admin", - Match = new() { Path = "/api/admin/{**catch-all}" } - }, - new RouteConfig - { - RouteId = "mini-route", - ClusterId = "mini", - Match = new() { Path = "/api/mini/{**catch-all}" } - }, - new RouteConfig - { - RouteId = "user-route", - ClusterId = "user", - Match = new() { Path = "/api/user/{**catch-all}" } - } -}; - -var clusters = new[] -{ - new ClusterConfig - { - ClusterId = "admin", - Destinations = new Dictionary - { - ["d1"] = new() { Address = "http://localhost:5001/" } - } - }, - new ClusterConfig - { - ClusterId = "mini", - Destinations = new Dictionary - { - ["d1"] = new() { Address = "http://localhost:5002/" } - } - }, - new ClusterConfig - { - ClusterId = "user", - Destinations = new Dictionary - { - ["d1"] = new() { Address = "http://localhost:5003/" } - } - } -}; - builder.Services.AddReverseProxy() - .LoadFromMemory(routes, clusters); + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); + +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => + { + const string partitionKey = "proxy-default"; + return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions + { + PermitLimit = 100, + Window = TimeSpan.FromSeconds(1), + QueueLimit = 50, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst + }); + }); +}); var app = builder.Build(); -app.MapReverseProxy(); +app.UseExceptionHandler(errorApp => +{ + errorApp.Run(async context => + { + var feature = context.Features.Get(); + var traceId = Activity.Current?.Id ?? context.TraceIdentifier; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = "application/json"; + + var payload = new + { + success = false, + code = 500, + message = "Gateway internal error", + traceId + }; + + var logger = context.RequestServices.GetRequiredService().CreateLogger("Gateway"); + logger.LogError(feature?.Error, "网关异常 {TraceId}", traceId); + await context.Response.WriteAsJsonAsync(payload, cancellationToken: context.RequestAborted); + }); +}); + +app.Use(async (context, next) => +{ + var logger = context.RequestServices.GetRequiredService().CreateLogger("Gateway"); + var start = DateTime.UtcNow; + await next(context); + var elapsed = DateTime.UtcNow - start; + logger.LogInformation("Gateway {Method} {Path} => {Status} ({Elapsed} ms)", + context.Request.Method, + context.Request.Path, + context.Response.StatusCode, + (int)elapsed.TotalMilliseconds); +}); + +app.UseRateLimiter(); + +app.Use(async (context, next) => +{ + // 确保存在请求 ID,便于上下游链路追踪。 + if (!context.Request.Headers.ContainsKey("X-Request-Id")) + { + context.Request.Headers["X-Request-Id"] = Guid.NewGuid().ToString("N"); + } + + // 透传租户与认证头。 + var tenantId = context.Request.Headers["X-Tenant-Id"]; + var tenantCode = context.Request.Headers["X-Tenant-Code"]; + if (!string.IsNullOrWhiteSpace(tenantId)) + { + context.Request.Headers["X-Tenant-Id"] = tenantId; + } + if (!string.IsNullOrWhiteSpace(tenantCode)) + { + context.Request.Headers["X-Tenant-Code"] = tenantCode; + } + + await next(context); +}); + +app.MapReverseProxy(proxyPipeline => +{ + proxyPipeline.Use(async (context, next) => + { + await next().ConfigureAwait(false); + }); +}); + +app.MapGet("/", () => Results.Json(new +{ + Service = "TakeoutSaaS.ApiGateway", + Status = "OK", + Timestamp = DateTimeOffset.UtcNow +})); app.Run(); - diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json b/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json new file mode 100644 index 0000000..9c7c501 --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json @@ -0,0 +1,38 @@ +{ + "ReverseProxy": { + "Routes": [ + { + "RouteId": "admin-route", + "ClusterId": "admin", + "Match": { "Path": "/api/admin/{**catch-all}" } + }, + { + "RouteId": "mini-route", + "ClusterId": "mini", + "Match": { "Path": "/api/mini/{**catch-all}" } + }, + { + "RouteId": "user-route", + "ClusterId": "user", + "Match": { "Path": "/api/user/{**catch-all}" } + } + ], + "Clusters": { + "admin": { + "Destinations": { + "d1": { "Address": "http://localhost:5001/" } + } + }, + "mini": { + "Destinations": { + "d1": { "Address": "http://localhost:5002/" } + } + }, + "user": { + "Destinations": { + "d1": { "Address": "http://localhost:5003/" } + } + } + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs new file mode 100644 index 0000000..bbc2b00 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs @@ -0,0 +1,86 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Infrastructure.Common.Options; +using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Data; + +namespace TakeoutSaaS.Infrastructure.Common.Extensions; + +/// +/// 数据访问与多数据源相关的服务注册扩展。 +/// +public static class DatabaseServiceCollectionExtensions +{ + /// + /// 注册数据库基础设施(多数据源配置、读写分离、Dapper 执行器)。 + /// + /// 服务集合。 + /// 配置源。 + /// 服务集合。 + public static IServiceCollection AddDatabaseInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection(DatabaseOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddSingleton(); + services.AddScoped(); + return services; + } + + /// + /// 为指定 DbContext 注册读写分离的 PostgreSQL 配置,同时提供读上下文工厂。 + /// + /// 上下文类型。 + /// 服务集合。 + /// 逻辑数据源名称。 + /// 服务集合。 + public static IServiceCollection AddPostgresDbContext( + this IServiceCollection services, + string dataSourceName) + where TContext : DbContext + { + services.AddDbContext((sp, options) => + { + ConfigureDbContextOptions(sp, options, dataSourceName, DatabaseConnectionRole.Write); + }); + + services.AddDbContextFactory((sp, options) => + { + ConfigureDbContextOptions(sp, options, dataSourceName, DatabaseConnectionRole.Read); + }); + + return services; + } + + /// + /// 配置 DbContextOptions,应用连接串、命令超时与重试策略。 + /// + /// 服务提供程序。 + /// 上下文配置器。 + /// 数据源名称。 + /// 连接角色。 + private static void ConfigureDbContextOptions( + IServiceProvider serviceProvider, + DbContextOptionsBuilder optionsBuilder, + string dataSourceName, + DatabaseConnectionRole role) + { + var connection = serviceProvider + .GetRequiredService() + .GetConnection(dataSourceName, role); + + optionsBuilder.UseNpgsql( + connection.ConnectionString, + npgsqlOptions => + { + npgsqlOptions.CommandTimeout(connection.CommandTimeoutSeconds); + npgsqlOptions.EnableRetryOnFailure( + connection.MaxRetryCount, + TimeSpan.FromSeconds(connection.MaxRetryDelaySeconds), + null); + }); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseDataSourceOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseDataSourceOptions.cs new file mode 100644 index 0000000..694c9e3 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseDataSourceOptions.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.Common.Options; + +/// +/// 单个数据源的连接配置,支持主写与多个从读。 +/// +public sealed class DatabaseDataSourceOptions +{ + /// + /// 主写连接串,读写分离缺省回退到此连接。 + /// + [Required] + public string? Write { get; set; } + + /// + /// 从读连接串集合,可为空。 + /// + public IList Reads { get; init; } = new List(); + + /// + /// 默认命令超时(秒),未设置时使用框架默认值。 + /// + [Range(1, 600)] + public int CommandTimeoutSeconds { get; set; } = 30; + + /// + /// 数据库重试次数。 + /// + [Range(0, 10)] + public int MaxRetryCount { get; set; } = 3; + + /// + /// 数据库重试最大延迟(秒)。 + /// + [Range(1, 60)] + public int MaxRetryDelaySeconds { get; set; } = 5; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseOptions.cs new file mode 100644 index 0000000..a2db8d2 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Options/DatabaseOptions.cs @@ -0,0 +1,33 @@ +namespace TakeoutSaaS.Infrastructure.Common.Options; + +/// +/// 数据源配置集合,键为逻辑数据源名称。 +/// +public sealed class DatabaseOptions +{ + /// + /// 配置节名称。 + /// + public const string SectionName = "Database"; + + /// + /// 数据源配置字典,键为数据源名称。 + /// + public IDictionary DataSources { get; init; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// 获取指定名称的数据源配置,不存在时返回 null。 + /// + /// 逻辑数据源名称。 + /// 数据源配置或 null。 + public DatabaseDataSourceOptions? Find(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return null; + } + + return DataSources.TryGetValue(name, out var options) ? options : null; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs new file mode 100644 index 0000000..0a59c66 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs @@ -0,0 +1,180 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TakeoutSaaS.Shared.Abstractions.Entities; +using TakeoutSaaS.Shared.Abstractions.Security; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// 应用基础 DbContext,统一处理审计字段、软删除与全局查询过滤。 +/// +public abstract class AppDbContext(DbContextOptions options, ICurrentUserAccessor? currentUserAccessor = null) : DbContext(options) +{ + private readonly ICurrentUserAccessor? _currentUserAccessor = currentUserAccessor; + + /// + /// 构建模型时应用软删除过滤器。 + /// + /// 模型构建器。 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + ApplySoftDeleteQueryFilters(modelBuilder); + } + + /// + /// 保存更改前应用元数据填充。 + /// + /// 受影响行数。 + public override int SaveChanges() + { + OnBeforeSaving(); + return base.SaveChanges(); + } + + /// + /// 异步保存更改前应用元数据填充。 + /// + /// 取消标记。 + /// 受影响行数。 + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + OnBeforeSaving(); + return base.SaveChangesAsync(cancellationToken); + } + + /// + /// 保存前处理审计、软删除等元数据,可在子类中扩展。 + /// + protected virtual void OnBeforeSaving() + { + ApplySoftDeleteMetadata(); + ApplyAuditMetadata(); + } + + /// + /// 将软删除实体的删除操作转换为设置 DeletedAt。 + /// + private void ApplySoftDeleteMetadata() + { + var utcNow = DateTime.UtcNow; + var actor = GetCurrentUserIdOrNull(); + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added && entry.Entity.DeletedAt.HasValue) + { + entry.Entity.DeletedAt = null; + } + + if (entry.State != EntityState.Deleted) + { + continue; + } + + entry.State = EntityState.Modified; + entry.Entity.DeletedAt = utcNow; + if (entry.Entity is IAuditableEntity auditable) + { + auditable.DeletedBy = actor; + if (!auditable.UpdatedAt.HasValue) + { + auditable.UpdatedAt = utcNow; + auditable.UpdatedBy = actor; + } + } + } + } + + /// + /// 对审计实体填充创建与更新时间。 + /// + private void ApplyAuditMetadata() + { + var utcNow = DateTime.UtcNow; + var actor = GetCurrentUserIdOrNull(); + + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedAt = utcNow; + entry.Entity.UpdatedAt = null; + entry.Entity.CreatedBy ??= actor; + entry.Entity.UpdatedBy = null; + entry.Entity.DeletedBy = null; + entry.Entity.DeletedAt = null; + } + else if (entry.State == EntityState.Modified) + { + entry.Entity.UpdatedAt = utcNow; + entry.Entity.UpdatedBy = actor; + } + } + } + + private Guid? GetCurrentUserIdOrNull() + { + var userId = _currentUserAccessor?.UserId ?? Guid.Empty; + return userId == Guid.Empty ? null : userId; + } + + /// + /// 应用软删除查询过滤器,自动排除 DeletedAt 不为 null 的记录。 + /// + /// 模型构建器。 + protected void ApplySoftDeleteQueryFilters(ModelBuilder modelBuilder) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (!typeof(ISoftDeleteEntity).IsAssignableFrom(entityType.ClrType)) + { + continue; + } + + var methodInfo = typeof(AppDbContext) + .GetMethod(nameof(SetSoftDeleteFilter), BindingFlags.Instance | BindingFlags.NonPublic)! + .MakeGenericMethod(entityType.ClrType); + + methodInfo.Invoke(this, new object[] { modelBuilder }); + } + } + + /// + /// 设置软删除查询过滤器。 + /// + /// 实体类型。 + /// 模型构建器。 + private void SetSoftDeleteFilter(ModelBuilder modelBuilder) + where TEntity : class, ISoftDeleteEntity + { + modelBuilder.Entity().HasQueryFilter(entity => entity.DeletedAt == null); + } + + /// + /// 配置审计字段的通用约束。 + /// + /// 实体类型。 + /// 实体构建器。 + protected static void ConfigureAuditableEntity(EntityTypeBuilder builder) + where TEntity : class, IAuditableEntity + { + builder.Property(x => x.CreatedAt).IsRequired(); + builder.Property(x => x.UpdatedAt); + builder.Property(x => x.DeletedAt); + builder.Property(x => x.CreatedBy); + builder.Property(x => x.UpdatedBy); + builder.Property(x => x.DeletedBy); + } + + /// + /// 配置软删除字段的通用约束。 + /// + /// 实体类型。 + /// 实体构建器。 + protected static void ConfigureSoftDeleteEntity(EntityTypeBuilder builder) + where TEntity : class, ISoftDeleteEntity + { + builder.Property(x => x.DeletedAt); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DapperExecutor.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DapperExecutor.cs new file mode 100644 index 0000000..e0761aa --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DapperExecutor.cs @@ -0,0 +1,80 @@ +using System.Data; +using Microsoft.Extensions.Logging; +using Npgsql; +using TakeoutSaaS.Shared.Abstractions.Data; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// 基于 Dapper 的执行器实现,封装连接创建与读写分离。 +/// +public sealed class DapperExecutor( + IDatabaseConnectionFactory connectionFactory, + ILogger logger) : IDapperExecutor +{ + /// + /// 使用指定数据源与读写角色执行异步查询。 + /// + public async Task QueryAsync( + string dataSourceName, + DatabaseConnectionRole role, + Func> query, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync( + dataSourceName, + role, + async (connection, token) => await query(connection, token), + cancellationToken); + } + + /// + /// 使用指定数据源与读写角色执行异步命令。 + /// + public async Task ExecuteAsync( + string dataSourceName, + DatabaseConnectionRole role, + Func command, + CancellationToken cancellationToken = default) + { + await ExecuteAsync( + dataSourceName, + role, + async (connection, token) => + { + await command(connection, token); + return true; + }, + cancellationToken); + } + + /// + /// 获取默认命令超时时间(秒)。 + /// + public int GetDefaultCommandTimeoutSeconds(string dataSourceName, DatabaseConnectionRole role = DatabaseConnectionRole.Read) + { + var details = connectionFactory.GetConnection(dataSourceName, role); + return details.CommandTimeoutSeconds; + } + + /// + /// 核心执行逻辑:创建连接、打开并执行委托。 + /// + private async Task ExecuteAsync( + string dataSourceName, + DatabaseConnectionRole role, + Func> action, + CancellationToken cancellationToken) + { + var details = connectionFactory.GetConnection(dataSourceName, role); + await using var connection = new NpgsqlConnection(details.ConnectionString); + + logger.LogDebug( + "打开数据库连接:DataSource={DataSource} Role={Role}", + dataSourceName, + role); + + await connection.OpenAsync(cancellationToken); + return await action(connection, cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionDetails.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionDetails.cs new file mode 100644 index 0000000..20e4850 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionDetails.cs @@ -0,0 +1,10 @@ +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// 数据库连接信息(连接串与超时/重试设置)。 +/// +public sealed record DatabaseConnectionDetails( + string ConnectionString, + int CommandTimeoutSeconds, + int MaxRetryCount, + int MaxRetryDelaySeconds); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs new file mode 100644 index 0000000..0f47cbf --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs @@ -0,0 +1,121 @@ +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Security.Cryptography; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Infrastructure.Common.Options; +using TakeoutSaaS.Shared.Abstractions.Data; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// 数据库连接工厂,支持读写分离及连接配置校验。 +/// +public sealed class DatabaseConnectionFactory( + IOptionsMonitor optionsMonitor, + IConfiguration configuration, + ILogger logger) : IDatabaseConnectionFactory +{ + private const int DefaultCommandTimeoutSeconds = 30; + private const int DefaultMaxRetryCount = 3; + private const int DefaultMaxRetryDelaySeconds = 5; + + /// + /// 获取指定数据源与读写角色的连接信息。 + /// + /// 逻辑数据源名称。 + /// 连接角色。 + /// 连接串与超时/重试配置。 + public DatabaseConnectionDetails GetConnection(string dataSourceName, DatabaseConnectionRole role) + { + if (string.IsNullOrWhiteSpace(dataSourceName)) + { + logger.LogWarning("请求的数据源名称为空,使用默认连接。"); + return BuildFallbackConnection(); + } + + var options = optionsMonitor.CurrentValue.Find(dataSourceName); + if (options != null) + { + if (!ValidateOptions(dataSourceName, options)) + { + return BuildFallbackConnection(); + } + + var connectionString = ResolveConnectionString(options, role); + return new DatabaseConnectionDetails( + connectionString, + options.CommandTimeoutSeconds, + options.MaxRetryCount, + options.MaxRetryDelaySeconds); + } + + var fallback = configuration.GetConnectionString(dataSourceName); + if (string.IsNullOrWhiteSpace(fallback)) + { + logger.LogError("缺少数据源 {DataSource} 的连接配置,回退到默认本地连接。", dataSourceName); + return BuildFallbackConnection(); + } + + logger.LogWarning("未找到数据源 {DataSource} 的 Database 节配置,回退使用 ConnectionStrings。", dataSourceName); + return new DatabaseConnectionDetails( + fallback, + DefaultCommandTimeoutSeconds, + DefaultMaxRetryCount, + DefaultMaxRetryDelaySeconds); + } + + /// + /// 校验数据源配置完整性。 + /// + /// 数据源名称。 + /// 数据源配置。 + /// 配置不合法时抛出。 + private bool ValidateOptions(string dataSourceName, DatabaseDataSourceOptions options) + { + var results = new List(); + var context = new ValidationContext(options); + if (!Validator.TryValidateObject(options, context, results, validateAllProperties: true)) + { + var errorMessages = string.Join("; ", results.Select(result => result.ErrorMessage)); + logger.LogError("数据源 {DataSource} 配置非法:{Errors},回退到默认连接。", dataSourceName, errorMessages); + return false; + } + + return true; + } + + /// + /// 根据读写角色选择连接串,从读连接随机分配。 + /// + /// 数据源配置。 + /// 连接角色。 + /// 可用连接串。 + private string ResolveConnectionString(DatabaseDataSourceOptions options, DatabaseConnectionRole role) + { + if (role == DatabaseConnectionRole.Read && options.Reads.Count > 0) + { + var index = RandomNumberGenerator.GetInt32(options.Reads.Count); + return options.Reads[index]; + } + + if (string.IsNullOrWhiteSpace(options.Write)) + { + return BuildFallbackConnection().ConnectionString; + } + + return options.Write; + } + + private DatabaseConnectionDetails BuildFallbackConnection() + { + const string fallback = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20"; + logger.LogWarning("使用默认回退连接串:{Connection}", fallback); + return new DatabaseConnectionDetails( + fallback, + DefaultCommandTimeoutSeconds, + DefaultMaxRetryCount, + DefaultMaxRetryDelaySeconds); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/IDatabaseConnectionFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/IDatabaseConnectionFactory.cs new file mode 100644 index 0000000..4a684df --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/IDatabaseConnectionFactory.cs @@ -0,0 +1,17 @@ +using TakeoutSaaS.Shared.Abstractions.Data; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// 数据库连接工厂,负责按读写角色选择对应连接串及配置。 +/// +public interface IDatabaseConnectionFactory +{ + /// + /// 获取指定数据源与读写角色的连接信息。 + /// + /// 逻辑数据源名称。 + /// 连接角色(读/写)。 + /// 连接串与相关配置。 + DatabaseConnectionDetails GetConnection(string dataSourceName, DatabaseConnectionRole role); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs index 10bd6d5..8681af9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs @@ -1,6 +1,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Shared.Abstractions.Entities; +using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Common.Persistence; @@ -8,14 +9,12 @@ namespace TakeoutSaaS.Infrastructure.Common.Persistence; /// /// 多租户感知 DbContext:自动应用租户过滤并填充租户字段。 /// -public abstract class TenantAwareDbContext : DbContext +public abstract class TenantAwareDbContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor? currentUserAccessor = null) : AppDbContext(options, currentUserAccessor) { - private readonly ITenantProvider _tenantProvider; - - protected TenantAwareDbContext(DbContextOptions options, ITenantProvider tenantProvider) : base(options) - { - _tenantProvider = tenantProvider; - } + private readonly ITenantProvider _tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider)); /// /// 当前请求租户 ID。 @@ -23,8 +22,18 @@ public abstract class TenantAwareDbContext : DbContext protected Guid CurrentTenantId => _tenantProvider.GetCurrentTenantId(); /// - /// 应用租户过滤器至所有实现 的实体。 + /// 保存前填充租户元数据并执行基础处理。 /// + protected override void OnBeforeSaving() + { + ApplyTenantMetadata(); + base.OnBeforeSaving(); + } + + /// + /// 应用租户过滤器到所有实现 的实体。 + /// + /// 模型构建器。 protected void ApplyTenantQueryFilters(ModelBuilder modelBuilder) { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) @@ -42,24 +51,20 @@ public abstract class TenantAwareDbContext : DbContext } } + /// + /// 为具体实体设置租户过滤器。 + /// + /// 实体类型。 + /// 模型构建器。 private void SetTenantFilter(ModelBuilder modelBuilder) where TEntity : class, IMultiTenantEntity { modelBuilder.Entity().HasQueryFilter(entity => entity.TenantId == CurrentTenantId); } - public override int SaveChanges() - { - ApplyTenantMetadata(); - return base.SaveChanges(); - } - - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - ApplyTenantMetadata(); - return base.SaveChangesAsync(cancellationToken); - } - + /// + /// 为新增实体填充租户 ID。 + /// private void ApplyTenantMetadata() { var tenantId = CurrentTenantId; @@ -71,19 +76,5 @@ public abstract class TenantAwareDbContext : DbContext entry.Entity.TenantId = tenantId; } } - - var utcNow = DateTime.UtcNow; - foreach (var entry in ChangeTracker.Entries()) - { - if (entry.State == EntityState.Added) - { - entry.Entity.CreatedAt = utcNow; - entry.Entity.UpdatedAt = null; - } - else if (entry.State == EntityState.Modified) - { - entry.Entity.UpdatedAt = utcNow; - } - } } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs index ad33dab..be31f5a 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs @@ -1,13 +1,15 @@ using System; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TakeoutSaaS.Application.Dictionary.Abstractions; using TakeoutSaaS.Domain.Dictionary.Repositories; +using TakeoutSaaS.Infrastructure.Common.Extensions; +using TakeoutSaaS.Infrastructure.Common.Options; using TakeoutSaaS.Infrastructure.Dictionary.Options; using TakeoutSaaS.Infrastructure.Dictionary.Persistence; using TakeoutSaaS.Infrastructure.Dictionary.Repositories; using TakeoutSaaS.Infrastructure.Dictionary.Services; +using TakeoutSaaS.Shared.Abstractions.Constants; namespace TakeoutSaaS.Infrastructure.Dictionary.Extensions; @@ -19,18 +21,14 @@ public static class DictionaryServiceCollectionExtensions /// /// 注册字典模块基础设施。 /// + /// 服务集合。 + /// 配置源。 + /// 服务集合。 + /// 缺少数据库配置时抛出。 public static IServiceCollection AddDictionaryInfrastructure(this IServiceCollection services, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString("AppDatabase"); - if (string.IsNullOrWhiteSpace(connectionString)) - { - throw new InvalidOperationException("缺少 AppDatabase 连接字符串配置"); - } - - services.AddDbContext(options => - { - options.UseNpgsql(connectionString); - }); + services.AddDatabaseInfrastructure(configuration); + services.AddPostgresDbContext(DatabaseConstants.AppDataSource); services.AddScoped(); services.AddScoped(); @@ -41,4 +39,15 @@ public static class DictionaryServiceCollectionExtensions return services; } + + /// + /// 确保数据库连接已配置(Database 节或 ConnectionStrings)。 + /// + /// 配置源。 + /// 数据源名称。 + /// 未配置时抛出。 + private static void EnsureDatabaseConnectionConfigured(IConfiguration configuration, string dataSourceName) + { + // 保留兼容接口,当前逻辑在 DatabaseConnectionFactory 中兜底并记录日志。 + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index 90351a8..a6aa6b3 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Domain.Dictionary.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; @@ -9,55 +10,79 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; /// /// 参数字典 DbContext。 /// -public sealed class DictionaryDbContext : TenantAwareDbContext +public sealed class DictionaryDbContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor? currentUserAccessor = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) { - public DictionaryDbContext(DbContextOptions options, ITenantProvider tenantProvider) - : base(options, tenantProvider) - { - } - + /// + /// 字典分组集。 + /// public DbSet DictionaryGroups => Set(); + + /// + /// 字典项集。 + /// public DbSet DictionaryItems => Set(); + /// + /// 配置实体模型。 + /// + /// 模型构建器。 protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); ConfigureGroup(modelBuilder.Entity()); ConfigureItem(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } + /// + /// 配置字典分组。 + /// + /// 实体构建器。 private static void ConfigureGroup(EntityTypeBuilder builder) { builder.ToTable("dictionary_groups"); builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); builder.Property(x => x.Scope).HasConversion().IsRequired(); builder.Property(x => x.Description).HasMaxLength(512); builder.Property(x => x.IsEnabled).HasDefaultValue(true); - builder.Property(x => x.CreatedAt).IsRequired(); - builder.Property(x => x.UpdatedAt); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); } + /// + /// 配置字典项。 + /// + /// 实体构建器。 private static void ConfigureItem(EntityTypeBuilder builder) { builder.ToTable("dictionary_items"); builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.GroupId).IsRequired(); builder.Property(x => x.Key).HasMaxLength(64).IsRequired(); builder.Property(x => x.Value).HasMaxLength(256).IsRequired(); builder.Property(x => x.Description).HasMaxLength(512); builder.Property(x => x.SortOrder).HasDefaultValue(100); builder.Property(x => x.IsEnabled).HasDefaultValue(true); - builder.Property(x => x.CreatedAt).IsRequired(); - builder.Property(x => x.UpdatedAt); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); builder.HasOne(x => x.Group) .WithMany(g => g.Items) .HasForeignKey(x => x.GroupId) .OnDelete(DeleteBehavior.Cascade); + builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.GroupId, x.Key }).IsUnique(); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs index 6b588c6..192b3a5 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -1,48 +1,47 @@ using System; using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Infrastructure.Common.Extensions; +using TakeoutSaaS.Infrastructure.Common.Options; using TakeoutSaaS.Infrastructure.Identity.Options; using TakeoutSaaS.Infrastructure.Identity.Persistence; using TakeoutSaaS.Infrastructure.Identity.Services; +using TakeoutSaaS.Shared.Abstractions.Constants; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; namespace TakeoutSaaS.Infrastructure.Identity.Extensions; /// -/// 身份认证基础设施注入 +/// 身份认证基础设施注入。 /// public static class ServiceCollectionExtensions { /// - /// 注册身份认证基础设施(数据库、Redis、JWT、限流等) + /// 注册身份认证基础设施(数据库、Redis、JWT、限流等)。 /// - /// 服务集合 - /// 配置源 - /// 是否启用小程序相关依赖(如微信登录) - /// 是否启用后台账号初始化 + /// 服务集合。 + /// 配置源。 + /// 是否启用小程序相关依赖(如微信登录)。 + /// 是否启用后台账号初始化。 + /// 服务集合。 + /// 配置缺失时抛出。 public static IServiceCollection AddIdentityInfrastructure( this IServiceCollection services, IConfiguration configuration, bool enableMiniFeatures = false, bool enableAdminSeed = false) { - var dbConnection = configuration.GetConnectionString("IdentityDatabase"); - if (string.IsNullOrWhiteSpace(dbConnection)) - { - throw new InvalidOperationException("缺少 IdentityDatabase 连接字符串配置"); - } - - services.AddDbContext(options => options.UseNpgsql(dbConnection)); + services.AddDatabaseInfrastructure(configuration); + services.AddPostgresDbContext(DatabaseConstants.IdentityDataSource); var redisConnection = configuration.GetConnectionString("Redis"); if (string.IsNullOrWhiteSpace(redisConnection)) { - throw new InvalidOperationException("缺少 Redis 连接字符串配置"); + throw new InvalidOperationException("缺少 Redis 连接字符串配置。"); } services.AddStackExchangeRedisCache(options => @@ -96,4 +95,15 @@ public static class ServiceCollectionExtensions return services; } + + /// + /// 确保数据库连接已配置(Database 节或 ConnectionStrings)。 + /// + /// 配置源。 + /// 数据源名称。 + /// 未配置时抛出。 + private static void EnsureDatabaseConnectionConfigured(IConfiguration configuration, string dataSourceName) + { + // 保留兼容接口,当前逻辑在 DatabaseConnectionFactory 中兜底并记录日志。 + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index d9b5b52..07ab279 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -5,30 +5,46 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// -/// 身份认证 DbContext,带多租户过滤。 +/// 身份认证 DbContext,带多租户过滤与审计字段处理。 /// -public sealed class IdentityDbContext : TenantAwareDbContext +public sealed class IdentityDbContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor? currentUserAccessor = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) { - public IdentityDbContext(DbContextOptions options, ITenantProvider tenantProvider) - : base(options, tenantProvider) - { - } - + /// + /// 管理后台用户集合。 + /// public DbSet IdentityUsers => Set(); + + /// + /// 小程序用户集合。 + /// public DbSet MiniUsers => Set(); + /// + /// 配置实体模型。 + /// + /// 模型构建器。 protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); ConfigureIdentityUser(modelBuilder.Entity()); ConfigureMiniUser(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } + /// + /// 配置管理后台用户实体。 + /// + /// 实体构建器。 private static void ConfigureIdentityUser(EntityTypeBuilder builder) { builder.ToTable("identity_users"); @@ -37,6 +53,9 @@ public sealed class IdentityDbContext : TenantAwareDbContext builder.Property(x => x.DisplayName).HasMaxLength(64).IsRequired(); builder.Property(x => x.PasswordHash).HasMaxLength(256).IsRequired(); builder.Property(x => x.Avatar).HasMaxLength(256); + builder.Property(x => x.TenantId).IsRequired(); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); var converter = new ValueConverter( v => string.Join(',', v), @@ -55,18 +74,27 @@ public sealed class IdentityDbContext : TenantAwareDbContext .HasConversion(converter) .Metadata.SetValueComparer(comparer); - builder.HasIndex(x => x.Account).IsUnique(); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Account }).IsUnique(); } + /// + /// 配置小程序用户实体。 + /// + /// 实体构建器。 private static void ConfigureMiniUser(EntityTypeBuilder builder) { builder.ToTable("mini_users"); builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); builder.Property(x => x.OpenId).HasMaxLength(128).IsRequired(); builder.Property(x => x.UnionId).HasMaxLength(128); builder.Property(x => x.Nickname).HasMaxLength(64).IsRequired(); builder.Property(x => x.Avatar).HasMaxLength(256); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => x.OpenId).IsUnique(); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.OpenId }).IsUnique(); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index 92fa65a..c3b590c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessagePublisher.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessagePublisher.cs new file mode 100644 index 0000000..666c554 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessagePublisher.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace TakeoutSaaS.Module.Messaging.Abstractions; + +/// +/// 消息发布抽象。 +/// +public interface IMessagePublisher +{ + /// + /// 发布消息到指定路由键。 + /// + Task PublishAsync(string routingKey, T message, CancellationToken cancellationToken = default); +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessageSubscriber.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessageSubscriber.cs new file mode 100644 index 0000000..1e7e0bd --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Abstractions/IMessageSubscriber.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace TakeoutSaaS.Module.Messaging.Abstractions; + +/// +/// 消息订阅抽象。 +/// +public interface IMessageSubscriber : IAsyncDisposable +{ + /// + /// 订阅指定队列与路由键,处理后返回是否消费成功。 + /// + Task SubscribeAsync(string queue, string routingKey, Func> handler, CancellationToken cancellationToken = default); +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Extensions/MessagingServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Extensions/MessagingServiceCollectionExtensions.cs new file mode 100644 index 0000000..9a53b1a --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Extensions/MessagingServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Module.Messaging.Abstractions; +using TakeoutSaaS.Module.Messaging.Options; +using TakeoutSaaS.Module.Messaging.Serialization; +using TakeoutSaaS.Module.Messaging.Services; + +namespace TakeoutSaaS.Module.Messaging.Extensions; + +/// +/// 消息队列模块注册扩展。 +/// +public static class MessagingServiceCollectionExtensions +{ + /// + /// 注册 RabbitMQ 发布/订阅能力。 + /// + public static IServiceCollection AddMessagingModule(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection("RabbitMQ")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Options/RabbitMqOptions.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Options/RabbitMqOptions.cs new file mode 100644 index 0000000..1b10e6e --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Options/RabbitMqOptions.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Messaging.Options; + +/// +/// RabbitMQ 连接与交换机配置。 +/// +public sealed class RabbitMqOptions +{ + /// + /// 主机名。 + /// + [Required] + public string Host { get; set; } = "localhost"; + + /// + /// 端口。 + /// + [Range(1, 65535)] + public int Port { get; set; } = 5672; + + /// + /// 用户名。 + /// + [Required] + public string Username { get; set; } = "guest"; + + /// + /// 密码。 + /// + [Required] + public string Password { get; set; } = "guest"; + + /// + /// 虚拟主机。 + /// + public string VirtualHost { get; set; } = "/"; + + /// + /// 默认交换机名称。 + /// + [Required] + public string Exchange { get; set; } = "takeout.events"; + + /// + /// 交换机类型,默认 topic。 + /// + public string ExchangeType { get; set; } = "topic"; + + /// + /// 消费预取数量。 + /// + [Range(1, 1000)] + public ushort PrefetchCount { get; set; } = 20; +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Serialization/JsonMessageSerializer.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Serialization/JsonMessageSerializer.cs new file mode 100644 index 0000000..a17186c --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Serialization/JsonMessageSerializer.cs @@ -0,0 +1,22 @@ +using System.Text; +using System.Text.Json; + +namespace TakeoutSaaS.Module.Messaging.Serialization; + +/// +/// 消息 JSON 序列化器。 +/// +public sealed class JsonMessageSerializer +{ + private static readonly JsonSerializerOptions DefaultOptions = new(JsonSerializerDefaults.Web); + + /// + /// 序列化消息。 + /// + public byte[] Serialize(T message) => Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message, DefaultOptions)); + + /// + /// 反序列化消息。 + /// + public T? Deserialize(byte[] body) => JsonSerializer.Deserialize(body, DefaultOptions); +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs new file mode 100644 index 0000000..ad844c2 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using TakeoutSaaS.Module.Messaging.Options; + +namespace TakeoutSaaS.Module.Messaging.Services; + +/// +/// RabbitMQ 连接工厂封装。 +/// +public sealed class RabbitMqConnectionFactory(IOptionsMonitor optionsMonitor) +{ + /// + /// 创建连接。 + /// + public IConnection CreateConnection() + { + var options = optionsMonitor.CurrentValue; + var factory = new ConnectionFactory + { + HostName = options.Host, + Port = options.Port, + UserName = options.Username, + Password = options.Password, + VirtualHost = options.VirtualHost, + DispatchConsumersAsync = true + }; + + return factory.CreateConnection(); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs new file mode 100644 index 0000000..113ee3c --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs @@ -0,0 +1,66 @@ +using System; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using TakeoutSaaS.Module.Messaging.Abstractions; +using TakeoutSaaS.Module.Messaging.Options; +using TakeoutSaaS.Module.Messaging.Serialization; + +namespace TakeoutSaaS.Module.Messaging.Services; + +/// +/// RabbitMQ 消息发布实现。 +/// +public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectionFactory, IOptionsMonitor optionsMonitor, JsonMessageSerializer serializer, ILogger logger) + : IMessagePublisher, IAsyncDisposable +{ + private IConnection? _connection; + private IModel? _channel; + private bool _disposed; + + /// + public Task PublishAsync(string routingKey, T message, CancellationToken cancellationToken = default) + { + EnsureChannel(); + var options = optionsMonitor.CurrentValue; + + _channel!.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); + var body = serializer.Serialize(message); + var props = _channel.CreateBasicProperties(); + props.ContentType = "application/json"; + props.DeliveryMode = 2; + props.MessageId = Guid.NewGuid().ToString("N"); + + _channel.BasicPublish(options.Exchange, routingKey, props, body); + logger.LogDebug("发布消息到交换机 {Exchange} RoutingKey {RoutingKey}", options.Exchange, routingKey); + return Task.CompletedTask; + } + + private void EnsureChannel() + { + if (_channel != null && _channel.IsOpen) + { + return; + } + + _connection ??= connectionFactory.CreateConnection(); + _channel = _connection.CreateModel(); + } + + /// + /// 释放 RabbitMQ 资源。 + /// + public ValueTask DisposeAsync() + { + if (_disposed) + { + return ValueTask.CompletedTask; + } + + _disposed = true; + _channel?.Dispose(); + _connection?.Dispose(); + return ValueTask.CompletedTask; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs new file mode 100644 index 0000000..88f19a9 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using TakeoutSaaS.Module.Messaging.Abstractions; +using TakeoutSaaS.Module.Messaging.Options; +using TakeoutSaaS.Module.Messaging.Serialization; + +namespace TakeoutSaaS.Module.Messaging.Services; + +/// +/// RabbitMQ 消费者实现。 +/// +public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connectionFactory, IOptionsMonitor optionsMonitor, JsonMessageSerializer serializer, ILogger logger) + : IMessageSubscriber +{ + private IConnection? _connection; + private IModel? _channel; + private bool _disposed; + + /// + public async Task SubscribeAsync(string queue, string routingKey, Func> handler, CancellationToken cancellationToken = default) + { + EnsureChannel(); + var options = optionsMonitor.CurrentValue; + + _channel!.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); + _channel.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false); + _channel.QueueBind(queue, options.Exchange, routingKey); + _channel.BasicQos(0, options.PrefetchCount, global: false); + + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.Received += async (_, ea) => + { + var message = serializer.Deserialize(ea.Body.ToArray()); + if (message == null) + { + _channel.BasicAck(ea.DeliveryTag, multiple: false); + return; + } + + var success = false; + try + { + success = await handler(message, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError(ex, "处理消息失败:{RoutingKey}", ea.RoutingKey); + } + + if (success) + { + _channel.BasicAck(ea.DeliveryTag, multiple: false); + } + else + { + _channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); + } + }; + + _channel.BasicConsume(queue, autoAck: false, consumer); + await Task.CompletedTask.ConfigureAwait(false); + } + + private void EnsureChannel() + { + if (_channel != null && _channel.IsOpen) + { + return; + } + + _connection ??= connectionFactory.CreateConnection(); + _channel = _connection.CreateModel(); + } + + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + await Task.Run(() => + { + _channel?.Dispose(); + _connection?.Dispose(); + }).ConfigureAwait(false); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj index 4e9d749..ef94b34 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj +++ b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj @@ -5,10 +5,15 @@ enable + + + + + + - diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Abstractions/IRecurringJobRegistrar.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Abstractions/IRecurringJobRegistrar.cs new file mode 100644 index 0000000..79f5a29 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Abstractions/IRecurringJobRegistrar.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace TakeoutSaaS.Module.Scheduler.Abstractions; + +/// +/// 周期性任务注册抽象。 +/// +public interface IRecurringJobRegistrar +{ + /// + /// 注册所有预设的周期性任务。 + /// + Task RegisterAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs new file mode 100644 index 0000000..f80b65d --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs @@ -0,0 +1,68 @@ +using Hangfire; +using Hangfire.PostgreSql; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Scheduler.Abstractions; +using TakeoutSaaS.Module.Scheduler.HostedServices; +using TakeoutSaaS.Module.Scheduler.Jobs; +using TakeoutSaaS.Module.Scheduler.Options; +using TakeoutSaaS.Module.Scheduler.Services; + +namespace TakeoutSaaS.Module.Scheduler.Extensions; + +/// +/// 调度模块注册扩展(默认 Hangfire)。 +/// +public static class SchedulerServiceCollectionExtensions +{ + /// + /// 注册调度模块。 + /// + public static IServiceCollection AddSchedulerModule(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection("Scheduler")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddHangfire((serviceProvider, config) => + { + var options = serviceProvider.GetRequiredService>().CurrentValue; + config + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UsePostgreSqlStorage(options.ConnectionString); + }); + + services.AddHangfireServer((serviceProvider, options) => + { + var scheduler = serviceProvider.GetRequiredService>().CurrentValue; + options.WorkerCount = scheduler.WorkerCount ?? options.WorkerCount; + }); + + services.AddSingleton(); + services.AddHostedService(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + + /// + /// 启用 Hangfire Dashboard(默认关闭,可通过配置开启)。 + /// + public static IApplicationBuilder UseSchedulerDashboard(this IApplicationBuilder app, IConfiguration configuration) + { + var options = configuration.GetSection("Scheduler").Get(); + if (options is { DashboardEnabled: true }) + { + app.UseHangfireDashboard(options.DashboardPath); + } + + return app; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/HostedServices/RecurringJobHostedService.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/HostedServices/RecurringJobHostedService.cs new file mode 100644 index 0000000..b2dca9b --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/HostedServices/RecurringJobHostedService.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Module.Scheduler.Abstractions; + +namespace TakeoutSaaS.Module.Scheduler.HostedServices; + +/// +/// 启动时注册周期性任务的宿主服务。 +/// +public sealed class RecurringJobHostedService(IRecurringJobRegistrar registrar, ILogger logger) : IHostedService +{ + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + await registrar.RegisterAsync(cancellationToken).ConfigureAwait(false); + logger.LogInformation("调度任务已注册"); + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/CouponExpireJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/CouponExpireJob.cs new file mode 100644 index 0000000..294c2da --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/CouponExpireJob.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; + +namespace TakeoutSaaS.Module.Scheduler.Jobs; + +/// +/// 优惠券过期处理任务(占位实现)。 +/// +public sealed class CouponExpireJob(ILogger logger) +{ + /// + /// 执行优惠券过期清理。 + /// + public Task ExecuteAsync() + { + logger.LogInformation("定时任务:处理已过期优惠券(占位实现)"); + return Task.CompletedTask; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/LogCleanupJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/LogCleanupJob.cs new file mode 100644 index 0000000..aa67c70 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/LogCleanupJob.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; + +namespace TakeoutSaaS.Module.Scheduler.Jobs; + +/// +/// 日志清理任务(占位实现)。 +/// +public sealed class LogCleanupJob(ILogger logger) +{ + /// + /// 执行日志清理。 + /// + public Task ExecuteAsync() + { + logger.LogInformation("定时任务:清理历史日志(占位实现)"); + return Task.CompletedTask; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/OrderTimeoutJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/OrderTimeoutJob.cs new file mode 100644 index 0000000..80d7513 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/OrderTimeoutJob.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; + +namespace TakeoutSaaS.Module.Scheduler.Jobs; + +/// +/// 订单超时取消任务(占位,后续接入订单服务)。 +/// +public sealed class OrderTimeoutJob(ILogger logger) +{ + /// + /// 执行超时订单检查。 + /// + public Task ExecuteAsync() + { + logger.LogInformation("定时任务:检查超时未支付订单并取消(占位实现)"); + return Task.CompletedTask; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Options/SchedulerOptions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Options/SchedulerOptions.cs new file mode 100644 index 0000000..880790c --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Options/SchedulerOptions.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Scheduler.Options; + +/// +/// 调度模块配置。 +/// +public sealed class SchedulerOptions +{ + /// + /// Hangfire 存储使用的连接字符串。 + /// + [Required] + public string ConnectionString { get; set; } = string.Empty; + + /// + /// 工作线程数,默认根据 CPU 计算。 + /// + [Range(1, 100)] + public int? WorkerCount { get; set; } + + /// + /// 是否启用 Dashboard(默认 false,待 AdminUI 接入)。 + /// + public bool DashboardEnabled { get; set; } + + /// + /// Dashboard 路径。 + /// + public string DashboardPath { get; set; } = "/hangfire"; +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs new file mode 100644 index 0000000..1da22a0 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs @@ -0,0 +1,37 @@ +using Hangfire; +using TakeoutSaaS.Module.Scheduler.Abstractions; +using TakeoutSaaS.Module.Scheduler.Jobs; + +namespace TakeoutSaaS.Module.Scheduler.Services; + +/// +/// 周期性任务注册器。 +/// +public sealed class RecurringJobRegistrar : IRecurringJobRegistrar +{ + private readonly OrderTimeoutJob _orderTimeoutJob; + private readonly CouponExpireJob _couponExpireJob; + private readonly LogCleanupJob _logCleanupJob; + + /// + /// 初始化注册器。 + /// + public RecurringJobRegistrar( + OrderTimeoutJob orderTimeoutJob, + CouponExpireJob couponExpireJob, + LogCleanupJob logCleanupJob) + { + _orderTimeoutJob = orderTimeoutJob; + _couponExpireJob = couponExpireJob; + _logCleanupJob = logCleanupJob; + } + + /// + public Task RegisterAsync(CancellationToken cancellationToken = default) + { + RecurringJob.AddOrUpdate("orders.timeout-cancel", () => _orderTimeoutJob.ExecuteAsync(), "*/5 * * * *"); + RecurringJob.AddOrUpdate("coupons.expire", () => _couponExpireJob.ExecuteAsync(), "0 */1 * * *"); + RecurringJob.AddOrUpdate("logs.cleanup", () => _logCleanupJob.ExecuteAsync(), "0 3 * * *"); + return Task.CompletedTask; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj index b407eac..8e4c663 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj @@ -4,8 +4,16 @@ enable enable + + + + + + + + + - diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSender.cs b/src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSender.cs new file mode 100644 index 0000000..5cf7dd5 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSender.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Module.Sms.Models; + +namespace TakeoutSaaS.Module.Sms.Abstractions; + +/// +/// 短信发送抽象。 +/// +public interface ISmsSender +{ + /// + /// 服务商类型。 + /// + SmsProviderKind Provider { get; } + + /// + /// 发送短信。 + /// + Task SendAsync(SmsSendRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSenderResolver.cs b/src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSenderResolver.cs new file mode 100644 index 0000000..f3385d2 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Abstractions/ISmsSenderResolver.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Module.Sms.Abstractions; + +/// +/// 短信服务商解析器。 +/// +public interface ISmsSenderResolver +{ + /// + /// 获取指定服务商的发送器。 + /// + ISmsSender Resolve(SmsProviderKind? provider = null); +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Extensions/SmsServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Sms/Extensions/SmsServiceCollectionExtensions.cs new file mode 100644 index 0000000..651c143 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Extensions/SmsServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Module.Sms.Abstractions; +using TakeoutSaaS.Module.Sms.Options; +using TakeoutSaaS.Module.Sms.Services; + +namespace TakeoutSaaS.Module.Sms.Extensions; + +/// +/// 短信模块 DI 注册扩展。 +/// +public static class SmsServiceCollectionExtensions +{ + /// + /// 注册短信模块(包含腾讯云、阿里云实现)。 + /// + public static IServiceCollection AddSmsModule(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection("Sms")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddHttpClient(nameof(TencentSmsSender)); + services.AddHttpClient(nameof(AliyunSmsSender)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendRequest.cs b/src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendRequest.cs new file mode 100644 index 0000000..643f299 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendRequest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace TakeoutSaaS.Module.Sms.Models; + +/// +/// 短信发送请求。 +/// +public sealed class SmsSendRequest +{ + /// + /// 初始化短信发送请求。 + /// + /// 目标手机号码(含国家码,如 +86xxxxxxxxxxx)。 + /// 模版编号。 + /// 模版变量。 + /// 短信签名。 + public SmsSendRequest(string phoneNumber, string templateCode, IDictionary variables, string? signName = null) + { + PhoneNumber = phoneNumber; + TemplateCode = templateCode; + Variables = new Dictionary(variables); + SignName = signName; + } + + /// + /// 目标手机号。 + /// + public string PhoneNumber { get; } + + /// + /// 模版编号。 + /// + public string TemplateCode { get; } + + /// + /// 模版变量。 + /// + public IReadOnlyDictionary Variables { get; } + + /// + /// 可选的签名。 + /// + public string? SignName { get; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendResult.cs b/src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendResult.cs new file mode 100644 index 0000000..afc81fb --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Models/SmsSendResult.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Module.Sms.Models; + +/// +/// 短信发送结果。 +/// +public sealed class SmsSendResult +{ + /// + /// 是否发送成功。 + /// + public bool Success { get; init; } + + /// + /// 平台返回的请求标识。 + /// + public string? RequestId { get; init; } + + /// + /// 描述信息。 + /// + public string? Message { get; init; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Options/AliyunSmsOptions.cs b/src/Modules/TakeoutSaaS.Module.Sms/Options/AliyunSmsOptions.cs new file mode 100644 index 0000000..cfa09b1 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Options/AliyunSmsOptions.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Sms.Options; + +/// +/// 阿里云短信配置。 +/// +public sealed class AliyunSmsOptions +{ + /// + /// AccessKeyId。 + /// + [Required] + public string AccessKeyId { get; set; } = string.Empty; + + /// + /// AccessKeySecret。 + /// + [Required] + public string AccessKeySecret { get; set; } = string.Empty; + + /// + /// 短信服务域名。 + /// + public string Endpoint { get; set; } = "dysmsapi.aliyuncs.com"; + + /// + /// 默认签名。 + /// + public string? SignName { get; set; } + + /// + /// 地域 ID。 + /// + public string Region { get; set; } = "cn-hangzhou"; +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Options/SmsOptions.cs b/src/Modules/TakeoutSaaS.Module.Sms/Options/SmsOptions.cs new file mode 100644 index 0000000..5520760 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Options/SmsOptions.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Module.Sms; + +namespace TakeoutSaaS.Module.Sms.Options; + +/// +/// 短信模块配置。 +/// +public sealed class SmsOptions +{ + /// + /// 默认服务商,默认为腾讯云。 + /// + public SmsProviderKind Provider { get; set; } = SmsProviderKind.Tencent; + + /// + /// 默认签名。 + /// + public string? DefaultSignName { get; set; } + + /// + /// 是否启用模拟发送(仅日志,不实际调用),方便开发环境。 + /// + public bool UseMock { get; set; } + + /// + /// 腾讯云短信配置。 + /// + [Required] + public TencentSmsOptions Tencent { get; set; } = new(); + + /// + /// 阿里云短信配置。 + /// + [Required] + public AliyunSmsOptions Aliyun { get; set; } = new(); + + /// + /// 场景与模板映射(如 login: TEMPLATE_ID)。 + /// + public Dictionary SceneTemplates { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Options/TencentSmsOptions.cs b/src/Modules/TakeoutSaaS.Module.Sms/Options/TencentSmsOptions.cs new file mode 100644 index 0000000..3e02bd4 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Options/TencentSmsOptions.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Sms.Options; + +/// +/// 腾讯云短信配置。 +/// +public sealed class TencentSmsOptions +{ + /// + /// SecretId。 + /// + [Required] + public string SecretId { get; set; } = string.Empty; + + /// + /// SecretKey。 + /// + [Required] + public string SecretKey { get; set; } = string.Empty; + + /// + /// 应用 SdkAppId。 + /// + [Required] + public string SdkAppId { get; set; } = string.Empty; + + /// + /// 默认签名。 + /// + public string? SignName { get; set; } + + /// + /// 默认地域。 + /// + public string Region { get; set; } = "ap-guangzhou"; + + /// + /// 接口域名。 + /// + public string Endpoint { get; set; } = "https://sms.tencentcloudapi.com"; +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Services/AliyunSmsSender.cs b/src/Modules/TakeoutSaaS.Module.Sms/Services/AliyunSmsSender.cs new file mode 100644 index 0000000..c94a47c --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Services/AliyunSmsSender.cs @@ -0,0 +1,35 @@ +using System.Net.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Sms.Abstractions; +using TakeoutSaaS.Module.Sms.Models; +using TakeoutSaaS.Module.Sms.Options; + +namespace TakeoutSaaS.Module.Sms.Services; + +/// +/// 阿里云短信发送实现(简化版,占位可扩展正式签名流程)。 +/// +public sealed class AliyunSmsSender(IHttpClientFactory httpClientFactory, IOptionsMonitor optionsMonitor, ILogger logger) + : ISmsSender +{ + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + + /// + public SmsProviderKind Provider => SmsProviderKind.Aliyun; + + /// + public Task SendAsync(SmsSendRequest request, CancellationToken cancellationToken = default) + { + var options = optionsMonitor.CurrentValue; + if (options.UseMock) + { + logger.LogInformation("Mock 发送阿里云短信到 {Phone}, Template:{Template}", request.PhoneNumber, request.TemplateCode); + return Task.FromResult(new SmsSendResult { Success = true, Message = "Mocked" }); + } + + // 占位:保留待接入阿里云正式签名流程,当前返回未实现。 + logger.LogWarning("阿里云短信尚未启用,请配置腾讯云或开启 UseMock。"); + return Task.FromResult(new SmsSendResult { Success = false, Message = "Aliyun SMS not enabled" }); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Services/SmsSenderResolver.cs b/src/Modules/TakeoutSaaS.Module.Sms/Services/SmsSenderResolver.cs new file mode 100644 index 0000000..e47f6be --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Services/SmsSenderResolver.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Sms.Abstractions; +using TakeoutSaaS.Module.Sms.Options; + +namespace TakeoutSaaS.Module.Sms.Services; + +/// +/// 短信服务商解析器。 +/// +public sealed class SmsSenderResolver(IOptionsMonitor optionsMonitor, IEnumerable senders) : ISmsSenderResolver +{ + private readonly IReadOnlyDictionary _map = senders.ToDictionary(x => x.Provider); + + /// + public ISmsSender Resolve(SmsProviderKind? provider = null) + { + var key = provider ?? optionsMonitor.CurrentValue.Provider; + if (_map.TryGetValue(key, out var sender)) + { + return sender; + } + + throw new InvalidOperationException($"未注册短信服务商:{key}"); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/Services/TencentSmsSender.cs b/src/Modules/TakeoutSaaS.Module.Sms/Services/TencentSmsSender.cs new file mode 100644 index 0000000..f7b9737 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/Services/TencentSmsSender.cs @@ -0,0 +1,136 @@ +using System.Globalization; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Sms.Abstractions; +using TakeoutSaaS.Module.Sms.Models; +using TakeoutSaaS.Module.Sms.Options; + +namespace TakeoutSaaS.Module.Sms.Services; + +/// +/// 腾讯云短信发送实现(TC3-HMAC 签名)。 +/// +public sealed class TencentSmsSender(IHttpClientFactory httpClientFactory, IOptionsMonitor optionsMonitor, ILogger logger) + : ISmsSender +{ + private const string Service = "sms"; + private const string Action = "SendSms"; + private const string Version = "2021-01-11"; + + /// + public SmsProviderKind Provider => SmsProviderKind.Tencent; + + /// + public async Task SendAsync(SmsSendRequest request, CancellationToken cancellationToken = default) + { + var options = optionsMonitor.CurrentValue; + if (options.UseMock) + { + logger.LogInformation("Mock 发送短信到 {Phone}, Template:{Template}, Vars:{Vars}", request.PhoneNumber, request.TemplateCode, JsonSerializer.Serialize(request.Variables)); + return new SmsSendResult { Success = true, Message = "Mocked" }; + } + + var tencent = options.Tencent; + var payload = BuildPayload(request, tencent); + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var date = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + var host = new Uri(tencent.Endpoint).Host; + var canonicalRequest = BuildCanonicalRequest(payload, host, tencent.Endpoint.StartsWith("https", StringComparison.OrdinalIgnoreCase)); + var stringToSign = BuildStringToSign(canonicalRequest, timestamp, date); + var signature = Sign(stringToSign, tencent.SecretKey, date); + + using var httpClient = httpClientFactory.CreateClient(nameof(TencentSmsSender)); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, tencent.Endpoint) + { + Content = new StringContent(payload, Encoding.UTF8, "application/json") + }; + + httpRequest.Headers.Add("Host", host); + httpRequest.Headers.Add("X-TC-Action", Action); + httpRequest.Headers.Add("X-TC-Version", Version); + httpRequest.Headers.Add("X-TC-Timestamp", timestamp.ToString(CultureInfo.InvariantCulture)); + httpRequest.Headers.Add("X-TC-Region", tencent.Region); + httpRequest.Headers.Add("Authorization", + $"TC3-HMAC-SHA256 Credential={tencent.SecretId}/{date}/{Service}/tc3_request, SignedHeaders=content-type;host, Signature={signature}"); + + var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + logger.LogWarning("腾讯云短信发送失败:{Status} {Content}", response.StatusCode, content); + return new SmsSendResult { Success = false, Message = content }; + } + + using var doc = JsonDocument.Parse(content); + var root = doc.RootElement.GetProperty("Response"); + var status = root.GetProperty("SendStatusSet")[0]; + var code = status.GetProperty("Code").GetString(); + var message = status.GetProperty("Message").GetString(); + var requestId = root.GetProperty("RequestId").GetString(); + + var success = string.Equals(code, "Ok", StringComparison.OrdinalIgnoreCase); + return new SmsSendResult + { + Success = success, + RequestId = requestId, + Message = message + }; + } + + private static string BuildPayload(SmsSendRequest request, TencentSmsOptions options) + { + var payload = new + { + PhoneNumberSet = new[] { request.PhoneNumber }, + SmsSdkAppId = options.SdkAppId, + SignName = request.SignName ?? options.SignName, + TemplateId = request.TemplateCode, + TemplateParamSet = request.Variables.Values.ToArray() + }; + + return JsonSerializer.Serialize(payload); + } + + private static string BuildCanonicalRequest(string payload, string host, bool useHttps) + { + _ = useHttps; + var hashedPayload = HashSha256(payload); + var canonicalHeaders = $"content-type:application/json\nhost:{host}\n"; + return $"POST\n/\n\n{canonicalHeaders}\ncontent-type;host\n{hashedPayload}"; + } + + private static string BuildStringToSign(string canonicalRequest, long timestamp, string date) + { + var hashedRequest = HashSha256(canonicalRequest); + return $"TC3-HMAC-SHA256\n{timestamp}\n{date}/{Service}/tc3_request\n{hashedRequest}"; + } + + private static string Sign(string stringToSign, string secretKey, string date) + { + static byte[] HmacSha256(byte[] key, string msg) => new HMACSHA256(key).ComputeHash(Encoding.UTF8.GetBytes(msg)); + + var secretDate = HmacSha256(Encoding.UTF8.GetBytes($"TC3{secretKey}"), date); + var secretService = HmacSha256(secretDate, Service); + var secretSigning = HmacSha256(secretService, "tc3_request"); + var signatureBytes = new HMACSHA256(secretSigning).ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); + return Convert.ToHexString(signatureBytes).ToLowerInvariant(); + } + + private static string HashSha256(string raw) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); + var builder = new StringBuilder(bytes.Length * 2); + foreach (var b in bytes) + { + builder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); + } + + return builder.ToString(); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/SmsProviderKind.cs b/src/Modules/TakeoutSaaS.Module.Sms/SmsProviderKind.cs new file mode 100644 index 0000000..e374d12 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Sms/SmsProviderKind.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Module.Sms; + +/// +/// 短信服务商类型。 +/// +public enum SmsProviderKind +{ + /// + /// 腾讯云短信。 + /// + Tencent = 1, + + /// + /// 阿里云短信。 + /// + Aliyun = 2 +} diff --git a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj index b407eac..7ac9fcd 100644 --- a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj +++ b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj @@ -4,8 +4,16 @@ enable enable + + + + + + + + + - diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IObjectStorageProvider.cs b/src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IObjectStorageProvider.cs new file mode 100644 index 0000000..c53009c --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IObjectStorageProvider.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Module.Storage.Models; + +namespace TakeoutSaaS.Module.Storage.Abstractions; + +/// +/// 对象存储提供商统一抽象。 +/// +public interface IObjectStorageProvider +{ + /// + /// 当前提供商类型。 + /// + StorageProviderKind Kind { get; } + + /// + /// 上传文件到对象存储。 + /// + Task UploadAsync(StorageUploadRequest request, CancellationToken cancellationToken = default); + + /// + /// 生成预签名直传参数(PUT 或表单直传)。 + /// + Task CreateDirectUploadAsync(StorageDirectUploadRequest request, CancellationToken cancellationToken = default); + + /// + /// 生成带过期时间的访问链接。 + /// + Task GenerateDownloadUrlAsync(string objectKey, TimeSpan expires, CancellationToken cancellationToken = default); + + /// + /// 生成公共访问地址(可结合 CDN)。 + /// + string BuildPublicUrl(string objectKey); +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IStorageProviderResolver.cs b/src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IStorageProviderResolver.cs new file mode 100644 index 0000000..63ae4cb --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Abstractions/IStorageProviderResolver.cs @@ -0,0 +1,14 @@ +namespace TakeoutSaaS.Module.Storage.Abstractions; + +/// +/// 存储提供商解析器,用于按需选择具体实现。 +/// +public interface IStorageProviderResolver +{ + /// + /// 根据配置解析出可用的存储提供商。 + /// + /// 目标提供商类型,空则使用默认配置。 + /// 对应的存储提供商。 + IObjectStorageProvider Resolve(StorageProviderKind? provider = null); +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Extensions/StorageServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Storage/Extensions/StorageServiceCollectionExtensions.cs new file mode 100644 index 0000000..e1972f1 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Extensions/StorageServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Module.Storage.Abstractions; +using TakeoutSaaS.Module.Storage.Options; +using TakeoutSaaS.Module.Storage.Providers; +using TakeoutSaaS.Module.Storage.Services; + +namespace TakeoutSaaS.Module.Storage.Extensions; + +/// +/// 存储模块服务注册扩展。 +/// +public static class StorageServiceCollectionExtensions +{ + /// + /// 注册存储模块所需的提供商与配置。 + /// + /// 服务集合。 + /// 配置源。 + public static IServiceCollection AddStorageModule(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection("Storage")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs new file mode 100644 index 0000000..2d8df5b --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs @@ -0,0 +1,42 @@ +namespace TakeoutSaaS.Module.Storage.Models; + +/// +/// 直传(预签名上传)请求参数。 +/// +public sealed class StorageDirectUploadRequest +{ + /// + /// 初始化请求。 + /// + /// 对象键。 + /// 内容类型。 + /// 内容长度。 + /// 签名有效期。 + public StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires) + { + ObjectKey = objectKey; + ContentType = contentType; + ContentLength = contentLength; + Expires = expires; + } + + /// + /// 目标对象键。 + /// + public string ObjectKey { get; } + + /// + /// 内容类型。 + /// + public string ContentType { get; } + + /// + /// 内容长度。 + /// + public long ContentLength { get; } + + /// + /// 签名有效期。 + /// + public TimeSpan Expires { get; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadResult.cs b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadResult.cs new file mode 100644 index 0000000..8bfade5 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadResult.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace TakeoutSaaS.Module.Storage.Models; + +/// +/// 直传(预签名上传)结果。 +/// +public sealed class StorageDirectUploadResult +{ + /// + /// 预签名上传地址(PUT 上传或表单地址)。 + /// + public string UploadUrl { get; init; } = string.Empty; + + /// + /// 直传附加字段(如表单直传所需字段),PUT 方式为空。 + /// + public IReadOnlyDictionary FormFields { get; init; } = new Dictionary(); + + /// + /// 预签名过期时间。 + /// + public DateTimeOffset ExpiresAt { get; init; } + + /// + /// 关联的对象键。 + /// + public string ObjectKey { get; init; } = string.Empty; + + /// + /// 上传成功后可选的签名下载地址。 + /// + public string? SignedDownloadUrl { get; init; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs new file mode 100644 index 0000000..80a2644 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.IO; + +namespace TakeoutSaaS.Module.Storage.Models; + +/// +/// 对象存储上传请求参数。 +/// +public sealed class StorageUploadRequest +{ + /// + /// 初始化上传请求。 + /// + /// 对象键(含路径)。 + /// 文件流。 + /// 内容类型。 + /// 内容长度。 + /// 是否返回签名访问链接。 + /// 签名有效期。 + /// 附加元数据。 + public StorageUploadRequest( + string objectKey, + Stream content, + string contentType, + long contentLength, + bool generateSignedUrl, + TimeSpan signedUrlExpires, + IDictionary? metadata = null) + { + ObjectKey = objectKey; + Content = content; + ContentType = contentType; + ContentLength = contentLength; + GenerateSignedUrl = generateSignedUrl; + SignedUrlExpires = signedUrlExpires; + Metadata = metadata == null + ? new Dictionary() + : new Dictionary(metadata); + } + + /// + /// 对象键。 + /// + public string ObjectKey { get; } + + /// + /// 文件流。 + /// + public Stream Content { get; } + + /// + /// 内容类型。 + /// + public string ContentType { get; } + + /// + /// 内容长度。 + /// + public long ContentLength { get; } + + /// + /// 是否需要签名访问链接。 + /// + public bool GenerateSignedUrl { get; } + + /// + /// 签名有效期。 + /// + public TimeSpan SignedUrlExpires { get; } + + /// + /// 元数据集合。 + /// + public IReadOnlyDictionary Metadata { get; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadResult.cs b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadResult.cs new file mode 100644 index 0000000..c3af710 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadResult.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Module.Storage.Models; + +/// +/// 上传结果信息。 +/// +public sealed class StorageUploadResult +{ + /// + /// 对象键。 + /// + public string ObjectKey { get; init; } = string.Empty; + + /// + /// 可访问的 URL(可能已包含签名)。 + /// + public string Url { get; init; } = string.Empty; + + /// + /// 带过期时间的签名 URL(若生成)。 + /// + public string? SignedUrl { get; init; } + + /// + /// 文件大小。 + /// + public long FileSize { get; init; } + + /// + /// 内容类型。 + /// + public string ContentType { get; init; } = string.Empty; +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Options/AliyunOssOptions.cs b/src/Modules/TakeoutSaaS.Module.Storage/Options/AliyunOssOptions.cs new file mode 100644 index 0000000..d17f548 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Options/AliyunOssOptions.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Storage.Options; + +/// +/// 阿里云 OSS 访问配置。 +/// +public sealed class AliyunOssOptions +{ + /// + /// 访问密钥 ID。 + /// + [Required] + public string AccessKeyId { get; set; } = string.Empty; + + /// + /// 访问密钥 Secret。 + /// + [Required] + public string AccessKeySecret { get; set; } = string.Empty; + + /// + /// Endpoint,如 https://oss-cn-hangzhou.aliyuncs.com。 + /// + [Required] + [Url] + public string Endpoint { get; set; } = string.Empty; + + /// + /// 目标存储桶名称。 + /// + [Required] + public string Bucket { get; set; } = string.Empty; + + /// + /// CDN 加速域名(可选)。 + /// + [Url] + public string? CdnBaseUrl { get; set; } + + /// + /// 是否默认使用 HTTPS。 + /// + public bool UseHttps { get; set; } = true; +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Options/QiniuKodoOptions.cs b/src/Modules/TakeoutSaaS.Module.Storage/Options/QiniuKodoOptions.cs new file mode 100644 index 0000000..7e6bf37 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Options/QiniuKodoOptions.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Storage.Options; + +/// +/// 七牛云 Kodo S3 兼容网关配置。 +/// +public sealed class QiniuKodoOptions +{ + /// + /// AccessKey。 + /// + [Required] + public string AccessKey { get; set; } = string.Empty; + + /// + /// SecretKey。 + /// + [Required] + public string SecretKey { get; set; } = string.Empty; + + /// + /// 绑定的空间名称。 + /// + [Required] + public string Bucket { get; set; } = string.Empty; + + /// + /// 下载域名(CDN 域名或测试域名),用于生成访问链接。 + /// + [Url] + public string? DownloadDomain { get; set; } + + /// + /// S3 兼容网关 Endpoint(如 https://s3-cn-south-1.qiniucs.com),为空则使用官方默认。 + /// + [Url] + public string? Endpoint { get; set; } + + /// + /// 是否使用 HTTPS。 + /// + public bool UseHttps { get; set; } = true; + + /// + /// 直传或下载时默认有效期(分钟),未设置时使用全局安全配置。 + /// + [Range(1, 24 * 60)] + public int? SignedUrlExpirationMinutes { get; set; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Options/StorageOptions.cs b/src/Modules/TakeoutSaaS.Module.Storage/Options/StorageOptions.cs new file mode 100644 index 0000000..465b1b7 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Options/StorageOptions.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Storage.Options; + +/// +/// 存储模块的统一配置项,决定默认提供商与全局安全策略。 +/// +public sealed class StorageOptions +{ + /// + /// 默认使用的存储提供商。 + /// + public StorageProviderKind Provider { get; set; } = StorageProviderKind.TencentCos; + + /// + /// CDN 访问域名(可选),若配置则优先使用 CDN 域名生成访问地址。 + /// + [Url] + public string? CdnBaseUrl { get; set; } + + /// + /// 腾讯云 COS 配置。 + /// + [Required] + public TencentCosOptions TencentCos { get; set; } = new(); + + /// + /// 七牛云 Kodo 配置。 + /// + [Required] + public QiniuKodoOptions QiniuKodo { get; set; } = new(); + + /// + /// 阿里云 OSS 配置。 + /// + [Required] + public AliyunOssOptions AliyunOss { get; set; } = new(); + + /// + /// 存储安全策略配置。 + /// + [Required] + public StorageSecurityOptions Security { get; set; } = new(); +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Options/StorageSecurityOptions.cs b/src/Modules/TakeoutSaaS.Module.Storage/Options/StorageSecurityOptions.cs new file mode 100644 index 0000000..0619faf --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Options/StorageSecurityOptions.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Storage.Options; + +/// +/// 文件安全与防盗链相关配置。 +/// +public sealed class StorageSecurityOptions +{ + /// + /// 单个文件最大尺寸(字节),默认 10MB。 + /// + [Range(1, long.MaxValue)] + public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; + + /// + /// 允许的图片后缀名白名单。 + /// + [MinLength(1)] + public string[] AllowedImageExtensions { get; set; } = { ".jpg", ".jpeg", ".png", ".webp", ".gif" }; + + /// + /// 允许的通用文件后缀名白名单。 + /// + [MinLength(1)] + public string[] AllowedFileExtensions { get; set; } = { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" }; + + /// + /// 默认签名有效期(分钟),用于生成带过期时间的访问链接。 + /// + [Range(1, 24 * 60)] + public int DefaultUrlExpirationMinutes { get; set; } = 30; + + /// + /// 是否启用来源校验(防盗链),为空则不校验。 + /// + public bool EnableRefererValidation { get; set; } = true; + + /// + /// 允许的 Referer/Origin 前缀列表,用于限制上传接口调用来源。 + /// + public string[] AllowedReferers { get; set; } = Array.Empty(); + + /// + /// 针对 CDN 防盗链的额外签名密钥(可选),用于生成二次校验签名。 + /// + public string? AntiLeechTokenSecret { get; set; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Options/TencentCosOptions.cs b/src/Modules/TakeoutSaaS.Module.Storage/Options/TencentCosOptions.cs new file mode 100644 index 0000000..0808821 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Options/TencentCosOptions.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Module.Storage.Options; + +/// +/// 腾讯云 COS 访问配置。 +/// +public sealed class TencentCosOptions +{ + /// + /// SecretId。 + /// + [Required] + public string SecretId { get; set; } = string.Empty; + + /// + /// SecretKey。 + /// + [Required] + public string SecretKey { get; set; } = string.Empty; + + /// + /// 存储地域(如 ap-guangzhou)。 + /// + [Required] + public string Region { get; set; } = string.Empty; + + /// + /// 存储桶名称(含 AppId,如 takeout-bucket-123456)。 + /// + [Required] + public string Bucket { get; set; } = string.Empty; + + /// + /// COS 自定义域名或 API Endpoint(可选),未配置则根据 Region 生成默认域名。 + /// + public string? Endpoint { get; set; } + + /// + /// CDN 域名(可选),用于生成加速访问地址。 + /// + [Url] + public string? CdnBaseUrl { get; set; } + + /// + /// 是否使用 HTTPS。 + /// + public bool UseHttps { get; set; } = true; + + /// + /// 是否强制使用 PathStyle 访问,COS 默认可使用虚拟主机形式。 + /// + public bool ForcePathStyle { get; set; } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Providers/AliyunOssStorageProvider.cs b/src/Modules/TakeoutSaaS.Module.Storage/Providers/AliyunOssStorageProvider.cs new file mode 100644 index 0000000..abb74ed --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Providers/AliyunOssStorageProvider.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Aliyun.OSS; +using Aliyun.OSS.Util; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Storage.Abstractions; +using TakeoutSaaS.Module.Storage.Models; +using TakeoutSaaS.Module.Storage.Options; + +namespace TakeoutSaaS.Module.Storage.Providers; + +/// +/// 阿里云 OSS 存储提供商实现。 +/// +public sealed class AliyunOssStorageProvider(IOptionsMonitor optionsMonitor) : IObjectStorageProvider, IDisposable +{ + private OssClient? _client; + private bool _disposed; + + private StorageOptions CurrentOptions => optionsMonitor.CurrentValue; + + /// + public StorageProviderKind Kind => StorageProviderKind.AliyunOss; + + /// + public async Task UploadAsync(StorageUploadRequest request, CancellationToken cancellationToken = default) + { + var options = CurrentOptions; + var metadata = new ObjectMetadata + { + ContentLength = request.ContentLength, + ContentType = request.ContentType + }; + + foreach (var kv in request.Metadata) + { + metadata.UserMetadata[kv.Key] = kv.Value; + } + + // Aliyun OSS SDK 支持异步方法,如未支持将同步封装为任务。 + await PutObjectAsync(options.AliyunOss.Bucket, request.ObjectKey, request.Content, metadata, cancellationToken) + .ConfigureAwait(false); + + var signedUrl = request.GenerateSignedUrl + ? await GenerateDownloadUrlAsync(request.ObjectKey, request.SignedUrlExpires, cancellationToken).ConfigureAwait(false) + : null; + + return new StorageUploadResult + { + ObjectKey = request.ObjectKey, + Url = signedUrl ?? BuildPublicUrl(request.ObjectKey), + SignedUrl = signedUrl, + FileSize = request.ContentLength, + ContentType = request.ContentType + }; + } + + /// + public Task CreateDirectUploadAsync(StorageDirectUploadRequest request, CancellationToken cancellationToken = default) + { + var expiresAt = DateTimeOffset.UtcNow.Add(request.Expires); + var uploadUrl = GeneratePresignedUrl(request.ObjectKey, request.Expires, SignHttpMethod.Put, request.ContentType); + var downloadUrl = GeneratePresignedUrl(request.ObjectKey, request.Expires, SignHttpMethod.Get, null); + + var result = new StorageDirectUploadResult + { + UploadUrl = uploadUrl, + FormFields = new Dictionary(), + ExpiresAt = expiresAt, + ObjectKey = request.ObjectKey, + SignedDownloadUrl = downloadUrl + }; + + return Task.FromResult(result); + } + + /// + public Task GenerateDownloadUrlAsync(string objectKey, TimeSpan expires, CancellationToken cancellationToken = default) + { + var url = GeneratePresignedUrl(objectKey, expires, SignHttpMethod.Get, null); + return Task.FromResult(url); + } + + /// + public string BuildPublicUrl(string objectKey) + { + var cdn = CurrentOptions.AliyunOss.CdnBaseUrl ?? CurrentOptions.CdnBaseUrl; + if (!string.IsNullOrWhiteSpace(cdn)) + { + return $"{cdn!.TrimEnd('/')}/{objectKey}"; + } + + var endpoint = CurrentOptions.AliyunOss.Endpoint.TrimEnd('/'); + var scheme = CurrentOptions.AliyunOss.UseHttps ? "https" : "http"; + // Endpoint 可能已包含协议,若没有则补充。 + if (!endpoint.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + endpoint = $"{scheme}://{endpoint}"; + } + + return $"{endpoint}/{CurrentOptions.AliyunOss.Bucket}/{objectKey}"; + } + + /// + /// 上传对象到 OSS。 + /// + private async Task PutObjectAsync(string bucket, string key, Stream content, ObjectMetadata metadata, CancellationToken cancellationToken) + { + var client = EnsureClient(); + await Task.Run(() => client.PutObject(bucket, key, content, metadata), cancellationToken).ConfigureAwait(false); + } + + /// + /// 生成预签名 URL。 + /// + private string GeneratePresignedUrl(string objectKey, TimeSpan expires, SignHttpMethod method, string? contentType) + { + var request = new GeneratePresignedUriRequest(CurrentOptions.AliyunOss.Bucket, objectKey, method) + { + Expiration = DateTime.Now.Add(expires) + }; + + if (!string.IsNullOrWhiteSpace(contentType)) + { + request.ContentType = contentType; + } + + var uri = EnsureClient().GeneratePresignedUri(request); + return uri.ToString(); + } + + /// + /// 构建或复用 OSS 客户端。 + /// + private OssClient EnsureClient() + { + if (_client != null) + { + return _client; + } + + var options = CurrentOptions.AliyunOss; + _client = new OssClient(options.Endpoint, options.AccessKeyId, options.AccessKeySecret); + return _client; + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Providers/QiniuKodoStorageProvider.cs b/src/Modules/TakeoutSaaS.Module.Storage/Providers/QiniuKodoStorageProvider.cs new file mode 100644 index 0000000..df6b05d --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Providers/QiniuKodoStorageProvider.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Storage.Options; + +namespace TakeoutSaaS.Module.Storage.Providers; + +/// +/// 七牛云 Kodo(S3 兼容网关)存储提供商。 +/// +public sealed class QiniuKodoStorageProvider(IOptionsMonitor optionsMonitor) + : S3StorageProviderBase +{ + private StorageOptions CurrentOptions => optionsMonitor.CurrentValue; + + /// + public override StorageProviderKind Kind => StorageProviderKind.QiniuKodo; + + /// + protected override string Bucket => CurrentOptions.QiniuKodo.Bucket; + + /// + protected override string ServiceUrl => string.IsNullOrWhiteSpace(CurrentOptions.QiniuKodo.Endpoint) + ? $"{(CurrentOptions.QiniuKodo.UseHttps ? "https" : "http")}://s3.qiniucs.com" + : CurrentOptions.QiniuKodo.Endpoint!; + + /// + protected override string AccessKey => CurrentOptions.QiniuKodo.AccessKey; + + /// + protected override string SecretKey => CurrentOptions.QiniuKodo.SecretKey; + + /// + protected override bool UseHttps => CurrentOptions.QiniuKodo.UseHttps; + + /// + protected override bool ForcePathStyle => true; + + /// + protected override string? CdnBaseUrl => !string.IsNullOrWhiteSpace(CurrentOptions.QiniuKodo.DownloadDomain) + ? CurrentOptions.QiniuKodo.DownloadDomain + : CurrentOptions.CdnBaseUrl; + + /// + protected override TimeSpan SignedUrlExpiry + { + get + { + var minutes = CurrentOptions.QiniuKodo.SignedUrlExpirationMinutes + ?? CurrentOptions.Security.DefaultUrlExpirationMinutes; + return TimeSpan.FromMinutes(Math.Max(1, minutes)); + } + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs b/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs new file mode 100644 index 0000000..6cc7773 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using TakeoutSaaS.Module.Storage.Abstractions; +using TakeoutSaaS.Module.Storage.Models; + +namespace TakeoutSaaS.Module.Storage.Providers; + +/// +/// 基于 AWS S3 SDK 的通用存储提供商基类,可复用到 COS 与 Kodo 等兼容实现。 +/// +public abstract class S3StorageProviderBase : IObjectStorageProvider, IDisposable +{ + private IAmazonS3? _client; + private bool _disposed; + + /// + public abstract StorageProviderKind Kind { get; } + + /// + /// 目标桶名称。 + /// + protected abstract string Bucket { get; } + + /// + /// S3 服务端点,需包含协议。 + /// + protected abstract string ServiceUrl { get; } + + /// + /// 访问凭证 ID。 + /// + protected abstract string AccessKey { get; } + + /// + /// 访问凭证密钥。 + /// + protected abstract string SecretKey { get; } + + /// + /// 是否使用 HTTPS。 + /// + protected abstract bool UseHttps { get; } + + /// + /// 是否强制 PathStyle 访问。 + /// + protected abstract bool ForcePathStyle { get; } + + /// + /// CDN 域名(可选)。 + /// + protected abstract string? CdnBaseUrl { get; } + + /// + /// 默认签名有效期。 + /// + protected abstract TimeSpan SignedUrlExpiry { get; } + + /// + public virtual async Task UploadAsync(StorageUploadRequest request, CancellationToken cancellationToken = default) + { + var putRequest = new PutObjectRequest + { + BucketName = Bucket, + Key = request.ObjectKey, + InputStream = request.Content, + AutoCloseStream = false, + ContentType = request.ContentType + }; + + foreach (var kv in request.Metadata) + { + putRequest.Metadata[kv.Key] = kv.Value; + } + + await Client.PutObjectAsync(putRequest, cancellationToken).ConfigureAwait(false); + + var signedUrl = request.GenerateSignedUrl + ? GenerateSignedUrl(request.ObjectKey, request.SignedUrlExpires) + : null; + + return new StorageUploadResult + { + ObjectKey = request.ObjectKey, + Url = signedUrl ?? BuildPublicUrl(request.ObjectKey), + SignedUrl = signedUrl, + FileSize = request.ContentLength, + ContentType = request.ContentType + }; + } + + /// + public virtual Task CreateDirectUploadAsync(StorageDirectUploadRequest request, CancellationToken cancellationToken = default) + { + var expiresAt = DateTimeOffset.UtcNow.Add(request.Expires); + var uploadUrl = GenerateSignedUrl(request.ObjectKey, request.Expires, HttpVerb.PUT, request.ContentType); + var signedDownload = GenerateSignedUrl(request.ObjectKey, request.Expires); + + var result = new StorageDirectUploadResult + { + UploadUrl = uploadUrl, + FormFields = new Dictionary(), + ExpiresAt = expiresAt, + ObjectKey = request.ObjectKey, + SignedDownloadUrl = signedDownload + }; + + return Task.FromResult(result); + } + + /// + public virtual Task GenerateDownloadUrlAsync(string objectKey, TimeSpan expires, CancellationToken cancellationToken = default) + { + var url = GenerateSignedUrl(objectKey, expires); + return Task.FromResult(url); + } + + /// + public virtual string BuildPublicUrl(string objectKey) + { + if (!string.IsNullOrWhiteSpace(CdnBaseUrl)) + { + return $"{CdnBaseUrl!.TrimEnd('/')}/{objectKey}"; + } + + var endpoint = new Uri(ServiceUrl); + var scheme = UseHttps ? "https" : "http"; + return $"{scheme}://{Bucket}.{endpoint.Host}/{objectKey}"; + } + + /// + /// 生成预签名 URL。 + /// + /// 对象键。 + /// 过期时间。 + /// HTTP 动作。 + /// 可选的内容类型约束。 + protected virtual string GenerateSignedUrl(string objectKey, TimeSpan expires, HttpVerb verb = HttpVerb.GET, string? contentType = null) + { + var request = new GetPreSignedUrlRequest + { + BucketName = Bucket, + Key = objectKey, + Verb = verb, + Expires = DateTime.UtcNow.Add(expires), + Protocol = UseHttps ? Protocol.HTTPS : Protocol.HTTP + }; + + if (!string.IsNullOrWhiteSpace(contentType)) + { + request.Headers["Content-Type"] = contentType; + } + + return Client.GetPreSignedURL(request); + } + + /// + /// 创建 S3 客户端。 + /// + protected virtual IAmazonS3 CreateClient() + { + var config = new AmazonS3Config + { + ServiceURL = ServiceUrl, + ForcePathStyle = ForcePathStyle, + UseHttp = !UseHttps, + SignatureVersion = "4" + }; + + var credentials = new BasicAWSCredentials(AccessKey, SecretKey); + return new AmazonS3Client(credentials, config); + } + + private IAmazonS3 Client => _client ??= CreateClient(); + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _client?.Dispose(); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Providers/TencentCosStorageProvider.cs b/src/Modules/TakeoutSaaS.Module.Storage/Providers/TencentCosStorageProvider.cs new file mode 100644 index 0000000..fdd7795 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Providers/TencentCosStorageProvider.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Storage.Options; + +namespace TakeoutSaaS.Module.Storage.Providers; + +/// +/// 腾讯云 COS 存储提供商实现。 +/// +public sealed class TencentCosStorageProvider(IOptionsMonitor optionsMonitor) + : S3StorageProviderBase +{ + private StorageOptions CurrentOptions => optionsMonitor.CurrentValue; + + /// + public override StorageProviderKind Kind => StorageProviderKind.TencentCos; + + /// + protected override string Bucket => CurrentOptions.TencentCos.Bucket; + + /// + protected override string ServiceUrl => string.IsNullOrWhiteSpace(CurrentOptions.TencentCos.Endpoint) + ? $"{(CurrentOptions.TencentCos.UseHttps ? "https" : "http")}://cos.{CurrentOptions.TencentCos.Region}.myqcloud.com" + : CurrentOptions.TencentCos.Endpoint!; + + /// + protected override string AccessKey => CurrentOptions.TencentCos.SecretId; + + /// + protected override string SecretKey => CurrentOptions.TencentCos.SecretKey; + + /// + protected override bool UseHttps => CurrentOptions.TencentCos.UseHttps; + + /// + protected override bool ForcePathStyle => CurrentOptions.TencentCos.ForcePathStyle; + + /// + protected override string? CdnBaseUrl => !string.IsNullOrWhiteSpace(CurrentOptions.TencentCos.CdnBaseUrl) + ? CurrentOptions.TencentCos.CdnBaseUrl + : CurrentOptions.CdnBaseUrl; + + /// + protected override TimeSpan SignedUrlExpiry => + TimeSpan.FromMinutes(Math.Max(1, CurrentOptions.Security.DefaultUrlExpirationMinutes)); +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Services/StorageProviderResolver.cs b/src/Modules/TakeoutSaaS.Module.Storage/Services/StorageProviderResolver.cs new file mode 100644 index 0000000..598d1ee --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/Services/StorageProviderResolver.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Module.Storage.Abstractions; +using TakeoutSaaS.Module.Storage.Options; + +namespace TakeoutSaaS.Module.Storage.Services; + +/// +/// 存储提供商解析器,实现基于配置的提供商选择。 +/// +public sealed class StorageProviderResolver(IOptionsMonitor optionsMonitor, IEnumerable providers) + : IStorageProviderResolver +{ + private readonly IDictionary _providerMap = + providers.ToDictionary(x => x.Kind, x => x); + + /// + public IObjectStorageProvider Resolve(StorageProviderKind? provider = null) + { + var target = provider ?? optionsMonitor.CurrentValue.Provider; + if (_providerMap.TryGetValue(target, out var instance)) + { + return instance; + } + + throw new InvalidOperationException($"未注册存储提供商:{target}"); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/StorageProviderKind.cs b/src/Modules/TakeoutSaaS.Module.Storage/StorageProviderKind.cs new file mode 100644 index 0000000..589b9f8 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Storage/StorageProviderKind.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Module.Storage; + +/// +/// 存储提供商类型枚举,便于通过配置选择具体的对象存储实现。 +/// +public enum StorageProviderKind +{ + /// + /// 腾讯云 COS 对象存储。 + /// + TencentCos = 1, + + /// + /// 七牛云 Kodo 存储。 + /// + QiniuKodo = 2, + + /// + /// 阿里云 OSS 存储。 + /// + AliyunOss = 3 +} diff --git a/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj index b407eac..fbeba2b 100644 --- a/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj +++ b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj @@ -4,8 +4,16 @@ enable enable + + + + + + + + + - From dc9b853c5c2f2aa40c102df54f9097c31fddf97b Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 26 Nov 2025 12:34:28 +0800 Subject: [PATCH 08/56] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/{11_下一步TODO.md => 11_WaitTODO.md} | 0 Document/12_阿里云网关服务器.md | 17 ++++ Document/13_腾讯云主PostgreSQL服务器.md | 18 ++++ Document/14_天翼云主应用服务器.md | 18 ++++ Document/15_腾讯云RedisRabbitMQ服务器.md | 19 ++++ Document/xmind_小程序版模块规划.md | 89 ++++++++++++++++++ Document/xmind_小程序版模块规划.xmind | Bin 0 -> 341473 bytes 7 files changed, 161 insertions(+) rename Document/{11_下一步TODO.md => 11_WaitTODO.md} (100%) create mode 100644 Document/12_阿里云网关服务器.md create mode 100644 Document/13_腾讯云主PostgreSQL服务器.md create mode 100644 Document/14_天翼云主应用服务器.md create mode 100644 Document/15_腾讯云RedisRabbitMQ服务器.md create mode 100644 Document/xmind_小程序版模块规划.md create mode 100644 Document/xmind_小程序版模块规划.xmind diff --git a/Document/11_下一步TODO.md b/Document/11_WaitTODO.md similarity index 100% rename from Document/11_下一步TODO.md rename to Document/11_WaitTODO.md diff --git a/Document/12_阿里云网关服务器.md b/Document/12_阿里云网关服务器.md new file mode 100644 index 0000000..e9f9c86 --- /dev/null +++ b/Document/12_阿里云网关服务器.md @@ -0,0 +1,17 @@ +# 阿里云网关服务器 + +## 基础信息 +- IP: 47.94.199.87 +- 账户: root +- 密码: C3SyytfBPAU#Ts8a +- 配置: 2 核 CPU / 2 GB 内存(阿里云) +- 地点: 北京 +- 用途: 网关 +- 到期时间: 2026-12-18 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充 +- 备份与监控: 待补充 +- 变更记录: 待补充 diff --git a/Document/13_腾讯云主PostgreSQL服务器.md b/Document/13_腾讯云主PostgreSQL服务器.md new file mode 100644 index 0000000..efa22e3 --- /dev/null +++ b/Document/13_腾讯云主PostgreSQL服务器.md @@ -0,0 +1,18 @@ +# 腾讯云主 PostgreSQL 服务器 + +## 基础信息 +- IP: 120.53.222.17 +- 账户: ubuntu +- 密码: P3y$nJt#zaa4%fh5 +- 配置: 2 核 CPU / 4 GB 内存(轻量应用服务器,腾讯云) +- 地点: 北京 +- 用途: 主 PostgreSQL / 数据库服务器 +- 到期时间: 2026-11-26 11:22:01 + +## 建议补充 +- 系统版本: 待补充(如 cat /etc/os-release) +- 带宽/磁盘: 待补充 +- 数据目录: 待补充(如 /var/lib/postgresql) +- 数据备份/监控: 待补充 +- 安全组/开放端口: 待补充 +- 变更记录: 待补充 diff --git a/Document/14_天翼云主应用服务器.md b/Document/14_天翼云主应用服务器.md new file mode 100644 index 0000000..e02f0ed --- /dev/null +++ b/Document/14_天翼云主应用服务器.md @@ -0,0 +1,18 @@ +# 天翼云主 PostgreSQL 服务器 + +## 基础信息 +- IP: 49.7.179.246 +- 账户: root +- 密码: 7zE&84XI6~w57W7N +- 配置: 4 核 CPU / 8 GB 内存(天翼云) +- 地点: 北京 +- 用途: 主 PostgreSQL 服务器 +- 到期时间: 2027-10-04 17:17:57 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 数据目录: 待补充(如 `/var/lib/postgresql`) +- 数据备份/监控: 待补充 +- 安全组/开放端口: 待补充 +- 变更记录: 待补充 diff --git a/Document/15_腾讯云RedisRabbitMQ服务器.md b/Document/15_腾讯云RedisRabbitMQ服务器.md new file mode 100644 index 0000000..1fe441d --- /dev/null +++ b/Document/15_腾讯云RedisRabbitMQ服务器.md @@ -0,0 +1,19 @@ +# 腾讯云 Redis/RabbitMQ 服务器(待购) + +## 基础信息(待补充) +- IP: 49.232.6.45 +- 账户: ubuntu +- 密码: Z7NsRjT&XnWg7%7X +- 配置: 2 核 CPU / 4 GB 内存(腾讯云) +- 地点: 北京 +- 用途: Redis 与 RabbitMQ +- 到期时间: 2028-11-26 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) +- 数据持久化与备份: 待补充 +- 监控与告警: 待补充 +- 变更记录: 待补充 + diff --git a/Document/xmind_小程序版模块规划.md b/Document/xmind_小程序版模块规划.md new file mode 100644 index 0000000..3af2007 --- /dev/null +++ b/Document/xmind_小程序版模块规划.md @@ -0,0 +1,89 @@ +# 外卖SaaS(小程序导向)模块脑图 + +## 1. 目标 +- 聚焦小程序用户体验:高效点单、扫码堂食、同城履约、实时状态 +- 平台/租户多租户隔离,支持商家快速入驻与运营 +- 门店同城即时配送为主,第三方配送对接兜底,统一回调验签 + +## 2. 端侧 +- 小程序用户端:扫码进店/堂食点餐、店铺浏览、菜品/套餐、购物车、下单支付、拼单、预购自提、同城配送、订单跟踪、优惠券/积分/会员、营销活动、评价、地址管理、搜索、签到、预约、社区、客服聊天、地图导航 +- 管理后台(商家/店员):门店配置、桌码管理、菜品与库存、订单/拼单/自提处理、配送调度与回调、营销配置、分销配置、客服工单/会话、数据看板 +- 平台运营后台:租户/商家审核、套餐与配额、全局配置、日志审计、监控看板、分销/返利策略、活动审核 + +## 3. 核心业务模块 +- 租户与平台 + - 租户生命周期:注册、实名认证/资质审核、套餐订阅、续费/升降配、停用/注销 + - 配额与权限:门店数/账号数/存储/短信/配送单量,RBAC 角色模板,租户级配置继承与覆盖 + - 平台运营:租户监控、账单/计费/催缴、公告/通知、合规与风控(黑名单/频率限制) +- 商家与门店 + - 入驻与资质审核:证照上传、合同、店铺类目、品牌与连锁 + - 门店配置:多店管理、营业时间、休息/打烊、门店状态、区域/配送范围、到店导航信息 + - 桌码/场景:桌码生成与绑定、桌台容量/区域、桌码标签(堂食/快餐/档口) + - 门店运营:员工账号与分工(前台/后厨/配送)、交班与收银、公告/售罄提示 +- 菜品与库存 + - 菜品建模:分类/标签/排序、规格与属性(辣度/份量)、套餐与组合、加料/做法/备注 + - 价格策略:会员价、门店价、时间段价、限时折扣、区域价 + - 库存与可售:总库存/门店库存/档期库存、售罄/预售、批量导入与同步、条码/编码 + - 媒资:图片/视频、SPU/SKU 编码、营养/过敏源/溯源信息 +- 扫码点餐/堂食 + - 桌码入口:扫码识别门店/桌台、桌台上下文、预加载菜单 + - 点餐流程:购物车并发锁、加菜/催单、口味备注、拆单/并单、多人同桌分付/代付 + - 账单与核销:桌台账单合并、结账/买单、电子小票、发票抬头、桌台释放 +- 预购自提 + - 档期配置:自提时间窗/容量、预售库存、截单时间 + - 下单与支付:自提地址确认、档期校验、预售与现制、库存锁定 + - 提货核销:提货码/手机号核销、自提柜/前台、超时/取消/退款、代取人 +- 下单与支付 + - 购物车与校验:库存/限购/门店状态/配送范围/桌台状态、券和积分叠加规则 + - 支付与结算:微信/支付宝/余额/优惠券/积分抵扣、回调幂等、预授权、分账/对账 + - 售后与状态机:退单/部分退款、异常单处理、状态机(待付→待接→制作→配送/自提→完成/取消) +- 同城即时配送(门店履约) + - 自配送:骑手管理、取/送件信息、路线估时、导航、取餐码、费用与补贴 + - 第三方配送:统一抽象(下单/取消/加价/查询)、多渠道择优、回调验签、异常重试与补偿 + - 配送体验:预计送达/计价、配送进度推送、无接触配送、收货码、投诉与赔付 +- 拼单 + - 团单规则:发起/加入、成团条件(人数/金额/时间)、拼主与参团人、支付/锁单策略 + - 失败与退款:超时/人数不足自动解散、自动退款/原路退、通知 + - 履约:同单配送至拼主地址、收件信息可见范围、团内消息/提醒 +- 优惠券与营销工具 + - 券种与发放:满减/折扣/新人券/裂变券/桌码专属券/会员券,渠道(领券中心/活动/分享/桌码) + - 核销规则:适用范围(门店/品类/菜品)、叠加/互斥、最低消费、有效期、库存与风控 + - 活动组件:抽奖、分享裂变、秒杀/限时抢购、满减活动、爆款/推荐位、裂变海报 +- 会员与积分/会员营销 + - 会员体系:付费会员/积分会员、等级/成长值、会员权益(专享价/券/运费/客服) + - 积分运营:获取(消费/签到/任务/活动)、消耗(抵扣/兑换)、有效期、黑名单 + - 会员运营:会员日、续费提醒、沉睡唤醒、专属活动、分层运营与 A/B +- 客服工具 + - 会话:实时聊天、机器人/人工切换、快捷回复、排队/转接、消息模板/通知 + - 质量与风控:会话记录与审计、敏感词、评价与回访、工单流转 +- 分销返利 + - 规则:商品/类目返利、佣金阶梯、结算周期、税务信息、违规处理 + - 链路:分享链接/小程序码、点击与下单跟踪、确认收货后结算、售后扣回 +- 地图导航 + - 门店/自提点定位、距离/路况、路线规划、附近门店/推荐、跳转原生导航 +- 签到打卡 + - 规则:每日/任务签到、连签奖励、补签、积分/券/成长值奖励、反作弊 +- 预约预订 + - 档期/资源:时间段、人员/座位/设备占用、容量校验 + - 流程:预约下单/支付、提醒/改期/取消、到店核销与履约记录 +- 搜索查询 + - 内容:门店/菜品/活动/优惠券搜索,过滤/排序,热门/历史搜索 + - 体验:纠错/联想、推荐、结果埋点与转化分析 +- 用户社区 + - 社区运营:动态发布/评论/点赞、话题/标签、图片/视频审核、举报与风控 + - 互动:店铺口碑、菜品晒图、官方/商家号发布、置顶与精选 +- 数据与分析 + - 交易分析:销售/订单/客单/转化漏斗、支付与退款、品类/单品分析 + - 营销分析:券发放与核销、活动效果(拼单/秒杀/抽奖/分销)、会员留存与复购 + - 履约分析:配送时效、超时/异常、堂食与自提拆分、投诉与赔付 + - 运营大盘:租户/商家健康度、活跃度、留存、GMV、成本与毛利 +- 系统与运维 + - 安全与合规:RBAC、租户隔离、限流与风控(设备/IP/账户/店铺)、敏感词与内容安全 + - 配置与网关:字典/参数、灰度/开关、网关限流/鉴权/租户透传 + - 可靠性:任务调度(订单/拼单超时、券过期、回调补偿)、幂等与重试、健康检查/告警、日志/链路追踪、备份恢复 + +## 4. 里程碑(建议) +- Phase 1:租户/商家入驻、门店与菜品、桌码扫码堂食、基础下单支付、预购自提、第三方配送骨架 +- Phase 2:拼单、优惠券与基础营销组件、会员积分/会员日、客服聊天、同城自配送调度、搜索 +- Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、营销活动丰富(秒杀/抽奖等)、风控与审计、补偿与告警体系 +- Phase 4:性能优化与缓存、运营大盘与细分报表、测试与文档完善、上线与监控 diff --git a/Document/xmind_小程序版模块规划.xmind b/Document/xmind_小程序版模块规划.xmind new file mode 100644 index 0000000000000000000000000000000000000000..9df8073cf64ee0cd98825de340684a92a7440f8f GIT binary patch literal 341473 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%0DR&%fi~R%RmH52!r@qo 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 Date: Mon, 1 Dec 2025 09:10:27 +0800 Subject: [PATCH 09/56] 1 --- Document/{11_下一步TODO.md => 11_WaitTODO.md} | 0 Document/12_阿里云网关服务器.md | 17 +++++++++++++++++ Document/13_腾讯云主应用服务器.md | 17 +++++++++++++++++ Document/14_天翼云主PostgreSQL服务器.md | 18 ++++++++++++++++++ Document/15_腾讯云RedisRabbitMQ服务器.md | 18 ++++++++++++++++++ 5 files changed, 70 insertions(+) rename Document/{11_下一步TODO.md => 11_WaitTODO.md} (100%) create mode 100644 Document/12_阿里云网关服务器.md create mode 100644 Document/13_腾讯云主应用服务器.md create mode 100644 Document/14_天翼云主PostgreSQL服务器.md create mode 100644 Document/15_腾讯云RedisRabbitMQ服务器.md diff --git a/Document/11_下一步TODO.md b/Document/11_WaitTODO.md similarity index 100% rename from Document/11_下一步TODO.md rename to Document/11_WaitTODO.md diff --git a/Document/12_阿里云网关服务器.md b/Document/12_阿里云网关服务器.md new file mode 100644 index 0000000..e9f9c86 --- /dev/null +++ b/Document/12_阿里云网关服务器.md @@ -0,0 +1,17 @@ +# 阿里云网关服务器 + +## 基础信息 +- IP: 47.94.199.87 +- 账户: root +- 密码: C3SyytfBPAU#Ts8a +- 配置: 2 核 CPU / 2 GB 内存(阿里云) +- 地点: 北京 +- 用途: 网关 +- 到期时间: 2026-12-18 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充 +- 备份与监控: 待补充 +- 变更记录: 待补充 diff --git a/Document/13_腾讯云主应用服务器.md b/Document/13_腾讯云主应用服务器.md new file mode 100644 index 0000000..e651409 --- /dev/null +++ b/Document/13_腾讯云主应用服务器.md @@ -0,0 +1,17 @@ +# 腾讯云主应用服务器 + +## 基础信息 +- IP: 43.142.81.224 +- 账户: ubuntu +- 密码: hN~~^2NZ+s_%A%tk +- 配置: 4 核 CPU / 4 GB 内存(轻量应用服务器,腾讯云) +- 地点: 上海 +- 用途: 主应用服务器 +- 到期时间: 2026-11-21 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充 +- 备份与监控: 待补充 +- 变更记录: 待补充 diff --git a/Document/14_天翼云主PostgreSQL服务器.md b/Document/14_天翼云主PostgreSQL服务器.md new file mode 100644 index 0000000..e02f0ed --- /dev/null +++ b/Document/14_天翼云主PostgreSQL服务器.md @@ -0,0 +1,18 @@ +# 天翼云主 PostgreSQL 服务器 + +## 基础信息 +- IP: 49.7.179.246 +- 账户: root +- 密码: 7zE&84XI6~w57W7N +- 配置: 4 核 CPU / 8 GB 内存(天翼云) +- 地点: 北京 +- 用途: 主 PostgreSQL 服务器 +- 到期时间: 2027-10-04 17:17:57 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 数据目录: 待补充(如 `/var/lib/postgresql`) +- 数据备份/监控: 待补充 +- 安全组/开放端口: 待补充 +- 变更记录: 待补充 diff --git a/Document/15_腾讯云RedisRabbitMQ服务器.md b/Document/15_腾讯云RedisRabbitMQ服务器.md new file mode 100644 index 0000000..a7a5351 --- /dev/null +++ b/Document/15_腾讯云RedisRabbitMQ服务器.md @@ -0,0 +1,18 @@ +# 腾讯云 Redis/RabbitMQ 服务器(待购) + +## 基础信息(待补充) +- IP: 待补充 +- 账户: 待补充 +- 密码: 待补充 +- 配置: 2 核 CPU / 4 GB 内存(腾讯云) +- 地点: 待补充 +- 用途: Redis 与 RabbitMQ +- 到期时间: 待补充 + +## 建议补充 +- 系统版本: 待补充(如 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) +- 数据持久化与备份: 待补充 +- 监控与告警: 待补充 +- 变更记录: 待补充 From a08804658b68b9715e707f102374991a12ae97da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BA=E7=88=B1=E6=B3=BD?= Date: Mon, 1 Dec 2025 12:06:39 +0800 Subject: [PATCH 10/56] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E7=BB=B4TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/05_部署运维.md | 8 + Document/12_阿里云网关服务器.md | 2 +- Document/13_腾讯云主PostgreSQL服务器.md | 2 +- Document/13_腾讯云主应用服务器.md | 17 --- Document/14_天翼云主PostgreSQL服务器.md | 18 --- Document/15_腾讯云RedisRabbitMQ服务器.md | 14 +- ...15_腾讯云RedisRabbitMQ服务器_BACKUP_878.md | 35 ----- .../15_腾讯云RedisRabbitMQ服务器_BASE_878.md | 0 .../15_腾讯云RedisRabbitMQ服务器_LOCAL_878.md | 18 --- ...15_腾讯云RedisRabbitMQ服务器_REMOTE_878.md | 19 --- .../appsettings.Development.json | 36 ++--- .../appsettings.Production.json | 142 ++++++++++++++++++ .../appsettings.Development.json | 34 ++--- .../appsettings.Production.json | 133 ++++++++++++++++ .../appsettings.Development.json | 12 +- .../appsettings.Production.json | 52 +++++++ .../Persistence/DatabaseConnectionFactory.cs | 2 +- 17 files changed, 386 insertions(+), 158 deletions(-) delete mode 100644 Document/13_腾讯云主应用服务器.md delete mode 100644 Document/14_天翼云主PostgreSQL服务器.md delete mode 100644 Document/15_腾讯云RedisRabbitMQ服务器_BACKUP_878.md delete mode 100644 Document/15_腾讯云RedisRabbitMQ服务器_BASE_878.md delete mode 100644 Document/15_腾讯云RedisRabbitMQ服务器_LOCAL_878.md delete mode 100644 Document/15_腾讯云RedisRabbitMQ服务器_REMOTE_878.md create mode 100644 src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json create mode 100644 src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json create mode 100644 src/Api/TakeoutSaaS.UserApi/appsettings.Production.json diff --git a/Document/05_部署运维.md b/Document/05_部署运维.md index f593ee9..1ff6392 100644 --- a/Document/05_部署运维.md +++ b/Document/05_部署运维.md @@ -508,6 +508,14 @@ crontab -e 0 2 * * * /path/to/backup_db.sh >> /var/log/backup.log 2>&1 ``` +## TODO:基础设施部署脚本 + +- [ ] PostgreSQL 主从:整理主库/从库初始化脚本、basebackup 步骤与故障切换手册。 +- [ ] Redis 哨兵/集群:补充 redis.conf/sentinel.conf 模板以及一主两从搭建命令。 +- [ ] RabbitMQ:编写单节点到镜像队列的安装脚本,记录 VHost、用户、权限、监控等操作。 +- [ ] 腾讯云 COS:整理桶创建、ACL、CDN 绑定与密钥轮换流程,并提供 coscmd/SDK 示例。 +- [ ] Hangfire 存储:确认 PostgreSQL Schema 初始化脚本,补充定期备份、清理、监控的 SOP。 + ## 6. Redis部署 ### 6.1 Redis哨兵模式 diff --git a/Document/12_阿里云网关服务器.md b/Document/12_阿里云网关服务器.md index e9f9c86..8fcea29 100644 --- a/Document/12_阿里云网关服务器.md +++ b/Document/12_阿里云网关服务器.md @@ -3,7 +3,7 @@ ## 基础信息 - IP: 47.94.199.87 - 账户: root -- 密码: C3SyytfBPAU#Ts8a +- 密码: cJ5q2k2iW7XnMA^! - 配置: 2 核 CPU / 2 GB 内存(阿里云) - 地点: 北京 - 用途: 网关 diff --git a/Document/13_腾讯云主PostgreSQL服务器.md b/Document/13_腾讯云主PostgreSQL服务器.md index efa22e3..549647b 100644 --- a/Document/13_腾讯云主PostgreSQL服务器.md +++ b/Document/13_腾讯云主PostgreSQL服务器.md @@ -4,7 +4,7 @@ - IP: 120.53.222.17 - 账户: ubuntu - 密码: P3y$nJt#zaa4%fh5 -- 配置: 2 核 CPU / 4 GB 内存(轻量应用服务器,腾讯云) +- 配置: 2 核 CPU / 4 GB 内存 - 地点: 北京 - 用途: 主 PostgreSQL / 数据库服务器 - 到期时间: 2026-11-26 11:22:01 diff --git a/Document/13_腾讯云主应用服务器.md b/Document/13_腾讯云主应用服务器.md deleted file mode 100644 index e651409..0000000 --- a/Document/13_腾讯云主应用服务器.md +++ /dev/null @@ -1,17 +0,0 @@ -# 腾讯云主应用服务器 - -## 基础信息 -- IP: 43.142.81.224 -- 账户: ubuntu -- 密码: hN~~^2NZ+s_%A%tk -- 配置: 4 核 CPU / 4 GB 内存(轻量应用服务器,腾讯云) -- 地点: 上海 -- 用途: 主应用服务器 -- 到期时间: 2026-11-21 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充 -- 备份与监控: 待补充 -- 变更记录: 待补充 diff --git a/Document/14_天翼云主PostgreSQL服务器.md b/Document/14_天翼云主PostgreSQL服务器.md deleted file mode 100644 index e02f0ed..0000000 --- a/Document/14_天翼云主PostgreSQL服务器.md +++ /dev/null @@ -1,18 +0,0 @@ -# 天翼云主 PostgreSQL 服务器 - -## 基础信息 -- IP: 49.7.179.246 -- 账户: root -- 密码: 7zE&84XI6~w57W7N -- 配置: 4 核 CPU / 8 GB 内存(天翼云) -- 地点: 北京 -- 用途: 主 PostgreSQL 服务器 -- 到期时间: 2027-10-04 17:17:57 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 数据目录: 待补充(如 `/var/lib/postgresql`) -- 数据备份/监控: 待补充 -- 安全组/开放端口: 待补充 -- 变更记录: 待补充 diff --git a/Document/15_腾讯云RedisRabbitMQ服务器.md b/Document/15_腾讯云RedisRabbitMQ服务器.md index a7a5351..3877313 100644 --- a/Document/15_腾讯云RedisRabbitMQ服务器.md +++ b/Document/15_腾讯云RedisRabbitMQ服务器.md @@ -1,13 +1,13 @@ -# 腾讯云 Redis/RabbitMQ 服务器(待购) +# 腾讯云 Redis/RabbitMQ 服务器 ## 基础信息(待补充) -- IP: 待补充 -- 账户: 待补充 -- 密码: 待补充 -- 配置: 2 核 CPU / 4 GB 内存(腾讯云) -- 地点: 待补充 +- IP: 49.232.6.45 +- 账户: ubuntu +- 密码: Z7NsRjT&XnWg7%7X +- 配置: 2 核 CPU / 4 GB 内存 +- 地点: 北京 - 用途: Redis 与 RabbitMQ -- 到期时间: 待补充 +- 到期时间: 2028-11-26 ## 建议补充 - 系统版本: 待补充(如 `cat /etc/os-release`) diff --git a/Document/15_腾讯云RedisRabbitMQ服务器_BACKUP_878.md b/Document/15_腾讯云RedisRabbitMQ服务器_BACKUP_878.md deleted file mode 100644 index 7db352d..0000000 --- a/Document/15_腾讯云RedisRabbitMQ服务器_BACKUP_878.md +++ /dev/null @@ -1,35 +0,0 @@ -<<<<<<< HEAD -# 腾讯云 Redis/RabbitMQ 服务器(待购) - -## 基础信息(待补充) -- IP: 待补充 -- 账户: 待补充 -- 密码: 待补充 -- 配置: 2 核 CPU / 4 GB 内存(腾讯云) -- 地点: 待补充 -- 用途: Redis 与 RabbitMQ -- 到期时间: 待补充 -======= -# 腾讯云 Redis/RabbitMQ 服务器(待购) - -## 基础信息(待补充) -- IP: 49.232.6.45 -- 账户: ubuntu -- 密码: Z7NsRjT&XnWg7%7X -- 配置: 2 核 CPU / 4 GB 内存(腾讯云) -- 地点: 北京 -- 用途: Redis 与 RabbitMQ -- 到期时间: 2028-11-26 ->>>>>>> dc9b853c5c2f2aa40c102df54f9097c31fddf97b - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) -- 数据持久化与备份: 待补充 -- 监控与告警: 待补充 -- 变更记录: 待补充 -<<<<<<< HEAD -======= - ->>>>>>> dc9b853c5c2f2aa40c102df54f9097c31fddf97b diff --git a/Document/15_腾讯云RedisRabbitMQ服务器_BASE_878.md b/Document/15_腾讯云RedisRabbitMQ服务器_BASE_878.md deleted file mode 100644 index e69de29..0000000 diff --git a/Document/15_腾讯云RedisRabbitMQ服务器_LOCAL_878.md b/Document/15_腾讯云RedisRabbitMQ服务器_LOCAL_878.md deleted file mode 100644 index a7a5351..0000000 --- a/Document/15_腾讯云RedisRabbitMQ服务器_LOCAL_878.md +++ /dev/null @@ -1,18 +0,0 @@ -# 腾讯云 Redis/RabbitMQ 服务器(待购) - -## 基础信息(待补充) -- IP: 待补充 -- 账户: 待补充 -- 密码: 待补充 -- 配置: 2 核 CPU / 4 GB 内存(腾讯云) -- 地点: 待补充 -- 用途: Redis 与 RabbitMQ -- 到期时间: 待补充 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) -- 数据持久化与备份: 待补充 -- 监控与告警: 待补充 -- 变更记录: 待补充 diff --git a/Document/15_腾讯云RedisRabbitMQ服务器_REMOTE_878.md b/Document/15_腾讯云RedisRabbitMQ服务器_REMOTE_878.md deleted file mode 100644 index 1fe441d..0000000 --- a/Document/15_腾讯云RedisRabbitMQ服务器_REMOTE_878.md +++ /dev/null @@ -1,19 +0,0 @@ -# 腾讯云 Redis/RabbitMQ 服务器(待购) - -## 基础信息(待补充) -- IP: 49.232.6.45 -- 账户: ubuntu -- 密码: Z7NsRjT&XnWg7%7X -- 配置: 2 核 CPU / 4 GB 内存(腾讯云) -- 地点: 北京 -- 用途: Redis 与 RabbitMQ -- 到期时间: 2028-11-26 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) -- 数据持久化与备份: 待补充 -- 监控与告警: 待补充 -- 变更记录: 待补充 - diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index cbdcf16..f015bba 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -2,18 +2,18 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, @@ -21,12 +21,12 @@ } } }, - "Redis": "localhost:6379,abortConnect=false", + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", "Identity": { "Jwt": { "Issuer": "takeout-saas", "Audience": "takeout-saas-clients", - "Secret": "ReplaceWithA32CharLongSecretKey_____", + "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", "AccessTokenExpirationMinutes": 120, "RefreshTokenExpirationMinutes": 10080 }, @@ -54,14 +54,14 @@ }, "Storage": { "Provider": "TencentCos", - "CdnBaseUrl": "https://cdn.example.com", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", "TencentCos": { - "SecretId": "COS_SECRET_ID", - "SecretKey": "COS_SECRET_KEY", - "Region": "ap-guangzhou", - "Bucket": "takeout-bucket-123456", - "Endpoint": "", - "CdnBaseUrl": "https://cdn.example.com", + "SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R", + "SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa", + "Region": "ap-beijing", + "Bucket": "saas2025-1388556178", + "Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", "UseHttps": true, "ForcePathStyle": false }, @@ -101,7 +101,7 @@ "SecretKey": "TENCENT_SMS_SECRET_KEY", "SdkAppId": "1400000000", "SignName": "外卖SaaS", - "Region": "ap-guangzhou", + "Region": "ap-beijing", "Endpoint": "https://sms.tencentcloudapi.com" }, "Aliyun": { @@ -124,17 +124,17 @@ } }, "RabbitMQ": { - "Host": "localhost", + "Host": "49.232.6.45", "Port": 5672, - "Username": "admin", - "Password": "password", + "Username": "Admin", + "Password": "MsuMshk112233", "VirtualHost": "/", "Exchange": "takeout.events", "ExchangeType": "topic", "PrefetchCount": 20 }, "Scheduler": { - "ConnectionString": "Host=localhost;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json new file mode 100644 index 0000000..f015bba --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json @@ -0,0 +1,142 @@ +{ + "Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "IdentityDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + } + } + }, + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", + "Identity": { + "Jwt": { + "Issuer": "takeout-saas", + "Audience": "takeout-saas-clients", + "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationMinutes": 10080 + }, + "LoginRateLimit": { + "WindowSeconds": 60, + "MaxAttempts": 5 + }, + "RefreshTokenStore": { + "Prefix": "identity:refresh:" + }, + "AdminSeed": { + "Users": [] + } + }, + "Dictionary": { + "Cache": { + "SlidingExpiration": "00:30:00" + } + }, + "Tenancy": { + "TenantIdHeaderName": "X-Tenant-Id", + "TenantCodeHeaderName": "X-Tenant-Code", + "IgnoredPaths": [ "/health" ], + "RootDomain": "" + }, + "Storage": { + "Provider": "TencentCos", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "TencentCos": { + "SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R", + "SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa", + "Region": "ap-beijing", + "Bucket": "saas2025-1388556178", + "Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "UseHttps": true, + "ForcePathStyle": false + }, + "QiniuKodo": { + "AccessKey": "QINIU_ACCESS_KEY", + "SecretKey": "QINIU_SECRET_KEY", + "Bucket": "takeout-files", + "DownloadDomain": "", + "Endpoint": "", + "UseHttps": true, + "SignedUrlExpirationMinutes": 30 + }, + "AliyunOss": { + "AccessKeyId": "OSS_ACCESS_KEY_ID", + "AccessKeySecret": "OSS_ACCESS_KEY_SECRET", + "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", + "Bucket": "takeout-files", + "CdnBaseUrl": "", + "UseHttps": true + }, + "Security": { + "MaxFileSizeBytes": 10485760, + "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], + "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "DefaultUrlExpirationMinutes": 30, + "EnableRefererValidation": true, + "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AntiLeechTokenSecret": "ReplaceWithARandomToken" + } + }, + "Sms": { + "Provider": "Tencent", + "DefaultSignName": "外卖SaaS", + "UseMock": true, + "Tencent": { + "SecretId": "TENCENT_SMS_SECRET_ID", + "SecretKey": "TENCENT_SMS_SECRET_KEY", + "SdkAppId": "1400000000", + "SignName": "外卖SaaS", + "Region": "ap-beijing", + "Endpoint": "https://sms.tencentcloudapi.com" + }, + "Aliyun": { + "AccessKeyId": "ALIYUN_SMS_AK", + "AccessKeySecret": "ALIYUN_SMS_SK", + "Endpoint": "dysmsapi.aliyuncs.com", + "SignName": "外卖SaaS", + "Region": "cn-hangzhou" + }, + "SceneTemplates": { + "login": "LOGIN_TEMPLATE_ID", + "register": "REGISTER_TEMPLATE_ID", + "reset": "RESET_TEMPLATE_ID" + }, + "VerificationCode": { + "CodeLength": 6, + "ExpireMinutes": 5, + "CooldownSeconds": 60, + "CachePrefix": "sms:code" + } + }, + "RabbitMQ": { + "Host": "49.232.6.45", + "Port": 5672, + "Username": "Admin", + "Password": "MsuMshk112233", + "VirtualHost": "/", + "Exchange": "takeout.events", + "ExchangeType": "topic", + "PrefetchCount": 20 + }, + "Scheduler": { + "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "WorkerCount": 5, + "DashboardEnabled": false, + "DashboardPath": "/hangfire" + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json index 1549e0a..0d6da09 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json @@ -2,18 +2,18 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, @@ -21,12 +21,12 @@ } } }, - "Redis": "localhost:6379,abortConnect=false", + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", "Identity": { "Jwt": { "Issuer": "takeout-saas", "Audience": "takeout-saas-clients", - "Secret": "ReplaceWithA32CharLongSecretKey_____", + "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", "AccessTokenExpirationMinutes": 120, "RefreshTokenExpirationMinutes": 10080 }, @@ -51,14 +51,14 @@ }, "Storage": { "Provider": "TencentCos", - "CdnBaseUrl": "https://cdn.example.com", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", "TencentCos": { - "SecretId": "COS_SECRET_ID", - "SecretKey": "COS_SECRET_KEY", - "Region": "ap-guangzhou", - "Bucket": "takeout-bucket-123456", - "Endpoint": "", - "CdnBaseUrl": "https://cdn.example.com", + "SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R", + "SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa", + "Region": "ap-beijing", + "Bucket": "saas2025-1388556178", + "Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", "UseHttps": true, "ForcePathStyle": false }, @@ -98,7 +98,7 @@ "SecretKey": "TENCENT_SMS_SECRET_KEY", "SdkAppId": "1400000000", "SignName": "外卖SaaS", - "Region": "ap-guangzhou", + "Region": "ap-beijing", "Endpoint": "https://sms.tencentcloudapi.com" }, "Aliyun": { @@ -121,10 +121,10 @@ } }, "RabbitMQ": { - "Host": "localhost", + "Host": "49.232.6.45", "Port": 5672, - "Username": "admin", - "Password": "password", + "Username": "Admin", + "Password": "MsuMshk112233", "VirtualHost": "/", "Exchange": "takeout.events", "ExchangeType": "topic", diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json new file mode 100644 index 0000000..0d6da09 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json @@ -0,0 +1,133 @@ +{ + "Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "IdentityDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + } + } + }, + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", + "Identity": { + "Jwt": { + "Issuer": "takeout-saas", + "Audience": "takeout-saas-clients", + "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationMinutes": 10080 + }, + "LoginRateLimit": { + "WindowSeconds": 60, + "MaxAttempts": 5 + }, + "RefreshTokenStore": { + "Prefix": "identity:refresh:" + } + }, + "Dictionary": { + "Cache": { + "SlidingExpiration": "00:30:00" + } + }, + "Tenancy": { + "TenantIdHeaderName": "X-Tenant-Id", + "TenantCodeHeaderName": "X-Tenant-Code", + "IgnoredPaths": [ "/health" ], + "RootDomain": "" + }, + "Storage": { + "Provider": "TencentCos", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "TencentCos": { + "SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R", + "SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa", + "Region": "ap-beijing", + "Bucket": "saas2025-1388556178", + "Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", + "UseHttps": true, + "ForcePathStyle": false + }, + "QiniuKodo": { + "AccessKey": "QINIU_ACCESS_KEY", + "SecretKey": "QINIU_SECRET_KEY", + "Bucket": "takeout-files", + "DownloadDomain": "", + "Endpoint": "", + "UseHttps": true, + "SignedUrlExpirationMinutes": 30 + }, + "AliyunOss": { + "AccessKeyId": "OSS_ACCESS_KEY_ID", + "AccessKeySecret": "OSS_ACCESS_KEY_SECRET", + "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", + "Bucket": "takeout-files", + "CdnBaseUrl": "", + "UseHttps": true + }, + "Security": { + "MaxFileSizeBytes": 10485760, + "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], + "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "DefaultUrlExpirationMinutes": 30, + "EnableRefererValidation": true, + "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AntiLeechTokenSecret": "ReplaceWithARandomToken" + } + }, + "Sms": { + "Provider": "Tencent", + "DefaultSignName": "外卖SaaS", + "UseMock": true, + "Tencent": { + "SecretId": "TENCENT_SMS_SECRET_ID", + "SecretKey": "TENCENT_SMS_SECRET_KEY", + "SdkAppId": "1400000000", + "SignName": "外卖SaaS", + "Region": "ap-beijing", + "Endpoint": "https://sms.tencentcloudapi.com" + }, + "Aliyun": { + "AccessKeyId": "ALIYUN_SMS_AK", + "AccessKeySecret": "ALIYUN_SMS_SK", + "Endpoint": "dysmsapi.aliyuncs.com", + "SignName": "外卖SaaS", + "Region": "cn-hangzhou" + }, + "SceneTemplates": { + "login": "LOGIN_TEMPLATE_ID", + "register": "REGISTER_TEMPLATE_ID", + "reset": "RESET_TEMPLATE_ID" + }, + "VerificationCode": { + "CodeLength": 6, + "ExpireMinutes": 5, + "CooldownSeconds": 60, + "CachePrefix": "sms:code" + } + }, + "RabbitMQ": { + "Host": "49.232.6.45", + "Port": 5672, + "Username": "Admin", + "Password": "MsuMshk112233", + "VirtualHost": "/", + "Exchange": "takeout.events", + "ExchangeType": "topic", + "PrefetchCount": 20 + } +} diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json index bf4d9de..1f7ff9f 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json @@ -2,18 +2,18 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, @@ -21,12 +21,12 @@ } } }, - "Redis": "localhost:6379,abortConnect=false", + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", "Identity": { "Jwt": { "Issuer": "takeout-saas", "Audience": "takeout-saas-clients", - "Secret": "ReplaceWithA32CharLongSecretKey_____", + "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", "AccessTokenExpirationMinutes": 120, "RefreshTokenExpirationMinutes": 10080 }, diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json new file mode 100644 index 0000000..1f7ff9f --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json @@ -0,0 +1,52 @@ +{ + "Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "IdentityDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + } + } + }, + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", + "Identity": { + "Jwt": { + "Issuer": "takeout-saas", + "Audience": "takeout-saas-clients", + "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationMinutes": 10080 + }, + "LoginRateLimit": { + "WindowSeconds": 60, + "MaxAttempts": 5 + }, + "RefreshTokenStore": { + "Prefix": "identity:refresh:" + } + }, + "Dictionary": { + "Cache": { + "SlidingExpiration": "00:30:00" + } + }, + "Tenancy": { + "TenantIdHeaderName": "X-Tenant-Id", + "TenantCodeHeaderName": "X-Tenant-Code", + "IgnoredPaths": [ "/health" ], + "RootDomain": "" + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs index 0f47cbf..7852aaa 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DatabaseConnectionFactory.cs @@ -110,7 +110,7 @@ public sealed class DatabaseConnectionFactory( private DatabaseConnectionDetails BuildFallbackConnection() { - const string fallback = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20"; + const string fallback = "Host=120.53.222.17;Port=5432;Database=postgres;Username=postgres;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=1;Maximum Pool Size=20"; logger.LogWarning("使用默认回退连接串:{Connection}", fallback); return new DatabaseConnectionDetails( fallback, From 5ddad07658ce58d25e1716e9a36625a0c58e289d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BA=E7=88=B1=E6=B3=BD?= Date: Mon, 1 Dec 2025 13:26:05 +0800 Subject: [PATCH 11/56] =?UTF-8?q?feat:=20=E6=89=A9=E5=B1=95=E9=A2=86?= =?UTF-8?q?=E5=9F=9F=E6=A8=A1=E5=9E=8B=E4=B8=8E=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .config/dotnet-tools.json | 13 + Document/05_部署运维.md | 26 + .../appsettings.Development.json | 2 +- .../appsettings.Production.json | 2 +- .../Analytics/Entities/MetricAlertRule.cs | 35 + .../Analytics/Entities/MetricDefinition.cs | 34 + .../Analytics/Entities/MetricSnapshot.cs | 34 + .../Analytics/Enums/MetricAlertSeverity.cs | 22 + .../Coupons/Entities/Coupon.cs | 50 + .../Coupons/Entities/CouponTemplate.cs | 90 ++ .../Coupons/Entities/PromotionCampaign.cs | 55 + .../Coupons/Enums/CouponStatus.cs | 32 + .../Coupons/Enums/CouponTemplateStatus.cs | 22 + .../Coupons/Enums/CouponType.cs | 32 + .../Coupons/Enums/PromotionStatus.cs | 27 + .../Coupons/Enums/PromotionType.cs | 27 + .../CustomerService/Entities/ChatMessage.cs | 45 + .../CustomerService/Entities/ChatSession.cs | 50 + .../CustomerService/Entities/SupportTicket.cs | 55 + .../CustomerService/Entities/TicketComment.cs | 34 + .../Enums/ChatSessionStatus.cs | 27 + .../Enums/MessageSenderType.cs | 27 + .../CustomerService/Enums/TicketPriority.cs | 22 + .../CustomerService/Enums/TicketStatus.cs | 32 + .../Deliveries/Entities/DeliveryEvent.cs | 35 + .../Deliveries/Entities/DeliveryOrder.cs | 62 ++ .../Deliveries/Enums/DeliveryEventType.cs | 22 + .../Deliveries/Enums/DeliveryProvider.cs | 14 + .../Deliveries/Enums/DeliveryStatus.cs | 15 + .../Distribution/Entities/AffiliateOrder.cs | 45 + .../Distribution/Entities/AffiliatePartner.cs | 45 + .../Distribution/Entities/AffiliatePayout.cs | 40 + .../Enums/AffiliateChannelType.cs | 27 + .../Enums/AffiliateOrderStatus.cs | 27 + .../Distribution/Enums/AffiliateStatus.cs | 27 + .../Distribution/Enums/PayoutStatus.cs | 22 + .../Engagement/Entities/CheckInCampaign.cs | 45 + .../Engagement/Entities/CheckInRecord.cs | 34 + .../Engagement/Entities/CommunityComment.cs | 34 + .../Engagement/Entities/CommunityPost.cs | 45 + .../Engagement/Entities/CommunityReaction.cs | 30 + .../Engagement/Enums/CheckInCampaignStatus.cs | 27 + .../Engagement/Enums/PostStatus.cs | 27 + .../Engagement/Enums/ReactionType.cs | 22 + .../GroupBuying/Entities/GroupOrder.cs | 70 ++ .../GroupBuying/Entities/GroupParticipant.cs | 35 + .../GroupBuying/Enums/GroupOrderStatus.cs | 27 + .../Enums/GroupParticipantStatus.cs | 22 + .../Inventory/Entities/InventoryAdjustment.cs | 40 + .../Inventory/Entities/InventoryBatch.cs | 44 + .../Inventory/Entities/InventoryItem.cs | 49 + .../Enums/InventoryAdjustmentType.cs | 32 + .../Membership/Entities/MemberGrowthLog.cs | 34 + .../Membership/Entities/MemberPointLedger.cs | 45 + .../Membership/Entities/MemberProfile.cs | 60 ++ .../Membership/Entities/MemberTier.cs | 29 + .../Membership/Enums/MemberStatus.cs | 22 + .../Membership/Enums/PointChangeReason.cs | 32 + .../Merchants/Entities/Merchant.cs | 120 +++ .../Merchants/Entities/MerchantContract.cs | 55 + .../Merchants/Entities/MerchantDocument.cs | 50 + .../Merchants/Entities/MerchantStaff.cs | 55 + .../Merchants/Enums/ContractStatus.cs | 27 + .../Merchants/Enums/MerchantDocumentStatus.cs | 27 + .../Merchants/Enums/MerchantDocumentType.cs | 27 + .../Merchants/Enums/MerchantStatus.cs | 12 + .../Merchants/Enums/StaffRoleType.cs | 32 + .../Merchants/Enums/StaffStatus.cs | 22 + .../Navigation/Entities/MapLocation.cs | 39 + .../Navigation/Entities/NavigationRequest.cs | 35 + .../Navigation/Enums/NavigationChannel.cs | 22 + .../Navigation/Enums/NavigationTargetApp.cs | 32 + .../Ordering/Entities/CartItem.cs | 55 + .../Ordering/Entities/CartItemAddon.cs | 29 + .../Ordering/Entities/CheckoutSession.cs | 40 + .../Ordering/Entities/ShoppingCart.cs | 40 + .../Ordering/Enums/CartItemStatus.cs | 22 + .../Ordering/Enums/CheckoutSessionStatus.cs | 27 + .../Ordering/Enums/ShoppingCartStatus.cs | 22 + .../Orders/Entities/Order.cs | 111 ++ .../Orders/Entities/OrderItem.cs | 59 ++ .../Orders/Entities/OrderStatusHistory.cs | 35 + .../Orders/Entities/RefundRequest.cs | 50 + .../Orders/Enums/DeliveryType.cs | 11 + .../Orders/Enums/OrderChannel.cs | 14 + .../Orders/Enums/OrderStatus.cs | 14 + .../Orders/Enums/RefundStatus.cs | 27 + .../Payments/Entities/PaymentRecord.cs | 55 + .../Payments/Entities/PaymentRefundRecord.cs | 50 + .../Payments/Enums/PaymentMethod.cs | 14 + .../Payments/Enums/PaymentRefundStatus.cs | 27 + .../Payments/Enums/PaymentStatus.cs | 13 + .../Products/Entities/Product.cs | 100 ++ .../Products/Entities/ProductAddonGroup.cs | 45 + .../Products/Entities/ProductAddonOption.cs | 34 + .../Entities/ProductAttributeGroup.cs | 35 + .../Entities/ProductAttributeOption.cs | 34 + .../Products/Entities/ProductCategory.cs | 34 + .../Products/Entities/ProductMediaAsset.cs | 35 + .../Products/Entities/ProductPricingRule.cs | 45 + .../Products/Entities/ProductSku.cs | 49 + .../Products/Enums/AddonSelectionType.cs | 17 + .../Products/Enums/AttributeSelectionType.cs | 17 + .../Products/Enums/MediaAssetType.cs | 22 + .../Products/Enums/PricingRuleType.cs | 32 + .../Products/Enums/ProductStatus.cs | 12 + .../Queues/Entities/QueueTicket.cs | 52 + .../Queues/Enums/QueueStatus.cs | 13 + .../Reservations/Entities/Reservation.cs | 70 ++ .../Reservations/Enums/ReservationStatus.cs | 13 + .../Stores/Entities/Store.cs | 130 +++ .../Stores/Entities/StoreBusinessHour.cs | 45 + .../Stores/Entities/StoreDeliveryZone.cs | 39 + .../Stores/Entities/StoreEmployeeShift.cs | 45 + .../Stores/Entities/StoreHoliday.cs | 29 + .../Stores/Entities/StoreTable.cs | 45 + .../Stores/Entities/StoreTableArea.cs | 24 + .../Stores/Enums/BusinessHourType.cs | 27 + .../Stores/Enums/StoreStatus.cs | 27 + .../Stores/Enums/StoreTableStatus.cs | 27 + .../Tenants/Entities/Tenant.cs | 125 +++ .../Entities/TenantBillingStatement.cs | 50 + .../Tenants/Entities/TenantNotification.cs | 45 + .../Tenants/Entities/TenantPackage.cs | 70 ++ .../Tenants/Entities/TenantQuotaUsage.cs | 35 + .../Tenants/Entities/TenantSubscription.cs | 50 + .../Tenants/Enums/SubscriptionStatus.cs | 32 + .../Tenants/Enums/TenantBillingStatus.cs | 27 + .../Enums/TenantNotificationChannel.cs | 27 + .../Enums/TenantNotificationSeverity.cs | 22 + .../Tenants/Enums/TenantPackageType.cs | 27 + .../Tenants/Enums/TenantQuotaType.cs | 37 + .../Tenants/Enums/TenantStatus.cs | 32 + .../20251201044927_InitialApp.Designer.cs | 949 ++++++++++++++++++ .../Migrations/20251201044927_InitialApp.cs | 497 +++++++++ .../TakeoutAppDbContextModelSnapshot.cs | 946 +++++++++++++++++ .../App/Persistence/TakeoutAppDbContext.cs | 221 ++++ .../TakeoutAppDesignTimeDbContextFactory.cs | 24 + .../DesignTimeDbContextFactoryBase.cs | 68 ++ ...251201042346_InitialDictionary.Designer.cs | 172 ++++ .../20251201042346_InitialDictionary.cs | 101 ++ .../DictionaryDbContextModelSnapshot.cs | 169 ++++ .../DictionaryDesignTimeDbContextFactory.cs | 24 + ...20251201042324_InitialIdentity.Designer.cs | 152 +++ .../20251201042324_InitialIdentity.cs | 94 ++ .../IdentityDbContextModelSnapshot.cs | 149 +++ .../IdentityDesignTimeDbContextFactory.cs | 24 + .../TakeoutSaaS.Infrastructure.csproj | 4 + 148 files changed, 8519 insertions(+), 2 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricDefinition.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Analytics/Enums/MetricAlertSeverity.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Entities/CouponTemplate.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PromotionCampaign.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponTemplateStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/ChatSessionStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/MessageSenderType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketPriority.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryEventType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateChannelType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateOrderStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Distribution/Enums/PayoutStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInCampaign.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Enums/CheckInCampaignStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Enums/PostStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Engagement/Enums/ReactionType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupOrderStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupParticipantStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Inventory/Enums/InventoryAdjustmentType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberTier.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/PointChangeReason.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Enums/ContractStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffRoleType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationChannel.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationTargetApp.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CartItemStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CheckoutSessionStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Ordering/Enums/ShoppingCartStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Enums/RefundStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentRefundStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Enums/AddonSelectionType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Enums/AttributeSelectionType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Enums/MediaAssetType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Enums/PricingRuleType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Enums/BusinessHourType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreTableStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantBillingStatement.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantNotification.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPackage.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaUsage.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/SubscriptionStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantBillingStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationChannel.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationSeverity.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantPackageType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantQuotaType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantStatus.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..0d6d304 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.0", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Document/05_部署运维.md b/Document/05_部署运维.md index 1ff6392..449ab67 100644 --- a/Document/05_部署运维.md +++ b/Document/05_部署运维.md @@ -198,6 +198,32 @@ dotnet ef database update dotnet run ``` +### 2.7 EF Core 迁移基线 + +现已内置 `dotnet-ef` 本地工具与设计时 DbContext 工厂,可直接在命令行生成/更新数据库。运行前可通过环境变量 `TAKEOUTSAAS_APP_CONNECTION`、`TAKEOUTSAAS_IDENTITY_CONNECTION` 覆盖默认连接串(默认指向本地 PostgreSQL)。 + +```powershell +# 业务主库(TakeoutAppDbContext,含租户/商户/门店/商品/订单等) +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext + +# 身份库(IdentityDbContext) +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext + +# 业务/字典库(DictionaryDbContext,归属 AppDatabase) +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext +``` + +> Hangfire 使用 Scheduler.ConnectionString 指向的数据库,首次启动服务会自动建表;只需提前创建空数据库并授予账号权限。 + ## 3. Docker部署 ### 3.1 创建Dockerfile diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index f015bba..d858ad3 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -134,7 +134,7 @@ "PrefetchCount": 20 }, "Scheduler": { - "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_hangfire;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json index f015bba..d858ad3 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json @@ -134,7 +134,7 @@ "PrefetchCount": 20 }, "Scheduler": { - "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_hangfire;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" diff --git a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs new file mode 100644 index 0000000..694dfca --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Analytics.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Analytics.Entities; + +///

+/// 指标告警规则。 +/// +public sealed class MetricAlertRule : MultiTenantEntityBase +{ + /// + /// 关联指标。 + /// + public Guid MetricDefinitionId { get; set; } + + /// + /// 触发条件 JSON。 + /// + public string ConditionJson { get; set; } = string.Empty; + + /// + /// 告警级别。 + /// + public MetricAlertSeverity Severity { get; set; } = MetricAlertSeverity.Warning; + + /// + /// 通知渠道。 + /// + public string NotificationChannels { get; set; } = "email"; + + /// + /// 是否启用。 + /// + public bool Enabled { get; set; } = true; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricDefinition.cs b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricDefinition.cs new file mode 100644 index 0000000..1180216 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricDefinition.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Analytics.Entities; + +/// +/// 指标定义,描述可观测的数据点。 +/// +public sealed class MetricDefinition : MultiTenantEntityBase +{ + /// + /// 指标编码。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 指标名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 说明。 + /// + public string? Description { get; set; } + + /// + /// 维度描述 JSON。 + /// + public string? DimensionsJson { get; set; } + + /// + /// 默认聚合方式。 + /// + public string DefaultAggregation { get; set; } = "sum"; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs new file mode 100644 index 0000000..cb0e726 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Analytics.Entities; + +/// +/// 指标快照,用于大盘展示。 +/// +public sealed class MetricSnapshot : MultiTenantEntityBase +{ + /// + /// 指标定义 ID。 + /// + public Guid MetricDefinitionId { get; set; } + + /// + /// 维度键(JSON)。 + /// + public string DimensionKey { get; set; } = string.Empty; + + /// + /// 统计时间窗口开始。 + /// + public DateTime WindowStart { get; set; } + + /// + /// 统计时间窗口结束。 + /// + public DateTime WindowEnd { get; set; } + + /// + /// 数值。 + /// + public decimal Value { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Analytics/Enums/MetricAlertSeverity.cs b/src/Domain/TakeoutSaaS.Domain/Analytics/Enums/MetricAlertSeverity.cs new file mode 100644 index 0000000..a1e79eb --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Analytics/Enums/MetricAlertSeverity.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Analytics.Enums; + +/// +/// 指标告警严重程度。 +/// +public enum MetricAlertSeverity +{ + /// + /// 信息提示。 + /// + Info = 0, + + /// + /// 告警。 + /// + Warning = 1, + + /// + /// 严重。 + /// + Critical = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs new file mode 100644 index 0000000..c327f0b --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Coupons.Entities; + +/// +/// 用户领取的券。 +/// +public sealed class Coupon : MultiTenantEntityBase +{ + /// + /// 模板标识。 + /// + public Guid CouponTemplateId { get; set; } + + /// + /// 券码或序列号。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 归属用户。 + /// + public Guid UserId { get; set; } + + /// + /// 订单 ID(已使用时记录)。 + /// + public Guid? OrderId { get; set; } + + /// + /// 状态。 + /// + public CouponStatus Status { get; set; } = CouponStatus.Available; + + /// + /// 发放时间。 + /// + public DateTime IssuedAt { get; set; } = DateTime.UtcNow; + + /// + /// 使用时间。 + /// + public DateTime? UsedAt { get; set; } + + /// + /// 到期时间。 + /// + public DateTime ExpireAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/CouponTemplate.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/CouponTemplate.cs new file mode 100644 index 0000000..8f795a6 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/CouponTemplate.cs @@ -0,0 +1,90 @@ +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Coupons.Entities; + +/// +/// 优惠券模板。 +/// +public sealed class CouponTemplate : MultiTenantEntityBase +{ + /// + /// 模板名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 券类型。 + /// + public CouponType CouponType { get; set; } = CouponType.AmountOff; + + /// + /// 面值或折扣额度。 + /// + public decimal Value { get; set; } + + /// + /// 折扣上限(针对折扣券)。 + /// + public decimal? DiscountCap { get; set; } + + /// + /// 最低消费门槛。 + /// + public decimal? MinimumSpend { get; set; } + + /// + /// 可用开始时间。 + /// + public DateTime? ValidFrom { get; set; } + + /// + /// 可用结束时间。 + /// + public DateTime? ValidTo { get; set; } + + /// + /// 有效天数(相对发放时间)。 + /// + public int? RelativeValidDays { get; set; } + + /// + /// 总发放数量上限。 + /// + public int? TotalQuantity { get; set; } + + /// + /// 已领取数量。 + /// + public int ClaimedQuantity { get; set; } + + /// + /// 适用门店 ID 集合(JSON)。 + /// + public string? StoreScopeJson { get; set; } + + /// + /// 适用品类或商品范围(JSON)。 + /// + public string? ProductScopeJson { get; set; } + + /// + /// 发放渠道(JSON)。 + /// + public string? ChannelsJson { get; set; } + + /// + /// 是否允许叠加其他优惠。 + /// + public bool AllowStack { get; set; } + + /// + /// 状态。 + /// + public CouponTemplateStatus Status { get; set; } = CouponTemplateStatus.Draft; + + /// + /// 备注。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PromotionCampaign.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PromotionCampaign.cs new file mode 100644 index 0000000..8cab151 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PromotionCampaign.cs @@ -0,0 +1,55 @@ +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Coupons.Entities; + +/// +/// 营销活动配置。 +/// +public sealed class PromotionCampaign : MultiTenantEntityBase +{ + /// + /// 活动名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 活动类型。 + /// + public PromotionType PromotionType { get; set; } = PromotionType.Coupon; + + /// + /// 活动状态。 + /// + public PromotionStatus Status { get; set; } = PromotionStatus.Draft; + + /// + /// 开始时间。 + /// + public DateTime StartAt { get; set; } + + /// + /// 结束时间。 + /// + public DateTime EndAt { get; set; } + + /// + /// 预算金额。 + /// + public decimal? Budget { get; set; } + + /// + /// 活动规则 JSON。 + /// + public string RulesJson { get; set; } = string.Empty; + + /// + /// 目标人群描述。 + /// + public string? AudienceDescription { get; set; } + + /// + /// 营销素材(如 banner)。 + /// + public string? BannerUrl { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponStatus.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponStatus.cs new file mode 100644 index 0000000..7897089 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponStatus.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 券使用状态。 +/// +public enum CouponStatus +{ + /// + /// 可使用。 + /// + Available = 0, + + /// + /// 已锁定。 + /// + Locked = 1, + + /// + /// 已使用。 + /// + Redeemed = 2, + + /// + /// 已过期。 + /// + Expired = 3, + + /// + /// 已作废。 + /// + Voided = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponTemplateStatus.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponTemplateStatus.cs new file mode 100644 index 0000000..d5aefd5 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponTemplateStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 券模板状态。 +/// +public enum CouponTemplateStatus +{ + /// + /// 草稿状态。 + /// + Draft = 0, + + /// + /// 已上线可发放。 + /// + Active = 1, + + /// + /// 已下架。 + /// + Archived = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponType.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponType.cs new file mode 100644 index 0000000..8206bd2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/CouponType.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 券类型。 +/// +public enum CouponType +{ + /// + /// 满减券。 + /// + AmountOff = 0, + + /// + /// 折扣券。 + /// + Percentage = 1, + + /// + /// 现金券/无门槛券。 + /// + Cash = 2, + + /// + /// 免配送费券。 + /// + DeliveryFee = 3, + + /// + /// 礼品/兑换券。 + /// + Gift = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionStatus.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionStatus.cs new file mode 100644 index 0000000..2b12ee1 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 营销活动状态。 +/// +public enum PromotionStatus +{ + /// + /// 草稿。 + /// + Draft = 0, + + /// + /// 进行中。 + /// + Active = 1, + + /// + /// 已结束。 + /// + Completed = 2, + + /// + /// 暂停。 + /// + Paused = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionType.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionType.cs new file mode 100644 index 0000000..e30893d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PromotionType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 营销活动类型。 +/// +public enum PromotionType +{ + /// + /// 优惠券活动。 + /// + Coupon = 0, + + /// + /// 秒杀/限时购。 + /// + FlashSale = 1, + + /// + /// 满减活动。 + /// + FullReduction = 2, + + /// + /// 抽奖活动。 + /// + Lottery = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs new file mode 100644 index 0000000..56799b5 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.CustomerService.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.CustomerService.Entities; + +/// +/// 会话消息。 +/// +public sealed class ChatMessage : MultiTenantEntityBase +{ + /// + /// 会话标识。 + /// + public Guid ChatSessionId { get; set; } + + /// + /// 发送方类型。 + /// + public MessageSenderType SenderType { get; set; } = MessageSenderType.Customer; + + /// + /// 发送方用户 ID。 + /// + public Guid? SenderUserId { get; set; } + + /// + /// 消息内容。 + /// + public string Content { get; set; } = string.Empty; + + /// + /// 消息类型(文字/图片/语音等)。 + /// + public string ContentType { get; set; } = "text/plain"; + + /// + /// 是否已读。 + /// + public bool IsRead { get; set; } + + /// + /// 读取时间。 + /// + public DateTime? ReadAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs new file mode 100644 index 0000000..d2528ea --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.CustomerService.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.CustomerService.Entities; + +/// +/// 客服会话。 +/// +public sealed class ChatSession : MultiTenantEntityBase +{ + /// + /// 会话编号。 + /// + public string SessionCode { get; set; } = string.Empty; + + /// + /// 顾客用户 ID。 + /// + public Guid CustomerUserId { get; set; } + + /// + /// 当前客服员工 ID。 + /// + public Guid? AgentUserId { get; set; } + + /// + /// 所属门店(可空为平台)。 + /// + public Guid? StoreId { get; set; } + + /// + /// 会话状态。 + /// + public ChatSessionStatus Status { get; set; } = ChatSessionStatus.Waiting; + + /// + /// 是否机器人接待中。 + /// + public bool IsBotActive { get; set; } + + /// + /// 开始时间。 + /// + public DateTime StartedAt { get; set; } = DateTime.UtcNow; + + /// + /// 结束时间。 + /// + public DateTime? EndedAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs new file mode 100644 index 0000000..931727d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs @@ -0,0 +1,55 @@ +using TakeoutSaaS.Domain.CustomerService.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.CustomerService.Entities; + +/// +/// 客服工单。 +/// +public sealed class SupportTicket : MultiTenantEntityBase +{ + /// + /// 工单编号。 + /// + public string TicketNo { get; set; } = string.Empty; + + /// + /// 客户用户 ID。 + /// + public Guid CustomerUserId { get; set; } + + /// + /// 关联订单(如有)。 + /// + public Guid? OrderId { get; set; } + + /// + /// 工单主题。 + /// + public string Subject { get; set; } = string.Empty; + + /// + /// 工单详情。 + /// + public string Description { get; set; } = string.Empty; + + /// + /// 优先级。 + /// + public TicketPriority Priority { get; set; } = TicketPriority.Normal; + + /// + /// 状态。 + /// + public TicketStatus Status { get; set; } = TicketStatus.Open; + + /// + /// 指派的客服。 + /// + public Guid? AssignedAgentId { get; set; } + + /// + /// 关闭时间。 + /// + public DateTime? ClosedAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs new file mode 100644 index 0000000..f6abba6 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.CustomerService.Entities; + +/// +/// 工单评论/流转记录。 +/// +public sealed class TicketComment : MultiTenantEntityBase +{ + /// + /// 工单标识。 + /// + public Guid SupportTicketId { get; set; } + + /// + /// 评论人 ID。 + /// + public Guid? AuthorUserId { get; set; } + + /// + /// 评论内容。 + /// + public string Content { get; set; } = string.Empty; + + /// + /// 是否内部备注。 + /// + public bool IsInternal { get; set; } + + /// + /// 附件 JSON。 + /// + public string? AttachmentsJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/ChatSessionStatus.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/ChatSessionStatus.cs new file mode 100644 index 0000000..e774652 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/ChatSessionStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.CustomerService.Enums; + +/// +/// 客服会话状态。 +/// +public enum ChatSessionStatus +{ + /// + /// 等待客服接入。 + /// + Waiting = 0, + + /// + /// 聊天进行中。 + /// + Active = 1, + + /// + /// 已转人工排队。 + /// + Queueing = 2, + + /// + /// 已结束。 + /// + Closed = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/MessageSenderType.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/MessageSenderType.cs new file mode 100644 index 0000000..826c923 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/MessageSenderType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.CustomerService.Enums; + +/// +/// 消息发送方类型。 +/// +public enum MessageSenderType +{ + /// + /// 顾客。 + /// + Customer = 0, + + /// + /// 客服人员。 + /// + Agent = 1, + + /// + /// 机器人。 + /// + Bot = 2, + + /// + /// 系统通知。 + /// + System = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketPriority.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketPriority.cs new file mode 100644 index 0000000..e875355 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketPriority.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.CustomerService.Enums; + +/// +/// 工单优先级。 +/// +public enum TicketPriority +{ + /// + /// 普通。 + /// + Normal = 0, + + /// + /// 高。 + /// + High = 1, + + /// + /// 紧急。 + /// + Urgent = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketStatus.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketStatus.cs new file mode 100644 index 0000000..40d6311 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Enums/TicketStatus.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.CustomerService.Enums; + +/// +/// 工单状态。 +/// +public enum TicketStatus +{ + /// + /// 新建未处理。 + /// + Open = 0, + + /// + /// 处理中。 + /// + InProgress = 1, + + /// + /// 等待客户反馈。 + /// + PendingCustomer = 2, + + /// + /// 已解决。 + /// + Resolved = 3, + + /// + /// 已关闭。 + /// + Closed = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs new file mode 100644 index 0000000..771f564 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Deliveries.Entities; + +/// +/// 配送状态事件流水。 +/// +public sealed class DeliveryEvent : MultiTenantEntityBase +{ + /// + /// 配送单标识。 + /// + public Guid DeliveryOrderId { get; set; } + + /// + /// 事件类型。 + /// + public DeliveryEventType EventType { get; set; } = DeliveryEventType.Updated; + + /// + /// 事件描述。 + /// + public string Message { get; set; } = string.Empty; + + /// + /// 原始数据 JSON。 + /// + public string? Payload { get; set; } + + /// + /// 发生时间。 + /// + public DateTime OccurredAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs new file mode 100644 index 0000000..31dbfc1 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs @@ -0,0 +1,62 @@ +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Deliveries.Entities; + +/// +/// 配送单。 +/// +public sealed class DeliveryOrder : MultiTenantEntityBase +{ + public Guid OrderId { get; set; } + + /// + /// 配送服务商。 + /// + public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse; + + /// + /// 第三方配送单号。 + /// + public string? ProviderOrderId { get; set; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending; + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; set; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; set; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; set; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; set; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; set; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryEventType.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryEventType.cs new file mode 100644 index 0000000..ce1914d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryEventType.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Deliveries.Enums; + +/// +/// 配送事件类型。 +/// +public enum DeliveryEventType +{ + /// + /// 状态更新。 + /// + Updated = 0, + + /// + /// 渠道回调。 + /// + Callback = 1, + + /// + /// 加价或异常通知。 + /// + Exception = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs new file mode 100644 index 0000000..70cff85 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs @@ -0,0 +1,14 @@ +namespace TakeoutSaaS.Domain.Deliveries.Enums; + +/// +/// 配送服务商类型。 +/// +public enum DeliveryProvider +{ + InHouse = 0, + Dada = 1, + FlashEx = 2, + Meituan = 3, + Eleme = 4, + Shunfeng = 5 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs new file mode 100644 index 0000000..7f6aaef --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs @@ -0,0 +1,15 @@ +namespace TakeoutSaaS.Domain.Deliveries.Enums; + +/// +/// 配送状态。 +/// +public enum DeliveryStatus +{ + Pending = 0, + Accepted = 1, + PickingUp = 2, + Delivering = 3, + Completed = 4, + Cancelled = 5, + Failed = 6 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs new file mode 100644 index 0000000..0b1bb24 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Distribution.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Distribution.Entities; + +/// +/// 分销订单记录。 +/// +public sealed class AffiliateOrder : MultiTenantEntityBase +{ + /// + /// 推广人标识。 + /// + public Guid AffiliatePartnerId { get; set; } + + /// + /// 关联订单。 + /// + public Guid OrderId { get; set; } + + /// + /// 用户 ID。 + /// + public Guid BuyerUserId { get; set; } + + /// + /// 订单金额。 + /// + public decimal OrderAmount { get; set; } + + /// + /// 预计佣金。 + /// + public decimal EstimatedCommission { get; set; } + + /// + /// 当前状态。 + /// + public AffiliateOrderStatus Status { get; set; } = AffiliateOrderStatus.Pending; + + /// + /// 结算完成时间。 + /// + public DateTime? SettledAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs new file mode 100644 index 0000000..d9ceaa1 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Distribution.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Distribution.Entities; + +/// +/// 分销/推广合作伙伴。 +/// +public sealed class AffiliatePartner : MultiTenantEntityBase +{ + /// + /// 用户 ID(如绑定平台账号)。 + /// + public Guid? UserId { get; set; } + + /// + /// 昵称或渠道名称。 + /// + public string DisplayName { get; set; } = string.Empty; + + /// + /// 联系电话。 + /// + public string? Phone { get; set; } + + /// + /// 渠道类型。 + /// + public AffiliateChannelType ChannelType { get; set; } = AffiliateChannelType.Personal; + + /// + /// 分成比例(0-1)。 + /// + public decimal CommissionRate { get; set; } + + /// + /// 当前状态。 + /// + public AffiliateStatus Status { get; set; } = AffiliateStatus.Pending; + + /// + /// 审核备注。 + /// + public string? Remarks { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs new file mode 100644 index 0000000..774c4e4 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs @@ -0,0 +1,40 @@ +using TakeoutSaaS.Domain.Distribution.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Distribution.Entities; + +/// +/// 佣金结算记录。 +/// +public sealed class AffiliatePayout : MultiTenantEntityBase +{ + /// + /// 合作伙伴标识。 + /// + public Guid AffiliatePartnerId { get; set; } + + /// + /// 结算周期描述。 + /// + public string Period { get; set; } = string.Empty; + + /// + /// 结算金额。 + /// + public decimal Amount { get; set; } + + /// + /// 状态。 + /// + public PayoutStatus Status { get; set; } = PayoutStatus.Pending; + + /// + /// 打款时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 备注。 + /// + public string? Remarks { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateChannelType.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateChannelType.cs new file mode 100644 index 0000000..963bd98 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateChannelType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Distribution.Enums; + +/// +/// 推广渠道类型。 +/// +public enum AffiliateChannelType +{ + /// + /// 个人。 + /// + Personal = 0, + + /// + /// 自媒体。 + /// + Media = 1, + + /// + /// 门店地推。 + /// + Offline = 2, + + /// + /// 第三方联盟。 + /// + Alliance = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateOrderStatus.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateOrderStatus.cs new file mode 100644 index 0000000..a28f953 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateOrderStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Distribution.Enums; + +/// +/// 分销订单状态。 +/// +public enum AffiliateOrderStatus +{ + /// + /// 待成交通知。 + /// + Pending = 0, + + /// + /// 已完成待结算。 + /// + AwaitingPayout = 1, + + /// + /// 已结算。 + /// + Settled = 2, + + /// + /// 因退款失效。 + /// + Invalidated = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateStatus.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateStatus.cs new file mode 100644 index 0000000..7e25a50 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/AffiliateStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Distribution.Enums; + +/// +/// 合作伙伴状态。 +/// +public enum AffiliateStatus +{ + /// + /// 待审核。 + /// + Pending = 0, + + /// + /// 已激活。 + /// + Active = 1, + + /// + /// 已冻结。 + /// + Suspended = 2, + + /// + /// 已退出。 + /// + Closed = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/PayoutStatus.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/PayoutStatus.cs new file mode 100644 index 0000000..0f96e3d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Enums/PayoutStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Distribution.Enums; + +/// +/// 佣金结算状态。 +/// +public enum PayoutStatus +{ + /// + /// 待打款。 + /// + Pending = 0, + + /// + /// 已打款。 + /// + Paid = 1, + + /// + /// 已驳回。 + /// + Rejected = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInCampaign.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInCampaign.cs new file mode 100644 index 0000000..1e279d5 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInCampaign.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Engagement.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Engagement.Entities; + +/// +/// 签到活动配置。 +/// +public sealed class CheckInCampaign : MultiTenantEntityBase +{ + /// + /// 活动名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 活动描述。 + /// + public string? Description { get; set; } + + /// + /// 开始日期。 + /// + public DateTime StartDate { get; set; } + + /// + /// 结束日期。 + /// + public DateTime EndDate { get; set; } + + /// + /// 支持补签次数。 + /// + public int AllowMakeupCount { get; set; } + + /// + /// 连签奖励 JSON。 + /// + public string RewardsJson { get; set; } = string.Empty; + + /// + /// 状态。 + /// + public CheckInCampaignStatus Status { get; set; } = CheckInCampaignStatus.Draft; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs new file mode 100644 index 0000000..9f41172 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Engagement.Entities; + +/// +/// 用户签到记录。 +/// +public sealed class CheckInRecord : MultiTenantEntityBase +{ + /// + /// 活动标识。 + /// + public Guid CheckInCampaignId { get; set; } + + /// + /// 用户标识。 + /// + public Guid UserId { get; set; } + + /// + /// 签到日期(本地)。 + /// + public DateTime CheckInDate { get; set; } + + /// + /// 是否补签。 + /// + public bool IsMakeup { get; set; } + + /// + /// 获得奖励 JSON。 + /// + public string RewardJson { get; set; } = string.Empty; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs new file mode 100644 index 0000000..3888bfd --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Engagement.Entities; + +/// +/// 社区评论。 +/// +public sealed class CommunityComment : MultiTenantEntityBase +{ + /// + /// 动态标识。 + /// + public Guid PostId { get; set; } + + /// + /// 评论人。 + /// + public Guid AuthorUserId { get; set; } + + /// + /// 评论内容。 + /// + public string Content { get; set; } = string.Empty; + + /// + /// 父级评论 ID。 + /// + public Guid? ParentId { get; set; } + + /// + /// 状态。 + /// + public bool IsDeleted { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs new file mode 100644 index 0000000..73c7e21 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Engagement.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Engagement.Entities; + +/// +/// 社区动态。 +/// +public sealed class CommunityPost : MultiTenantEntityBase +{ + /// + /// 作者用户 ID。 + /// + public Guid AuthorUserId { get; set; } + + /// + /// 标题。 + /// + public string? Title { get; set; } + + /// + /// 内容。 + /// + public string Content { get; set; } = string.Empty; + + /// + /// 媒体资源 JSON。 + /// + public string? MediaJson { get; set; } + + /// + /// 状态。 + /// + public PostStatus Status { get; set; } = PostStatus.PendingReview; + + /// + /// 点赞数。 + /// + public int LikeCount { get; set; } + + /// + /// 评论数。 + /// + public int CommentCount { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs new file mode 100644 index 0000000..b7d5c19 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs @@ -0,0 +1,30 @@ +using TakeoutSaaS.Domain.Engagement.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Engagement.Entities; + +/// +/// 社区互动反馈。 +/// +public sealed class CommunityReaction : MultiTenantEntityBase +{ + /// + /// 动态 ID。 + /// + public Guid PostId { get; set; } + + /// + /// 用户 ID。 + /// + public Guid UserId { get; set; } + + /// + /// 反应类型。 + /// + public ReactionType ReactionType { get; set; } = ReactionType.Like; + + /// + /// 时间戳。 + /// + public DateTime ReactedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/CheckInCampaignStatus.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/CheckInCampaignStatus.cs new file mode 100644 index 0000000..57a3f0c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/CheckInCampaignStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Engagement.Enums; + +/// +/// 签到活动状态。 +/// +public enum CheckInCampaignStatus +{ + /// + /// 草稿。 + /// + Draft = 0, + + /// + /// 进行中。 + /// + Active = 1, + + /// + /// 已结束。 + /// + Completed = 2, + + /// + /// 已停用。 + /// + Disabled = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/PostStatus.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/PostStatus.cs new file mode 100644 index 0000000..9ce168b --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/PostStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Engagement.Enums; + +/// +/// 社区动态状态。 +/// +public enum PostStatus +{ + /// + /// 待审核。 + /// + PendingReview = 0, + + /// + /// 已发布。 + /// + Published = 1, + + /// + /// 已屏蔽。 + /// + Blocked = 2, + + /// + /// 已删除。 + /// + Deleted = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/ReactionType.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/ReactionType.cs new file mode 100644 index 0000000..7328bf7 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Enums/ReactionType.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Engagement.Enums; + +/// +/// 互动类型。 +/// +public enum ReactionType +{ + /// + /// 点赞。 + /// + Like = 0, + + /// + /// 收藏。 + /// + Favorite = 1, + + /// + /// 点踩。 + /// + Dislike = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs new file mode 100644 index 0000000..e5412de --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs @@ -0,0 +1,70 @@ +using TakeoutSaaS.Domain.GroupBuying.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.GroupBuying.Entities; + +/// +/// 拼单活动。 +/// +public sealed class GroupOrder : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 关联商品或套餐。 + /// + public Guid ProductId { get; set; } + + /// + /// 拼单编号。 + /// + public string GroupOrderNo { get; set; } = string.Empty; + + /// + /// 团长用户 ID。 + /// + public Guid LeaderUserId { get; set; } + + /// + /// 成团需要的人数。 + /// + public int TargetCount { get; set; } + + /// + /// 当前已参与人数。 + /// + public int CurrentCount { get; set; } + + /// + /// 拼团价格。 + /// + public decimal GroupPrice { get; set; } + + /// + /// 开始时间。 + /// + public DateTime StartAt { get; set; } + + /// + /// 结束时间。 + /// + public DateTime EndAt { get; set; } + + /// + /// 拼团状态。 + /// + public GroupOrderStatus Status { get; set; } = GroupOrderStatus.Open; + + /// + /// 成团时间。 + /// + public DateTime? SucceededAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs new file mode 100644 index 0000000..61e169a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.GroupBuying.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.GroupBuying.Entities; + +/// +/// 拼单参与者。 +/// +public sealed class GroupParticipant : MultiTenantEntityBase +{ + /// + /// 拼单活动标识。 + /// + public Guid GroupOrderId { get; set; } + + /// + /// 对应订单标识。 + /// + public Guid OrderId { get; set; } + + /// + /// 用户标识。 + /// + public Guid UserId { get; set; } + + /// + /// 参与状态。 + /// + public GroupParticipantStatus Status { get; set; } = GroupParticipantStatus.Joined; + + /// + /// 参与时间。 + /// + public DateTime JoinedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupOrderStatus.cs b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupOrderStatus.cs new file mode 100644 index 0000000..b5969fd --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupOrderStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.GroupBuying.Enums; + +/// +/// 拼单状态。 +/// +public enum GroupOrderStatus +{ + /// + /// 开放中。 + /// + Open = 0, + + /// + /// 已成团。 + /// + Succeeded = 1, + + /// + /// 已取消。 + /// + Cancelled = 2, + + /// + /// 超时失败。 + /// + Failed = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupParticipantStatus.cs b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupParticipantStatus.cs new file mode 100644 index 0000000..7b5f807 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Enums/GroupParticipantStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.GroupBuying.Enums; + +/// +/// 拼单参与者状态。 +/// +public enum GroupParticipantStatus +{ + /// + /// 已参团。 + /// + Joined = 0, + + /// + /// 因退款或取消退出。 + /// + Exited = 1, + + /// + /// 团失败待退款。 + /// + PendingRefund = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs new file mode 100644 index 0000000..36b7d59 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs @@ -0,0 +1,40 @@ +using TakeoutSaaS.Domain.Inventory.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Inventory.Entities; + +/// +/// 库存调整记录。 +/// +public sealed class InventoryAdjustment : MultiTenantEntityBase +{ + /// + /// 对应的库存记录标识。 + /// + public Guid InventoryItemId { get; set; } + + /// + /// 调整类型。 + /// + public InventoryAdjustmentType AdjustmentType { get; set; } = InventoryAdjustmentType.Manual; + + /// + /// 调整数量,正数增加,负数减少。 + /// + public int Quantity { get; set; } + + /// + /// 原因说明。 + /// + public string? Reason { get; set; } + + /// + /// 操作人标识。 + /// + public Guid? OperatorId { get; set; } + + /// + /// 发生时间。 + /// + public DateTime OccurredAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs new file mode 100644 index 0000000..8a764b3 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs @@ -0,0 +1,44 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Inventory.Entities; + +/// +/// SKU 批次信息。 +/// +public sealed class InventoryBatch : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// SKU 标识。 + /// + public Guid ProductSkuId { get; set; } + + /// + /// 批次编号。 + /// + public string BatchNumber { get; set; } = string.Empty; + + /// + /// 生产日期。 + /// + public DateTime? ProductionDate { get; set; } + + /// + /// 过期日期。 + /// + public DateTime? ExpireDate { get; set; } + + /// + /// 入库数量。 + /// + public int Quantity { get; set; } + + /// + /// 剩余数量。 + /// + public int RemainingQuantity { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs new file mode 100644 index 0000000..6432253 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs @@ -0,0 +1,49 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Inventory.Entities; + +/// +/// SKU 在门店的库存信息。 +/// +public sealed class InventoryItem : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// SKU 标识。 + /// + public Guid ProductSkuId { get; set; } + + /// + /// 批次编号,可为空表示混批。 + /// + public string? BatchNumber { get; set; } + + /// + /// 可用库存。 + /// + public int QuantityOnHand { get; set; } + + /// + /// 已锁定库存(订单占用)。 + /// + public int QuantityReserved { get; set; } + + /// + /// 安全库存阈值。 + /// + public int? SafetyStock { get; set; } + + /// + /// 储位或仓位信息。 + /// + public string? Location { get; set; } + + /// + /// 过期日期。 + /// + public DateTime? ExpireDate { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Enums/InventoryAdjustmentType.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Enums/InventoryAdjustmentType.cs new file mode 100644 index 0000000..58e0913 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Enums/InventoryAdjustmentType.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Inventory.Enums; + +/// +/// 库存调整类型。 +/// +public enum InventoryAdjustmentType +{ + /// + /// 手动盘点调整。 + /// + Manual = 0, + + /// + /// 采购入库。 + /// + Purchase = 1, + + /// + /// 退货回库。 + /// + Return = 2, + + /// + /// 报损。 + /// + Damage = 3, + + /// + /// 过期销毁。 + /// + Expiration = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs new file mode 100644 index 0000000..b06f23f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Membership.Entities; + +/// +/// 成长值变动日志。 +/// +public sealed class MemberGrowthLog : MultiTenantEntityBase +{ + /// + /// 会员标识。 + /// + public Guid MemberId { get; set; } + + /// + /// 变动数量。 + /// + public int ChangeValue { get; set; } + + /// + /// 当前成长值。 + /// + public int CurrentValue { get; set; } + + /// + /// 备注。 + /// + public string? Notes { get; set; } + + /// + /// 发生时间。 + /// + public DateTime OccurredAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs new file mode 100644 index 0000000..07c4fbe --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Membership.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Membership.Entities; + +/// +/// 积分变动流水。 +/// +public sealed class MemberPointLedger : MultiTenantEntityBase +{ + /// + /// 会员标识。 + /// + public Guid MemberId { get; set; } + + /// + /// 变动数量,可为负值。 + /// + public int ChangeAmount { get; set; } + + /// + /// 变动后余额。 + /// + public int BalanceAfterChange { get; set; } + + /// + /// 变动原因。 + /// + public PointChangeReason Reason { get; set; } = PointChangeReason.Purchase; + + /// + /// 来源 ID(订单、活动等)。 + /// + public Guid? SourceId { get; set; } + + /// + /// 发生时间。 + /// + public DateTime OccurredAt { get; set; } = DateTime.UtcNow; + + /// + /// 过期时间(如适用)。 + /// + public DateTime? ExpireAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs new file mode 100644 index 0000000..7378ccd --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs @@ -0,0 +1,60 @@ +using TakeoutSaaS.Domain.Membership.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Membership.Entities; + +/// +/// 会员档案。 +/// +public sealed class MemberProfile : MultiTenantEntityBase +{ + /// + /// 用户标识。 + /// + public Guid UserId { get; set; } + + /// + /// 手机号。 + /// + public string Mobile { get; set; } = string.Empty; + + /// + /// 昵称。 + /// + public string? Nickname { get; set; } + + /// + /// 头像。 + /// + public string? AvatarUrl { get; set; } + + /// + /// 当前会员等级 ID。 + /// + public Guid? MemberTierId { get; set; } + + /// + /// 会员状态。 + /// + public MemberStatus Status { get; set; } = MemberStatus.Active; + + /// + /// 会员积分余额。 + /// + public int PointsBalance { get; set; } + + /// + /// 成长值/经验值。 + /// + public int GrowthValue { get; set; } + + /// + /// 生日。 + /// + public DateTime? BirthDate { get; set; } + + /// + /// 注册时间。 + /// + public DateTime JoinedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberTier.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberTier.cs new file mode 100644 index 0000000..bd40724 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberTier.cs @@ -0,0 +1,29 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Membership.Entities; + +/// +/// 会员等级定义。 +/// +public sealed class MemberTier : MultiTenantEntityBase +{ + /// + /// 等级名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 所需成长值。 + /// + public int RequiredGrowth { get; set; } + + /// + /// 等级权益(JSON)。 + /// + public string BenefitsJson { get; set; } = string.Empty; + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberStatus.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberStatus.cs new file mode 100644 index 0000000..5383e3a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Membership.Enums; + +/// +/// 会员状态。 +/// +public enum MemberStatus +{ + /// + /// 正常。 + /// + Active = 0, + + /// + /// 已冻结。 + /// + Frozen = 1, + + /// + /// 已注销。 + /// + Cancelled = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Enums/PointChangeReason.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Enums/PointChangeReason.cs new file mode 100644 index 0000000..61579b2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Enums/PointChangeReason.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Membership.Enums; + +/// +/// 积分变动原因。 +/// +public enum PointChangeReason +{ + /// + /// 正常消费获得。 + /// + Purchase = 0, + + /// + /// 活动奖励。 + /// + Promotion = 1, + + /// + /// 签到或任务。 + /// + Task = 2, + + /// + /// 管理员调整。 + /// + Manual = 3, + + /// + /// 抵扣消费。 + /// + Redeem = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs new file mode 100644 index 0000000..d05ae41 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs @@ -0,0 +1,120 @@ +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Merchants.Entities; + +/// +/// 商户主体信息,承载入驻和资质审核结果。 +/// +public sealed class Merchant : MultiTenantEntityBase +{ + /// + /// 品牌名称(对外展示)。 + /// + public string BrandName { get; set; } = string.Empty; + + /// + /// 品牌简称或别名。 + /// + public string? BrandAlias { get; set; } + + /// + /// 品牌 Logo。 + /// + public string? LogoUrl { get; set; } + + /// + /// 品牌所属品类,如火锅、咖啡等。 + /// + public string? Category { get; set; } + + /// + /// 营业执照号。 + /// + public string? BusinessLicenseNumber { get; set; } + + /// + /// 营业执照扫描件地址。 + /// + public string? BusinessLicenseImageUrl { get; set; } + + /// + /// 税号/统一社会信用代码。 + /// + public string? TaxNumber { get; set; } + + /// + /// 法人或负责人姓名。 + /// + public string? LegalPerson { get; set; } + + /// + /// 联系电话。 + /// + public string ContactPhone { get; set; } = string.Empty; + + /// + /// 联系邮箱。 + /// + public string? ContactEmail { get; set; } + + /// + /// 客服电话。 + /// + public string? ServicePhone { get; set; } + + /// + /// 客服邮箱。 + /// + public string? SupportEmail { get; set; } + + /// + /// 所在省份。 + /// + public string? Province { get; set; } + + /// + /// 所在城市。 + /// + public string? City { get; set; } + + /// + /// 所在区县。 + /// + public string? District { get; set; } + + /// + /// 详细地址。 + /// + public string? Address { get; set; } + + /// + /// 经度信息。 + /// + public double? Longitude { get; set; } + + /// + /// 纬度信息。 + /// + public double? Latitude { get; set; } + + /// + /// 入驻状态。 + /// + public MerchantStatus Status { get; set; } = MerchantStatus.Pending; + + /// + /// 审核备注或驳回原因。 + /// + public string? ReviewRemarks { get; set; } + + /// + /// 入驻时间。 + /// + public DateTime? JoinedAt { get; set; } + + /// + /// 最近一次审核时间。 + /// + public DateTime? LastReviewedAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs new file mode 100644 index 0000000..e8a21bb --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs @@ -0,0 +1,55 @@ +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Merchants.Entities; + +/// +/// 商户合同记录。 +/// +public sealed class MerchantContract : MultiTenantEntityBase +{ + /// + /// 所属商户标识。 + /// + public Guid MerchantId { get; set; } + + /// + /// 合同编号。 + /// + public string ContractNumber { get; set; } = string.Empty; + + /// + /// 合同状态。 + /// + public ContractStatus Status { get; set; } = ContractStatus.Draft; + + /// + /// 合同开始时间。 + /// + public DateTime StartDate { get; set; } + + /// + /// 合同结束时间。 + /// + public DateTime EndDate { get; set; } + + /// + /// 合同文件存储地址。 + /// + public string FileUrl { get; set; } = string.Empty; + + /// + /// 签署时间。 + /// + public DateTime? SignedAt { get; set; } + + /// + /// 终止时间。 + /// + public DateTime? TerminatedAt { get; set; } + + /// + /// 终止原因。 + /// + public string? TerminationReason { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs new file mode 100644 index 0000000..572f718 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Merchants.Entities; + +/// +/// 商户提交的资质或证照材料。 +/// +public sealed class MerchantDocument : MultiTenantEntityBase +{ + /// + /// 所属商户标识。 + /// + public Guid MerchantId { get; set; } + + /// + /// 证照类型。 + /// + public MerchantDocumentType DocumentType { get; set; } = MerchantDocumentType.BusinessLicense; + + /// + /// 审核状态。 + /// + public MerchantDocumentStatus Status { get; set; } = MerchantDocumentStatus.Pending; + + /// + /// 证照文件链接。 + /// + public string FileUrl { get; set; } = string.Empty; + + /// + /// 证照编号。 + /// + public string? DocumentNumber { get; set; } + + /// + /// 签发日期。 + /// + public DateTime? IssuedAt { get; set; } + + /// + /// 到期日期。 + /// + public DateTime? ExpiresAt { get; set; } + + /// + /// 审核备注或驳回原因。 + /// + public string? Remarks { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs new file mode 100644 index 0000000..273054a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs @@ -0,0 +1,55 @@ +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Merchants.Entities; + +/// +/// 商户员工账号,支持门店维度分配。 +/// +public sealed class MerchantStaff : MultiTenantEntityBase +{ + /// + /// 所属商户标识。 + /// + public Guid MerchantId { get; set; } + + /// + /// 可选的关联门店 ID。 + /// + public Guid? StoreId { get; set; } + + /// + /// 员工姓名。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 手机号。 + /// + public string Phone { get; set; } = string.Empty; + + /// + /// 邮箱地址。 + /// + public string? Email { get; set; } + + /// + /// 登录账号 ID(指向统一身份体系)。 + /// + public Guid? IdentityUserId { get; set; } + + /// + /// 员工角色类型。 + /// + public StaffRoleType RoleType { get; set; } = StaffRoleType.FrontDesk; + + /// + /// 员工状态。 + /// + public StaffStatus Status { get; set; } = StaffStatus.Active; + + /// + /// 自定义权限(JSON)。 + /// + public string? PermissionsJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/ContractStatus.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/ContractStatus.cs new file mode 100644 index 0000000..c38ebff --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/ContractStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Merchants.Enums; + +/// +/// 商户合同状态。 +/// +public enum ContractStatus +{ + /// + /// 草拟中。 + /// + Draft = 0, + + /// + /// 已生效。 + /// + Active = 1, + + /// + /// 已到期。 + /// + Expired = 2, + + /// + /// 已解除。 + /// + Terminated = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentStatus.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentStatus.cs new file mode 100644 index 0000000..9d1bf46 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Merchants.Enums; + +/// +/// 证照审核状态。 +/// +public enum MerchantDocumentStatus +{ + /// + /// 等待审核。 + /// + Pending = 0, + + /// + /// 审核通过。 + /// + Approved = 1, + + /// + /// 审核驳回。 + /// + Rejected = 2, + + /// + /// 已过期待更新。 + /// + Expired = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentType.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentType.cs new file mode 100644 index 0000000..0dffa2d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantDocumentType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Merchants.Enums; + +/// +/// 商户证照类型。 +/// +public enum MerchantDocumentType +{ + /// + /// 营业执照。 + /// + BusinessLicense = 0, + + /// + /// 餐饮服务许可证。 + /// + CateringPermit = 1, + + /// + /// 税务登记证。 + /// + TaxCertificate = 2, + + /// + /// 其他补充资质。 + /// + Other = 99 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs new file mode 100644 index 0000000..6ed8097 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Domain.Merchants.Enums; + +/// +/// 商户入驻状态。 +/// +public enum MerchantStatus +{ + Pending = 0, + Approved = 1, + Rejected = 2, + Frozen = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffRoleType.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffRoleType.cs new file mode 100644 index 0000000..c5b3d33 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffRoleType.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Merchants.Enums; + +/// +/// 商户员工角色。 +/// +public enum StaffRoleType +{ + /// + /// 管理员。 + /// + Admin = 0, + + /// + /// 前台收银。 + /// + FrontDesk = 1, + + /// + /// 后厨制作。 + /// + Kitchen = 2, + + /// + /// 配送骑手。 + /// + Courier = 3, + + /// + /// 运营人员。 + /// + Operator = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffStatus.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffStatus.cs new file mode 100644 index 0000000..20ab741 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/StaffStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Merchants.Enums; + +/// +/// 员工账号状态。 +/// +public enum StaffStatus +{ + /// + /// 正常在职。 + /// + Active = 0, + + /// + /// 停用。 + /// + Disabled = 1, + + /// + /// 已离职。 + /// + Resigned = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs new file mode 100644 index 0000000..74d9e14 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs @@ -0,0 +1,39 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Navigation.Entities; + +/// +/// 地图 POI 信息,用于门店定位和推荐。 +/// +public sealed class MapLocation : MultiTenantEntityBase +{ + /// + /// 关联门店 ID,可空表示独立 POI。 + /// + public Guid? StoreId { get; set; } + + /// + /// 名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 地址。 + /// + public string Address { get; set; } = string.Empty; + + /// + /// 经度。 + /// + public double Longitude { get; set; } + + /// + /// 纬度。 + /// + public double Latitude { get; set; } + + /// + /// 打车/导航落点描述。 + /// + public string? Landmark { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs new file mode 100644 index 0000000..284f1a5 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Navigation.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Navigation.Entities; + +/// +/// 用户发起的导航请求日志。 +/// +public sealed class NavigationRequest : MultiTenantEntityBase +{ + /// + /// 用户 ID。 + /// + public Guid UserId { get; set; } + + /// + /// 门店 ID。 + /// + public Guid StoreId { get; set; } + + /// + /// 来源通道(小程序、H5 等)。 + /// + public NavigationChannel Channel { get; set; } = NavigationChannel.MiniProgram; + + /// + /// 跳转的地图应用。 + /// + public NavigationTargetApp TargetApp { get; set; } = NavigationTargetApp.WechatMap; + + /// + /// 请求时间。 + /// + public DateTime RequestedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationChannel.cs b/src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationChannel.cs new file mode 100644 index 0000000..ed60c26 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationChannel.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Navigation.Enums; + +/// +/// 导航请求来源渠道。 +/// +public enum NavigationChannel +{ + /// + /// 小程序。 + /// + MiniProgram = 0, + + /// + /// H5/公众号。 + /// + Web = 1, + + /// + /// App。 + /// + MobileApp = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationTargetApp.cs b/src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationTargetApp.cs new file mode 100644 index 0000000..7e9d834 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Navigation/Enums/NavigationTargetApp.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Navigation.Enums; + +/// +/// 导航目标应用。 +/// +public enum NavigationTargetApp +{ + /// + /// 微信地图。 + /// + WechatMap = 0, + + /// + /// 腾讯地图。 + /// + Tencent = 1, + + /// + /// 高德。 + /// + Amap = 2, + + /// + /// 百度地图。 + /// + Baidu = 3, + + /// + /// Apple Map。 + /// + Apple = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs new file mode 100644 index 0000000..d19d659 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs @@ -0,0 +1,55 @@ +using TakeoutSaaS.Domain.Ordering.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Ordering.Entities; + +/// +/// 购物车条目。 +/// +public sealed class CartItem : MultiTenantEntityBase +{ + /// + /// 所属购物车标识。 + /// + public Guid ShoppingCartId { get; set; } + + /// + /// 商品或 SKU 标识。 + /// + public Guid ProductId { get; set; } + + /// + /// SKU 标识。 + /// + public Guid? ProductSkuId { get; set; } + + /// + /// 商品名称快照。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// 单价快照。 + /// + public decimal UnitPrice { get; set; } + + /// + /// 数量。 + /// + public int Quantity { get; set; } + + /// + /// 自定义备注(口味要求)。 + /// + public string? Remark { get; set; } + + /// + /// 状态。 + /// + public CartItemStatus Status { get; set; } = CartItemStatus.Normal; + + /// + /// 扩展 JSON(规格、加料选项等)。 + /// + public string? AttributesJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs new file mode 100644 index 0000000..eb99a0d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs @@ -0,0 +1,29 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Ordering.Entities; + +/// +/// 购物车条目的加料/附加项。 +/// +public sealed class CartItemAddon : MultiTenantEntityBase +{ + /// + /// 所属购物车条目。 + /// + public Guid CartItemId { get; set; } + + /// + /// 选项名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 附加价格。 + /// + public decimal ExtraPrice { get; set; } + + /// + /// 选项 ID(可对应 ProductAddonOption)。 + /// + public Guid? OptionId { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs new file mode 100644 index 0000000..a3beb7a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs @@ -0,0 +1,40 @@ +using TakeoutSaaS.Domain.Ordering.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Ordering.Entities; + +/// +/// 结账会话,记录校验上下文。 +/// +public sealed class CheckoutSession : MultiTenantEntityBase +{ + /// + /// 用户标识。 + /// + public Guid UserId { get; set; } + + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 会话 Token。 + /// + public string SessionToken { get; set; } = string.Empty; + + /// + /// 会话状态。 + /// + public CheckoutSessionStatus Status { get; set; } = CheckoutSessionStatus.Pending; + + /// + /// 校验结果明细 JSON。 + /// + public string ValidationResultJson { get; set; } = string.Empty; + + /// + /// 过期时间(UTC)。 + /// + public DateTime ExpiresAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs new file mode 100644 index 0000000..7928c1e --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs @@ -0,0 +1,40 @@ +using TakeoutSaaS.Domain.Ordering.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Ordering.Entities; + +/// +/// 用户购物车,按租户/门店隔离。 +/// +public sealed class ShoppingCart : MultiTenantEntityBase +{ + /// + /// 用户标识。 + /// + public Guid UserId { get; set; } + + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 购物车状态,包含正常/锁定。 + /// + public ShoppingCartStatus Status { get; set; } = ShoppingCartStatus.Active; + + /// + /// 桌码或场景标识(扫码点餐)。 + /// + public string? TableContext { get; set; } + + /// + /// 履约方式(堂食/自提/配送)缓存。 + /// + public string? DeliveryPreference { get; set; } + + /// + /// 最近一次修改时间(UTC)。 + /// + public DateTime LastModifiedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CartItemStatus.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CartItemStatus.cs new file mode 100644 index 0000000..f717b97 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CartItemStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Ordering.Enums; + +/// +/// 购物车条目状态。 +/// +public enum CartItemStatus +{ + /// + /// 正常可结算。 + /// + Normal = 0, + + /// + /// 不可售(售罄或下架)。 + /// + Unavailable = 1, + + /// + /// 已被删除。 + /// + Removed = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CheckoutSessionStatus.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CheckoutSessionStatus.cs new file mode 100644 index 0000000..fbb71c8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/CheckoutSessionStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Ordering.Enums; + +/// +/// 结账会话状态。 +/// +public enum CheckoutSessionStatus +{ + /// + /// 等待用户提交或支付。 + /// + Pending = 0, + + /// + /// 校验失败。 + /// + Failed = 1, + + /// + /// 已用于创建订单。 + /// + Completed = 2, + + /// + /// 超时作废。 + /// + Expired = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/ShoppingCartStatus.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/ShoppingCartStatus.cs new file mode 100644 index 0000000..88d16af --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Enums/ShoppingCartStatus.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Ordering.Enums; + +/// +/// 购物车状态。 +/// +public enum ShoppingCartStatus +{ + /// + /// 可正常使用。 + /// + Active = 0, + + /// + /// 已锁定(进行结账中)。 + /// + Locked = 1, + + /// + /// 已清空或失效。 + /// + Cleared = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs new file mode 100644 index 0000000..9a1c295 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs @@ -0,0 +1,111 @@ +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Orders.Entities; + +/// +/// 交易订单。 +/// +public sealed class Order : MultiTenantEntityBase +{ + /// + /// 订单号。 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 门店。 + /// + public Guid StoreId { get; set; } + + /// + /// 下单渠道。 + /// + public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram; + + /// + /// 履约类型。 + /// + public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn; + + /// + /// 当前状态。 + /// + public OrderStatus Status { get; set; } = OrderStatus.PendingPayment; + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid; + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; set; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; set; } + + /// + /// 就餐桌号。 + /// + public string? TableNo { get; set; } + + /// + /// 排队号(如有)。 + /// + public string? QueueNumber { get; set; } + + /// + /// 预约 ID。 + /// + public Guid? ReservationId { get; set; } + + /// + /// 商品总额。 + /// + public decimal ItemsAmount { get; set; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; set; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs new file mode 100644 index 0000000..83681ca --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs @@ -0,0 +1,59 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Orders.Entities; + +/// +/// 订单明细。 +/// +public sealed class OrderItem : MultiTenantEntityBase +{ + /// + /// 订单 ID。 + /// + public Guid OrderId { get; set; } + + /// + /// 商品 ID。 + /// + public Guid ProductId { get; set; } + + /// + /// 商品名称。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// SKU/规格描述。 + /// + public string? SkuName { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 数量。 + /// + public int Quantity { get; set; } + + /// + /// 单价。 + /// + public decimal UnitPrice { get; set; } + + /// + /// 折扣金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 小计。 + /// + public decimal SubTotal { get; set; } + + /// + /// 自定义属性 JSON。 + /// + public string? AttributesJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs new file mode 100644 index 0000000..26823f2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Orders.Entities; + +/// +/// 订单状态流转记录。 +/// +public sealed class OrderStatusHistory : MultiTenantEntityBase +{ + /// + /// 订单标识。 + /// + public Guid OrderId { get; set; } + + /// + /// 变更后的状态。 + /// + public OrderStatus Status { get; set; } + + /// + /// 操作人标识(可为空表示系统)。 + /// + public Guid? OperatorId { get; set; } + + /// + /// 备注信息。 + /// + public string? Notes { get; set; } + + /// + /// 发生时间。 + /// + public DateTime OccurredAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs new file mode 100644 index 0000000..d18d4ab --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Orders.Entities; + +/// +/// 售后/退款申请。 +/// +public sealed class RefundRequest : MultiTenantEntityBase +{ + /// + /// 关联订单标识。 + /// + public Guid OrderId { get; set; } + + /// + /// 退款单号。 + /// + public string RefundNo { get; set; } = string.Empty; + + /// + /// 申请金额。 + /// + public decimal Amount { get; set; } + + /// + /// 申请原因。 + /// + public string Reason { get; set; } = string.Empty; + + /// + /// 退款状态。 + /// + public RefundStatus Status { get; set; } = RefundStatus.Pending; + + /// + /// 用户提交时间。 + /// + public DateTime RequestedAt { get; set; } = DateTime.UtcNow; + + /// + /// 审核完成时间。 + /// + public DateTime? ProcessedAt { get; set; } + + /// + /// 审核备注。 + /// + public string? ReviewNotes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs new file mode 100644 index 0000000..4a7249f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs @@ -0,0 +1,11 @@ +namespace TakeoutSaaS.Domain.Orders.Enums; + +/// +/// 履约/交付方式。 +/// +public enum DeliveryType +{ + DineIn = 0, + Pickup = 1, + Delivery = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs new file mode 100644 index 0000000..6908ac6 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs @@ -0,0 +1,14 @@ +namespace TakeoutSaaS.Domain.Orders.Enums; + +/// +/// 下单渠道。 +/// +public enum OrderChannel +{ + Unknown = 0, + MiniProgram = 1, + ScanToOrder = 2, + StaffConsole = 3, + PhoneReservation = 4, + ThirdPartyDelivery = 5 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs new file mode 100644 index 0000000..4132e46 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs @@ -0,0 +1,14 @@ +namespace TakeoutSaaS.Domain.Orders.Enums; + +/// +/// 订单状态。 +/// +public enum OrderStatus +{ + PendingPayment = 0, + AwaitingPreparation = 1, + InProgress = 2, + Ready = 3, + Completed = 4, + Cancelled = 5 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/RefundStatus.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/RefundStatus.cs new file mode 100644 index 0000000..3f2dc41 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/RefundStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Orders.Enums; + +/// +/// 退款申请状态。 +/// +public enum RefundStatus +{ + /// + /// 等待审核。 + /// + Pending = 0, + + /// + /// 审核通过,待原路退款。 + /// + Approved = 1, + + /// + /// 已拒绝。 + /// + Rejected = 2, + + /// + /// 已完成退款。 + /// + Refunded = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs new file mode 100644 index 0000000..2d0923f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs @@ -0,0 +1,55 @@ +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Payments.Entities; + +/// +/// 支付流水。 +/// +public sealed class PaymentRecord : MultiTenantEntityBase +{ + /// + /// 关联订单。 + /// + public Guid OrderId { get; set; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; set; } = PaymentMethod.Unknown; + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid; + + /// + /// 支付金额。 + /// + public decimal Amount { get; set; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; set; } + + /// + /// 第三方渠道单号。 + /// + public string? ChannelTransactionId { get; set; } + + /// + /// 支付完成时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 错误/备注。 + /// + public string? Remark { get; set; } + + /// + /// 原始回调内容。 + /// + public string? Payload { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs new file mode 100644 index 0000000..9e91973 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Payments.Entities; + +/// +/// 支付渠道退款流水。 +/// +public sealed class PaymentRefundRecord : MultiTenantEntityBase +{ + /// + /// 原支付记录标识。 + /// + public Guid PaymentRecordId { get; set; } + + /// + /// 关联订单标识。 + /// + public Guid OrderId { get; set; } + + /// + /// 退款金额。 + /// + public decimal Amount { get; set; } + + /// + /// 渠道退款流水号。 + /// + public string? ChannelRefundId { get; set; } + + /// + /// 退款状态。 + /// + public PaymentRefundStatus Status { get; set; } = PaymentRefundStatus.Pending; + + /// + /// 退款请求时间。 + /// + public DateTime RequestedAt { get; set; } = DateTime.UtcNow; + + /// + /// 完成时间。 + /// + public DateTime? CompletedAt { get; set; } + + /// + /// 渠道返回的原始数据 JSON。 + /// + public string? Payload { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs new file mode 100644 index 0000000..2453be2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs @@ -0,0 +1,14 @@ +namespace TakeoutSaaS.Domain.Payments.Enums; + +/// +/// 支付方式。 +/// +public enum PaymentMethod +{ + Unknown = 0, + WeChatPay = 1, + Alipay = 2, + Cash = 3, + Card = 4, + Balance = 5 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentRefundStatus.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentRefundStatus.cs new file mode 100644 index 0000000..0ee24db --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentRefundStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Payments.Enums; + +/// +/// 支付退款状态。 +/// +public enum PaymentRefundStatus +{ + /// + /// 已提交至渠道。 + /// + Pending = 0, + + /// + /// 退款成功。 + /// + Succeeded = 1, + + /// + /// 退款失败。 + /// + Failed = 2, + + /// + /// 渠道处理中。 + /// + Processing = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs new file mode 100644 index 0000000..6b5912a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs @@ -0,0 +1,13 @@ +namespace TakeoutSaaS.Domain.Payments.Enums; + +/// +/// 支付记录状态。 +/// +public enum PaymentStatus +{ + Unpaid = 0, + Paying = 1, + Paid = 2, + Failed = 3, + Refunded = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs new file mode 100644 index 0000000..c9617e2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs @@ -0,0 +1,100 @@ +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品(SPU)信息。 +/// +public sealed class Product : MultiTenantEntityBase +{ + /// + /// 所属门店。 + /// + public Guid StoreId { get; set; } + + /// + /// 所属分类。 + /// + public Guid CategoryId { get; set; } + + /// + /// 商品编码。 + /// + public string SpuCode { get; set; } = string.Empty; + + /// + /// 商品名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 副标题/卖点。 + /// + public string? Subtitle { get; set; } + + /// + /// 售卖单位(份/杯等)。 + /// + public string? Unit { get; set; } + + /// + /// 现价。 + /// + public decimal Price { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 库存数量(可选)。 + /// + public int? StockQuantity { get; set; } + + /// + /// 最大每单限购。 + /// + public int? MaxQuantityPerOrder { get; set; } + + /// + /// 商品状态。 + /// + public ProductStatus Status { get; set; } = ProductStatus.Draft; + + /// + /// 主图。 + /// + public string? CoverImage { get; set; } + + /// + /// Gallery 图片逗号分隔。 + /// + public string? GalleryImages { get; set; } + + /// + /// 商品描述。 + /// + public string? Description { get; set; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; set; } = true; + + /// + /// 是否热门推荐。 + /// + public bool IsFeatured { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs new file mode 100644 index 0000000..dfd8dd5 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 加料/做法分组。 +/// +public sealed class ProductAddonGroup : MultiTenantEntityBase +{ + /// + /// 所属商品。 + /// + public Guid ProductId { get; set; } + + /// + /// 分组名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 选择类型。 + /// + public AddonSelectionType SelectionType { get; set; } = AddonSelectionType.Single; + + /// + /// 最小选择数量。 + /// + public int? MinSelect { get; set; } + + /// + /// 最大选择数量。 + /// + public int? MaxSelect { get; set; } + + /// + /// 是否必选。 + /// + public bool IsRequired { get; set; } + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs new file mode 100644 index 0000000..3d27185 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 加料选项。 +/// +public sealed class ProductAddonOption : MultiTenantEntityBase +{ + /// + /// 所属加料分组。 + /// + public Guid AddonGroupId { get; set; } + + /// + /// 选项名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 附加价格。 + /// + public decimal? ExtraPrice { get; set; } + + /// + /// 是否默认选项。 + /// + public bool IsDefault { get; set; } + + /// + /// 排序。 + /// + public int SortOrder { get; set; } = 100; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs new file mode 100644 index 0000000..62a9af1 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品规格/属性分组。 +/// +public sealed class ProductAttributeGroup : MultiTenantEntityBase +{ + /// + /// 关联门店,可为空表示所有门店共享。 + /// + public Guid? StoreId { get; set; } + + /// + /// 分组名称,例如“辣度”“份量”。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 选择类型(单选/多选)。 + /// + public AttributeSelectionType SelectionType { get; set; } = AttributeSelectionType.Single; + + /// + /// 是否必选。 + /// + public bool IsRequired { get; set; } + + /// + /// 显示排序。 + /// + public int SortOrder { get; set; } = 100; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs new file mode 100644 index 0000000..80332d3 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品规格选项。 +/// +public sealed class ProductAttributeOption : MultiTenantEntityBase +{ + /// + /// 所属规格组。 + /// + public Guid AttributeGroupId { get; set; } + + /// + /// 选项名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 附加价格。 + /// + public decimal? ExtraPrice { get; set; } + + /// + /// 排序。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 是否默认选中。 + /// + public bool IsDefault { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs new file mode 100644 index 0000000..4d055a2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品分类。 +/// +public sealed class ProductCategory : MultiTenantEntityBase +{ + /// + /// 所属门店。 + /// + public Guid StoreId { get; set; } + + /// + /// 分类名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 分类描述。 + /// + public string? Description { get; set; } + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs new file mode 100644 index 0000000..c07ef44 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品媒资素材。 +/// +public sealed class ProductMediaAsset : MultiTenantEntityBase +{ + /// + /// 商品标识。 + /// + public Guid ProductId { get; set; } + + /// + /// 媒体类型。 + /// + public MediaAssetType MediaType { get; set; } = MediaAssetType.Image; + + /// + /// 媒资链接。 + /// + public string Url { get; set; } = string.Empty; + + /// + /// 描述或标题。 + /// + public string? Caption { get; set; } + + /// + /// 排序。 + /// + public int SortOrder { get; set; } = 100; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs new file mode 100644 index 0000000..bace92a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品价格策略,支持会员价/时段价等。 +/// +public sealed class ProductPricingRule : MultiTenantEntityBase +{ + /// + /// 所属商品。 + /// + public Guid ProductId { get; set; } + + /// + /// 策略类型。 + /// + public PricingRuleType RuleType { get; set; } = PricingRuleType.Member; + + /// + /// 条件描述(JSON),如会员等级、渠道等。 + /// + public string ConditionsJson { get; set; } = string.Empty; + + /// + /// 特殊价格。 + /// + public decimal Price { get; set; } + + /// + /// 生效开始时间。 + /// + public DateTime? StartTime { get; set; } + + /// + /// 生效结束时间。 + /// + public DateTime? EndTime { get; set; } + + /// + /// 生效星期(JSON 数组)。 + /// + public string? WeekdaysJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs new file mode 100644 index 0000000..1c1e6ce --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs @@ -0,0 +1,49 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Products.Entities; + +/// +/// 商品 SKU,记录具体规格组合价格。 +/// +public sealed class ProductSku : MultiTenantEntityBase +{ + /// + /// 所属商品标识。 + /// + public Guid ProductId { get; set; } + + /// + /// SKU 编码。 + /// + public string SkuCode { get; set; } = string.Empty; + + /// + /// 条形码。 + /// + public string? Barcode { get; set; } + + /// + /// 售价。 + /// + public decimal Price { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 可售库存。 + /// + public int? StockQuantity { get; set; } + + /// + /// 重量(千克)。 + /// + public decimal? Weight { get; set; } + + /// + /// 规格属性 JSON(记录选项 ID)。 + /// + public string AttributesJson { get; set; } = string.Empty; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Enums/AddonSelectionType.cs b/src/Domain/TakeoutSaaS.Domain/Products/Enums/AddonSelectionType.cs new file mode 100644 index 0000000..65bea66 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Enums/AddonSelectionType.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Products.Enums; + +/// +/// 加料选择类型。 +/// +public enum AddonSelectionType +{ + /// + /// 单选。 + /// + Single = 0, + + /// + /// 多选。 + /// + Multiple = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Enums/AttributeSelectionType.cs b/src/Domain/TakeoutSaaS.Domain/Products/Enums/AttributeSelectionType.cs new file mode 100644 index 0000000..9b5f69d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Enums/AttributeSelectionType.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Products.Enums; + +/// +/// 规格/加料的选择方式。 +/// +public enum AttributeSelectionType +{ + /// + /// 单选。 + /// + Single = 0, + + /// + /// 多选。 + /// + Multiple = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Enums/MediaAssetType.cs b/src/Domain/TakeoutSaaS.Domain/Products/Enums/MediaAssetType.cs new file mode 100644 index 0000000..27a0f2d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Enums/MediaAssetType.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Products.Enums; + +/// +/// 商品媒资类型。 +/// +public enum MediaAssetType +{ + /// + /// 图片。 + /// + Image = 0, + + /// + /// 视频。 + /// + Video = 1, + + /// + /// PDF 或说明文档。 + /// + Document = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Enums/PricingRuleType.cs b/src/Domain/TakeoutSaaS.Domain/Products/Enums/PricingRuleType.cs new file mode 100644 index 0000000..613283a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Enums/PricingRuleType.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Products.Enums; + +/// +/// 价格策略类型。 +/// +public enum PricingRuleType +{ + /// + /// 会员价格。 + /// + Member = 0, + + /// + /// 不同门店价格。 + /// + Store = 1, + + /// + /// 时间段价格。 + /// + TimePeriod = 2, + + /// + /// 区域价格。 + /// + Region = 3, + + /// + /// 活动价格。 + /// + Promotion = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs b/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs new file mode 100644 index 0000000..8678fcc --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs @@ -0,0 +1,12 @@ +namespace TakeoutSaaS.Domain.Products.Enums; + +/// +/// 商品状态。 +/// +public enum ProductStatus +{ + Draft = 0, + OnSale = 1, + OffShelf = 2, + Archived = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs b/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs new file mode 100644 index 0000000..f09c94e --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs @@ -0,0 +1,52 @@ +using TakeoutSaaS.Domain.Queues.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Queues.Entities; + +/// +/// 排队叫号。 +/// +public sealed class QueueTicket : MultiTenantEntityBase +{ + public Guid StoreId { get; set; } + + /// + /// 排队编号。 + /// + public string TicketNumber { get; set; } = string.Empty; + + /// + /// 就餐人数。 + /// + public int PartySize { get; set; } + + /// + /// 状态。 + /// + public QueueStatus Status { get; set; } = QueueStatus.Waiting; + + /// + /// 预计等待分钟。 + /// + public int? EstimatedWaitMinutes { get; set; } + + /// + /// 叫号时间。 + /// + public DateTime? CalledAt { get; set; } + + /// + /// 过号时间。 + /// + public DateTime? ExpiredAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs b/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs new file mode 100644 index 0000000..f217a70 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs @@ -0,0 +1,13 @@ +namespace TakeoutSaaS.Domain.Queues.Enums; + +/// +/// 排队状态。 +/// +public enum QueueStatus +{ + Waiting = 0, + Calling = 1, + Completed = 2, + Cancelled = 3, + Expired = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs b/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs new file mode 100644 index 0000000..3214ee3 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs @@ -0,0 +1,70 @@ +using TakeoutSaaS.Domain.Reservations.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Reservations.Entities; + +/// +/// 预约/预订记录。 +/// +public sealed class Reservation : MultiTenantEntityBase +{ + /// + /// 门店。 + /// + public Guid StoreId { get; set; } + + /// + /// 预约号。 + /// + public string ReservationNo { get; set; } = string.Empty; + + /// + /// 客户姓名。 + /// + public string CustomerName { get; set; } = string.Empty; + + /// + /// 联系电话。 + /// + public string CustomerPhone { get; set; } = string.Empty; + + /// + /// 用餐人数。 + /// + public int PeopleCount { get; set; } + + /// + /// 预约时间(UTC)。 + /// + public DateTime ReservationTime { get; set; } + + /// + /// 状态。 + /// + public ReservationStatus Status { get; set; } = ReservationStatus.Pending; + + /// + /// 桌型/标签。 + /// + public string? TablePreference { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 核销码/到店码。 + /// + public string? CheckInCode { get; set; } + + /// + /// 实际签到时间。 + /// + public DateTime? CheckedInAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs b/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs new file mode 100644 index 0000000..2bf53a3 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs @@ -0,0 +1,13 @@ +namespace TakeoutSaaS.Domain.Reservations.Enums; + +/// +/// 预约状态。 +/// +public enum ReservationStatus +{ + Pending = 0, + Confirmed = 1, + CheckedIn = 2, + Cancelled = 3, + NoShow = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs new file mode 100644 index 0000000..5c82a91 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs @@ -0,0 +1,130 @@ +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店信息,承载营业配置与能力。 +/// +public sealed class Store : MultiTenantEntityBase +{ + /// + /// 所属商户标识。 + /// + public Guid MerchantId { get; set; } + + /// + /// 门店编码,便于扫码及外部对接。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 联系电话。 + /// + public string? Phone { get; set; } + + /// + /// 门店负责人姓名。 + /// + public string? ManagerName { get; set; } + + /// + /// 门店当前运营状态。 + /// + public StoreStatus Status { get; set; } = StoreStatus.Closed; + + /// + /// 所在国家或地区。 + /// + public string? Country { get; set; } + + /// + /// 所在省份。 + /// + public string? Province { get; set; } + + /// + /// 所在城市。 + /// + public string? City { get; set; } + + /// + /// 区县信息。 + /// + public string? District { get; set; } + + /// + /// 详细地址。 + /// + public string? Address { get; set; } + + /// + /// 高德/腾讯地图经度。 + /// + public double? Longitude { get; set; } + + /// + /// 纬度。 + /// + public double? Latitude { get; set; } + + /// + /// 门店描述或公告。 + /// + public string? Description { get; set; } + + /// + /// 门店营业时段描述(备用字符串)。 + /// + public string? BusinessHours { get; set; } + + /// + /// 是否支持堂食。 + /// + public bool SupportsDineIn { get; set; } = true; + + /// + /// 是否支持自提。 + /// + public bool SupportsPickup { get; set; } = true; + + /// + /// 是否支持配送。 + /// + public bool SupportsDelivery { get; set; } = true; + + /// + /// 支持预约。 + /// + public bool SupportsReservation { get; set; } + + /// + /// 支持排队叫号。 + /// + public bool SupportsQueueing { get; set; } + + /// + /// 默认配送半径(公里)。 + /// + public decimal DeliveryRadiusKm { get; set; } = 3m; + + /// + /// 门店公告。 + /// + public string? Announcement { get; set; } + + /// + /// 门店标签(逗号分隔)。 + /// + public string? Tags { get; set; } + + /// + /// 门店海报。 + /// + public string? CoverImageUrl { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs new file mode 100644 index 0000000..a85b0d8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店营业时段配置。 +/// +public sealed class StoreBusinessHour : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 星期几,0 表示周日。 + /// + public DayOfWeek DayOfWeek { get; set; } + + /// + /// 时段类型(正常营业、休息、预约等)。 + /// + public BusinessHourType HourType { get; set; } = BusinessHourType.Normal; + + /// + /// 开始时间(本地时间)。 + /// + public TimeSpan StartTime { get; set; } + + /// + /// 结束时间(本地时间)。 + /// + public TimeSpan EndTime { get; set; } + + /// + /// 最大接待容量或单量限制。 + /// + public int? CapacityLimit { get; set; } + + /// + /// 备注。 + /// + public string? Notes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs new file mode 100644 index 0000000..e067e1f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs @@ -0,0 +1,39 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店配送范围配置。 +/// +public sealed class StoreDeliveryZone : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 区域名称。 + /// + public string ZoneName { get; set; } = string.Empty; + + /// + /// GeoJSON 表示的多边形范围。 + /// + public string PolygonGeoJson { get; set; } = string.Empty; + + /// + /// 起送价。 + /// + public decimal? MinimumOrderAmount { get; set; } + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; set; } + + /// + /// 预计送达分钟。 + /// + public int? EstimatedMinutes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs new file mode 100644 index 0000000..a1bc39a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店员工排班记录。 +/// +public sealed class StoreEmployeeShift : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 员工标识。 + /// + public Guid StaffId { get; set; } + + /// + /// 班次日期。 + /// + public DateTime ShiftDate { get; set; } + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; set; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; set; } + + /// + /// 排班角色。 + /// + public StaffRoleType RoleType { get; set; } = StaffRoleType.FrontDesk; + + /// + /// 备注。 + /// + public string? Notes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs new file mode 100644 index 0000000..c464849 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs @@ -0,0 +1,29 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店休息日或特殊营业日。 +/// +public sealed class StoreHoliday : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 日期。 + /// + public DateTime Date { get; set; } + + /// + /// 是否全天闭店。 + /// + public bool IsClosed { get; set; } = true; + + /// + /// 说明内容。 + /// + public string? Reason { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs new file mode 100644 index 0000000..1b55549 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 桌台信息与二维码绑定。 +/// +public sealed class StoreTable : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 所在区域 ID。 + /// + public Guid? AreaId { get; set; } + + /// + /// 桌码。 + /// + public string TableCode { get; set; } = string.Empty; + + /// + /// 可容纳人数。 + /// + public int Capacity { get; set; } + + /// + /// 桌台标签(堂食、快餐等)。 + /// + public string? Tags { get; set; } + + /// + /// 当前桌台状态。 + /// + public StoreTableStatus Status { get; set; } = StoreTableStatus.Idle; + + /// + /// 桌码二维码地址。 + /// + public string? QrCodeUrl { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs new file mode 100644 index 0000000..6255266 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs @@ -0,0 +1,24 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店桌台区域配置。 +/// +public sealed class StoreTableArea : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public Guid StoreId { get; set; } + + /// + /// 区域名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 区域描述。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Enums/BusinessHourType.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/BusinessHourType.cs new file mode 100644 index 0000000..9f545a0 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/BusinessHourType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Stores.Enums; + +/// +/// 营业时段类型。 +/// +public enum BusinessHourType +{ + /// + /// 正常营业时段。 + /// + Normal = 0, + + /// + /// 预留给预约的时段。 + /// + ReservationOnly = 1, + + /// + /// 仅自提或配送的时段。 + /// + PickupOrDelivery = 2, + + /// + /// 休息时段。 + /// + Closed = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStatus.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStatus.cs new file mode 100644 index 0000000..df82fb9 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Stores.Enums; + +/// +/// 门店运营状态。 +/// +public enum StoreStatus +{ + /// + /// 未开业或休眠。 + /// + Closed = 0, + + /// + /// 准备营业。 + /// + Preparing = 1, + + /// + /// 正常营业中。 + /// + Operating = 2, + + /// + /// 暂停营业。 + /// + Suspended = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreTableStatus.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreTableStatus.cs new file mode 100644 index 0000000..073b832 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreTableStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Stores.Enums; + +/// +/// 桌台占用状态。 +/// +public enum StoreTableStatus +{ + /// + /// 空闲可用。 + /// + Idle = 0, + + /// + /// 已被占用。 + /// + Occupied = 1, + + /// + /// 正在清理。 + /// + Cleaning = 2, + + /// + /// 暂停使用。 + /// + Disabled = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs new file mode 100644 index 0000000..fd65958 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs @@ -0,0 +1,125 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 平台租户信息,描述租户的生命周期与基础资料。 +/// +public sealed class Tenant : AuditableEntityBase +{ + /// + /// 租户短编码,作为跨系统引用的唯一标识。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 租户全称或品牌名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 对外展示的简称。 + /// + public string? ShortName { get; set; } + + /// + /// 法人或公司主体名称。 + /// + public string? LegalEntityName { get; set; } + + /// + /// 所属行业,如餐饮、零售等。 + /// + public string? Industry { get; set; } + + /// + /// LOGO 图片地址。 + /// + public string? LogoUrl { get; set; } + + /// + /// 品牌海报或封面图。 + /// + public string? CoverImageUrl { get; set; } + + /// + /// 官网或主要宣传链接。 + /// + public string? Website { get; set; } + + /// + /// 所在国家/地区。 + /// + public string? Country { get; set; } + + /// + /// 所在省份或州。 + /// + public string? Province { get; set; } + + /// + /// 所在城市。 + /// + public string? City { get; set; } + + /// + /// 详细地址信息。 + /// + public string? Address { get; set; } + + /// + /// 主联系人姓名。 + /// + public string? ContactName { get; set; } + + /// + /// 主联系人电话。 + /// + public string? ContactPhone { get; set; } + + /// + /// 主联系人邮箱。 + /// + public string? ContactEmail { get; set; } + + /// + /// 系统内对应的租户所有者账号 ID。 + /// + public Guid? PrimaryOwnerUserId { get; set; } + + /// + /// 租户当前状态,涵盖审核、启用、停用等场景。 + /// + public TenantStatus Status { get; set; } = TenantStatus.PendingReview; + + /// + /// 服务生效时间(UTC)。 + /// + public DateTime? EffectiveFrom { get; set; } + + /// + /// 服务到期时间(UTC)。 + /// + public DateTime? EffectiveTo { get; set; } + + /// + /// 最近一次暂停服务时间。 + /// + public DateTime? SuspendedAt { get; set; } + + /// + /// 暂停或终止的原因说明。 + /// + public string? SuspensionReason { get; set; } + + /// + /// 业务标签集合(逗号分隔)。 + /// + public string? Tags { get; set; } + + /// + /// 备注信息,用于运营记录特殊说明。 + /// + public string? Remarks { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantBillingStatement.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantBillingStatement.cs new file mode 100644 index 0000000..4fb50e2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantBillingStatement.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户账单,用于呈现周期性收费。 +/// +public sealed class TenantBillingStatement : MultiTenantEntityBase +{ + /// + /// 账单编号,供对账查询。 + /// + public string StatementNo { get; set; } = string.Empty; + + /// + /// 账单周期开始时间。 + /// + public DateTime PeriodStart { get; set; } + + /// + /// 账单周期结束时间。 + /// + public DateTime PeriodEnd { get; set; } + + /// + /// 应付金额。 + /// + public decimal AmountDue { get; set; } + + /// + /// 实付金额。 + /// + public decimal AmountPaid { get; set; } + + /// + /// 当前付款状态。 + /// + public TenantBillingStatus Status { get; set; } = TenantBillingStatus.Pending; + + /// + /// 到期日。 + /// + public DateTime DueDate { get; set; } + + /// + /// 账单明细 JSON,记录各项费用。 + /// + public string? LineItemsJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantNotification.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantNotification.cs new file mode 100644 index 0000000..8d2b881 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantNotification.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 面向租户的站内通知或消息推送。 +/// +public sealed class TenantNotification : MultiTenantEntityBase +{ + /// + /// 通知标题。 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 通知正文。 + /// + public string Message { get; set; } = string.Empty; + + /// + /// 发布通道(站内、邮件、短信等)。 + /// + public TenantNotificationChannel Channel { get; set; } = TenantNotificationChannel.InApp; + + /// + /// 通知重要级别。 + /// + public TenantNotificationSeverity Severity { get; set; } = TenantNotificationSeverity.Info; + + /// + /// 推送时间。 + /// + public DateTime SentAt { get; set; } = DateTime.UtcNow; + + /// + /// 租户是否已阅读。 + /// + public DateTime? ReadAt { get; set; } + + /// + /// 附加元数据 JSON。 + /// + public string? MetadataJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPackage.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPackage.cs new file mode 100644 index 0000000..807f159 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPackage.cs @@ -0,0 +1,70 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 平台提供的租户套餐定义。 +/// +public sealed class TenantPackage : AuditableEntityBase +{ + /// + /// 套餐名称,展示给租户的简称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 套餐描述,包含适用场景、权益等。 + /// + public string? Description { get; set; } + + /// + /// 套餐分类(试用、标准、旗舰等)。 + /// + public TenantPackageType PackageType { get; set; } = TenantPackageType.Standard; + + /// + /// 月付价格,单位:人民币元。 + /// + public decimal? MonthlyPrice { get; set; } + + /// + /// 年付价格,单位:人民币元。 + /// + public decimal? YearlyPrice { get; set; } + + /// + /// 允许的最大门店数。 + /// + public int? MaxStoreCount { get; set; } + + /// + /// 允许创建的最大账号数。 + /// + public int? MaxAccountCount { get; set; } + + /// + /// 存储容量上限(GB)。 + /// + public int? MaxStorageGb { get; set; } + + /// + /// 每月短信额度上限。 + /// + public int? MaxSmsCredits { get; set; } + + /// + /// 每月可调用的配送单数量上限。 + /// + public int? MaxDeliveryOrders { get; set; } + + /// + /// 权益明细 JSON,记录自定义特性开关。 + /// + public string? FeaturePoliciesJson { get; set; } + + /// + /// 是否仍可售卖。 + /// + public bool IsActive { get; set; } = true; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaUsage.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaUsage.cs new file mode 100644 index 0000000..5b2f4c5 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaUsage.cs @@ -0,0 +1,35 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户配额使用情况快照。 +/// +public sealed class TenantQuotaUsage : MultiTenantEntityBase +{ + /// + /// 配额类型,例如门店数、短信条数等。 + /// + public TenantQuotaType QuotaType { get; set; } + + /// + /// 当前配额上限。 + /// + public decimal LimitValue { get; set; } + + /// + /// 已消耗的数量。 + /// + public decimal UsedValue { get; set; } + + /// + /// 配额刷新周期描述(如月、年)。 + /// + public string? ResetCycle { get; set; } + + /// + /// 最近一次重置时间。 + /// + public DateTime? LastResetAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs new file mode 100644 index 0000000..a54fce2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户套餐订阅记录。 +/// +public sealed class TenantSubscription : MultiTenantEntityBase +{ + /// + /// 当前订阅关联的套餐标识。 + /// + public Guid TenantPackageId { get; set; } + + /// + /// 订阅生效时间(UTC)。 + /// + public DateTime EffectiveFrom { get; set; } + + /// + /// 订阅到期时间(UTC)。 + /// + public DateTime EffectiveTo { get; set; } + + /// + /// 下一个计费时间,配合自动续费使用。 + /// + public DateTime? NextBillingDate { get; set; } + + /// + /// 订阅当前状态。 + /// + public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Pending; + + /// + /// 是否开启自动续费。 + /// + public bool AutoRenew { get; set; } + + /// + /// 若已排期升降配,对应的新套餐 ID。 + /// + public Guid? ScheduledPackageId { get; set; } + + /// + /// 运营备注信息。 + /// + public string? Notes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/SubscriptionStatus.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/SubscriptionStatus.cs new file mode 100644 index 0000000..d1d1a49 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/SubscriptionStatus.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 订阅状态。 +/// +public enum SubscriptionStatus +{ + /// + /// 尚未支付或等待审批。 + /// + Pending = 0, + + /// + /// 订阅已生效。 + /// + Active = 1, + + /// + /// 已到期但仍保留数据。 + /// + GracePeriod = 2, + + /// + /// 已取消。 + /// + Cancelled = 3, + + /// + /// 因欠费被暂停。 + /// + Suspended = 4 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantBillingStatus.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantBillingStatus.cs new file mode 100644 index 0000000..11671d2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantBillingStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 账单状态。 +/// +public enum TenantBillingStatus +{ + /// + /// 等待付款。 + /// + Pending = 0, + + /// + /// 已付款结清。 + /// + Paid = 1, + + /// + /// 已逾期。 + /// + Overdue = 2, + + /// + /// 已取消或作废。 + /// + Cancelled = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationChannel.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationChannel.cs new file mode 100644 index 0000000..25a3a36 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationChannel.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 通知推送渠道。 +/// +public enum TenantNotificationChannel +{ + /// + /// 站内消息。 + /// + InApp = 0, + + /// + /// 邮件推送。 + /// + Email = 1, + + /// + /// 短信提醒。 + /// + Sms = 2, + + /// + /// 管理后台弹窗。 + /// + Portal = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationSeverity.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationSeverity.cs new file mode 100644 index 0000000..7947059 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantNotificationSeverity.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 租户通知的重要程度。 +/// +public enum TenantNotificationSeverity +{ + /// + /// 普通提示。 + /// + Info = 0, + + /// + /// 需要关注的提醒。 + /// + Warning = 1, + + /// + /// 影响业务的严重事件。 + /// + Critical = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantPackageType.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantPackageType.cs new file mode 100644 index 0000000..111ae2c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantPackageType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 套餐类型枚举。 +/// +public enum TenantPackageType +{ + /// + /// 免费试用套餐。 + /// + Trial = 0, + + /// + /// 标准商业套餐。 + /// + Standard = 1, + + /// + /// 面向成长型商户的高级套餐。 + /// + Professional = 2, + + /// + /// 提供完整能力的旗舰套餐。 + /// + Enterprise = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantQuotaType.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantQuotaType.cs new file mode 100644 index 0000000..5392bca --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantQuotaType.cs @@ -0,0 +1,37 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 配额类型,覆盖容量及调用次数。 +/// +public enum TenantQuotaType +{ + /// + /// 门店数量限制。 + /// + StoreCount = 0, + + /// + /// 员工账号数量限制。 + /// + AccountCount = 1, + + /// + /// 存储空间限制。 + /// + Storage = 2, + + /// + /// 短信额度。 + /// + SmsCredits = 3, + + /// + /// 配送订单数量限制。 + /// + DeliveryOrders = 4, + + /// + /// 营销活动并发数量。 + /// + PromotionSlots = 5 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantStatus.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantStatus.cs new file mode 100644 index 0000000..369ed3a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantStatus.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 租户服务状态。 +/// +public enum TenantStatus +{ + /// + /// 已提交信息,等待审核。 + /// + PendingReview = 0, + + /// + /// 审核通过并正常运营。 + /// + Active = 1, + + /// + /// 因欠费或违规被暂时停用。 + /// + Suspended = 2, + + /// + /// 服务到期尚未续费。 + /// + Expired = 3, + + /// + /// 主动或被动注销,数据进入归档状态。 + /// + Closed = 4 +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs new file mode 100644 index 0000000..56454dc --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs @@ -0,0 +1,949 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.App.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + [DbContext(typeof(TakeoutAppDbContext))] + [Migration("20251201044927_InitialApp")] + partial class InitialApp + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CourierName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CourierPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DispatchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FailureReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedUpAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ProviderOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BrandAlias") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LegalPerson") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OnboardedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("merchants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Channel") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CustomerPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryType") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemsAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PayableAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaymentStatus") + .HasColumnType("integer"); + + b.Property("QueueNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ReservationId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributesJson") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("SkuName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SubTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("order_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ChannelTransactionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("payment_records", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CoverImage") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EnableDelivery") + .HasColumnType("boolean"); + + b.Property("EnableDineIn") + .HasColumnType("boolean"); + + b.Property("EnablePickup") + .HasColumnType("boolean"); + + b.Property("GalleryImages") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IsFeatured") + .HasColumnType("boolean"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SpuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StockQuantity") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SpuCode") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("products", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("product_categories", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EstimatedWaitMinutes") + .HasColumnType("integer"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TicketNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.HasIndex("TenantId", "StoreId", "TicketNumber") + .IsUnique(); + + b.ToTable("queue_tickets", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CheckInCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CheckedInAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CustomerPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("PeopleCount") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ReservationNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ReservationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TablePreference") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ReservationNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("reservations", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Announcement") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("BusinessHours") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryRadiusKm") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("ManagerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("QueueEnabled") + .HasColumnType("boolean"); + + b.Property("ReservationEnabled") + .HasColumnType("boolean"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SupportsDelivery") + .HasColumnType("boolean"); + + b.Property("SupportsDineIn") + .HasColumnType("boolean"); + + b.Property("SupportsPickup") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MerchantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "MerchantId"); + + b.ToTable("stores", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContactName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LogoUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Remarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ShortName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.HasOne("TakeoutSaaS.Domain.Merchants.Entities.Merchant", "Merchant") + .WithMany() + .HasForeignKey("MerchantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Merchant"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs new file mode 100644 index 0000000..f322c41 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs @@ -0,0 +1,497 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + /// + public partial class InitialApp : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "delivery_orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Provider = table.Column(type: "integer", nullable: false), + ProviderOrderId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Status = table.Column(type: "integer", nullable: false), + DeliveryFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + CourierName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + CourierPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + DispatchedAt = table.Column(type: "timestamp with time zone", nullable: true), + PickedUpAt = table.Column(type: "timestamp with time zone", nullable: true), + DeliveredAt = table.Column(type: "timestamp with time zone", nullable: true), + FailureReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_delivery_orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "merchants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BrandName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + BrandAlias = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + LegalPerson = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + BusinessLicenseNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + Province = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + City = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + District = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Status = table.Column(type: "integer", nullable: false), + ReviewRemarks = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + OnboardedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_merchants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + Channel = table.Column(type: "integer", nullable: false), + DeliveryType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + PaymentStatus = table.Column(type: "integer", nullable: false), + CustomerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + CustomerPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + TableNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + QueueNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + ReservationId = table.Column(type: "uuid", nullable: true), + ItemsAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + PayableAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + PaidAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true), + FinishedAt = table.Column(type: "timestamp with time zone", nullable: true), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), + CancelReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Remark = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "payment_records", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Method = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TradeNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ChannelTransactionId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Payload = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_payment_records", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_categories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "products", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + CategoryId = table.Column(type: "uuid", nullable: false), + SpuCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Subtitle = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Unit = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + StockQuantity = table.Column(type: "integer", nullable: true), + MaxQuantityPerOrder = table.Column(type: "integer", nullable: true), + Status = table.Column(type: "integer", nullable: false), + CoverImage = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + GalleryImages = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + Description = table.Column(type: "text", nullable: true), + EnableDineIn = table.Column(type: "boolean", nullable: false), + EnablePickup = table.Column(type: "boolean", nullable: false), + EnableDelivery = table.Column(type: "boolean", nullable: false), + IsFeatured = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "queue_tickets", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + TicketNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + PartySize = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + EstimatedWaitMinutes = table.Column(type: "integer", nullable: true), + CalledAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_queue_tickets", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "reservations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + ReservationNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + CustomerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + CustomerPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + PeopleCount = table.Column(type: "integer", nullable: false), + ReservationTime = table.Column(type: "timestamp with time zone", nullable: false), + Status = table.Column(type: "integer", nullable: false), + TablePreference = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Remark = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CheckInCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + CheckedInAt = table.Column(type: "timestamp with time zone", nullable: true), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_reservations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ShortName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ContactName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + Industry = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + LogoUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: true), + EffectiveTo = table.Column(type: "timestamp with time zone", nullable: true), + Status = table.Column(type: "integer", nullable: false), + Remarks = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_tenants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "stores", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MerchantId = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + ManagerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Status = table.Column(type: "integer", nullable: false), + Province = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + City = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + District = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Longitude = table.Column(type: "double precision", nullable: true), + Latitude = table.Column(type: "double precision", nullable: true), + BusinessHours = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + SupportsDineIn = table.Column(type: "boolean", nullable: false), + SupportsPickup = table.Column(type: "boolean", nullable: false), + SupportsDelivery = table.Column(type: "boolean", nullable: false), + DeliveryRadiusKm = table.Column(type: "numeric(6,2)", precision: 6, scale: 2, nullable: false), + QueueEnabled = table.Column(type: "boolean", nullable: false), + ReservationEnabled = table.Column(type: "boolean", nullable: false), + Announcement = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_stores", x => x.Id); + table.ForeignKey( + name: "FK_stores_merchants_MerchantId", + column: x => x.MerchantId, + principalTable: "merchants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "order_items", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + SkuName = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + Unit = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + Quantity = table.Column(type: "integer", nullable: false), + UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + SubTotal = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AttributesJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_order_items", x => x.Id); + table.ForeignKey( + name: "FK_order_items_orders_OrderId", + column: x => x.OrderId, + principalTable: "orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_delivery_orders_TenantId_OrderId", + table: "delivery_orders", + columns: new[] { "TenantId", "OrderId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_merchants_TenantId", + table: "merchants", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_order_items_OrderId", + table: "order_items", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_order_items_TenantId_OrderId", + table: "order_items", + columns: new[] { "TenantId", "OrderId" }); + + migrationBuilder.CreateIndex( + name: "IX_orders_TenantId_OrderNo", + table: "orders", + columns: new[] { "TenantId", "OrderNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_orders_TenantId_StoreId_Status", + table: "orders", + columns: new[] { "TenantId", "StoreId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_payment_records_TenantId_OrderId", + table: "payment_records", + columns: new[] { "TenantId", "OrderId" }); + + migrationBuilder.CreateIndex( + name: "IX_product_categories_TenantId_StoreId", + table: "product_categories", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_products_TenantId_SpuCode", + table: "products", + columns: new[] { "TenantId", "SpuCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_products_TenantId_StoreId", + table: "products", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_queue_tickets_TenantId_StoreId", + table: "queue_tickets", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_queue_tickets_TenantId_StoreId_TicketNumber", + table: "queue_tickets", + columns: new[] { "TenantId", "StoreId", "TicketNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_reservations_TenantId_ReservationNo", + table: "reservations", + columns: new[] { "TenantId", "ReservationNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_reservations_TenantId_StoreId", + table: "reservations", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_stores_MerchantId", + table: "stores", + column: "MerchantId"); + + migrationBuilder.CreateIndex( + name: "IX_stores_TenantId_Code", + table: "stores", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_stores_TenantId_MerchantId", + table: "stores", + columns: new[] { "TenantId", "MerchantId" }); + + migrationBuilder.CreateIndex( + name: "IX_tenants_Code", + table: "tenants", + column: "Code", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "delivery_orders"); + + migrationBuilder.DropTable( + name: "order_items"); + + migrationBuilder.DropTable( + name: "payment_records"); + + migrationBuilder.DropTable( + name: "product_categories"); + + migrationBuilder.DropTable( + name: "products"); + + migrationBuilder.DropTable( + name: "queue_tickets"); + + migrationBuilder.DropTable( + name: "reservations"); + + migrationBuilder.DropTable( + name: "stores"); + + migrationBuilder.DropTable( + name: "tenants"); + + migrationBuilder.DropTable( + name: "orders"); + + migrationBuilder.DropTable( + name: "merchants"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs new file mode 100644 index 0000000..d0c482a --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs @@ -0,0 +1,946 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.App.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + [DbContext(typeof(TakeoutAppDbContext))] + partial class TakeoutAppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CourierName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CourierPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DispatchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FailureReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedUpAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ProviderOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BrandAlias") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LegalPerson") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OnboardedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("merchants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Channel") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CustomerPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryType") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemsAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PayableAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaymentStatus") + .HasColumnType("integer"); + + b.Property("QueueNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ReservationId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributesJson") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("SkuName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SubTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("order_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ChannelTransactionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("payment_records", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CoverImage") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EnableDelivery") + .HasColumnType("boolean"); + + b.Property("EnableDineIn") + .HasColumnType("boolean"); + + b.Property("EnablePickup") + .HasColumnType("boolean"); + + b.Property("GalleryImages") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IsFeatured") + .HasColumnType("boolean"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SpuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StockQuantity") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SpuCode") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("products", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("product_categories", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EstimatedWaitMinutes") + .HasColumnType("integer"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TicketNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.HasIndex("TenantId", "StoreId", "TicketNumber") + .IsUnique(); + + b.ToTable("queue_tickets", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CheckInCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CheckedInAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CustomerPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("PeopleCount") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ReservationNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ReservationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TablePreference") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ReservationNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("reservations", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Announcement") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("BusinessHours") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryRadiusKm") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("ManagerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("QueueEnabled") + .HasColumnType("boolean"); + + b.Property("ReservationEnabled") + .HasColumnType("boolean"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SupportsDelivery") + .HasColumnType("boolean"); + + b.Property("SupportsDineIn") + .HasColumnType("boolean"); + + b.Property("SupportsPickup") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MerchantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "MerchantId"); + + b.ToTable("stores", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContactName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LogoUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Remarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ShortName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.HasOne("TakeoutSaaS.Domain.Merchants.Entities.Merchant", "Merchant") + .WithMany() + .HasForeignKey("MerchantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Merchant"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs new file mode 100644 index 0000000..328a3a2 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -0,0 +1,221 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Merchants.Entities; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Queues.Entities; +using TakeoutSaaS.Domain.Reservations.Entities; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.App.Persistence; + +/// +/// 业务主库 DbContext。 +/// +public sealed class TakeoutAppDbContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor? currentUserAccessor = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) +{ + public DbSet Tenants => Set(); + public DbSet Merchants => Set(); + public DbSet Stores => Set(); + public DbSet ProductCategories => Set(); + public DbSet Products => Set(); + public DbSet Orders => Set(); + public DbSet OrderItems => Set(); + public DbSet PaymentRecords => Set(); + public DbSet Reservations => Set(); + public DbSet QueueTickets => Set(); + public DbSet DeliveryOrders => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + ConfigureTenant(modelBuilder.Entity()); + ConfigureMerchant(modelBuilder.Entity()); + ConfigureStore(modelBuilder.Entity()); + ConfigureProductCategory(modelBuilder.Entity()); + ConfigureProduct(modelBuilder.Entity()); + ConfigureOrder(modelBuilder.Entity()); + ConfigureOrderItem(modelBuilder.Entity()); + ConfigurePaymentRecord(modelBuilder.Entity()); + ConfigureReservation(modelBuilder.Entity()); + ConfigureQueueTicket(modelBuilder.Entity()); + ConfigureDelivery(modelBuilder.Entity()); + + ApplyTenantQueryFilters(modelBuilder); + } + + private static void ConfigureTenant(EntityTypeBuilder builder) + { + builder.ToTable("tenants"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.ShortName).HasMaxLength(64); + builder.Property(x => x.ContactName).HasMaxLength(64); + builder.Property(x => x.ContactPhone).HasMaxLength(32); + builder.Property(x => x.ContactEmail).HasMaxLength(128); + builder.Property(x => x.Industry).HasMaxLength(64); + builder.Property(x => x.LogoUrl).HasMaxLength(256); + builder.Property(x => x.Remarks).HasMaxLength(512); + builder.HasIndex(x => x.Code).IsUnique(); + } + + private static void ConfigureMerchant(EntityTypeBuilder builder) + { + builder.ToTable("merchants"); + builder.HasKey(x => x.Id); + builder.Property(x => x.BrandName).HasMaxLength(128).IsRequired(); + builder.Property(x => x.BrandAlias).HasMaxLength(64); + builder.Property(x => x.LegalPerson).HasMaxLength(64); + builder.Property(x => x.BusinessLicenseNumber).HasMaxLength(64); + builder.Property(x => x.ContactPhone).HasMaxLength(32).IsRequired(); + builder.Property(x => x.ContactEmail).HasMaxLength(128); + builder.Property(x => x.Province).HasMaxLength(64); + builder.Property(x => x.City).HasMaxLength(64); + builder.Property(x => x.District).HasMaxLength(64); + builder.Property(x => x.Address).HasMaxLength(256); + builder.Property(x => x.ReviewRemarks).HasMaxLength(512); + builder.HasIndex(x => x.TenantId); + } + + private static void ConfigureStore(EntityTypeBuilder builder) + { + builder.ToTable("stores"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Code).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Phone).HasMaxLength(32); + builder.Property(x => x.ManagerName).HasMaxLength(64); + builder.Property(x => x.Province).HasMaxLength(64); + builder.Property(x => x.City).HasMaxLength(64); + builder.Property(x => x.District).HasMaxLength(64); + builder.Property(x => x.Address).HasMaxLength(256); + builder.Property(x => x.BusinessHours).HasMaxLength(256); + builder.Property(x => x.Announcement).HasMaxLength(512); + builder.Property(x => x.DeliveryRadiusKm).HasPrecision(6, 2); + builder.HasIndex(x => new { x.TenantId, x.MerchantId }); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigureProductCategory(EntityTypeBuilder builder) + { + builder.ToTable("product_categories"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.StoreId }); + } + + private static void ConfigureProduct(EntityTypeBuilder builder) + { + builder.ToTable("products"); + builder.HasKey(x => x.Id); + builder.Property(x => x.SpuCode).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Subtitle).HasMaxLength(256); + builder.Property(x => x.Unit).HasMaxLength(16); + builder.Property(x => x.Price).HasPrecision(18, 2); + builder.Property(x => x.OriginalPrice).HasPrecision(18, 2); + builder.Property(x => x.CoverImage).HasMaxLength(256); + builder.Property(x => x.GalleryImages).HasMaxLength(1024); + builder.Property(x => x.Description).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.StoreId }); + builder.HasIndex(x => new { x.TenantId, x.SpuCode }).IsUnique(); + } + + private static void ConfigureOrder(EntityTypeBuilder builder) + { + builder.ToTable("orders"); + builder.HasKey(x => x.Id); + builder.Property(x => x.OrderNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.CustomerName).HasMaxLength(64); + builder.Property(x => x.CustomerPhone).HasMaxLength(32); + builder.Property(x => x.TableNo).HasMaxLength(32); + builder.Property(x => x.QueueNumber).HasMaxLength(32); + builder.Property(x => x.CancelReason).HasMaxLength(256); + builder.Property(x => x.Remark).HasMaxLength(512); + builder.Property(x => x.ItemsAmount).HasPrecision(18, 2); + builder.Property(x => x.DiscountAmount).HasPrecision(18, 2); + builder.Property(x => x.PayableAmount).HasPrecision(18, 2); + builder.Property(x => x.PaidAmount).HasPrecision(18, 2); + builder.HasIndex(x => new { x.TenantId, x.OrderNo }).IsUnique(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status }); + } + + private static void ConfigureOrderItem(EntityTypeBuilder builder) + { + builder.ToTable("order_items"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ProductName).HasMaxLength(128).IsRequired(); + builder.Property(x => x.SkuName).HasMaxLength(128); + builder.Property(x => x.Unit).HasMaxLength(16); + builder.Property(x => x.UnitPrice).HasPrecision(18, 2); + builder.Property(x => x.DiscountAmount).HasPrecision(18, 2); + builder.Property(x => x.SubTotal).HasPrecision(18, 2); + builder.Property(x => x.AttributesJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.OrderId }); + builder.HasOne() + .WithMany() + .HasForeignKey(x => x.OrderId) + .OnDelete(DeleteBehavior.Cascade); + } + + private static void ConfigurePaymentRecord(EntityTypeBuilder builder) + { + builder.ToTable("payment_records"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Amount).HasPrecision(18, 2); + builder.Property(x => x.TradeNo).HasMaxLength(64); + builder.Property(x => x.ChannelTransactionId).HasMaxLength(64); + builder.Property(x => x.Remark).HasMaxLength(256); + builder.Property(x => x.Payload).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.OrderId }); + } + + private static void ConfigureReservation(EntityTypeBuilder builder) + { + builder.ToTable("reservations"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ReservationNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.CustomerName).HasMaxLength(64).IsRequired(); + builder.Property(x => x.CustomerPhone).HasMaxLength(32).IsRequired(); + builder.Property(x => x.TablePreference).HasMaxLength(64); + builder.Property(x => x.Remark).HasMaxLength(512); + builder.Property(x => x.CheckInCode).HasMaxLength(32); + builder.HasIndex(x => new { x.TenantId, x.StoreId }); + builder.HasIndex(x => new { x.TenantId, x.ReservationNo }).IsUnique(); + } + + private static void ConfigureQueueTicket(EntityTypeBuilder builder) + { + builder.ToTable("queue_tickets"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TicketNumber).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Remark).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.StoreId }); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.TicketNumber }).IsUnique(); + } + + private static void ConfigureDelivery(EntityTypeBuilder builder) + { + builder.ToTable("delivery_orders"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ProviderOrderId).HasMaxLength(64); + builder.Property(x => x.DeliveryFee).HasPrecision(18, 2); + builder.Property(x => x.CourierName).HasMaxLength(64); + builder.Property(x => x.CourierPhone).HasMaxLength(32); + builder.Property(x => x.FailureReason).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.OrderId }).IsUnique(); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs new file mode 100644 index 0000000..b56f2d0 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.App.Persistence; + +/// +/// 设计时工厂,供 EF CLI 使用。 +/// +internal sealed class TakeoutAppDesignTimeDbContextFactory + : DesignTimeDbContextFactoryBase +{ + public TakeoutAppDesignTimeDbContextFactory() + : base("TAKEOUTSAAS_APP_CONNECTION", "takeout_saas_app") + { + } + + protected override TakeoutAppDbContext CreateContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor currentUserAccessor) + => new(options, tenantProvider, currentUserAccessor); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs new file mode 100644 index 0000000..88ba403 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; + +/// +/// EF Core 设计时 DbContext 工厂基类,提供统一的连接串与依赖替身。 +/// +internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDbContextFactory + where TContext : TenantAwareDbContext +{ + private readonly string _connectionStringEnvVar; + private readonly string _defaultDatabase; + + protected DesignTimeDbContextFactoryBase(string connectionStringEnvVar, string defaultDatabase) + { + _connectionStringEnvVar = connectionStringEnvVar; + _defaultDatabase = defaultDatabase; + } + + public TContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql( + ResolveConnectionString(), + npgsql => + { + npgsql.CommandTimeout(30); + npgsql.EnableRetryOnFailure(); + }); + + return CreateContext( + optionsBuilder.Options, + new DesignTimeTenantProvider(), + new DesignTimeCurrentUserAccessor()); + } + + protected abstract TContext CreateContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor currentUserAccessor); + + private string ResolveConnectionString() + { + var env = Environment.GetEnvironmentVariable(_connectionStringEnvVar); + if (!string.IsNullOrWhiteSpace(env)) + { + return env; + } + + return $"Host=localhost;Port=5432;Database={_defaultDatabase};Username=postgres;Password=postgres"; + } + + private sealed class DesignTimeTenantProvider : ITenantProvider + { + public Guid GetCurrentTenantId() => Guid.Empty; + } + + private sealed class DesignTimeCurrentUserAccessor : ICurrentUserAccessor + { + public Guid UserId => Guid.Empty; + public bool IsAuthenticated => false; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs new file mode 100644 index 0000000..ed76f4c --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs @@ -0,0 +1,172 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +{ + [DbContext(typeof(DictionaryDbContext))] + [Migration("20251201042346_InitialDictionary")] + partial class InitialDictionary + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("dictionary_groups", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("GroupId", "Key") + .IsUnique(); + + b.ToTable("dictionary_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") + .WithMany("Items") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs new file mode 100644 index 0000000..3e0084a --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +{ + /// + public partial class InitialDictionary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "dictionary_groups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Scope = table.Column(type: "integer", nullable: false), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_dictionary_groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "dictionary_items", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + GroupId = table.Column(type: "uuid", nullable: false), + Key = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Value = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + IsDefault = table.Column(type: "boolean", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_dictionary_items", x => x.Id); + table.ForeignKey( + name: "FK_dictionary_items_dictionary_groups_GroupId", + column: x => x.GroupId, + principalTable: "dictionary_groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_groups_TenantId", + table: "dictionary_groups", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_groups_TenantId_Code", + table: "dictionary_groups", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_items_GroupId_Key", + table: "dictionary_items", + columns: new[] { "GroupId", "Key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_items_TenantId", + table: "dictionary_items", + column: "TenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "dictionary_items"); + + migrationBuilder.DropTable( + name: "dictionary_groups"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs new file mode 100644 index 0000000..0c08edf --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs @@ -0,0 +1,169 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +{ + [DbContext(typeof(DictionaryDbContext))] + partial class DictionaryDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("dictionary_groups", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("GroupId", "Key") + .IsUnique(); + + b.ToTable("dictionary_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") + .WithMany("Items") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs new file mode 100644 index 0000000..c69074c --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +/// +/// 设计时 DictionaryDbContext 工厂。 +/// +internal sealed class DictionaryDesignTimeDbContextFactory + : DesignTimeDbContextFactoryBase +{ + public DictionaryDesignTimeDbContextFactory() + : base("TAKEOUTSAAS_APP_CONNECTION", "takeout_saas_app") + { + } + + protected override DictionaryDbContext CreateContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor currentUserAccessor) + => new(options, tenantProvider, currentUserAccessor); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs new file mode 100644 index 0000000..9ea6285 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Identity.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Identity.Migrations +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20251201042324_InitialIdentity")] + partial class InitialIdentity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Account") + .IsUnique(); + + b.ToTable("identity_users", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OpenId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnionId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "OpenId") + .IsUnique(); + + b.ToTable("mini_users", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs new file mode 100644 index 0000000..5a5a2c7 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs @@ -0,0 +1,94 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Identity.Migrations +{ + /// + public partial class InitialIdentity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "identity_users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Account = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + DisplayName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + PasswordHash = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + MerchantId = table.Column(type: "uuid", nullable: true), + Roles = table.Column(type: "text", nullable: false), + Permissions = table.Column(type: "text", nullable: false), + Avatar = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_identity_users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "mini_users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OpenId = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + UnionId = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + Nickname = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Avatar = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_mini_users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_identity_users_TenantId", + table: "identity_users", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_identity_users_TenantId_Account", + table: "identity_users", + columns: new[] { "TenantId", "Account" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_mini_users_TenantId", + table: "mini_users", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_mini_users_TenantId_OpenId", + table: "mini_users", + columns: new[] { "TenantId", "OpenId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "identity_users"); + + migrationBuilder.DropTable( + name: "mini_users"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs new file mode 100644 index 0000000..385d79c --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs @@ -0,0 +1,149 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Identity.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Identity.Migrations +{ + [DbContext(typeof(IdentityDbContext))] + partial class IdentityDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Account") + .IsUnique(); + + b.ToTable("identity_users", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OpenId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnionId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "OpenId") + .IsUnique(); + + b.ToTable("mini_users", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs new file mode 100644 index 0000000..48667fb --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// 设计时 IdentityDbContext 工厂,供 EF Core CLI 生成迁移使用。 +/// +internal sealed class IdentityDesignTimeDbContextFactory + : DesignTimeDbContextFactoryBase +{ + public IdentityDesignTimeDbContextFactory() + : base("TAKEOUTSAAS_IDENTITY_CONNECTION", "takeout_saas_identity") + { + } + + protected override IdentityDbContext CreateContext( + DbContextOptions options, + ITenantProvider tenantProvider, + ICurrentUserAccessor currentUserAccessor) + => new(options, tenantProvider, currentUserAccessor); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index c3b590c..90cd078 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 84ac31158cf32b2395655ad142792065bb8b2a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BA=E7=88=B1=E6=B3=BD?= Date: Mon, 1 Dec 2025 13:31:28 +0800 Subject: [PATCH 12/56] =?UTF-8?q?docs:=20=E8=A1=A5=E9=BD=90=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=9E=9A=E4=B8=BE=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Deliveries/Enums/DeliveryProvider.cs | 23 ++++++++++++++++ .../Deliveries/Enums/DeliveryStatus.cs | 27 +++++++++++++++++++ .../Merchants/Enums/MerchantStatus.cs | 15 +++++++++++ .../Orders/Enums/DeliveryType.cs | 11 ++++++++ .../Orders/Enums/OrderChannel.cs | 23 ++++++++++++++++ .../Orders/Enums/OrderStatus.cs | 23 ++++++++++++++++ .../Payments/Enums/PaymentMethod.cs | 23 ++++++++++++++++ .../Payments/Enums/PaymentStatus.cs | 19 +++++++++++++ .../Products/Enums/ProductStatus.cs | 15 +++++++++++ .../Queues/Enums/QueueStatus.cs | 19 +++++++++++++ .../Reservations/Enums/ReservationStatus.cs | 19 +++++++++++++ 11 files changed, 217 insertions(+) diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs index 70cff85..7957eb7 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryProvider.cs @@ -5,10 +5,33 @@ namespace TakeoutSaaS.Domain.Deliveries.Enums; ///
public enum DeliveryProvider { + /// + /// 自建配送团队。 + /// InHouse = 0, + + /// + /// 达达。 + /// Dada = 1, + + /// + /// 闪送。 + /// FlashEx = 2, + + /// + /// 美团配送。 + /// Meituan = 3, + + /// + /// 饿了么配送。 + /// Eleme = 4, + + /// + /// 顺丰同城。 + /// Shunfeng = 5 } diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs index 7f6aaef..1d2ae38 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Enums/DeliveryStatus.cs @@ -5,11 +5,38 @@ namespace TakeoutSaaS.Domain.Deliveries.Enums; /// public enum DeliveryStatus { + /// + /// 待接单。 + /// Pending = 0, + + /// + /// 骑手已接单。 + /// Accepted = 1, + + /// + /// 正在取餐。 + /// PickingUp = 2, + + /// + /// 配送途中。 + /// Delivering = 3, + + /// + /// 已送达完成。 + /// Completed = 4, + + /// + /// 被取消。 + /// Cancelled = 5, + + /// + /// 配送失败。 + /// Failed = 6 } diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs index 6ed8097..820e8cd 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantStatus.cs @@ -5,8 +5,23 @@ namespace TakeoutSaaS.Domain.Merchants.Enums; /// public enum MerchantStatus { + /// + /// 等待审核。 + /// Pending = 0, + + /// + /// 审核通过,可运营。 + /// Approved = 1, + + /// + /// 审核未通过。 + /// Rejected = 2, + + /// + /// 因违规或欠费被冻结。 + /// Frozen = 3 } diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs index 4a7249f..799365b 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs @@ -5,7 +5,18 @@ namespace TakeoutSaaS.Domain.Orders.Enums; /// public enum DeliveryType { + /// + /// 堂食。 + /// DineIn = 0, + + /// + /// 门店自提。 + /// Pickup = 1, + + /// + /// 同城配送。 + /// Delivery = 2 } diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs index 6908ac6..e7a1306 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs @@ -5,10 +5,33 @@ namespace TakeoutSaaS.Domain.Orders.Enums; /// public enum OrderChannel { + /// + /// 未知渠道。 + /// Unknown = 0, + + /// + /// 小程序下单。 + /// MiniProgram = 1, + + /// + /// 扫码点餐。 + /// ScanToOrder = 2, + + /// + /// 员工操作台。 + /// StaffConsole = 3, + + /// + /// 电话预约。 + /// PhoneReservation = 4, + + /// + /// 第三方配送渠道。 + /// ThirdPartyDelivery = 5 } diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs index 4132e46..347d61f 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs @@ -5,10 +5,33 @@ namespace TakeoutSaaS.Domain.Orders.Enums; /// public enum OrderStatus { + /// + /// 待付款。 + /// PendingPayment = 0, + + /// + /// 已付款待制作。 + /// AwaitingPreparation = 1, + + /// + /// 制作/履约中。 + /// InProgress = 2, + + /// + /// 可取餐/可自提。 + /// Ready = 3, + + /// + /// 已完成。 + /// Completed = 4, + + /// + /// 已取消。 + /// Cancelled = 5 } diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs index 2453be2..7aa7807 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentMethod.cs @@ -5,10 +5,33 @@ namespace TakeoutSaaS.Domain.Payments.Enums; /// public enum PaymentMethod { + /// + /// 未知或待确定方式。 + /// Unknown = 0, + + /// + /// 微信支付。 + /// WeChatPay = 1, + + /// + /// 支付宝。 + /// Alipay = 2, + + /// + /// 现金。 + /// Cash = 3, + + /// + /// 刷卡(POS)。 + /// Card = 4, + + /// + /// 余额或储值账户。 + /// Balance = 5 } diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs index 6b5912a..550e1ee 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Enums/PaymentStatus.cs @@ -5,9 +5,28 @@ namespace TakeoutSaaS.Domain.Payments.Enums; /// public enum PaymentStatus { + /// + /// 未支付。 + /// Unpaid = 0, + + /// + /// 支付处理中。 + /// Paying = 1, + + /// + /// 支付成功。 + /// Paid = 2, + + /// + /// 支付失败。 + /// Failed = 3, + + /// + /// 已退款。 + /// Refunded = 4 } diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs b/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs index 8678fcc..916679b 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Enums/ProductStatus.cs @@ -5,8 +5,23 @@ namespace TakeoutSaaS.Domain.Products.Enums; /// public enum ProductStatus { + /// + /// 草稿,尚未上架。 + /// Draft = 0, + + /// + /// 已上架售卖中。 + /// OnSale = 1, + + /// + /// 临时下架。 + /// OffShelf = 2, + + /// + /// 归档不再使用。 + /// Archived = 3 } diff --git a/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs b/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs index f217a70..8149725 100644 --- a/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Queues/Enums/QueueStatus.cs @@ -5,9 +5,28 @@ namespace TakeoutSaaS.Domain.Queues.Enums; /// public enum QueueStatus { + /// + /// 正在排队等待。 + /// Waiting = 0, + + /// + /// 已叫号。 + /// Calling = 1, + + /// + /// 完成入座或取餐。 + /// Completed = 2, + + /// + /// 用户主动取消。 + /// Cancelled = 3, + + /// + /// 过号未到。 + /// Expired = 4 } diff --git a/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs b/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs index 2bf53a3..024bcfb 100644 --- a/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs +++ b/src/Domain/TakeoutSaaS.Domain/Reservations/Enums/ReservationStatus.cs @@ -5,9 +5,28 @@ namespace TakeoutSaaS.Domain.Reservations.Enums; /// public enum ReservationStatus { + /// + /// 已提交待确认。 + /// Pending = 0, + + /// + /// 预约已确认。 + /// Confirmed = 1, + + /// + /// 客户已到店签到。 + /// CheckedIn = 2, + + /// + /// 已取消。 + /// Cancelled = 3, + + /// + /// 未到店被标记为爽约。 + /// NoShow = 4 } From 15fc000cfc9dd76eb7a36fd6f666ec08bac9bea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BA=E7=88=B1=E6=B3=BD?= Date: Mon, 1 Dec 2025 18:16:49 +0800 Subject: [PATCH 13/56] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=85=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=84=9A=E6=9C=AC=E5=92=8C=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/10_TODO.md | 99 +- Document/11_WaitTODO.md | 92 +- Document/16_设计时DbContext配置指引.md | 89 + Document/infra/postgres_redis.md | 101 + deploy/postgres/README.md | 47 + deploy/postgres/bootstrap.ps1 | 37 + deploy/postgres/create_databases.sql | 83 + deploy/redis/README.md | 34 + deploy/redis/redis.conf | 25 + .../appsettings.Development.json | 19 +- .../appsettings.Production.json | 19 +- .../appsettings.Development.json | 17 +- .../appsettings.Production.json | 17 +- .../appsettings.Development.json | 17 +- .../appsettings.Production.json | 17 +- .../Constants/DatabaseConstants.cs | 5 + .../TakeoutSaaS.Shared.Abstractions.csproj | 2 + .../TakeoutSaaS.Domain.csproj | 2 + ...51201055852_ExpandDomainSchema.Designer.cs | 4330 +++ .../20251201055852_ExpandDomainSchema.cs | 2206 ++ ...251201094254_AddEntityComments.Designer.cs | 5641 ++++ .../20251201094254_AddEntityComments.cs | 22401 ++++++++++++++++ .../TakeoutAppDbContextModelSnapshot.cs | 5198 +++- .../App/Persistence/TakeoutAppDbContext.cs | 771 + .../TakeoutAppDesignTimeDbContextFactory.cs | 3 +- .../Common/Persistence/AppDbContext.cs | 1 + .../DesignTimeDbContextFactoryBase.cs | 104 +- .../ModelBuilderCommentExtensions.cs | 136 + .../DictionaryServiceCollectionExtensions.cs | 2 +- ...251201094456_AddEntityComments.Designer.cs | 206 + .../20251201094456_AddEntityComments.cs | 599 + .../DictionaryDbContextModelSnapshot.cs | 94 +- .../DictionaryDesignTimeDbContextFactory.cs | 3 +- ...251201094410_AddEntityComments.Designer.cs | 185 + .../20251201094410_AddEntityComments.cs | 581 + .../IdentityDbContextModelSnapshot.cs | 91 +- .../IdentityDesignTimeDbContextFactory.cs | 3 +- 37 files changed, 42829 insertions(+), 448 deletions(-) create mode 100644 Document/16_设计时DbContext配置指引.md create mode 100644 Document/infra/postgres_redis.md create mode 100644 deploy/postgres/README.md create mode 100644 deploy/postgres/bootstrap.ps1 create mode 100644 deploy/postgres/create_databases.sql create mode 100644 deploy/redis/README.md create mode 100644 deploy/redis/redis.conf create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs diff --git a/Document/10_TODO.md b/Document/10_TODO.md index 31f7d26..48327f0 100644 --- a/Document/10_TODO.md +++ b/Document/10_TODO.md @@ -1,62 +1,57 @@ -# TODO Roadmap +# TODO Roadmap -说明:本清单覆盖当前阶段的骨架搭建与核心基础能力(不含部署与CI/CD,留到项目跑通后再做)。 +> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。 -## A. 基础骨架与规范 -- [x] 统一返回结果/异常处理中间件(Shared.Web) -- [x] 模型验证、验证失败统一输出(Shared.Web) -- [x] 统一日志(Serilog)与请求日志/TraceId(Shared.Web) -- [x] API 版本化与分组(AdminApi、MiniApi、UserApi) -- [x] Swagger 定制(鉴权按钮、分组说明、示例) -- [x] 安全中间件:Security Headers、CORS 策略(按端区分) +## 1. 配置与基础设施(高优) +- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。 +- [x] Redis 服务部署完毕并记录配置。 +- [x] RabbitMQ 服务部署完毕并记录配置。 +- [x] COS 密钥配置补录完毕。 +- [ ] OSS 密钥配置补录完毕(待采购)。 +- [ ] SMS 平台密钥配置补录完毕(待采购)。 +- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。 +- [x] PostgreSQL 基础实例部署完毕并记录配置。 +- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。 +- [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). +- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。 +- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。 -## B. 认证与权限 -- [x] JWT 颁发与刷新(AdminApi、MiniApi) -- [x] RBAC 权限模型(角色/权限/策略)与特性授权(AdminApi) -- [x] 小程序登录(微信 code2Session)并绑定用户账户(MiniApi) -- [x] 登录防刷限流(MiniApi) +## 2. 数据与迁移(高优) +- [ ] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 +- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 +- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 -## C. 多租户与参数字典 -- [x] 多租户中间件:从 Header/域名解析租户(Shared.Web + Tenancy) -- [x] EF Core 全局查询过滤(tenant_id) -- [x] 参数字典模块(系统参数/业务参数)CRUD 与缓存(Dictionary 模块) +## 3. 稳定性与质量 +- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 +- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。 +- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 -## D. 数据访问与多数据源 -- [x] EF Core 10 基础上下文、实体基类、审计字段 -- [x] 读写分离/多数据源配置(主写、从读) -- [x] Dapper 基础设施封装(统计/报表类查询) +## 4. 安全与合规 +- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 +- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 +- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 +- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 -## E. 文件与存储 -- [x] 存储模块抽象(腾讯云COS/七牛云/阿里云OSS) -- [x] 上传接口(AdminApi、MiniApi)与签名直传预留 -- [x] 图片/文件访问安全策略(防盗链、过期签名) +## 5. 观测与运维 +- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。 +- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 +- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 -## F. 短信与消息队列 -- [x] 短信模块(阿里云/腾讯云 适配占位)与验证码发送 -- [x] MQ 模块(RabbitMQ)Publisher/Subscriber 抽象 -- [x] 业务事件定义(订单创建/支付成功等)与事件发布入口 +## 6. 业务能力补全 +- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。 +- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。 +- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。 -## G. 调度与定时任务 -- [x] 调度模块(Quartz/Hangfire 二选一,默认 Hangfire) -- [x] 基础任务:订单超时取消、优惠券过期处理、日志清理 -- [x] 调度面板(后续 AdminUI 对接) +## 7. 前后台 UI 对接 +- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。 +- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。 -## H. 第三方配送对接(仅第三方) -- [ ] 配送适配抽象(达达/闪送/顺丰同城等) -- [ ] 统一下单/取消/查询接口与回调验签 -- [ ] AdminApi 后台运力单查询与补单 +## 8. CI/CD 与发布 +- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。 +- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。 +- [ ] 版本与发布说明模板整理并在仓库中提供示例。 -## I. 网关与横切能力 -- [x] YARP 路由拆分(/api/admin、/api/mini、/api/user) -- [x] 网关级限流与请求日志 -- [x] 透传鉴权/租户标识与统一错误页 - -## J. 测试与质量 -- [ ] 单元测试工程骨架(xUnit + FluentAssertions) -- [ ] 集成测试基座(WebApplicationFactory、测试容器) -- [ ] 静态分析与风格规范(.editorconfig) - -## K. 文档与规范落地 -- [ ] 在文档中补充:仅第三方配送的接口与回调规范 -- [ ] MiniApi 认证流程图(微信登录)与错误码 -- [ ] 模块间调用关系图与依赖边界 +## 9. 文档与知识库 +- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。 +- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。 +- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。 diff --git a/Document/11_WaitTODO.md b/Document/11_WaitTODO.md index d812c4d..700f366 100644 --- a/Document/11_WaitTODO.md +++ b/Document/11_WaitTODO.md @@ -1,49 +1,53 @@ -# 下一步 TODO(骨架完成后) +# 里程碑待办追踪 -说明:当前骨架已覆盖认证、权限、多租户、存储、短信、MQ、调度、网关等基础能力。下面的清单用于进入“可运行/可上线”的补全与质量阶段,可按优先级推进。 +> 按“小程序版模块规划”划分四个里程碑;每个里程碑只含对应范围的任务,便于分阶段推进。 -## 1. 配置与基础设施落地(高优) -- 补充真实配置:数据库/Redis/RabbitMQ/对象存储/SMS/WeChat Mini/身份密钥,并分环境管理(Development/Staging/Production)。 -- 准备基础设施:PostgreSQL 主从、Redis(哨兵/集群)、RabbitMQ、COS/OSS、Hangfire 存储库;完善 docker-compose 与部署说明。 -- 网关与服务域名规划:为 admin/mini/user/gateway 配置实际域名、TLS 证书与 CORS 列表。 -- Hangfire Dashboard 鉴权:开启并加上 Admin 角色校验或网关白名单。 +--- +## Phase 1(当前阶段):租户/商家入驻、门店与菜品、扫码堂食、基础下单支付、预购自提、第三方配送骨架 +- [ ] 管理端租户 API:注册、实名认证、套餐订阅/续费/升降配、审核流,Swagger ≥6 个端点,含审核日志。 +- [ ] 商家入驻 API:证照上传、合同管理、类目选择,驱动待审/审核/驳回/通过状态机,文件持久在 COS。 +- [ ] RBAC 模板:平台管理员、租户管理员、店长、店员四角色模板;API 可复制并允许租户自定义扩展。 +- [ ] 配额与套餐:TenantPackage CRUD、订阅/续费/配额校验(门店/账号/短信/配送单量),超额返回 409 并记录 TenantQuotaUsage。 +- [ ] 租户运营面板:欠费/到期告警、账单列表、公告通知接口,支持已读状态并在 Admin UI 展示。 +- [ ] 门店管理:Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。 +- [ ] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 +- [ ] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 +- [ ] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。 +- [ ] 菜品建模:分类、SPU、SKU、规格/加料组、价格策略、媒资 CRUD + 上下架流程;Mini 端可拉取完整 JSON。 +- [ ] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。 +- [ ] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。 +- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。 +- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。 +- [ ] 桌台账单:合单/拆单、结账、电子小票、桌台释放,完成结账后恢复 Idle 并生成票据 URL。 +- [ ] 自配送骨架:骑手管理、取送件信息录入、费用补贴记录,Admin 端可派单并更新 DeliveryOrder。 +- [ ] 第三方配送抽象:统一下单/取消/加价/查询接口,支持达达、美团、闪送等,含回调验签与异常补偿骨架。 +- [ ] 预购自提核销:提货码生成、手机号/二维码核销、自提柜/前台流程,超时自动取消或退款,记录操作者与时间。 +- [ ] 指标与日志:Prometheus 输出订单创建、支付成功率、配送回调耗时等,Grafana ≥8 个图表;关键流程日志记录 TraceId + 业务 ID。 +- [ ] 测试:Phase 1 核心 API 具备 ≥30 条自动化用例(单元 + 集成),覆盖租户→商户→下单链路。 -## 2. 数据与迁移(高优) -- 建立 EF Core Migration 基线并生成数据库(App/Identity/Dictionary/Hangfire)。 -- 设计并落地核心业务表(商户/门店/商品/订单/支付/配送等),补齐 Domain 与 Infrastructure 仓储。 -- 数据初始化/种子:系统参数、默认租户、管理员、基础字典。 +--- +## Phase 2(下一阶段):拼单、优惠券与基础营销、会员积分/会员日、客服聊天、同城自配送调度、搜索 +- [ ] 拼单引擎:GroupOrder/Participant CRUD、发起/加入/成团条件、自动解散与退款、团内消息与提醒。 +- [ ] 优惠券与基础营销:模板管理、领券、核销、库存/有效期/叠加规则,基础抽奖/秒杀/满减活动。 +- [ ] 会员与积分:会员档案、等级/成长值、会员日通知;积分获取/消耗、有效期、黑名单。 +- [ ] 客服聊天:实时会话、机器人/人工切换、排队/转接、消息模板、敏感词审查、工单流转与评价。 +- [ ] 同城自配送调度:骑手智能指派、路线估时、无接触配送、费用补贴策略、调度看板。 +- [ ] 搜索:门店/菜品/活动/优惠券搜索,过滤/排序、热门/历史记录、联想与纠错。 -## 3. 质量与测试(高优) -- 单元测试骨架:xUnit + FluentAssertions(Dictionary、Identity、Storage、Sms、Messaging、Scheduler)。 -- 集成测试基座:WebApplicationFactory + Testcontainers(Postgres/Redis/RabbitMQ/MinIO 可选)。 -- 静态分析:添加 .editorconfig/.globalconfig,启用可空警告、风格规则,接入 Roslyn 分析器。 +--- +## Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、高阶营销、风控与补偿 +- [ ] 分销返利:AffiliatePartner/Order/Payout 管理,佣金阶梯、结算周期、税务信息、违规处理。 +- [ ] 签到打卡:CheckInCampaign/Record、连签奖励、补签、积分/券/成长值奖励、反作弊机制。 +- [ ] 预约预订:档期/资源占用、预约下单/支付、提醒/改期/取消、到店核销与履约记录。 +- [ ] 地图导航扩展:附近门店/推荐、距离/路线规划、跳转原生导航、导航请求埋点。 +- [ ] 社区:动态发布、评论、点赞、话题/标签、图片/视频审核、举报与风控,店铺口碑展示。 +- [ ] 高阶营销:秒杀/抽奖/裂变、裂变海报、爆款推荐位、多渠道投放分析。 +- [ ] 风控与审计:黑名单、频率限制、异常行为监控、审计日志、补偿与告警体系。 -## 4. 安全与合规 -- 完善鉴权:网关透传与后端校验的租户/用户/权限;Swagger 鉴权示例。 -- 输入校验与防刷:全局限流策略(按 IP/租户),登录与验证码防刷策略参数化。 -- 日志与审计:敏感字段脱敏,登录/权限/管理操作审计日志模型与落库。 -- 配置机密:使用 Secret Store/环境变量/KMS 管理密钥,禁止明文提交。 - -## 5. 可观测性与运维 -- 日志链路:统一 TraceId 透传(网关→服务),配置 Serilog 输出(Console/File/ELK)与留存策略。 -- 指标/监控:Prometheus exporter、健康检查探针(/health)、告警规则草案。 -- 备份恢复:PostgreSQL 全量/增量备份脚本,恢复演练记录。 - -## 6. 业务功能补全 -- 订单/商品/商户等领域建模与应用服务接口实现,结合 MQ 事件发布(订单创建、支付成功等)。 -- 配送对接抽象实现(达达/闪送/顺丰同城)占位,提供下单/取消/查询接口与回调验签。 -- 小程序端接口补齐:商品浏览、下单、支付、评价、上传图片直传联调。 - -## 7. 前台/后台 UI 对接 -- Admin UI:接入 Swagger 导出的 OpenAPI,生成或手写管理端界面;接入 Hangfire Dashboard/MQ 监控只读访问。 -- MiniApp:小程序登录流程与错误码文档完善,联调上传、下单、支付链路。 - -## 8. CI/CD 与发布 -- 建立流水线:构建/测试/扫描(SAST)、镜像推送、数据库迁移步骤。 -- 多环境部署策略:Dev/Staging/Prod 配置隔离,蓝绿或滚动发布方案草拟。 -- 版本与变更管理:约定版本号/发布说明模板。 - -## 9. 文档补全 -- 更新接口文档(新增业务 API、错误码、回调规范)、模块依赖关系图。 -- 运维手册:启动参数、环境变量列表、端口/域名映射、常见故障排查。 -- 安全与合规清单:数据分类分级、审计、留存周期。 +--- +## Phase 4:性能优化、缓存、运营大盘、测试与文档、上线与监控 +- [ ] 性能与缓存:热点接口缓存、慢查询治理、批处理优化、异步化改造。 +- [ ] 可靠性:幂等与重试策略、任务调度补偿、链路追踪、告警联动。 +- [ ] 运营大盘:交易/营销/履约/用户维度的细分报表、GMV/成本/毛利分析。 +- [ ] 文档与测试:完整测试矩阵、性能测试报告、上线手册、回滚方案。 +- [ ] 监控与运维:上线发布流程、灰度/回滚策略、系统稳定性指标、24x7 监控与告警。 diff --git a/Document/16_设计时DbContext配置指引.md b/Document/16_设计时DbContext配置指引.md new file mode 100644 index 0000000..d1fa968 --- /dev/null +++ b/Document/16_设计时DbContext配置指引.md @@ -0,0 +1,89 @@ +# 设计时 DbContext 配置指引 + +> 目的:在执行 `dotnet ef` 命令时无需硬编码数据库连接,可根据 appsettings 与环境变量自动加载。本文覆盖环境变量设置、配置目录指定等细节。 + +## 一、设计时工厂读取逻辑概述 +设计时工厂(`DesignTimeDbContextFactoryBase`)按下面顺序解析连接串: +1. 若设置了 `TAKEOUTSAAS_APP_CONNECTION` / `TAKEOUTSAAS_IDENTITY_CONNECTION` / `TAKEOUTSAAS_DICTIONARY_CONNECTION` 等环境变量,则优先使用。 +2. 否则查找配置文件: + - 从当前目录开始向上找到含 `TakeoutSaaS.sln` 的仓库根。 + - 依次检查 `src/Api/TakeoutSaaS.AdminApi`、`src/Api/TakeoutSaaS.UserApi`、`src/Api/TakeoutSaaS.MiniApi` 等目录,如果存在 `appsettings.json` 或 `appsettings.{Environment}.json` 则加载。 + - 若未找到,可通过环境变量 `TAKEOUTSAAS_APPSETTINGS_DIR` 指定包含 appsettings 文件的目录。 + +配置结构示例(出现在 AdminApi/MiniApi/UserApi 的 appsettings): +```json +"Database": { + "DataSources": { + "AppDatabase": { + "Write": "Host=120.53...;Database=takeout_app_db;Username=...;Password=...", + "Reads": [ + "Host=120.53...;Database=takeout_app_db;Username=...;Password=..." + ] + }, + "IdentityDatabase": { + "Write": "...", + "Reads": [ "..." ] + }, + "DictionaryDatabase": { + "Write": "...", + "Reads": [ "..." ] + } + } +} +``` +设计时工厂会根据数据源名称(`DatabaseConstants.AppDataSource` 等)读取 `Write` 连接串,实现与运行时一致。 + +## 二、环境变量配置 +### 1. Windows PowerShell +```powershell +# 指向包含 appsettings.json 的目录 +$env:TAKEOUTSAAS_APPSETTINGS_DIR = \"D:\\HAZCode\\TakeOut\\src\\Api\\TakeoutSaaS.AdminApi\" + +#(可选)覆盖 AppDatabase 连接串 +$env:TAKEOUTSAAS_APP_CONNECTION = \"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=***\" + +#(可选)覆盖 IdentityDatabase 连接串 +$env:TAKEOUTSAAS_IDENTITY_CONNECTION = \"Host=...;Database=takeout_identity_db;Username=...;Password=...\" + +#����ѡ������ DictionaryDatabase ���Ӵ� +$env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=...;Database=takeout_dictionary_db;Username=...;Password=..." +``` + +### 2. Linux / macOS +```bash +export TAKEOUTSAAS_APPSETTINGS_DIR=/home/user/TakeOut/src/Api/TakeoutSaaS.AdminApi +export TAKEOUTSAAS_APP_CONNECTION=\"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=***\" +export TAKEOUTSAAS_IDENTITY_CONNECTION=\"Host=...;Database=takeout_identity_db;Username=...;Password=...\" +export TAKEOUTSAAS_DICTIONARY_CONNECTION="Host=...;Database=takeout_dictionary_db;Username=...;Password=..." +``` + +> 注意:若设置了 `TAKEOUTSAAS_APP_CONNECTION`,则无需在 appsettings 中提供 `Write` 连接串,反之亦然。不要将明文密码写入代码仓库,建议使用 Secret Manager 或部署环境的安全存储。 + +## 三、执行脚本示例 +完成上述环境变量配置后即可执行: +```powershell +# TakeoutAppDbContext(业务库) +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext + +# IdentityDbContext(身份库) +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext + +# DictionaryDbContext(字典库) +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext +``` + +若需迁移 Identity/Dictionary 等上下文,替换 `--context` 参数为对应类型即可。 + +## 四、常见问题 +1. **未找到 appsettings**:确保 `TAKEOUTSAAS_APPSETTINGS_DIR` 指向存在 `appsettings.json` 的目录,或将命令在 API 项目目录中执行。 +2. **密码错误**:确认远程 PostgreSQL 用户/密码是否与 appsettings 或环境变量一致,避免在 CLI 中使用默认的账号。 +3. **多环境配置**:`ASPNETCORE_ENVIRONMENT` 变量可控制加载 `appsettings.{Environment}.json`;默认是 Development。 diff --git a/Document/infra/postgres_redis.md b/Document/infra/postgres_redis.md new file mode 100644 index 0000000..bf6e076 --- /dev/null +++ b/Document/infra/postgres_redis.md @@ -0,0 +1,101 @@ +# PostgreSQL 与 Redis 接入手册 + +> 本文档补齐 `Document/10_TODO.md` 中“Postgres/Redis 接入文档与 IaC/脚本”的要求,统一描述连接信息、账号权限、运维流程,以及可复用的部署脚本位置。 + +## 1. 运行环境总览 + +| 组件 | 地址/端口 | 主要数据库/实例 | 说明 | +| --- | --- | --- | --- | +| PostgreSQL | `120.53.222.17:5432` | `takeout_app_db`、`takeout_identity_db`、`takeout_dictionary_db`、`takeout_hangfire_db` | 线上实例,所有业务上下文共用。 | +| Redis | `49.232.6.45:6379` | 单节点 | 业务缓存/登录限流/刷新令牌存储。 | + +> 注意:所有业务账号都只具备既有库的读写权限,无 `CREATEDB`。若需新库,需使用平台管理员账号(`postgres`)或联系 DBA。 + +## 2. 账号与库映射 + +| 数据库 | 角色 | 密码 | 用途 | +| --- | --- | --- | --- | +| `takeout_app_db` | `app_user` | `AppUser112233` | 业务域 (`TakeoutAppDbContext`) | +| `takeout_identity_db` | `identity_user` | `IdentityUser112233` | 身份域 (`IdentityDbContext`) | +| `takeout_dictionary_db` | `dictionary_user` | `DictionaryUser112233` | 字典域 (`DictionaryDbContext`) | +| `takeout_hangfire_db` | `hangfire_user` | `HangFire112233` | 后台调度/Hangfire | + +Redis 密码:`MsuMshk112233`,见 `appsettings.*.json -> Redis`。 + +## 3. 环境变量/配置注入 + +### PowerShell + +```powershell +$env:TAKEOUTSAAS_APPSETTINGS_DIR = "D:\HAZCode\TakeOut\src\Api\TakeoutSaaS.AdminApi" +$env:TAKEOUTSAAS_APP_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true" +$env:TAKEOUTSAAS_IDENTITY_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true" +$env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true" +``` + +### Bash + +```bash +export TAKEOUTSAAS_APPSETTINGS_DIR=/home/user/TakeOut/src/Api/TakeoutSaaS.AdminApi +export TAKEOUTSAAS_APP_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true" +export TAKEOUTSAAS_IDENTITY_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true" +export TAKEOUTSAAS_DICTIONARY_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true" +``` + +Redis 连接字符串直接写入 `appsettings.*.json` 即可,如: + +```jsonc +"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false" +``` + +## 4. 运维指南 + +### PostgreSQL + +1. **只读账号验证** + ```powershell + psql "host=120.53.222.17 port=5432 dbname=takeout_app_db user=app_user password=AppUser112233" + ``` +2. **备份** + ```bash + pg_dump -h 120.53.222.17 -p 5432 -U postgres -F c -d takeout_app_db -f backup/takeout_app_db_$(date +%Y%m%d).dump + pg_dumpall -h 120.53.222.17 -p 5432 -U postgres > backup/all_$(date +%Y%m%d).sql + ``` +3. **恢复** + ```bash + pg_restore -h 120.53.222.17 -p 5432 -U postgres -d takeout_app_db backup/takeout_app_db_xxx.dump + psql -h 120.53.222.17 -p 5432 -U postgres -f backup/all_yyyymmdd.sql + ``` +4. **账号/权限策略** + - `app_user` / `identity_user` / `dictionary_user` 拥有 `CONNECT`、`TEMP`、Schema `public` 的 CRUD 权限。 + - `hangfire_user` 仅能访问 `takeout_hangfire_db`,不可访问业务库。 + - 创建新表/列时,通过 EF Migration 自动添加 COMMENT。 + +### Redis + +1. **连接验证** + ```bash + redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 ping + ``` +2. **备份** + ```bash + redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 save # 触发 RDB + redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 bgsave # 后台 + ``` + RDB/AOF 文件在服务器 `redis.conf` 定义的目录(默认 `/var/lib/redis`)。 +3. **常见运维项** + - `CONFIG GET dir` / `CONFIG GET dbfilename` 可查看持久化路径。 + - `INFO memory` 监控内存;开启 `maxmemory` + `allkeys-lru` 保护。 + +## 5. IaC / 脚本 + +| 文件 | 说明 | +| --- | --- | +| `deploy/postgres/create_databases.sql` | 基于 `postgres` 管理员执行,创建四个业务库及角色、授予权限、补 COMMENT。 | +| `deploy/postgres/bootstrap.ps1` | PowerShell 包装脚本,调用 `psql` 执行上面的 SQL(默认读取 `postgres` 管理员账号)。 | +| `deploy/postgres/README.md` | 介绍如何在本地/测试环境执行 bootstrap 并校验连接。 | +| `deploy/redis/docker-compose.yml` | 可复用的 Redis 部署(Redis 7 + AOF),便于本地或测试环境一键拉起。 | +| `deploy/redis/redis.conf` | compose/裸机均可共用的配置(`requirepass`、持久化等已写好)。 | +| `deploy/redis/README.md` | 说明如何使用 compose 或将 `redis.conf` 部署到现有实例。 | + +> 线上目前为裸机安装(非容器),如需创建新环境/快速恢复,可直接运行上述脚本达到同样配置;即使在现有机器上,也可把 SQL/配置当作“最终规范”确保环境一致性。 diff --git a/deploy/postgres/README.md b/deploy/postgres/README.md new file mode 100644 index 0000000..04e7222 --- /dev/null +++ b/deploy/postgres/README.md @@ -0,0 +1,47 @@ +# PostgreSQL 部署脚本 + +本目录提供在测试/预发布环境快速拉起 PostgreSQL 的脚本,复用线上同名数据库与账号,方便迁移/恢复。 + +## 目录结构 + +- `create_databases.sql`:创建四个业务库与对应角色(可多次执行,存在则跳过)。 +- `bootstrap.ps1`:PowerShell 包装脚本,调用 `psql` 执行 SQL。 + +## 前置条件 + +1. 已安装 PostgreSQL 12+,并能以管理员身份访问(默认使用 `postgres`)。 +2. 本地已配置 `psql` 可执行命令。 + +## 使用方法 + +```powershell +cd deploy/postgres +.\bootstrap.ps1 ` + -Host 120.53.222.17 ` + -Port 5432 ` + -AdminUser postgres ` + -AdminPassword "超级管理员密码" +``` + +脚本会: + +1. 创建/更新以下角色与库: + - `app_user` / `takeout_app_db` + - `identity_user` / `takeout_identity_db` + - `dictionary_user` / `takeout_dictionary_db` + - `hangfire_user` / `takeout_hangfire_db` +2. 为库设置 COMMENT,授予 Schema `public` 的 CRUD 权限。 +3. 输出执行日志,失败时终止。 + +## 自定义 + +- 如需修改密码或新增库,编辑 `create_databases.sql` 后重新运行脚本。 +- 若在本地拉起测试库,可把 `Host` 指向 `localhost`,其余参数保持一致。 + +## 常见问题 + +| 问题 | 处理方式 | +| --- | --- | +| `psql : command not found` | 确认 PostgreSQL bin 目录已加入 PATH。 | +| `permission denied to create database` | 改用具有 `CREATEDB` 权限的管理员执行脚本。 | +| 需要删除库 | 先 `DROP DATABASE xxx`,再运行脚本重新创建。 | diff --git a/deploy/postgres/bootstrap.ps1 b/deploy/postgres/bootstrap.ps1 new file mode 100644 index 0000000..3ea0bea --- /dev/null +++ b/deploy/postgres/bootstrap.ps1 @@ -0,0 +1,37 @@ +param( + [string]$Host = "120.53.222.17", + [int]$Port = 5432, + [string]$AdminUser = "postgres", + [string]$AdminPassword = "" +) + +if (-not (Get-Command psql -ErrorAction SilentlyContinue)) { + throw "psql command not found. Add PostgreSQL bin directory to PATH." +} + +if ([string]::IsNullOrWhiteSpace($AdminPassword)) { + Write-Warning "AdminPassword not provided. You will be prompted by psql." +} + +$sqlPath = Join-Path $PSScriptRoot "create_databases.sql" +if (-not (Test-Path $sqlPath)) { + throw "Cannot find create_databases.sql under $PSScriptRoot." +} + +$env:PGPASSWORD = $AdminPassword + +$arguments = @( + "-h", $Host, + "-p", $Port, + "-U", $AdminUser, + "-f", $sqlPath +) + +Write-Host "Executing create_databases.sql on $Host:$Port as $AdminUser ..." +& psql @arguments + +if ($LASTEXITCODE -ne 0) { + throw "psql returned non-zero exit code ($LASTEXITCODE)." +} + +Write-Host "PostgreSQL databases and roles ensured successfully." diff --git a/deploy/postgres/create_databases.sql b/deploy/postgres/create_databases.sql new file mode 100644 index 0000000..c8732f6 --- /dev/null +++ b/deploy/postgres/create_databases.sql @@ -0,0 +1,83 @@ +-- Reusable provisioning script for Takeout SaaS PostgreSQL databases. +-- Execute with a superuser (e.g. postgres). Safe to re-run. + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN + CREATE ROLE app_user LOGIN PASSWORD 'AppUser112233'; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'identity_user') THEN + CREATE ROLE identity_user LOGIN PASSWORD 'IdentityUser112233'; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dictionary_user') THEN + CREATE ROLE dictionary_user LOGIN PASSWORD 'DictionaryUser112233'; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hangfire_user') THEN + CREATE ROLE hangfire_user LOGIN PASSWORD 'HangFire112233'; + END IF; +END $$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_app_db') THEN + CREATE DATABASE takeout_app_db OWNER app_user ENCODING 'UTF8'; + END IF; +END $$; +COMMENT ON DATABASE takeout_app_db IS 'Takeout SaaS 业务域数据库'; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_identity_db') THEN + CREATE DATABASE takeout_identity_db OWNER identity_user ENCODING 'UTF8'; + END IF; +END $$; +COMMENT ON DATABASE takeout_identity_db IS 'Takeout SaaS 身份域数据库'; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_dictionary_db') THEN + CREATE DATABASE takeout_dictionary_db OWNER dictionary_user ENCODING 'UTF8'; + END IF; +END $$; +COMMENT ON DATABASE takeout_dictionary_db IS 'Takeout SaaS 字典域数据库'; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_hangfire_db') THEN + CREATE DATABASE takeout_hangfire_db OWNER hangfire_user ENCODING 'UTF8'; + END IF; +END $$; +COMMENT ON DATABASE takeout_hangfire_db IS 'Takeout SaaS 调度/Hangfire 数据库'; + +-- Ensure privileges and default schema permissions +\connect takeout_app_db +GRANT CONNECT, TEMP ON DATABASE takeout_app_db TO app_user; +GRANT USAGE ON SCHEMA public TO app_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user; +GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user; + +\connect takeout_identity_db +GRANT CONNECT, TEMP ON DATABASE takeout_identity_db TO identity_user; +GRANT USAGE ON SCHEMA public TO identity_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO identity_user; +GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO identity_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO identity_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO identity_user; + +\connect takeout_dictionary_db +GRANT CONNECT, TEMP ON DATABASE takeout_dictionary_db TO dictionary_user; +GRANT USAGE ON SCHEMA public TO dictionary_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO dictionary_user; +GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO dictionary_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dictionary_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO dictionary_user; + +\connect takeout_hangfire_db +GRANT CONNECT, TEMP ON DATABASE takeout_hangfire_db TO hangfire_user; +GRANT USAGE ON SCHEMA public TO hangfire_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO hangfire_user; +GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO hangfire_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO hangfire_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO hangfire_user; diff --git a/deploy/redis/README.md b/deploy/redis/README.md new file mode 100644 index 0000000..c955032 --- /dev/null +++ b/deploy/redis/README.md @@ -0,0 +1,34 @@ +# Redis 部署脚本 + +本目录提供可复用的 Redis 配置,既可在本地通过 Docker Compose 启动,也可将 `redis.conf` 拷贝到现有服务器,确保与线上一致。 + +## 1. 部署步骤 (裸机)\n\n1. 将 \\ edis.conf\\ 拷贝到服务器(例如 /etc/redis/redis.conf)。\n2. 根据需要修改数据目录(\\dir\\)和绑定地址。\n3. 使用系统服务或 \\ edis-server redis.conf\\ 启动。\n4. 确认开放端口 6379,保证通过 \\ edis-cli -h -a ping\\ 可访问。\n\n## 2. 配置说明\n\n- \\ equirepass\\ 已设置为 MsuMshk112233。\n- 启用 appendonly(AOF),并每秒 fsync。\n- \\maxmemory-policy\\ 为 allkeys-lru,适合缓存场景。\n- \\protected-mode no\\ 允许远程连接,需结合安全组或防火墙限制来源 IP。\n\n## 3. 常用命令使用 `redis.conf` + +1. 把 `redis.conf` 拷贝到服务器 `/etc/redis/redis.conf`(或自定义目录)。 +2. 修改 `dir` 指向实际数据目录。 +3. 使用系统服务或 `redis-server redis.conf` 启动。 + +关键配置已包含: + +- `requirepass`(密码) +- `protected-mode no`(允许远程连接) +- `appendonly yes` + `appendfsync everysec` +- `maxmemory-policy allkeys-lru` + +## 3. 常用命令 + +在应用或 CLI 中使用: + +```bash +redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 ping +``` + +`appsettings.*.json` 的格式:`"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false"` + +## 4. 备份 + +- RDB 文件:`dump.rdb` +- AOF 文件:`appendonly.aof` + +通过 `redis-cli -a save` 或 `bgsave` 触发。确保备份目录已纳入快照/对象存储。 + diff --git a/deploy/redis/redis.conf b/deploy/redis/redis.conf new file mode 100644 index 0000000..5d20bb0 --- /dev/null +++ b/deploy/redis/redis.conf @@ -0,0 +1,25 @@ +bind 0.0.0.0 +port 6379 +protected-mode no + +requirepass MsuMshk112233 + +timeout 0 +tcp-keepalive 300 + +daemonize no + +loglevel notice +databases 16 + +save 900 1 +save 300 10 +save 60 10000 + +appendonly yes +appendfilename "appendonly.aof" +appendfsync everysec + +dir /data + +maxmemory-policy allkeys-lru diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index d858ad3..6762606 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -2,18 +2,27 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "DictionaryDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, @@ -134,7 +143,7 @@ "PrefetchCount": 20 }, "Scheduler": { - "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_hangfire;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_hangfire_db;Username=hangfire_user;Password=HangFire112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json index d858ad3..6762606 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json @@ -2,18 +2,27 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "DictionaryDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, @@ -134,7 +143,7 @@ "PrefetchCount": 20 }, "Scheduler": { - "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_hangfire;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_hangfire_db;Username=hangfire_user;Password=HangFire112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json index 0d6da09..3b288cf 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json @@ -2,18 +2,27 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "DictionaryDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json index 0d6da09..3b288cf 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json @@ -2,18 +2,27 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "DictionaryDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json index 1f7ff9f..7a3af9b 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json @@ -2,18 +2,27 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "DictionaryDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json index 1f7ff9f..7a3af9b 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json @@ -2,18 +2,27 @@ "Database": { "DataSources": { "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + ], + "CommandTimeoutSeconds": 30, + "MaxRetryCount": 3, + "MaxRetryDelaySeconds": 5 + }, + "DictionaryDatabase": { + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Reads": [ + "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs index 6fe8a35..cc18844 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs @@ -14,4 +14,9 @@ public static class DatabaseConstants /// 身份认证库(IdentityDatabase)。 /// public const string IdentityDataSource = "IdentityDatabase"; + + /// + /// �����ֵ�⣨DictionaryDatabase���� + /// + public const string DictionaryDataSource = "DictionaryDatabase"; } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj index 05f0387..2de50de 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj @@ -3,6 +3,8 @@ net10.0 enable enable + true + 1591 diff --git a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj index b407eac..3f9021d 100644 --- a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj +++ b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj @@ -3,6 +3,8 @@ net10.0 enable enable + true + 1591 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs new file mode 100644 index 0000000..c901204 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs @@ -0,0 +1,4330 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.App.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + [DbContext(typeof(TakeoutAppDbContext))] + [Migration("20251201055852_ExpandDomainSchema")] + partial class ExpandDomainSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConditionJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("MetricDefinitionId") + .HasColumnType("uuid"); + + b.Property("NotificationChannels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "Severity"); + + b.ToTable("metric_alert_rules", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DefaultAggregation") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DimensionsJson") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("metric_definitions", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DimensionKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MetricDefinitionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("WindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("WindowStart") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd") + .IsUnique(); + + b.ToTable("metric_snapshots", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CouponTemplateId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("coupons", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowStack") + .HasColumnType("boolean"); + + b.Property("ChannelsJson") + .HasColumnType("text"); + + b.Property("ClaimedQuantity") + .HasColumnType("integer"); + + b.Property("CouponType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiscountCap") + .HasColumnType("numeric"); + + b.Property("MinimumSpend") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProductScopeJson") + .HasColumnType("text"); + + b.Property("RelativeValidDays") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreScopeJson") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalQuantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("coupon_templates", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudienceDescription") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("BannerUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Budget") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("PromotionType") + .HasColumnType("integer"); + + b.Property("RulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("promotion_campaigns", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatSessionId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderType") + .HasColumnType("integer"); + + b.Property("SenderUserId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ChatSessionId", "CreatedAt"); + + b.ToTable("chat_messages", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBotActive") + .HasColumnType("boolean"); + + b.Property("SessionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionCode") + .IsUnique(); + + b.ToTable("chat_sessions", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAgentId") + .HasColumnType("uuid"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TicketNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TicketNo") + .IsUnique(); + + b.ToTable("support_tickets", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttachmentsJson") + .HasColumnType("text"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("SupportTicketId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SupportTicketId"); + + b.ToTable("ticket_comments", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryOrderId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DeliveryOrderId", "EventType"); + + b.ToTable("delivery_events", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CourierName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CourierPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DispatchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FailureReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedUpAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ProviderOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AffiliatePartnerId") + .HasColumnType("uuid"); + + b.Property("BuyerUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EstimatedCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("SettledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "OrderId") + .IsUnique(); + + b.ToTable("affiliate_orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChannelType") + .HasColumnType("integer"); + + b.Property("CommissionRate") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DisplayName"); + + b.ToTable("affiliate_partners", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AffiliatePartnerId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Period") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "Period") + .IsUnique(); + + b.ToTable("affiliate_payouts", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowMakeupCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RewardsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("checkin_campaigns", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CheckInCampaignId") + .HasColumnType("uuid"); + + b.Property("CheckInDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsMakeup") + .HasColumnType("boolean"); + + b.Property("RewardJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CheckInCampaignId", "UserId", "CheckInDate") + .IsUnique(); + + b.ToTable("checkin_records", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("PostId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "CreatedAt"); + + b.ToTable("community_comments", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("CommentCount") + .HasColumnType("integer"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("LikeCount") + .HasColumnType("integer"); + + b.Property("MediaJson") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AuthorUserId", "CreatedAt"); + + b.ToTable("community_posts", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("PostId") + .HasColumnType("uuid"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReactionType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "UserId") + .IsUnique(); + + b.ToTable("community_reactions", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CurrentCount") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupOrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("GroupPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("LeaderUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderNo") + .IsUnique(); + + b.ToTable("group_orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("GroupOrderId") + .HasColumnType("uuid"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderId", "UserId") + .IsUnique(); + + b.ToTable("group_participants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdjustmentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("InventoryItemId") + .HasColumnType("uuid"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "InventoryItemId", "OccurredAt"); + + b.ToTable("inventory_adjustments", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductSkuId") + .HasColumnType("uuid"); + + b.Property("ProductionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RemainingQuantity") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber") + .IsUnique(); + + b.ToTable("inventory_batches", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Location") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProductSkuId") + .HasColumnType("uuid"); + + b.Property("QuantityOnHand") + .HasColumnType("integer"); + + b.Property("QuantityReserved") + .HasColumnType("integer"); + + b.Property("SafetyStock") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber"); + + b.ToTable("inventory_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChangeValue") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CurrentValue") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("MemberId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_growth_logs", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BalanceAfterChange") + .HasColumnType("integer"); + + b.Property("ChangeAmount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasColumnType("uuid"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("SourceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_point_ledgers", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AvatarUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("GrowthValue") + .HasColumnType("integer"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberTierId") + .HasColumnType("uuid"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Nickname") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PointsBalance") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Mobile") + .IsUnique(); + + b.ToTable("member_profiles", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BenefitsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RequiredGrowth") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.ToTable("member_tiers", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BrandAlias") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BusinessLicenseImageUrl") + .HasColumnType("text"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("LegalPerson") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ServicePhone") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SupportEmail") + .HasColumnType("text"); + + b.Property("TaxNumber") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("merchants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContractNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TerminatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TerminationReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "ContractNumber") + .IsUnique(); + + b.ToTable("merchant_contracts", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DocumentNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("Remarks") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "DocumentType"); + + b.ToTable("merchant_documents", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IdentityUserId") + .HasColumnType("uuid"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PermissionsJson") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RoleType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "Phone"); + + b.ToTable("merchant_staff", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Landmark") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("map_locations", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Channel") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TargetApp") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId", "RequestedAt"); + + b.ToTable("navigation_requests", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributesJson") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProductSkuId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ShoppingCartId"); + + b.ToTable("cart_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OptionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("cart_item_addons", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ValidationResultJson") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionToken") + .IsUnique(); + + b.ToTable("checkout_sessions", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryPreference") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TableContext") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId") + .IsUnique(); + + b.ToTable("shopping_carts", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Channel") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CustomerPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryType") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemsAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PayableAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PaymentStatus") + .HasColumnType("integer"); + + b.Property("QueueNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ReservationId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("orders", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributesJson") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("SkuName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SubTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("order_items", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId", "OccurredAt"); + + b.ToTable("order_status_histories", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNotes") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique(); + + b.ToTable("refund_requests", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ChannelTransactionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("payment_records", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ChannelRefundId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("PaymentRecordId") + .HasColumnType("uuid"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PaymentRecordId"); + + b.ToTable("payment_refund_records", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CoverImage") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EnableDelivery") + .HasColumnType("boolean"); + + b.Property("EnableDineIn") + .HasColumnType("boolean"); + + b.Property("EnablePickup") + .HasColumnType("boolean"); + + b.Property("GalleryImages") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IsFeatured") + .HasColumnType("boolean"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SpuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StockQuantity") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SpuCode") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("products", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("MaxSelect") + .HasColumnType("integer"); + + b.Property("MinSelect") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SelectionType") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "Name"); + + b.ToTable("product_addon_groups", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddonGroupId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("product_addon_options", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SelectionType") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("product_attribute_groups", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeGroupId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AttributeGroupId", "Name") + .IsUnique(); + + b.ToTable("product_attribute_options", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("product_categories", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("MediaType") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Id"); + + b.ToTable("product_media_assets", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConditionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("RuleType") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("WeekdaysJson") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "RuleType"); + + b.ToTable("product_pricing_rules", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Barcode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SkuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("StockQuantity") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Weight") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SkuCode") + .IsUnique(); + + b.ToTable("product_skus", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EstimatedWaitMinutes") + .HasColumnType("integer"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TicketNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.HasIndex("TenantId", "StoreId", "TicketNumber") + .IsUnique(); + + b.ToTable("queue_tickets", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CheckInCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CheckedInAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CustomerPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("PeopleCount") + .HasColumnType("integer"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ReservationNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ReservationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TablePreference") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ReservationNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("reservations", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Announcement") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("BusinessHours") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryRadiusKm") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("ManagerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MerchantId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SupportsDelivery") + .HasColumnType("boolean"); + + b.Property("SupportsDineIn") + .HasColumnType("boolean"); + + b.Property("SupportsPickup") + .HasColumnType("boolean"); + + b.Property("SupportsQueueing") + .HasColumnType("boolean"); + + b.Property("SupportsReservation") + .HasColumnType("boolean"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "MerchantId"); + + b.ToTable("stores", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CapacityLimit") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("HourType") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "DayOfWeek"); + + b.ToTable("store_business_hours", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("PolygonGeoJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("ZoneName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ZoneName"); + + b.ToTable("store_delivery_zones", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RoleType") + .HasColumnType("integer"); + + b.Property("ShiftDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StaffId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ShiftDate", "StaffId") + .IsUnique(); + + b.ToTable("store_employee_shifts", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsClosed") + .HasColumnType("boolean"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Date") + .IsUnique(); + + b.ToTable("store_holidays", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaId") + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("QrCodeUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TableCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Tags") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "TableCode") + .IsUnique(); + + b.ToTable("store_tables", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.ToTable("store_table_areas", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContactName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LegalEntityName") + .HasColumnType("text"); + + b.Property("LogoUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("PrimaryOwnerUserId") + .HasColumnType("uuid"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Remarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ShortName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SuspendedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SuspensionReason") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AmountDue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LineItemsJson") + .HasColumnType("text"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("StatementNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StatementNo") + .IsUnique(); + + b.ToTable("tenant_billing_statements", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Channel") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("MetadataJson") + .HasColumnType("text"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Channel", "SentAt"); + + b.ToTable("tenant_notifications", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("FeaturePoliciesJson") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxAccountCount") + .HasColumnType("integer"); + + b.Property("MaxDeliveryOrders") + .HasColumnType("integer"); + + b.Property("MaxSmsCredits") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("integer"); + + b.Property("MaxStoreCount") + .HasColumnType("integer"); + + b.Property("MonthlyPrice") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("PackageType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("YearlyPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("tenant_packages", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("LastResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LimitValue") + .HasColumnType("numeric"); + + b.Property("QuotaType") + .HasColumnType("integer"); + + b.Property("ResetCycle") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("UsedValue") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "QuotaType") + .IsUnique(); + + b.ToTable("tenant_quota_usages", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AutoRenew") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("NextBillingDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("ScheduledPackageId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TenantPackageId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantPackageId"); + + b.ToTable("tenant_subscriptions", (string)null); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs new file mode 100644 index 0000000..fc4cb04 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs @@ -0,0 +1,2206 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + /// + public partial class ExpandDomainSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_stores_merchants_MerchantId", + table: "stores"); + + migrationBuilder.DropIndex( + name: "IX_stores_MerchantId", + table: "stores"); + + migrationBuilder.RenameColumn( + name: "ReservationEnabled", + table: "stores", + newName: "SupportsReservation"); + + migrationBuilder.RenameColumn( + name: "QueueEnabled", + table: "stores", + newName: "SupportsQueueing"); + + migrationBuilder.RenameColumn( + name: "OnboardedAt", + table: "merchants", + newName: "LastReviewedAt"); + + migrationBuilder.AddColumn( + name: "Address", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "City", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Country", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "CoverImageUrl", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "LegalEntityName", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PrimaryOwnerUserId", + table: "tenants", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Province", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SuspendedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "SuspensionReason", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Tags", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Website", + table: "tenants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Country", + table: "stores", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "CoverImageUrl", + table: "stores", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Description", + table: "stores", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Tags", + table: "stores", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "BusinessLicenseImageUrl", + table: "merchants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Category", + table: "merchants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "JoinedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "Latitude", + table: "merchants", + type: "double precision", + nullable: true); + + migrationBuilder.AddColumn( + name: "LogoUrl", + table: "merchants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Longitude", + table: "merchants", + type: "double precision", + nullable: true); + + migrationBuilder.AddColumn( + name: "ServicePhone", + table: "merchants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SupportEmail", + table: "merchants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "TaxNumber", + table: "merchants", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "affiliate_orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + AffiliatePartnerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + BuyerUserId = table.Column(type: "uuid", nullable: false), + OrderAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + EstimatedCommission = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "integer", nullable: false), + SettledAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_affiliate_orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "affiliate_partners", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + DisplayName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + ChannelType = table.Column(type: "integer", nullable: false), + CommissionRate = table.Column(type: "numeric", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Remarks = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_affiliate_partners", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "affiliate_payouts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + AffiliatePartnerId = table.Column(type: "uuid", nullable: false), + Period = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "integer", nullable: false), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true), + Remarks = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_affiliate_payouts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "cart_item_addons", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CartItemId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OptionId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cart_item_addons", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "cart_items", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + ProductSkuId = table.Column(type: "uuid", nullable: true), + ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Status = table.Column(type: "integer", nullable: false), + AttributesJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cart_items", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "chat_messages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ChatSessionId = table.Column(type: "uuid", nullable: false), + SenderType = table.Column(type: "integer", nullable: false), + SenderUserId = table.Column(type: "uuid", nullable: true), + Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + ContentType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + IsRead = table.Column(type: "boolean", nullable: false), + ReadAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_chat_messages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "chat_sessions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + SessionCode = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + CustomerUserId = table.Column(type: "uuid", nullable: false), + AgentUserId = table.Column(type: "uuid", nullable: true), + StoreId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + IsBotActive = table.Column(type: "boolean", nullable: false), + StartedAt = table.Column(type: "timestamp with time zone", nullable: false), + EndedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_chat_sessions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "checkin_campaigns", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + AllowMakeupCount = table.Column(type: "integer", nullable: false), + RewardsJson = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_checkin_campaigns", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "checkin_records", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CheckInCampaignId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + CheckInDate = table.Column(type: "timestamp with time zone", nullable: false), + IsMakeup = table.Column(type: "boolean", nullable: false), + RewardJson = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_checkin_records", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "checkout_sessions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + SessionToken = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Status = table.Column(type: "integer", nullable: false), + ValidationResultJson = table.Column(type: "text", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_checkout_sessions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "community_comments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PostId = table.Column(type: "uuid", nullable: false), + AuthorUserId = table.Column(type: "uuid", nullable: false), + Content = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + ParentId = table.Column(type: "uuid", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_community_comments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "community_posts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + AuthorUserId = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + Content = table.Column(type: "text", nullable: false), + MediaJson = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + LikeCount = table.Column(type: "integer", nullable: false), + CommentCount = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_community_posts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "community_reactions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PostId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ReactionType = table.Column(type: "integer", nullable: false), + ReactedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_community_reactions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "coupon_templates", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + CouponType = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "numeric", nullable: false), + DiscountCap = table.Column(type: "numeric", nullable: true), + MinimumSpend = table.Column(type: "numeric", nullable: true), + ValidFrom = table.Column(type: "timestamp with time zone", nullable: true), + ValidTo = table.Column(type: "timestamp with time zone", nullable: true), + RelativeValidDays = table.Column(type: "integer", nullable: true), + TotalQuantity = table.Column(type: "integer", nullable: true), + ClaimedQuantity = table.Column(type: "integer", nullable: false), + StoreScopeJson = table.Column(type: "text", nullable: true), + ProductScopeJson = table.Column(type: "text", nullable: true), + ChannelsJson = table.Column(type: "text", nullable: true), + AllowStack = table.Column(type: "boolean", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_coupon_templates", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "coupons", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CouponTemplateId = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), + UsedAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_coupons", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "delivery_events", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + DeliveryOrderId = table.Column(type: "uuid", nullable: false), + EventType = table.Column(type: "integer", nullable: false), + Message = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Payload = table.Column(type: "text", nullable: true), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_delivery_events", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "group_orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + GroupOrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + LeaderUserId = table.Column(type: "uuid", nullable: false), + TargetCount = table.Column(type: "integer", nullable: false), + CurrentCount = table.Column(type: "integer", nullable: false), + GroupPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + StartAt = table.Column(type: "timestamp with time zone", nullable: false), + EndAt = table.Column(type: "timestamp with time zone", nullable: false), + Status = table.Column(type: "integer", nullable: false), + SucceededAt = table.Column(type: "timestamp with time zone", nullable: true), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_group_orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "group_participants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + GroupOrderId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_group_participants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "inventory_adjustments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + InventoryItemId = table.Column(type: "uuid", nullable: false), + AdjustmentType = table.Column(type: "integer", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + OperatorId = table.Column(type: "uuid", nullable: true), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_inventory_adjustments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "inventory_batches", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + ProductSkuId = table.Column(type: "uuid", nullable: false), + BatchNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + ProductionDate = table.Column(type: "timestamp with time zone", nullable: true), + ExpireDate = table.Column(type: "timestamp with time zone", nullable: true), + Quantity = table.Column(type: "integer", nullable: false), + RemainingQuantity = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_inventory_batches", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "inventory_items", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + ProductSkuId = table.Column(type: "uuid", nullable: false), + BatchNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + QuantityOnHand = table.Column(type: "integer", nullable: false), + QuantityReserved = table.Column(type: "integer", nullable: false), + SafetyStock = table.Column(type: "integer", nullable: true), + Location = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ExpireDate = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_inventory_items", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "map_locations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: true), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Longitude = table.Column(type: "double precision", nullable: false), + Latitude = table.Column(type: "double precision", nullable: false), + Landmark = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_map_locations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "member_growth_logs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MemberId = table.Column(type: "uuid", nullable: false), + ChangeValue = table.Column(type: "integer", nullable: false), + CurrentValue = table.Column(type: "integer", nullable: false), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_member_growth_logs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "member_point_ledgers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MemberId = table.Column(type: "uuid", nullable: false), + ChangeAmount = table.Column(type: "integer", nullable: false), + BalanceAfterChange = table.Column(type: "integer", nullable: false), + Reason = table.Column(type: "integer", nullable: false), + SourceId = table.Column(type: "uuid", nullable: true), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_member_point_ledgers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "member_profiles", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Mobile = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Nickname = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + AvatarUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + MemberTierId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + PointsBalance = table.Column(type: "integer", nullable: false), + GrowthValue = table.Column(type: "integer", nullable: false), + BirthDate = table.Column(type: "timestamp with time zone", nullable: true), + JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_member_profiles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "member_tiers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + RequiredGrowth = table.Column(type: "integer", nullable: false), + BenefitsJson = table.Column(type: "text", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_member_tiers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "merchant_contracts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MerchantId = table.Column(type: "uuid", nullable: false), + ContractNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Status = table.Column(type: "integer", nullable: false), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + FileUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + SignedAt = table.Column(type: "timestamp with time zone", nullable: true), + TerminatedAt = table.Column(type: "timestamp with time zone", nullable: true), + TerminationReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_merchant_contracts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "merchant_documents", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MerchantId = table.Column(type: "uuid", nullable: false), + DocumentType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + FileUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + DocumentNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + Remarks = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_merchant_documents", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "merchant_staff", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MerchantId = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: true), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Email = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + IdentityUserId = table.Column(type: "uuid", nullable: true), + RoleType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + PermissionsJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_merchant_staff", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "metric_alert_rules", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MetricDefinitionId = table.Column(type: "uuid", nullable: false), + ConditionJson = table.Column(type: "text", nullable: false), + Severity = table.Column(type: "integer", nullable: false), + NotificationChannels = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Enabled = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_metric_alert_rules", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "metric_definitions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + DimensionsJson = table.Column(type: "text", nullable: true), + DefaultAggregation = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_metric_definitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "metric_snapshots", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + MetricDefinitionId = table.Column(type: "uuid", nullable: false), + DimensionKey = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + WindowStart = table.Column(type: "timestamp with time zone", nullable: false), + WindowEnd = table.Column(type: "timestamp with time zone", nullable: false), + Value = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_metric_snapshots", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "navigation_requests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + Channel = table.Column(type: "integer", nullable: false), + TargetApp = table.Column(type: "integer", nullable: false), + RequestedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_navigation_requests", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "order_status_histories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + OperatorId = table.Column(type: "uuid", nullable: true), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_order_status_histories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "payment_refund_records", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PaymentRecordId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + ChannelRefundId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Status = table.Column(type: "integer", nullable: false), + RequestedAt = table.Column(type: "timestamp with time zone", nullable: false), + CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), + Payload = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_payment_refund_records", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_addon_groups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + SelectionType = table.Column(type: "integer", nullable: false), + MinSelect = table.Column(type: "integer", nullable: true), + MaxSelect = table.Column(type: "integer", nullable: true), + IsRequired = table.Column(type: "boolean", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_addon_groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_addon_options", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + AddonGroupId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + IsDefault = table.Column(type: "boolean", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_addon_options", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_attribute_groups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: true), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + SelectionType = table.Column(type: "integer", nullable: false), + IsRequired = table.Column(type: "boolean", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_attribute_groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_attribute_options", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + AttributeGroupId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + IsDefault = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_attribute_options", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_media_assets", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + MediaType = table.Column(type: "integer", nullable: false), + Url = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Caption = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_media_assets", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_pricing_rules", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + RuleType = table.Column(type: "integer", nullable: false), + ConditionsJson = table.Column(type: "text", nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + WeekdaysJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_pricing_rules", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "product_skus", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + SkuCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Barcode = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + StockQuantity = table.Column(type: "integer", nullable: true), + Weight = table.Column(type: "numeric(10,3)", precision: 10, scale: 3, nullable: true), + AttributesJson = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_product_skus", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "promotion_campaigns", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + PromotionType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + StartAt = table.Column(type: "timestamp with time zone", nullable: false), + EndAt = table.Column(type: "timestamp with time zone", nullable: false), + Budget = table.Column(type: "numeric", nullable: true), + RulesJson = table.Column(type: "text", nullable: false), + AudienceDescription = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + BannerUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_promotion_campaigns", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "refund_requests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + RefundNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false), + RequestedAt = table.Column(type: "timestamp with time zone", nullable: false), + ProcessedAt = table.Column(type: "timestamp with time zone", nullable: true), + ReviewNotes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_refund_requests", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "shopping_carts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + TableContext = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + DeliveryPreference = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + LastModifiedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_shopping_carts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "store_business_hours", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + DayOfWeek = table.Column(type: "integer", nullable: false), + HourType = table.Column(type: "integer", nullable: false), + StartTime = table.Column(type: "interval", nullable: false), + EndTime = table.Column(type: "interval", nullable: false), + CapacityLimit = table.Column(type: "integer", nullable: true), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_store_business_hours", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "store_delivery_zones", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + ZoneName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + PolygonGeoJson = table.Column(type: "text", nullable: false), + MinimumOrderAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + DeliveryFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + EstimatedMinutes = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_store_delivery_zones", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "store_employee_shifts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + StaffId = table.Column(type: "uuid", nullable: false), + ShiftDate = table.Column(type: "timestamp with time zone", nullable: false), + StartTime = table.Column(type: "interval", nullable: false), + EndTime = table.Column(type: "interval", nullable: false), + RoleType = table.Column(type: "integer", nullable: false), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_store_employee_shifts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "store_holidays", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + Date = table.Column(type: "timestamp with time zone", nullable: false), + IsClosed = table.Column(type: "boolean", nullable: false), + Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_store_holidays", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "store_table_areas", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_store_table_areas", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "store_tables", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StoreId = table.Column(type: "uuid", nullable: false), + AreaId = table.Column(type: "uuid", nullable: true), + TableCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Capacity = table.Column(type: "integer", nullable: false), + Tags = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + Status = table.Column(type: "integer", nullable: false), + QrCodeUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_store_tables", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "support_tickets", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TicketNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + CustomerUserId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + Subject = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "text", nullable: false), + Priority = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + AssignedAgentId = table.Column(type: "uuid", nullable: true), + ClosedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_support_tickets", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenant_billing_statements", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StatementNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + PeriodStart = table.Column(type: "timestamp with time zone", nullable: false), + PeriodEnd = table.Column(type: "timestamp with time zone", nullable: false), + AmountDue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AmountPaid = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "integer", nullable: false), + DueDate = table.Column(type: "timestamp with time zone", nullable: false), + LineItemsJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_billing_statements", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenant_notifications", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Message = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + Channel = table.Column(type: "integer", nullable: false), + Severity = table.Column(type: "integer", nullable: false), + SentAt = table.Column(type: "timestamp with time zone", nullable: false), + ReadAt = table.Column(type: "timestamp with time zone", nullable: true), + MetadataJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_notifications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenant_packages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + PackageType = table.Column(type: "integer", nullable: false), + MonthlyPrice = table.Column(type: "numeric", nullable: true), + YearlyPrice = table.Column(type: "numeric", nullable: true), + MaxStoreCount = table.Column(type: "integer", nullable: true), + MaxAccountCount = table.Column(type: "integer", nullable: true), + MaxStorageGb = table.Column(type: "integer", nullable: true), + MaxSmsCredits = table.Column(type: "integer", nullable: true), + MaxDeliveryOrders = table.Column(type: "integer", nullable: true), + FeaturePoliciesJson = table.Column(type: "text", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_packages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenant_quota_usages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + QuotaType = table.Column(type: "integer", nullable: false), + LimitValue = table.Column(type: "numeric", nullable: false), + UsedValue = table.Column(type: "numeric", nullable: false), + ResetCycle = table.Column(type: "text", nullable: true), + LastResetAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_quota_usages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenant_subscriptions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantPackageId = table.Column(type: "uuid", nullable: false), + EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: false), + EffectiveTo = table.Column(type: "timestamp with time zone", nullable: false), + NextBillingDate = table.Column(type: "timestamp with time zone", nullable: true), + Status = table.Column(type: "integer", nullable: false), + AutoRenew = table.Column(type: "boolean", nullable: false), + ScheduledPackageId = table.Column(type: "uuid", nullable: true), + Notes = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_subscriptions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ticket_comments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + SupportTicketId = table.Column(type: "uuid", nullable: false), + AuthorUserId = table.Column(type: "uuid", nullable: true), + Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + IsInternal = table.Column(type: "boolean", nullable: false), + AttachmentsJson = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: true), + UpdatedBy = table.Column(type: "uuid", nullable: true), + DeletedBy = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ticket_comments", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_affiliate_orders_TenantId_AffiliatePartnerId_OrderId", + table: "affiliate_orders", + columns: new[] { "TenantId", "AffiliatePartnerId", "OrderId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_affiliate_partners_TenantId_DisplayName", + table: "affiliate_partners", + columns: new[] { "TenantId", "DisplayName" }); + + migrationBuilder.CreateIndex( + name: "IX_affiliate_payouts_TenantId_AffiliatePartnerId_Period", + table: "affiliate_payouts", + columns: new[] { "TenantId", "AffiliatePartnerId", "Period" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_cart_items_TenantId_ShoppingCartId", + table: "cart_items", + columns: new[] { "TenantId", "ShoppingCartId" }); + + migrationBuilder.CreateIndex( + name: "IX_chat_messages_TenantId_ChatSessionId_CreatedAt", + table: "chat_messages", + columns: new[] { "TenantId", "ChatSessionId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_chat_sessions_TenantId_SessionCode", + table: "chat_sessions", + columns: new[] { "TenantId", "SessionCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_checkin_campaigns_TenantId_Name", + table: "checkin_campaigns", + columns: new[] { "TenantId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_checkin_records_TenantId_CheckInCampaignId_UserId_CheckInDa~", + table: "checkin_records", + columns: new[] { "TenantId", "CheckInCampaignId", "UserId", "CheckInDate" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_checkout_sessions_TenantId_SessionToken", + table: "checkout_sessions", + columns: new[] { "TenantId", "SessionToken" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_community_comments_TenantId_PostId_CreatedAt", + table: "community_comments", + columns: new[] { "TenantId", "PostId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_community_posts_TenantId_AuthorUserId_CreatedAt", + table: "community_posts", + columns: new[] { "TenantId", "AuthorUserId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_community_reactions_TenantId_PostId_UserId", + table: "community_reactions", + columns: new[] { "TenantId", "PostId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_coupons_TenantId_Code", + table: "coupons", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_delivery_events_TenantId_DeliveryOrderId_EventType", + table: "delivery_events", + columns: new[] { "TenantId", "DeliveryOrderId", "EventType" }); + + migrationBuilder.CreateIndex( + name: "IX_group_orders_TenantId_GroupOrderNo", + table: "group_orders", + columns: new[] { "TenantId", "GroupOrderNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_group_participants_TenantId_GroupOrderId_UserId", + table: "group_participants", + columns: new[] { "TenantId", "GroupOrderId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_inventory_adjustments_TenantId_InventoryItemId_OccurredAt", + table: "inventory_adjustments", + columns: new[] { "TenantId", "InventoryItemId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_inventory_batches_TenantId_StoreId_ProductSkuId_BatchNumber", + table: "inventory_batches", + columns: new[] { "TenantId", "StoreId", "ProductSkuId", "BatchNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_inventory_items_TenantId_StoreId_ProductSkuId_BatchNumber", + table: "inventory_items", + columns: new[] { "TenantId", "StoreId", "ProductSkuId", "BatchNumber" }); + + migrationBuilder.CreateIndex( + name: "IX_map_locations_TenantId_StoreId", + table: "map_locations", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_member_growth_logs_TenantId_MemberId_OccurredAt", + table: "member_growth_logs", + columns: new[] { "TenantId", "MemberId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_member_point_ledgers_TenantId_MemberId_OccurredAt", + table: "member_point_ledgers", + columns: new[] { "TenantId", "MemberId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_member_profiles_TenantId_Mobile", + table: "member_profiles", + columns: new[] { "TenantId", "Mobile" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_member_tiers_TenantId_Name", + table: "member_tiers", + columns: new[] { "TenantId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_merchant_contracts_TenantId_MerchantId_ContractNumber", + table: "merchant_contracts", + columns: new[] { "TenantId", "MerchantId", "ContractNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_merchant_documents_TenantId_MerchantId_DocumentType", + table: "merchant_documents", + columns: new[] { "TenantId", "MerchantId", "DocumentType" }); + + migrationBuilder.CreateIndex( + name: "IX_merchant_staff_TenantId_MerchantId_Phone", + table: "merchant_staff", + columns: new[] { "TenantId", "MerchantId", "Phone" }); + + migrationBuilder.CreateIndex( + name: "IX_metric_alert_rules_TenantId_MetricDefinitionId_Severity", + table: "metric_alert_rules", + columns: new[] { "TenantId", "MetricDefinitionId", "Severity" }); + + migrationBuilder.CreateIndex( + name: "IX_metric_definitions_TenantId_Code", + table: "metric_definitions", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_metric_snapshots_TenantId_MetricDefinitionId_DimensionKey_W~", + table: "metric_snapshots", + columns: new[] { "TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_navigation_requests_TenantId_UserId_StoreId_RequestedAt", + table: "navigation_requests", + columns: new[] { "TenantId", "UserId", "StoreId", "RequestedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_order_status_histories_TenantId_OrderId_OccurredAt", + table: "order_status_histories", + columns: new[] { "TenantId", "OrderId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_payment_refund_records_TenantId_PaymentRecordId", + table: "payment_refund_records", + columns: new[] { "TenantId", "PaymentRecordId" }); + + migrationBuilder.CreateIndex( + name: "IX_product_addon_groups_TenantId_ProductId_Name", + table: "product_addon_groups", + columns: new[] { "TenantId", "ProductId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_product_attribute_groups_TenantId_StoreId_Name", + table: "product_attribute_groups", + columns: new[] { "TenantId", "StoreId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_product_attribute_options_TenantId_AttributeGroupId_Name", + table: "product_attribute_options", + columns: new[] { "TenantId", "AttributeGroupId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_product_pricing_rules_TenantId_ProductId_RuleType", + table: "product_pricing_rules", + columns: new[] { "TenantId", "ProductId", "RuleType" }); + + migrationBuilder.CreateIndex( + name: "IX_product_skus_TenantId_SkuCode", + table: "product_skus", + columns: new[] { "TenantId", "SkuCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_refund_requests_TenantId_RefundNo", + table: "refund_requests", + columns: new[] { "TenantId", "RefundNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_shopping_carts_TenantId_UserId_StoreId", + table: "shopping_carts", + columns: new[] { "TenantId", "UserId", "StoreId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_business_hours_TenantId_StoreId_DayOfWeek", + table: "store_business_hours", + columns: new[] { "TenantId", "StoreId", "DayOfWeek" }); + + migrationBuilder.CreateIndex( + name: "IX_store_delivery_zones_TenantId_StoreId_ZoneName", + table: "store_delivery_zones", + columns: new[] { "TenantId", "StoreId", "ZoneName" }); + + migrationBuilder.CreateIndex( + name: "IX_store_employee_shifts_TenantId_StoreId_ShiftDate_StaffId", + table: "store_employee_shifts", + columns: new[] { "TenantId", "StoreId", "ShiftDate", "StaffId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_holidays_TenantId_StoreId_Date", + table: "store_holidays", + columns: new[] { "TenantId", "StoreId", "Date" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_table_areas_TenantId_StoreId_Name", + table: "store_table_areas", + columns: new[] { "TenantId", "StoreId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_tables_TenantId_StoreId_TableCode", + table: "store_tables", + columns: new[] { "TenantId", "StoreId", "TableCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_support_tickets_TenantId_TicketNo", + table: "support_tickets", + columns: new[] { "TenantId", "TicketNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_tenant_billing_statements_TenantId_StatementNo", + table: "tenant_billing_statements", + columns: new[] { "TenantId", "StatementNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_tenant_notifications_TenantId_Channel_SentAt", + table: "tenant_notifications", + columns: new[] { "TenantId", "Channel", "SentAt" }); + + migrationBuilder.CreateIndex( + name: "IX_tenant_quota_usages_TenantId_QuotaType", + table: "tenant_quota_usages", + columns: new[] { "TenantId", "QuotaType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_tenant_subscriptions_TenantId_TenantPackageId", + table: "tenant_subscriptions", + columns: new[] { "TenantId", "TenantPackageId" }); + + migrationBuilder.CreateIndex( + name: "IX_ticket_comments_TenantId_SupportTicketId", + table: "ticket_comments", + columns: new[] { "TenantId", "SupportTicketId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "affiliate_orders"); + + migrationBuilder.DropTable( + name: "affiliate_partners"); + + migrationBuilder.DropTable( + name: "affiliate_payouts"); + + migrationBuilder.DropTable( + name: "cart_item_addons"); + + migrationBuilder.DropTable( + name: "cart_items"); + + migrationBuilder.DropTable( + name: "chat_messages"); + + migrationBuilder.DropTable( + name: "chat_sessions"); + + migrationBuilder.DropTable( + name: "checkin_campaigns"); + + migrationBuilder.DropTable( + name: "checkin_records"); + + migrationBuilder.DropTable( + name: "checkout_sessions"); + + migrationBuilder.DropTable( + name: "community_comments"); + + migrationBuilder.DropTable( + name: "community_posts"); + + migrationBuilder.DropTable( + name: "community_reactions"); + + migrationBuilder.DropTable( + name: "coupon_templates"); + + migrationBuilder.DropTable( + name: "coupons"); + + migrationBuilder.DropTable( + name: "delivery_events"); + + migrationBuilder.DropTable( + name: "group_orders"); + + migrationBuilder.DropTable( + name: "group_participants"); + + migrationBuilder.DropTable( + name: "inventory_adjustments"); + + migrationBuilder.DropTable( + name: "inventory_batches"); + + migrationBuilder.DropTable( + name: "inventory_items"); + + migrationBuilder.DropTable( + name: "map_locations"); + + migrationBuilder.DropTable( + name: "member_growth_logs"); + + migrationBuilder.DropTable( + name: "member_point_ledgers"); + + migrationBuilder.DropTable( + name: "member_profiles"); + + migrationBuilder.DropTable( + name: "member_tiers"); + + migrationBuilder.DropTable( + name: "merchant_contracts"); + + migrationBuilder.DropTable( + name: "merchant_documents"); + + migrationBuilder.DropTable( + name: "merchant_staff"); + + migrationBuilder.DropTable( + name: "metric_alert_rules"); + + migrationBuilder.DropTable( + name: "metric_definitions"); + + migrationBuilder.DropTable( + name: "metric_snapshots"); + + migrationBuilder.DropTable( + name: "navigation_requests"); + + migrationBuilder.DropTable( + name: "order_status_histories"); + + migrationBuilder.DropTable( + name: "payment_refund_records"); + + migrationBuilder.DropTable( + name: "product_addon_groups"); + + migrationBuilder.DropTable( + name: "product_addon_options"); + + migrationBuilder.DropTable( + name: "product_attribute_groups"); + + migrationBuilder.DropTable( + name: "product_attribute_options"); + + migrationBuilder.DropTable( + name: "product_media_assets"); + + migrationBuilder.DropTable( + name: "product_pricing_rules"); + + migrationBuilder.DropTable( + name: "product_skus"); + + migrationBuilder.DropTable( + name: "promotion_campaigns"); + + migrationBuilder.DropTable( + name: "refund_requests"); + + migrationBuilder.DropTable( + name: "shopping_carts"); + + migrationBuilder.DropTable( + name: "store_business_hours"); + + migrationBuilder.DropTable( + name: "store_delivery_zones"); + + migrationBuilder.DropTable( + name: "store_employee_shifts"); + + migrationBuilder.DropTable( + name: "store_holidays"); + + migrationBuilder.DropTable( + name: "store_table_areas"); + + migrationBuilder.DropTable( + name: "store_tables"); + + migrationBuilder.DropTable( + name: "support_tickets"); + + migrationBuilder.DropTable( + name: "tenant_billing_statements"); + + migrationBuilder.DropTable( + name: "tenant_notifications"); + + migrationBuilder.DropTable( + name: "tenant_packages"); + + migrationBuilder.DropTable( + name: "tenant_quota_usages"); + + migrationBuilder.DropTable( + name: "tenant_subscriptions"); + + migrationBuilder.DropTable( + name: "ticket_comments"); + + migrationBuilder.DropColumn( + name: "Address", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "City", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "Country", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "CoverImageUrl", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "LegalEntityName", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "PrimaryOwnerUserId", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "Province", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "SuspendedAt", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "SuspensionReason", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "Tags", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "Website", + table: "tenants"); + + migrationBuilder.DropColumn( + name: "Country", + table: "stores"); + + migrationBuilder.DropColumn( + name: "CoverImageUrl", + table: "stores"); + + migrationBuilder.DropColumn( + name: "Description", + table: "stores"); + + migrationBuilder.DropColumn( + name: "Tags", + table: "stores"); + + migrationBuilder.DropColumn( + name: "BusinessLicenseImageUrl", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "Category", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "JoinedAt", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "Latitude", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "LogoUrl", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "Longitude", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "ServicePhone", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "SupportEmail", + table: "merchants"); + + migrationBuilder.DropColumn( + name: "TaxNumber", + table: "merchants"); + + migrationBuilder.RenameColumn( + name: "SupportsReservation", + table: "stores", + newName: "ReservationEnabled"); + + migrationBuilder.RenameColumn( + name: "SupportsQueueing", + table: "stores", + newName: "QueueEnabled"); + + migrationBuilder.RenameColumn( + name: "LastReviewedAt", + table: "merchants", + newName: "OnboardedAt"); + + migrationBuilder.CreateIndex( + name: "IX_stores_MerchantId", + table: "stores", + column: "MerchantId"); + + migrationBuilder.AddForeignKey( + name: "FK_stores_merchants_MerchantId", + table: "stores", + column: "MerchantId", + principalTable: "merchants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs new file mode 100644 index 0000000..2baf184 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs @@ -0,0 +1,5641 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.App.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + [DbContext(typeof(TakeoutAppDbContext))] + [Migration("20251201094254_AddEntityComments")] + partial class AddEntityComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ConditionJson") + .IsRequired() + .HasColumnType("text") + .HasComment("触发条件 JSON。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("MetricDefinitionId") + .HasColumnType("uuid") + .HasComment("关联指标。"); + + b.Property("NotificationChannels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("通知渠道。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("告警级别。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "Severity"); + + b.ToTable("metric_alert_rules", null, t => + { + t.HasComment("指标告警规则。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("指标编码。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultAggregation") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("默认聚合方式。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("说明。"); + + b.Property("DimensionsJson") + .HasColumnType("text") + .HasComment("维度描述 JSON。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("指标名称。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("metric_definitions", null, t => + { + t.HasComment("指标定义,描述可观测的数据点。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DimensionKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("维度键(JSON)。"); + + b.Property("MetricDefinitionId") + .HasColumnType("uuid") + .HasComment("指标定义 ID。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)") + .HasComment("数值。"); + + b.Property("WindowEnd") + .HasColumnType("timestamp with time zone") + .HasComment("统计时间窗口结束。"); + + b.Property("WindowStart") + .HasColumnType("timestamp with time zone") + .HasComment("统计时间窗口开始。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd") + .IsUnique(); + + b.ToTable("metric_snapshots", null, t => + { + t.HasComment("指标快照,用于大盘展示。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("券码或序列号。"); + + b.Property("CouponTemplateId") + .HasColumnType("uuid") + .HasComment("模板标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("发放时间。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("订单 ID(已使用时记录)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("归属用户。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("coupons", null, t => + { + t.HasComment("用户领取的券。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AllowStack") + .HasColumnType("boolean") + .HasComment("是否允许叠加其他优惠。"); + + b.Property("ChannelsJson") + .HasColumnType("text") + .HasComment("发放渠道(JSON)。"); + + b.Property("ClaimedQuantity") + .HasColumnType("integer") + .HasComment("已领取数量。"); + + b.Property("CouponType") + .HasColumnType("integer") + .HasComment("券类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("DiscountCap") + .HasColumnType("numeric") + .HasComment("折扣上限(针对折扣券)。"); + + b.Property("MinimumSpend") + .HasColumnType("numeric") + .HasComment("最低消费门槛。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("模板名称。"); + + b.Property("ProductScopeJson") + .HasColumnType("text") + .HasComment("适用品类或商品范围(JSON)。"); + + b.Property("RelativeValidDays") + .HasColumnType("integer") + .HasComment("有效天数(相对发放时间)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreScopeJson") + .HasColumnType("text") + .HasComment("适用门店 ID 集合(JSON)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TotalQuantity") + .HasColumnType("integer") + .HasComment("总发放数量上限。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasComment("可用开始时间。"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasComment("可用结束时间。"); + + b.Property("Value") + .HasColumnType("numeric") + .HasComment("面值或折扣额度。"); + + b.HasKey("Id"); + + b.ToTable("coupon_templates", null, t => + { + t.HasComment("优惠券模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AudienceDescription") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("目标人群描述。"); + + b.Property("BannerUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("营销素材(如 banner)。"); + + b.Property("Budget") + .HasColumnType("numeric") + .HasComment("预算金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("PromotionType") + .HasColumnType("integer") + .HasComment("活动类型。"); + + b.Property("RulesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("活动规则 JSON。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("活动状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("promotion_campaigns", null, t => + { + t.HasComment("营销活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ChatSessionId") + .HasColumnType("uuid") + .HasComment("会话标识。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("消息内容。"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("消息类型(文字/图片/语音等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRead") + .HasColumnType("boolean") + .HasComment("是否已读。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("读取时间。"); + + b.Property("SenderType") + .HasColumnType("integer") + .HasComment("发送方类型。"); + + b.Property("SenderUserId") + .HasColumnType("uuid") + .HasComment("发送方用户 ID。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ChatSessionId", "CreatedAt"); + + b.ToTable("chat_messages", null, t => + { + t.HasComment("会话消息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AgentUserId") + .HasColumnType("uuid") + .HasComment("当前客服员工 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("uuid") + .HasComment("顾客用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("IsBotActive") + .HasColumnType("boolean") + .HasComment("是否机器人接待中。"); + + b.Property("SessionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话编号。"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("所属门店(可空为平台)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionCode") + .IsUnique(); + + b.ToTable("chat_sessions", null, t => + { + t.HasComment("客服会话。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AssignedAgentId") + .HasColumnType("uuid") + .HasComment("指派的客服。"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("关闭时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("uuid") + .HasComment("客户用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("工单详情。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单(如有)。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("优先级。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("工单主题。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TicketNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("工单编号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TicketNo") + .IsUnique(); + + b.ToTable("support_tickets", null, t => + { + t.HasComment("客服工单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttachmentsJson") + .HasColumnType("text") + .HasComment("附件 JSON。"); + + b.Property("AuthorUserId") + .HasColumnType("uuid") + .HasComment("评论人 ID。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("评论内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsInternal") + .HasColumnType("boolean") + .HasComment("是否内部备注。"); + + b.Property("SupportTicketId") + .HasColumnType("uuid") + .HasComment("工单标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SupportTicketId"); + + b.ToTable("ticket_comments", null, t => + { + t.HasComment("工单评论/流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryOrderId") + .HasColumnType("uuid") + .HasComment("配送单标识。"); + + b.Property("EventType") + .HasColumnType("integer") + .HasComment("事件类型。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("事件描述。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始数据 JSON。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DeliveryOrderId", "EventType"); + + b.ToTable("delivery_events", null, t => + { + t.HasComment("配送状态事件流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CourierName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("骑手姓名。"); + + b.Property("CourierPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("骑手电话。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("DispatchedAt") + .HasColumnType("timestamp with time zone") + .HasComment("下发时间。"); + + b.Property("FailureReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("异常原因。"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedUpAt") + .HasColumnType("timestamp with time zone") + .HasComment("取餐时间。"); + + b.Property("Provider") + .HasColumnType("integer") + .HasComment("配送服务商。"); + + b.Property("ProviderOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("第三方配送单号。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", null, t => + { + t.HasComment("配送单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AffiliatePartnerId") + .HasColumnType("uuid") + .HasComment("推广人标识。"); + + b.Property("BuyerUserId") + .HasColumnType("uuid") + .HasComment("用户 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EstimatedCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("预计佣金。"); + + b.Property("OrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("订单金额。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单。"); + + b.Property("SettledAt") + .HasColumnType("timestamp with time zone") + .HasComment("结算完成时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "OrderId") + .IsUnique(); + + b.ToTable("affiliate_orders", null, t => + { + t.HasComment("分销订单记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ChannelType") + .HasColumnType("integer") + .HasComment("渠道类型。"); + + b.Property("CommissionRate") + .HasColumnType("numeric") + .HasComment("分成比例(0-1)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称或渠道名称。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户 ID(如绑定平台账号)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DisplayName"); + + b.ToTable("affiliate_partners", null, t => + { + t.HasComment("分销/推广合作伙伴。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AffiliatePartnerId") + .HasColumnType("uuid") + .HasComment("合作伙伴标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("结算金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("打款时间。"); + + b.Property("Period") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("结算周期描述。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "Period") + .IsUnique(); + + b.ToTable("affiliate_payouts", null, t => + { + t.HasComment("佣金结算记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AllowMakeupCount") + .HasColumnType("integer") + .HasComment("支持补签次数。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("活动描述。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("结束日期。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("RewardsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("连签奖励 JSON。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("开始日期。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("checkin_campaigns", null, t => + { + t.HasComment("签到活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CheckInCampaignId") + .HasColumnType("uuid") + .HasComment("活动标识。"); + + b.Property("CheckInDate") + .HasColumnType("timestamp with time zone") + .HasComment("签到日期(本地)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsMakeup") + .HasColumnType("boolean") + .HasComment("是否补签。"); + + b.Property("RewardJson") + .IsRequired() + .HasColumnType("text") + .HasComment("获得奖励 JSON。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CheckInCampaignId", "UserId", "CheckInDate") + .IsUnique(); + + b.ToTable("checkin_records", null, t => + { + t.HasComment("用户签到记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AuthorUserId") + .HasColumnType("uuid") + .HasComment("评论人。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("评论内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasComment("状态。"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasComment("父级评论 ID。"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasComment("动态标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "CreatedAt"); + + b.ToTable("community_comments", null, t => + { + t.HasComment("社区评论。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AuthorUserId") + .HasColumnType("uuid") + .HasComment("作者用户 ID。"); + + b.Property("CommentCount") + .HasColumnType("integer") + .HasComment("评论数。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LikeCount") + .HasColumnType("integer") + .HasComment("点赞数。"); + + b.Property("MediaJson") + .HasColumnType("text") + .HasComment("媒体资源 JSON。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AuthorUserId", "CreatedAt"); + + b.ToTable("community_posts", null, t => + { + t.HasComment("社区动态。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasComment("动态 ID。"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone") + .HasComment("时间戳。"); + + b.Property("ReactionType") + .HasColumnType("integer") + .HasComment("反应类型。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "UserId") + .IsUnique(); + + b.ToTable("community_reactions", null, t => + { + t.HasComment("社区互动反馈。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentCount") + .HasColumnType("integer") + .HasComment("当前已参与人数。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("GroupOrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("拼单编号。"); + + b.Property("GroupPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("拼团价格。"); + + b.Property("LeaderUserId") + .HasColumnType("uuid") + .HasComment("团长用户 ID。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("关联商品或套餐。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("拼团状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasComment("成团时间。"); + + b.Property("TargetCount") + .HasColumnType("integer") + .HasComment("成团需要的人数。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderNo") + .IsUnique(); + + b.ToTable("group_orders", null, t => + { + t.HasComment("拼单活动。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GroupOrderId") + .HasColumnType("uuid") + .HasComment("拼单活动标识。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("参与时间。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("对应订单标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("参与状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderId", "UserId") + .IsUnique(); + + b.ToTable("group_participants", null, t => + { + t.HasComment("拼单参与者。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AdjustmentType") + .HasColumnType("integer") + .HasComment("调整类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("InventoryItemId") + .HasColumnType("uuid") + .HasComment("对应的库存记录标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasComment("操作人标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("调整数量,正数增加,负数减少。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("原因说明。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "InventoryItemId", "OccurredAt"); + + b.ToTable("inventory_adjustments", null, t => + { + t.HasComment("库存调整记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BatchNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("批次编号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("ProductSkuId") + .HasColumnType("uuid") + .HasComment("SKU 标识。"); + + b.Property("ProductionDate") + .HasColumnType("timestamp with time zone") + .HasComment("生产日期。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("入库数量。"); + + b.Property("RemainingQuantity") + .HasColumnType("integer") + .HasComment("剩余数量。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber") + .IsUnique(); + + b.ToTable("inventory_batches", null, t => + { + t.HasComment("SKU 批次信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BatchNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("批次编号,可为空表示混批。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("Location") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("储位或仓位信息。"); + + b.Property("ProductSkuId") + .HasColumnType("uuid") + .HasComment("SKU 标识。"); + + b.Property("QuantityOnHand") + .HasColumnType("integer") + .HasComment("可用库存。"); + + b.Property("QuantityReserved") + .HasColumnType("integer") + .HasComment("已锁定库存(订单占用)。"); + + b.Property("SafetyStock") + .HasColumnType("integer") + .HasComment("安全库存阈值。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber"); + + b.ToTable("inventory_items", null, t => + { + t.HasComment("SKU 在门店的库存信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ChangeValue") + .HasColumnType("integer") + .HasComment("变动数量。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentValue") + .HasColumnType("integer") + .HasComment("当前成长值。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MemberId") + .HasColumnType("uuid") + .HasComment("会员标识。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_growth_logs", null, t => + { + t.HasComment("成长值变动日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BalanceAfterChange") + .HasColumnType("integer") + .HasComment("变动后余额。"); + + b.Property("ChangeAmount") + .HasColumnType("integer") + .HasComment("变动数量,可为负值。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(如适用)。"); + + b.Property("MemberId") + .HasColumnType("uuid") + .HasComment("会员标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Reason") + .HasColumnType("integer") + .HasComment("变动原因。"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasComment("来源 ID(订单、活动等)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_point_ledgers", null, t => + { + t.HasComment("积分变动流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AvatarUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像。"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone") + .HasComment("生日。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GrowthValue") + .HasColumnType("integer") + .HasComment("成长值/经验值。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("注册时间。"); + + b.Property("MemberTierId") + .HasColumnType("uuid") + .HasComment("当前会员等级 ID。"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("Nickname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("PointsBalance") + .HasColumnType("integer") + .HasComment("会员积分余额。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会员状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Mobile") + .IsUnique(); + + b.ToTable("member_profiles", null, t => + { + t.HasComment("会员档案。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BenefitsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("等级权益(JSON)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("等级名称。"); + + b.Property("RequiredGrowth") + .HasColumnType("integer") + .HasComment("所需成长值。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.ToTable("member_tiers", null, t => + { + t.HasComment("会员等级定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); + + b.Property("BrandAlias") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("品牌简称或别名。"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("品牌名称(对外展示)。"); + + b.Property("BusinessLicenseImageUrl") + .HasColumnType("text") + .HasComment("营业执照扫描件地址。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("营业执照号。"); + + b.Property("Category") + .HasColumnType("text") + .HasComment("品牌所属品类,如火锅、咖啡等。"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("联系邮箱。"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在区县。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("入驻时间。"); + + b.Property("LastReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次审核时间。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度信息。"); + + b.Property("LegalPerson") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("法人或负责人姓名。"); + + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("品牌 Logo。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度信息。"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("审核备注或驳回原因。"); + + b.Property("ServicePhone") + .HasColumnType("text") + .HasComment("客服电话。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("入驻状态。"); + + b.Property("SupportEmail") + .HasColumnType("text") + .HasComment("客服邮箱。"); + + b.Property("TaxNumber") + .HasColumnType("text") + .HasComment("税号/统一社会信用代码。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("merchants", null, t => + { + t.HasComment("商户主体信息,承载入驻和资质审核结果。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ContractNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("合同编号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同结束时间。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("合同文件存储地址。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签署时间。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("合同状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TerminatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("终止时间。"); + + b.Property("TerminationReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("终止原因。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "ContractNumber") + .IsUnique(); + + b.ToTable("merchant_contracts", null, t => + { + t.HasComment("商户合同记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DocumentNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("证照编号。"); + + b.Property("DocumentType") + .HasColumnType("integer") + .HasComment("证照类型。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期日期。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("证照文件链接。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签发日期。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("Remarks") + .HasColumnType("text") + .HasComment("审核备注或驳回原因。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("审核状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "DocumentType"); + + b.ToTable("merchant_documents", null, t => + { + t.HasComment("商户提交的资质或证照材料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("邮箱地址。"); + + b.Property("IdentityUserId") + .HasColumnType("uuid") + .HasComment("登录账号 ID(指向统一身份体系)。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("员工姓名。"); + + b.Property("PermissionsJson") + .HasColumnType("text") + .HasComment("自定义权限(JSON)。"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("员工角色类型。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("员工状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("可选的关联门店 ID。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "Phone"); + + b.ToTable("merchant_staff", null, t => + { + t.HasComment("商户员工账号,支持门店维度分配。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Landmark") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("打车/导航落点描述。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("名称。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("关联门店 ID,可空表示独立 POI。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("map_locations", null, t => + { + t.HasComment("地图 POI 信息,用于门店定位和推荐。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("来源通道(小程序、H5 等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("请求时间。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店 ID。"); + + b.Property("TargetApp") + .HasColumnType("integer") + .HasComment("跳转的地图应用。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId", "RequestedAt"); + + b.ToTable("navigation_requests", null, t => + { + t.HasComment("用户发起的导航请求日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("扩展 JSON(规格、加料选项等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("商品或 SKU 标识。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称快照。"); + + b.Property("ProductSkuId") + .HasColumnType("uuid") + .HasComment("SKU 标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("自定义备注(口味要求)。"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid") + .HasComment("所属购物车标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价快照。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ShoppingCartId"); + + b.ToTable("cart_items", null, t => + { + t.HasComment("购物车条目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CartItemId") + .HasColumnType("uuid") + .HasComment("所属购物车条目。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("OptionId") + .HasColumnType("uuid") + .HasComment("选项 ID(可对应 ProductAddonOption)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("cart_item_addons", null, t => + { + t.HasComment("购物车条目的加料/附加项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话 Token。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.Property("ValidationResultJson") + .IsRequired() + .HasColumnType("text") + .HasComment("校验结果明细 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionToken") + .IsUnique(); + + b.ToTable("checkout_sessions", null, t => + { + t.HasComment("结账会话,记录校验上下文。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryPreference") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("履约方式(堂食/自提/配送)缓存。"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次修改时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("购物车状态,包含正常/锁定。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TableContext") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("桌码或场景标识(扫码点餐)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId") + .IsUnique(); + + b.ToTable("shopping_carts", null, t => + { + t.HasComment("用户购物车,按租户/门店隔离。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CancelReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("取消原因。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("下单渠道。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("顾客姓名。"); + + b.Property("CustomerPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("顾客手机号。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryType") + .HasColumnType("integer") + .HasComment("履约类型。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("优惠金额。"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("ItemsAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("商品总额。"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("订单号。"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("PayableAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额。"); + + b.Property("PaymentStatus") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("QueueNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("排队号(如有)。"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("ReservationId") + .HasColumnType("uuid") + .HasComment("预约 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店。"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("就餐桌号。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("orders", null, t => + { + t.HasComment("交易订单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("自定义属性 JSON。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("订单 ID。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("商品 ID。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("SkuName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("SKU/规格描述。"); + + b.Property("SubTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("小计。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("单位。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("order_items", null, t => + { + t.HasComment("订单明细。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注信息。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasComment("操作人标识(可为空表示系统)。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("订单标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("变更后的状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId", "OccurredAt"); + + b.ToTable("order_status_histories", null, t => + { + t.HasComment("订单状态流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("申请金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单标识。"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核完成时间。"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("申请原因。"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("退款单号。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("用户提交时间。"); + + b.Property("ReviewNotes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("退款状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique(); + + b.ToTable("refund_requests", null, t => + { + t.HasComment("售后/退款申请。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("ChannelTransactionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("第三方渠道单号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付完成时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始回调内容。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("错误/备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TradeNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("平台交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("payment_records", null, t => + { + t.HasComment("支付流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("退款金额。"); + + b.Property("ChannelRefundId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("渠道退款流水号。"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单标识。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("渠道返回的原始数据 JSON。"); + + b.Property("PaymentRecordId") + .HasColumnType("uuid") + .HasComment("原支付记录标识。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款请求时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("退款状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PaymentRecordId"); + + b.ToTable("payment_refund_records", null, t => + { + t.HasComment("支付渠道退款流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasComment("所属分类。"); + + b.Property("CoverImage") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("主图。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("商品描述。"); + + b.Property("EnableDelivery") + .HasColumnType("boolean") + .HasComment("支持配送。"); + + b.Property("EnableDineIn") + .HasColumnType("boolean") + .HasComment("支持堂食。"); + + b.Property("EnablePickup") + .HasColumnType("boolean") + .HasComment("支持自提。"); + + b.Property("GalleryImages") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Gallery 图片逗号分隔。"); + + b.Property("IsFeatured") + .HasColumnType("boolean") + .HasComment("是否热门推荐。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("最大每单限购。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("现价。"); + + b.Property("SpuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("商品编码。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("商品状态。"); + + b.Property("StockQuantity") + .HasColumnType("integer") + .HasComment("库存数量(可选)。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("所属门店。"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("副标题/卖点。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("售卖单位(份/杯等)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SpuCode") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("products", null, t => + { + t.HasComment("商品(SPU)信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("MaxSelect") + .HasColumnType("integer") + .HasComment("最大选择数量。"); + + b.Property("MinSelect") + .HasColumnType("integer") + .HasComment("最小选择数量。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("所属商品。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "Name"); + + b.ToTable("product_addon_groups", null, t => + { + t.HasComment("加料/做法分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AddonGroupId") + .HasColumnType("uuid") + .HasComment("所属加料分组。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选项。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("product_addon_options", null, t => + { + t.HasComment("加料选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称,例如“辣度”“份量”。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型(单选/多选)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("显示排序。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("关联门店,可为空表示所有门店共享。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("product_attribute_groups", null, t => + { + t.HasComment("商品规格/属性分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributeGroupId") + .HasColumnType("uuid") + .HasComment("所属规格组。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选中。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AttributeGroupId", "Name") + .IsUnique(); + + b.ToTable("product_attribute_options", null, t => + { + t.HasComment("商品规格选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("分类描述。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分类名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("所属门店。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("product_categories", null, t => + { + t.HasComment("商品分类。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Caption") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述或标题。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MediaType") + .HasColumnType("integer") + .HasComment("媒体类型。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("商品标识。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("媒资链接。"); + + b.HasKey("Id"); + + b.ToTable("product_media_assets", null, t => + { + t.HasComment("商品媒资素材。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ConditionsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("条件描述(JSON),如会员等级、渠道等。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效结束时间。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("特殊价格。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("所属商品。"); + + b.Property("RuleType") + .HasColumnType("integer") + .HasComment("策略类型。"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效开始时间。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("WeekdaysJson") + .HasColumnType("text") + .HasComment("生效星期(JSON 数组)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "RuleType"); + + b.ToTable("product_pricing_rules", null, t => + { + t.HasComment("商品价格策略,支持会员价/时段价等。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("规格属性 JSON(记录选项 ID)。"); + + b.Property("Barcode") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("条形码。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("所属商品标识。"); + + b.Property("SkuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("SKU 编码。"); + + b.Property("StockQuantity") + .HasColumnType("integer") + .HasComment("可售库存。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Weight") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)") + .HasComment("重量(千克)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SkuCode") + .IsUnique(); + + b.ToTable("product_skus", null, t => + { + t.HasComment("商品 SKU,记录具体规格组合价格。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CalledAt") + .HasColumnType("timestamp with time zone") + .HasComment("叫号时间。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EstimatedWaitMinutes") + .HasColumnType("integer") + .HasComment("预计等待分钟。"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过号时间。"); + + b.Property("PartySize") + .HasColumnType("integer") + .HasComment("就餐人数。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TicketNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("排队编号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.HasIndex("TenantId", "StoreId", "TicketNumber") + .IsUnique(); + + b.ToTable("queue_tickets", null, t => + { + t.HasComment("排队叫号。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CheckInCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("核销码/到店码。"); + + b.Property("CheckedInAt") + .HasColumnType("timestamp with time zone") + .HasComment("实际签到时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("客户姓名。"); + + b.Property("CustomerPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PeopleCount") + .HasColumnType("integer") + .HasComment("用餐人数。"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("ReservationNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("预约号。"); + + b.Property("ReservationTime") + .HasColumnType("timestamp with time zone") + .HasComment("预约时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店。"); + + b.Property("TablePreference") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("桌型/标签。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ReservationNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("reservations", null, t => + { + t.HasComment("预约/预订记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); + + b.Property("Announcement") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("门店公告。"); + + b.Property("BusinessHours") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("门店营业时段描述(备用字符串)。"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("门店编码,便于扫码及外部对接。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家或地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .HasComment("门店海报。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryRadiusKm") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)") + .HasComment("默认配送半径(公里)。"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("门店描述或公告。"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区县信息。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("高德/腾讯地图经度。"); + + b.Property("ManagerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("门店负责人姓名。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("门店名称。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("门店当前运营状态。"); + + b.Property("SupportsDelivery") + .HasColumnType("boolean") + .HasComment("是否支持配送。"); + + b.Property("SupportsDineIn") + .HasColumnType("boolean") + .HasComment("是否支持堂食。"); + + b.Property("SupportsPickup") + .HasColumnType("boolean") + .HasComment("是否支持自提。"); + + b.Property("SupportsQueueing") + .HasColumnType("boolean") + .HasComment("支持排队叫号。"); + + b.Property("SupportsReservation") + .HasColumnType("boolean") + .HasComment("支持预约。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("门店标签(逗号分隔)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "MerchantId"); + + b.ToTable("stores", null, t => + { + t.HasComment("门店信息,承载营业配置与能力。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CapacityLimit") + .HasColumnType("integer") + .HasComment("最大接待容量或单量限制。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期几,0 表示周日。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(本地时间)。"); + + b.Property("HourType") + .HasColumnType("integer") + .HasComment("时段类型(正常营业、休息、预约等)。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(本地时间)。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "DayOfWeek"); + + b.ToTable("store_business_hours", null, t => + { + t.HasComment("门店营业时段配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer") + .HasComment("预计送达分钟。"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("起送价。"); + + b.Property("PolygonGeoJson") + .IsRequired() + .HasColumnType("text") + .HasComment("GeoJSON 表示的多边形范围。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("ZoneName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ZoneName"); + + b.ToTable("store_delivery_zones", null, t => + { + t.HasComment("门店配送范围配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("排班角色。"); + + b.Property("ShiftDate") + .HasColumnType("timestamp with time zone") + .HasComment("班次日期。"); + + b.Property("StaffId") + .HasColumnType("uuid") + .HasComment("员工标识。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ShiftDate", "StaffId") + .IsUnique(); + + b.ToTable("store_employee_shifts", null, t => + { + t.HasComment("门店员工排班记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasComment("日期。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsClosed") + .HasColumnType("boolean") + .HasComment("是否全天闭店。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("说明内容。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Date") + .IsUnique(); + + b.ToTable("store_holidays", null, t => + { + t.HasComment("门店休息日或特殊营业日。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AreaId") + .HasColumnType("uuid") + .HasComment("所在区域 ID。"); + + b.Property("Capacity") + .HasColumnType("integer") + .HasComment("可容纳人数。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("QrCodeUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("桌码二维码地址。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前桌台状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TableCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("桌码。"); + + b.Property("Tags") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("桌台标签(堂食、快餐等)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "TableCode") + .IsUnique(); + + b.ToTable("store_tables", null, t => + { + t.HasComment("桌台信息与二维码绑定。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("区域描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.ToTable("store_table_areas", null, t => + { + t.HasComment("门店桌台区域配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Address") + .HasColumnType("text") + .HasComment("详细地址信息。"); + + b.Property("City") + .HasColumnType("text") + .HasComment("所在城市。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("租户短编码,作为跨系统引用的唯一标识。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("主联系人邮箱。"); + + b.Property("ContactName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("主联系人姓名。"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("主联系人电话。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家/地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .HasComment("品牌海报或封面图。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("服务生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("服务到期时间(UTC)。"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所属行业,如餐饮、零售等。"); + + b.Property("LegalEntityName") + .HasColumnType("text") + .HasComment("法人或公司主体名称。"); + + b.Property("LogoUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("LOGO 图片地址。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("租户全称或品牌名称。"); + + b.Property("PrimaryOwnerUserId") + .HasColumnType("uuid") + .HasComment("系统内对应的租户所有者账号 ID。"); + + b.Property("Province") + .HasColumnType("text") + .HasComment("所在省份或州。"); + + b.Property("Remarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息,用于运营记录特殊说明。"); + + b.Property("ShortName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("对外展示的简称。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("租户当前状态,涵盖审核、启用、停用等场景。"); + + b.Property("SuspendedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次暂停服务时间。"); + + b.Property("SuspensionReason") + .HasColumnType("text") + .HasComment("暂停或终止的原因说明。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("业务标签集合(逗号分隔)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Website") + .HasColumnType("text") + .HasComment("官网或主要宣传链接。"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("tenants", null, t => + { + t.HasComment("平台租户信息,描述租户的生命周期与基础资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AmountDue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额。"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone") + .HasComment("到期日。"); + + b.Property("LineItemsJson") + .HasColumnType("text") + .HasComment("账单明细 JSON,记录各项费用。"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期结束时间。"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期开始时间。"); + + b.Property("StatementNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("账单编号,供对账查询。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前付款状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StatementNo") + .IsUnique(); + + b.ToTable("tenant_billing_statements", null, t => + { + t.HasComment("租户账单,用于呈现周期性收费。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("发布通道(站内、邮件、短信等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("通知正文。"); + + b.Property("MetadataJson") + .HasColumnType("text") + .HasComment("附加元数据 JSON。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("租户是否已阅读。"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasComment("推送时间。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("通知重要级别。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("通知标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Channel", "SentAt"); + + b.ToTable("tenant_notifications", null, t => + { + t.HasComment("面向租户的站内通知或消息推送。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("套餐描述,包含适用场景、权益等。"); + + b.Property("FeaturePoliciesJson") + .HasColumnType("text") + .HasComment("权益明细 JSON,记录自定义特性开关。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否仍可售卖。"); + + b.Property("MaxAccountCount") + .HasColumnType("integer") + .HasComment("允许创建的最大账号数。"); + + b.Property("MaxDeliveryOrders") + .HasColumnType("integer") + .HasComment("每月可调用的配送单数量上限。"); + + b.Property("MaxSmsCredits") + .HasColumnType("integer") + .HasComment("每月短信额度上限。"); + + b.Property("MaxStorageGb") + .HasColumnType("integer") + .HasComment("存储容量上限(GB)。"); + + b.Property("MaxStoreCount") + .HasColumnType("integer") + .HasComment("允许的最大门店数。"); + + b.Property("MonthlyPrice") + .HasColumnType("numeric") + .HasComment("月付价格,单位:人民币元。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("套餐名称,展示给租户的简称。"); + + b.Property("PackageType") + .HasColumnType("integer") + .HasComment("套餐分类(试用、标准、旗舰等)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("YearlyPrice") + .HasColumnType("numeric") + .HasComment("年付价格,单位:人民币元。"); + + b.HasKey("Id"); + + b.ToTable("tenant_packages", null, t => + { + t.HasComment("平台提供的租户套餐定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LastResetAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次重置时间。"); + + b.Property("LimitValue") + .HasColumnType("numeric") + .HasComment("当前配额上限。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型,例如门店数、短信条数等。"); + + b.Property("ResetCycle") + .HasColumnType("text") + .HasComment("配额刷新周期描述(如月、年)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedValue") + .HasColumnType("numeric") + .HasComment("已消耗的数量。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "QuotaType") + .IsUnique(); + + b.ToTable("tenant_quota_usages", null, t => + { + t.HasComment("租户配额使用情况快照。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AutoRenew") + .HasColumnType("boolean") + .HasComment("是否开启自动续费。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("订阅生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("订阅到期时间(UTC)。"); + + b.Property("NextBillingDate") + .HasColumnType("timestamp with time zone") + .HasComment("下一个计费时间,配合自动续费使用。"); + + b.Property("Notes") + .HasColumnType("text") + .HasComment("运营备注信息。"); + + b.Property("ScheduledPackageId") + .HasColumnType("uuid") + .HasComment("若已排期升降配,对应的新套餐 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("订阅当前状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TenantPackageId") + .HasColumnType("uuid") + .HasComment("当前订阅关联的套餐标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantPackageId"); + + b.ToTable("tenant_subscriptions", null, t => + { + t.HasComment("租户套餐订阅记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs new file mode 100644 index 0000000..38504cd --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs @@ -0,0 +1,22401 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.App.Migrations +{ + /// + public partial class AddEntityComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterTable( + name: "ticket_comments", + comment: "工单评论/流转记录。"); + + migrationBuilder.AlterTable( + name: "tenants", + comment: "平台租户信息,描述租户的生命周期与基础资料。"); + + migrationBuilder.AlterTable( + name: "tenant_subscriptions", + comment: "租户套餐订阅记录。"); + + migrationBuilder.AlterTable( + name: "tenant_quota_usages", + comment: "租户配额使用情况快照。"); + + migrationBuilder.AlterTable( + name: "tenant_packages", + comment: "平台提供的租户套餐定义。"); + + migrationBuilder.AlterTable( + name: "tenant_notifications", + comment: "面向租户的站内通知或消息推送。"); + + migrationBuilder.AlterTable( + name: "tenant_billing_statements", + comment: "租户账单,用于呈现周期性收费。"); + + migrationBuilder.AlterTable( + name: "support_tickets", + comment: "客服工单。"); + + migrationBuilder.AlterTable( + name: "stores", + comment: "门店信息,承载营业配置与能力。"); + + migrationBuilder.AlterTable( + name: "store_tables", + comment: "桌台信息与二维码绑定。"); + + migrationBuilder.AlterTable( + name: "store_table_areas", + comment: "门店桌台区域配置。"); + + migrationBuilder.AlterTable( + name: "store_holidays", + comment: "门店休息日或特殊营业日。"); + + migrationBuilder.AlterTable( + name: "store_employee_shifts", + comment: "门店员工排班记录。"); + + migrationBuilder.AlterTable( + name: "store_delivery_zones", + comment: "门店配送范围配置。"); + + migrationBuilder.AlterTable( + name: "store_business_hours", + comment: "门店营业时段配置。"); + + migrationBuilder.AlterTable( + name: "shopping_carts", + comment: "用户购物车,按租户/门店隔离。"); + + migrationBuilder.AlterTable( + name: "reservations", + comment: "预约/预订记录。"); + + migrationBuilder.AlterTable( + name: "refund_requests", + comment: "售后/退款申请。"); + + migrationBuilder.AlterTable( + name: "queue_tickets", + comment: "排队叫号。"); + + migrationBuilder.AlterTable( + name: "promotion_campaigns", + comment: "营销活动配置。"); + + migrationBuilder.AlterTable( + name: "products", + comment: "商品(SPU)信息。"); + + migrationBuilder.AlterTable( + name: "product_skus", + comment: "商品 SKU,记录具体规格组合价格。"); + + migrationBuilder.AlterTable( + name: "product_pricing_rules", + comment: "商品价格策略,支持会员价/时段价等。"); + + migrationBuilder.AlterTable( + name: "product_media_assets", + comment: "商品媒资素材。"); + + migrationBuilder.AlterTable( + name: "product_categories", + comment: "商品分类。"); + + migrationBuilder.AlterTable( + name: "product_attribute_options", + comment: "商品规格选项。"); + + migrationBuilder.AlterTable( + name: "product_attribute_groups", + comment: "商品规格/属性分组。"); + + migrationBuilder.AlterTable( + name: "product_addon_options", + comment: "加料选项。"); + + migrationBuilder.AlterTable( + name: "product_addon_groups", + comment: "加料/做法分组。"); + + migrationBuilder.AlterTable( + name: "payment_refund_records", + comment: "支付渠道退款流水。"); + + migrationBuilder.AlterTable( + name: "payment_records", + comment: "支付流水。"); + + migrationBuilder.AlterTable( + name: "orders", + comment: "交易订单。"); + + migrationBuilder.AlterTable( + name: "order_status_histories", + comment: "订单状态流转记录。"); + + migrationBuilder.AlterTable( + name: "order_items", + comment: "订单明细。"); + + migrationBuilder.AlterTable( + name: "navigation_requests", + comment: "用户发起的导航请求日志。"); + + migrationBuilder.AlterTable( + name: "metric_snapshots", + comment: "指标快照,用于大盘展示。"); + + migrationBuilder.AlterTable( + name: "metric_definitions", + comment: "指标定义,描述可观测的数据点。"); + + migrationBuilder.AlterTable( + name: "metric_alert_rules", + comment: "指标告警规则。"); + + migrationBuilder.AlterTable( + name: "merchants", + comment: "商户主体信息,承载入驻和资质审核结果。"); + + migrationBuilder.AlterTable( + name: "merchant_staff", + comment: "商户员工账号,支持门店维度分配。"); + + migrationBuilder.AlterTable( + name: "merchant_documents", + comment: "商户提交的资质或证照材料。"); + + migrationBuilder.AlterTable( + name: "merchant_contracts", + comment: "商户合同记录。"); + + migrationBuilder.AlterTable( + name: "member_tiers", + comment: "会员等级定义。"); + + migrationBuilder.AlterTable( + name: "member_profiles", + comment: "会员档案。"); + + migrationBuilder.AlterTable( + name: "member_point_ledgers", + comment: "积分变动流水。"); + + migrationBuilder.AlterTable( + name: "member_growth_logs", + comment: "成长值变动日志。"); + + migrationBuilder.AlterTable( + name: "map_locations", + comment: "地图 POI 信息,用于门店定位和推荐。"); + + migrationBuilder.AlterTable( + name: "inventory_items", + comment: "SKU 在门店的库存信息。"); + + migrationBuilder.AlterTable( + name: "inventory_batches", + comment: "SKU 批次信息。"); + + migrationBuilder.AlterTable( + name: "inventory_adjustments", + comment: "库存调整记录。"); + + migrationBuilder.AlterTable( + name: "group_participants", + comment: "拼单参与者。"); + + migrationBuilder.AlterTable( + name: "group_orders", + comment: "拼单活动。"); + + migrationBuilder.AlterTable( + name: "delivery_orders", + comment: "配送单。"); + + migrationBuilder.AlterTable( + name: "delivery_events", + comment: "配送状态事件流水。"); + + migrationBuilder.AlterTable( + name: "coupons", + comment: "用户领取的券。"); + + migrationBuilder.AlterTable( + name: "coupon_templates", + comment: "优惠券模板。"); + + migrationBuilder.AlterTable( + name: "community_reactions", + comment: "社区互动反馈。"); + + migrationBuilder.AlterTable( + name: "community_posts", + comment: "社区动态。"); + + migrationBuilder.AlterTable( + name: "community_comments", + comment: "社区评论。"); + + migrationBuilder.AlterTable( + name: "checkout_sessions", + comment: "结账会话,记录校验上下文。"); + + migrationBuilder.AlterTable( + name: "checkin_records", + comment: "用户签到记录。"); + + migrationBuilder.AlterTable( + name: "checkin_campaigns", + comment: "签到活动配置。"); + + migrationBuilder.AlterTable( + name: "chat_sessions", + comment: "客服会话。"); + + migrationBuilder.AlterTable( + name: "chat_messages", + comment: "会话消息。"); + + migrationBuilder.AlterTable( + name: "cart_items", + comment: "购物车条目。"); + + migrationBuilder.AlterTable( + name: "cart_item_addons", + comment: "购物车条目的加料/附加项。"); + + migrationBuilder.AlterTable( + name: "affiliate_payouts", + comment: "佣金结算记录。"); + + migrationBuilder.AlterTable( + name: "affiliate_partners", + comment: "分销/推广合作伙伴。"); + + migrationBuilder.AlterTable( + name: "affiliate_orders", + comment: "分销订单记录。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "ticket_comments", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ticket_comments", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "ticket_comments", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SupportTicketId", + table: "ticket_comments", + type: "uuid", + nullable: false, + comment: "工单标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "IsInternal", + table: "ticket_comments", + type: "boolean", + nullable: false, + comment: "是否内部备注。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "ticket_comments", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ticket_comments", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "ticket_comments", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ticket_comments", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "ticket_comments", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + comment: "评论内容。", + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024); + + migrationBuilder.AlterColumn( + name: "AuthorUserId", + table: "ticket_comments", + type: "uuid", + nullable: true, + comment: "评论人 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AttachmentsJson", + table: "ticket_comments", + type: "text", + nullable: true, + comment: "附件 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "ticket_comments", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Website", + table: "tenants", + type: "text", + nullable: true, + comment: "官网或主要宣传链接。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenants", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Tags", + table: "tenants", + type: "text", + nullable: true, + comment: "业务标签集合(逗号分隔)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SuspensionReason", + table: "tenants", + type: "text", + nullable: true, + comment: "暂停或终止的原因说明。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SuspendedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次暂停服务时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Status", + table: "tenants", + type: "integer", + nullable: false, + comment: "租户当前状态,涵盖审核、启用、停用等场景。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ShortName", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "对外展示的简称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "tenants", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "备注信息,用于运营记录特殊说明。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Province", + table: "tenants", + type: "text", + nullable: true, + comment: "所在省份或州。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PrimaryOwnerUserId", + table: "tenants", + type: "uuid", + nullable: true, + comment: "系统内对应的租户所有者账号 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "tenants", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "租户全称或品牌名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "LogoUrl", + table: "tenants", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "LOGO 图片地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LegalEntityName", + table: "tenants", + type: "text", + nullable: true, + comment: "法人或公司主体名称。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Industry", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "所属行业,如餐饮、零售等。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EffectiveTo", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + comment: "服务到期时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EffectiveFrom", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + comment: "服务生效时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenants", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenants", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CoverImageUrl", + table: "tenants", + type: "text", + nullable: true, + comment: "品牌海报或封面图。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Country", + table: "tenants", + type: "text", + nullable: true, + comment: "所在国家/地区。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ContactPhone", + table: "tenants", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "主联系人电话。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ContactName", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "主联系人姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ContactEmail", + table: "tenants", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "主联系人邮箱。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Code", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "租户短编码,作为跨系统引用的唯一标识。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "City", + table: "tenants", + type: "text", + nullable: true, + comment: "所在城市。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Address", + table: "tenants", + type: "text", + nullable: true, + comment: "详细地址信息。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenants", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantPackageId", + table: "tenant_subscriptions", + type: "uuid", + nullable: false, + comment: "当前订阅关联的套餐标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_subscriptions", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "tenant_subscriptions", + type: "integer", + nullable: false, + comment: "订阅当前状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ScheduledPackageId", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + comment: "若已排期升降配,对应的新套餐 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "tenant_subscriptions", + type: "text", + nullable: true, + comment: "运营备注信息。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "NextBillingDate", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: true, + comment: "下一个计费时间,配合自动续费使用。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EffectiveTo", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: false, + comment: "订阅到期时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "EffectiveFrom", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: false, + comment: "订阅生效时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AutoRenew", + table: "tenant_subscriptions", + type: "boolean", + nullable: false, + comment: "是否开启自动续费。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_subscriptions", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UsedValue", + table: "tenant_quota_usages", + type: "numeric", + nullable: false, + comment: "已消耗的数量。", + oldClrType: typeof(decimal), + oldType: "numeric"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_quota_usages", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_quota_usages", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ResetCycle", + table: "tenant_quota_usages", + type: "text", + nullable: true, + comment: "配额刷新周期描述(如月、年)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "QuotaType", + table: "tenant_quota_usages", + type: "integer", + nullable: false, + comment: "配额类型,例如门店数、短信条数等。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "LimitValue", + table: "tenant_quota_usages", + type: "numeric", + nullable: false, + comment: "当前配额上限。", + oldClrType: typeof(decimal), + oldType: "numeric"); + + migrationBuilder.AlterColumn( + name: "LastResetAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次重置时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_quota_usages", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_quota_usages", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_quota_usages", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "YearlyPrice", + table: "tenant_packages", + type: "numeric", + nullable: true, + comment: "年付价格,单位:人民币元。", + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_packages", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_packages", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PackageType", + table: "tenant_packages", + type: "integer", + nullable: false, + comment: "套餐分类(试用、标准、旗舰等)。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "tenant_packages", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "套餐名称,展示给租户的简称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "MonthlyPrice", + table: "tenant_packages", + type: "numeric", + nullable: true, + comment: "月付价格,单位:人民币元。", + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaxStoreCount", + table: "tenant_packages", + type: "integer", + nullable: true, + comment: "允许的最大门店数。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaxStorageGb", + table: "tenant_packages", + type: "integer", + nullable: true, + comment: "存储容量上限(GB)。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaxSmsCredits", + table: "tenant_packages", + type: "integer", + nullable: true, + comment: "每月短信额度上限。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaxDeliveryOrders", + table: "tenant_packages", + type: "integer", + nullable: true, + comment: "每月可调用的配送单数量上限。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaxAccountCount", + table: "tenant_packages", + type: "integer", + nullable: true, + comment: "允许创建的最大账号数。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IsActive", + table: "tenant_packages", + type: "boolean", + nullable: false, + comment: "是否仍可售卖。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "FeaturePoliciesJson", + table: "tenant_packages", + type: "text", + nullable: true, + comment: "权益明细 JSON,记录自定义特性开关。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "tenant_packages", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "套餐描述,包含适用场景、权益等。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_packages", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_packages", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_packages", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_packages", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_packages", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_notifications", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Title", + table: "tenant_notifications", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "通知标题。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_notifications", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Severity", + table: "tenant_notifications", + type: "integer", + nullable: false, + comment: "通知重要级别。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SentAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: false, + comment: "推送时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ReadAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: true, + comment: "租户是否已阅读。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MetadataJson", + table: "tenant_notifications", + type: "text", + nullable: true, + comment: "附加元数据 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Message", + table: "tenant_notifications", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + comment: "通知正文。", + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_notifications", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_notifications", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Channel", + table: "tenant_notifications", + type: "integer", + nullable: false, + comment: "发布通道(站内、邮件、短信等)。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_notifications", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_billing_statements", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_billing_statements", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "tenant_billing_statements", + type: "integer", + nullable: false, + comment: "当前付款状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StatementNo", + table: "tenant_billing_statements", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "账单编号,供对账查询。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "PeriodStart", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + comment: "账单周期开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "PeriodEnd", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + comment: "账单周期结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "LineItemsJson", + table: "tenant_billing_statements", + type: "text", + nullable: true, + comment: "账单明细 JSON,记录各项费用。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DueDate", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + comment: "到期日。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_billing_statements", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_billing_statements", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AmountPaid", + table: "tenant_billing_statements", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "实付金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "AmountDue", + table: "tenant_billing_statements", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "应付金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_billing_statements", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "support_tickets", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TicketNo", + table: "support_tickets", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "工单编号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "support_tickets", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Subject", + table: "support_tickets", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "工单主题。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "Status", + table: "support_tickets", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Priority", + table: "support_tickets", + type: "integer", + nullable: false, + comment: "优先级。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "support_tickets", + type: "uuid", + nullable: true, + comment: "关联订单(如有)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "support_tickets", + type: "text", + nullable: false, + comment: "工单详情。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "support_tickets", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CustomerUserId", + table: "support_tickets", + type: "uuid", + nullable: false, + comment: "客户用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "support_tickets", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ClosedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "关闭时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AssignedAgentId", + table: "support_tickets", + type: "uuid", + nullable: true, + comment: "指派的客服。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "support_tickets", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "stores", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "stores", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "stores", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Tags", + table: "stores", + type: "text", + nullable: true, + comment: "门店标签(逗号分隔)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SupportsReservation", + table: "stores", + type: "boolean", + nullable: false, + comment: "支持预约。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "SupportsQueueing", + table: "stores", + type: "boolean", + nullable: false, + comment: "支持排队叫号。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "SupportsPickup", + table: "stores", + type: "boolean", + nullable: false, + comment: "是否支持自提。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "SupportsDineIn", + table: "stores", + type: "boolean", + nullable: false, + comment: "是否支持堂食。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "SupportsDelivery", + table: "stores", + type: "boolean", + nullable: false, + comment: "是否支持配送。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "stores", + type: "integer", + nullable: false, + comment: "门店当前运营状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Province", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "所在省份。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "stores", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "联系电话。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "stores", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "门店名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "stores", + type: "uuid", + nullable: false, + comment: "所属商户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ManagerName", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "门店负责人姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Longitude", + table: "stores", + type: "double precision", + nullable: true, + comment: "高德/腾讯地图经度。", + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Latitude", + table: "stores", + type: "double precision", + nullable: true, + comment: "纬度。", + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "District", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "区县信息。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "stores", + type: "text", + nullable: true, + comment: "门店描述或公告。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeliveryRadiusKm", + table: "stores", + type: "numeric(6,2)", + precision: 6, + scale: 2, + nullable: false, + comment: "默认配送半径(公里)。", + oldClrType: typeof(decimal), + oldType: "numeric(6,2)", + oldPrecision: 6, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "stores", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "stores", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "stores", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "stores", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CoverImageUrl", + table: "stores", + type: "text", + nullable: true, + comment: "门店海报。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Country", + table: "stores", + type: "text", + nullable: true, + comment: "所在国家或地区。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Code", + table: "stores", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "门店编码,便于扫码及外部对接。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "City", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "所在城市。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BusinessHours", + table: "stores", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "门店营业时段描述(备用字符串)。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Announcement", + table: "stores", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "门店公告。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Address", + table: "stores", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "详细地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "stores", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_tables", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_tables", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_tables", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Tags", + table: "store_tables", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "桌台标签(堂食、快餐等)。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TableCode", + table: "store_tables", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "桌码。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_tables", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "store_tables", + type: "integer", + nullable: false, + comment: "当前桌台状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "QrCodeUrl", + table: "store_tables", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "桌码二维码地址。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_tables", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_tables", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_tables", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_tables", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Capacity", + table: "store_tables", + type: "integer", + nullable: false, + comment: "可容纳人数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "AreaId", + table: "store_tables", + type: "uuid", + nullable: true, + comment: "所在区域 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_tables", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_table_areas", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_table_areas", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_table_areas", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_table_areas", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "store_table_areas", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "区域名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Description", + table: "store_table_areas", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "区域描述。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_table_areas", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_table_areas", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_table_areas", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_table_areas", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_table_areas", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_holidays", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_holidays", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_holidays", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_holidays", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "store_holidays", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "说明内容。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IsClosed", + table: "store_holidays", + type: "boolean", + nullable: false, + comment: "是否全天闭店。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_holidays", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_holidays", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Date", + table: "store_holidays", + type: "timestamp with time zone", + nullable: false, + comment: "日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_holidays", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_holidays", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_holidays", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_employee_shifts", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "store_employee_shifts", + type: "interval", + nullable: false, + comment: "开始时间。", + oldClrType: typeof(TimeSpan), + oldType: "interval"); + + migrationBuilder.AlterColumn( + name: "StaffId", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + comment: "员工标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ShiftDate", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: false, + comment: "班次日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "RoleType", + table: "store_employee_shifts", + type: "integer", + nullable: false, + comment: "排班角色。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "store_employee_shifts", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "store_employee_shifts", + type: "interval", + nullable: false, + comment: "结束时间。", + oldClrType: typeof(TimeSpan), + oldType: "interval"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_employee_shifts", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_employee_shifts", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ZoneName", + table: "store_delivery_zones", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "区域名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_delivery_zones", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_delivery_zones", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_delivery_zones", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_delivery_zones", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "PolygonGeoJson", + table: "store_delivery_zones", + type: "text", + nullable: false, + comment: "GeoJSON 表示的多边形范围。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "MinimumOrderAmount", + table: "store_delivery_zones", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "起送价。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EstimatedMinutes", + table: "store_delivery_zones", + type: "integer", + nullable: true, + comment: "预计送达分钟。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeliveryFee", + table: "store_delivery_zones", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "配送费。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_delivery_zones", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_delivery_zones", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_delivery_zones", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_delivery_zones", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_delivery_zones", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_business_hours", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_business_hours", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_business_hours", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_business_hours", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "store_business_hours", + type: "interval", + nullable: false, + comment: "开始时间(本地时间)。", + oldClrType: typeof(TimeSpan), + oldType: "interval"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "store_business_hours", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "HourType", + table: "store_business_hours", + type: "integer", + nullable: false, + comment: "时段类型(正常营业、休息、预约等)。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "store_business_hours", + type: "interval", + nullable: false, + comment: "结束时间(本地时间)。", + oldClrType: typeof(TimeSpan), + oldType: "interval"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_business_hours", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_business_hours", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DayOfWeek", + table: "store_business_hours", + type: "integer", + nullable: false, + comment: "星期几,0 表示周日。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_business_hours", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_business_hours", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CapacityLimit", + table: "store_business_hours", + type: "integer", + nullable: true, + comment: "最大接待容量或单量限制。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_business_hours", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "shopping_carts", + type: "uuid", + nullable: false, + comment: "用户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "shopping_carts", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "shopping_carts", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TableContext", + table: "shopping_carts", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "桌码或场景标识(扫码点餐)。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "shopping_carts", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "shopping_carts", + type: "integer", + nullable: false, + comment: "购物车状态,包含正常/锁定。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "LastModifiedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: false, + comment: "最近一次修改时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeliveryPreference", + table: "shopping_carts", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "履约方式(堂食/自提/配送)缓存。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "shopping_carts", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "shopping_carts", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "shopping_carts", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "reservations", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "reservations", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TablePreference", + table: "reservations", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "桌型/标签。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "reservations", + type: "uuid", + nullable: false, + comment: "门店。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "reservations", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ReservationTime", + table: "reservations", + type: "timestamp with time zone", + nullable: false, + comment: "预约时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ReservationNo", + table: "reservations", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "预约号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "reservations", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PeopleCount", + table: "reservations", + type: "integer", + nullable: false, + comment: "用餐人数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "reservations", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CustomerPhone", + table: "reservations", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "联系电话。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "CustomerName", + table: "reservations", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "客户姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "reservations", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "reservations", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CheckedInAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + comment: "实际签到时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CheckInCode", + table: "reservations", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "核销码/到店码。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + comment: "取消时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "reservations", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "refund_requests", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "refund_requests", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "refund_requests", + type: "integer", + nullable: false, + comment: "退款状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ReviewNotes", + table: "refund_requests", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "审核备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "RequestedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: false, + comment: "用户提交时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "RefundNo", + table: "refund_requests", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "退款单号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "refund_requests", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "申请原因。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "ProcessedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: true, + comment: "审核完成时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "refund_requests", + type: "uuid", + nullable: false, + comment: "关联订单标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "refund_requests", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "refund_requests", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "refund_requests", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "申请金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "Id", + table: "refund_requests", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "queue_tickets", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TicketNumber", + table: "queue_tickets", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "排队编号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "queue_tickets", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "queue_tickets", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "queue_tickets", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PartySize", + table: "queue_tickets", + type: "integer", + nullable: false, + comment: "就餐人数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ExpiredAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "过号时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EstimatedWaitMinutes", + table: "queue_tickets", + type: "integer", + nullable: true, + comment: "预计等待分钟。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "queue_tickets", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "queue_tickets", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "取消时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CalledAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + comment: "叫号时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "queue_tickets", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "promotion_campaigns", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "promotion_campaigns", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "promotion_campaigns", + type: "integer", + nullable: false, + comment: "活动状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StartAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: false, + comment: "开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "RulesJson", + table: "promotion_campaigns", + type: "text", + nullable: false, + comment: "活动规则 JSON。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "PromotionType", + table: "promotion_campaigns", + type: "integer", + nullable: false, + comment: "活动类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "promotion_campaigns", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "活动名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "EndAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: false, + comment: "结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "promotion_campaigns", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "promotion_campaigns", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Budget", + table: "promotion_campaigns", + type: "numeric", + nullable: true, + comment: "预算金额。", + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BannerUrl", + table: "promotion_campaigns", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "营销素材(如 banner)。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AudienceDescription", + table: "promotion_campaigns", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "目标人群描述。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "promotion_campaigns", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "products", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "products", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Unit", + table: "products", + type: "character varying(16)", + maxLength: 16, + nullable: true, + comment: "售卖单位(份/杯等)。", + oldClrType: typeof(string), + oldType: "character varying(16)", + oldMaxLength: 16, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "products", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Subtitle", + table: "products", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "副标题/卖点。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "products", + type: "uuid", + nullable: false, + comment: "所属门店。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StockQuantity", + table: "products", + type: "integer", + nullable: true, + comment: "库存数量(可选)。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Status", + table: "products", + type: "integer", + nullable: false, + comment: "商品状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SpuCode", + table: "products", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "商品编码。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "Price", + table: "products", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "现价。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "OriginalPrice", + table: "products", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "原价。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "products", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "商品名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "MaxQuantityPerOrder", + table: "products", + type: "integer", + nullable: true, + comment: "最大每单限购。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IsFeatured", + table: "products", + type: "boolean", + nullable: false, + comment: "是否热门推荐。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "GalleryImages", + table: "products", + type: "character varying(1024)", + maxLength: 1024, + nullable: true, + comment: "Gallery 图片逗号分隔。", + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EnablePickup", + table: "products", + type: "boolean", + nullable: false, + comment: "支持自提。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "EnableDineIn", + table: "products", + type: "boolean", + nullable: false, + comment: "支持堂食。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "EnableDelivery", + table: "products", + type: "boolean", + nullable: false, + comment: "支持配送。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "products", + type: "text", + nullable: true, + comment: "商品描述。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "products", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "products", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "products", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "products", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CoverImage", + table: "products", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "主图。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "products", + type: "uuid", + nullable: false, + comment: "所属分类。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "products", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Weight", + table: "product_skus", + type: "numeric(10,3)", + precision: 10, + scale: 3, + nullable: true, + comment: "重量(千克)。", + oldClrType: typeof(decimal), + oldType: "numeric(10,3)", + oldPrecision: 10, + oldScale: 3, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_skus", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_skus", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_skus", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StockQuantity", + table: "product_skus", + type: "integer", + nullable: true, + comment: "可售库存。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SkuCode", + table: "product_skus", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "SKU 编码。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_skus", + type: "uuid", + nullable: false, + comment: "所属商品标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Price", + table: "product_skus", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "售价。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "OriginalPrice", + table: "product_skus", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "原价。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_skus", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_skus", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_skus", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_skus", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Barcode", + table: "product_skus", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "条形码。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AttributesJson", + table: "product_skus", + type: "text", + nullable: false, + comment: "规格属性 JSON(记录选项 ID)。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_skus", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "WeekdaysJson", + table: "product_pricing_rules", + type: "text", + nullable: true, + comment: "生效星期(JSON 数组)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_pricing_rules", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_pricing_rules", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + comment: "生效开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "RuleType", + table: "product_pricing_rules", + type: "integer", + nullable: false, + comment: "策略类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_pricing_rules", + type: "uuid", + nullable: false, + comment: "所属商品。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Price", + table: "product_pricing_rules", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "特殊价格。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + comment: "生效结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_pricing_rules", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_pricing_rules", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ConditionsJson", + table: "product_pricing_rules", + type: "text", + nullable: false, + comment: "条件描述(JSON),如会员等级、渠道等。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_pricing_rules", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Url", + table: "product_media_assets", + type: "character varying(512)", + maxLength: 512, + nullable: false, + comment: "媒资链接。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_media_assets", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_media_assets", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_media_assets", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_media_assets", + type: "integer", + nullable: false, + comment: "排序。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_media_assets", + type: "uuid", + nullable: false, + comment: "商品标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "MediaType", + table: "product_media_assets", + type: "integer", + nullable: false, + comment: "媒体类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_media_assets", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_media_assets", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_media_assets", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_media_assets", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Caption", + table: "product_media_assets", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "描述或标题。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_media_assets", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_categories", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_categories", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_categories", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "product_categories", + type: "uuid", + nullable: false, + comment: "所属门店。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_categories", + type: "integer", + nullable: false, + comment: "排序值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_categories", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "分类名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "IsEnabled", + table: "product_categories", + type: "boolean", + nullable: false, + comment: "是否启用。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "product_categories", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "分类描述。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_categories", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_categories", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_categories", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_categories", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_categories", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_attribute_options", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_attribute_options", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_attribute_options", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_attribute_options", + type: "integer", + nullable: false, + comment: "排序。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_attribute_options", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "选项名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "IsDefault", + table: "product_attribute_options", + type: "boolean", + nullable: false, + comment: "是否默认选中。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "ExtraPrice", + table: "product_attribute_options", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "附加价格。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_attribute_options", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_attribute_options", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_attribute_options", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_attribute_options", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AttributeGroupId", + table: "product_attribute_options", + type: "uuid", + nullable: false, + comment: "所属规格组。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_attribute_options", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_attribute_groups", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_attribute_groups", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + comment: "关联门店,可为空表示所有门店共享。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_attribute_groups", + type: "integer", + nullable: false, + comment: "显示排序。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SelectionType", + table: "product_attribute_groups", + type: "integer", + nullable: false, + comment: "选择类型(单选/多选)。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_attribute_groups", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "分组名称,例如“辣度”“份量”。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "IsRequired", + table: "product_attribute_groups", + type: "boolean", + nullable: false, + comment: "是否必选。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_attribute_groups", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_attribute_groups", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_attribute_groups", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_addon_options", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_addon_options", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_addon_options", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_addon_options", + type: "integer", + nullable: false, + comment: "排序。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_addon_options", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "选项名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "IsDefault", + table: "product_addon_options", + type: "boolean", + nullable: false, + comment: "是否默认选项。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "ExtraPrice", + table: "product_addon_options", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "附加价格。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_addon_options", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_addon_options", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_addon_options", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_addon_options", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AddonGroupId", + table: "product_addon_options", + type: "uuid", + nullable: false, + comment: "所属加料分组。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_addon_options", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_addon_groups", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_addon_groups", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_addon_groups", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_addon_groups", + type: "integer", + nullable: false, + comment: "排序值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SelectionType", + table: "product_addon_groups", + type: "integer", + nullable: false, + comment: "选择类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_addon_groups", + type: "uuid", + nullable: false, + comment: "所属商品。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_addon_groups", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "分组名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "MinSelect", + table: "product_addon_groups", + type: "integer", + nullable: true, + comment: "最小选择数量。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaxSelect", + table: "product_addon_groups", + type: "integer", + nullable: true, + comment: "最大选择数量。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IsRequired", + table: "product_addon_groups", + type: "boolean", + nullable: false, + comment: "是否必选。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_addon_groups", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_addon_groups", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_addon_groups", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_addon_groups", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_addon_groups", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "payment_refund_records", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "payment_refund_records", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "payment_refund_records", + type: "integer", + nullable: false, + comment: "退款状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "RequestedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: false, + comment: "退款请求时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "PaymentRecordId", + table: "payment_refund_records", + type: "uuid", + nullable: false, + comment: "原支付记录标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Payload", + table: "payment_refund_records", + type: "text", + nullable: true, + comment: "渠道返回的原始数据 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "payment_refund_records", + type: "uuid", + nullable: false, + comment: "关联订单标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "payment_refund_records", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "payment_refund_records", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CompletedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: true, + comment: "完成时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ChannelRefundId", + table: "payment_refund_records", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "渠道退款流水号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "payment_refund_records", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "退款金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "Id", + table: "payment_refund_records", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "payment_records", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TradeNo", + table: "payment_records", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "平台交易号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "payment_records", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "payment_records", + type: "integer", + nullable: false, + comment: "支付状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "payment_records", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "错误/备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Payload", + table: "payment_records", + type: "text", + nullable: true, + comment: "原始回调内容。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PaidAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: true, + comment: "支付完成时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "payment_records", + type: "uuid", + nullable: false, + comment: "关联订单。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Method", + table: "payment_records", + type: "integer", + nullable: false, + comment: "支付方式。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "payment_records", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "payment_records", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ChannelTransactionId", + table: "payment_records", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "第三方渠道单号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "payment_records", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "支付金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "Id", + table: "payment_records", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "orders", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "orders", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TableNo", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "就餐桌号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "orders", + type: "uuid", + nullable: false, + comment: "门店。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "orders", + type: "integer", + nullable: false, + comment: "当前状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ReservationId", + table: "orders", + type: "uuid", + nullable: true, + comment: "预约 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "orders", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "QueueNumber", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "排队号(如有)。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PaymentStatus", + table: "orders", + type: "integer", + nullable: false, + comment: "支付状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "PayableAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "应付金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "PaidAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + comment: "支付时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PaidAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "实付金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "OrderNo", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "订单号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "ItemsAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "商品总额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "FinishedAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + comment: "完成时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DiscountAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "优惠金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "DeliveryType", + table: "orders", + type: "integer", + nullable: false, + comment: "履约类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "orders", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CustomerPhone", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "顾客手机号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CustomerName", + table: "orders", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "顾客姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "orders", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "orders", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Channel", + table: "orders", + type: "integer", + nullable: false, + comment: "下单渠道。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + comment: "取消时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CancelReason", + table: "orders", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "取消原因。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "orders", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "order_status_histories", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "order_status_histories", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "order_status_histories", + type: "integer", + nullable: false, + comment: "变更后的状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "order_status_histories", + type: "uuid", + nullable: false, + comment: "订单标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OperatorId", + table: "order_status_histories", + type: "uuid", + nullable: true, + comment: "操作人标识(可为空表示系统)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: false, + comment: "发生时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "order_status_histories", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "备注信息。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "order_status_histories", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "order_status_histories", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "order_status_histories", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "order_items", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "order_items", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UnitPrice", + table: "order_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "单价。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "Unit", + table: "order_items", + type: "character varying(16)", + maxLength: 16, + nullable: true, + comment: "单位。", + oldClrType: typeof(string), + oldType: "character varying(16)", + oldMaxLength: 16, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "order_items", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SubTotal", + table: "order_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "小计。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "SkuName", + table: "order_items", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "SKU/规格描述。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "order_items", + type: "integer", + nullable: false, + comment: "数量。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductName", + table: "order_items", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "商品名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "order_items", + type: "uuid", + nullable: false, + comment: "商品 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "order_items", + type: "uuid", + nullable: false, + comment: "订单 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DiscountAmount", + table: "order_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "折扣金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "order_items", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "order_items", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "order_items", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "order_items", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AttributesJson", + table: "order_items", + type: "text", + nullable: true, + comment: "自定义属性 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "order_items", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "navigation_requests", + type: "uuid", + nullable: false, + comment: "用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "navigation_requests", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "navigation_requests", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TargetApp", + table: "navigation_requests", + type: "integer", + nullable: false, + comment: "跳转的地图应用。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "navigation_requests", + type: "uuid", + nullable: false, + comment: "门店 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "RequestedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: false, + comment: "请求时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "navigation_requests", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "navigation_requests", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Channel", + table: "navigation_requests", + type: "integer", + nullable: false, + comment: "来源通道(小程序、H5 等)。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "navigation_requests", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "WindowStart", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: false, + comment: "统计时间窗口开始。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "WindowEnd", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: false, + comment: "统计时间窗口结束。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Value", + table: "metric_snapshots", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: false, + comment: "数值。", + oldClrType: typeof(decimal), + oldType: "numeric(18,4)", + oldPrecision: 18, + oldScale: 4); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "metric_snapshots", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "metric_snapshots", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "MetricDefinitionId", + table: "metric_snapshots", + type: "uuid", + nullable: false, + comment: "指标定义 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DimensionKey", + table: "metric_snapshots", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "维度键(JSON)。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "metric_snapshots", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "metric_snapshots", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "metric_snapshots", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "metric_definitions", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "metric_definitions", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "metric_definitions", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "metric_definitions", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "指标名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "DimensionsJson", + table: "metric_definitions", + type: "text", + nullable: true, + comment: "维度描述 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "metric_definitions", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "说明。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "metric_definitions", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "metric_definitions", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DefaultAggregation", + table: "metric_definitions", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "默认聚合方式。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "metric_definitions", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "metric_definitions", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "metric_definitions", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "指标编码。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Id", + table: "metric_definitions", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "metric_alert_rules", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "metric_alert_rules", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "metric_alert_rules", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Severity", + table: "metric_alert_rules", + type: "integer", + nullable: false, + comment: "告警级别。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "NotificationChannels", + table: "metric_alert_rules", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "通知渠道。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "MetricDefinitionId", + table: "metric_alert_rules", + type: "uuid", + nullable: false, + comment: "关联指标。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Enabled", + table: "metric_alert_rules", + type: "boolean", + nullable: false, + comment: "是否启用。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "metric_alert_rules", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "metric_alert_rules", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "metric_alert_rules", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "metric_alert_rules", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ConditionJson", + table: "metric_alert_rules", + type: "text", + nullable: false, + comment: "触发条件 JSON。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "metric_alert_rules", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchants", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchants", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TaxNumber", + table: "merchants", + type: "text", + nullable: true, + comment: "税号/统一社会信用代码。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SupportEmail", + table: "merchants", + type: "text", + nullable: true, + comment: "客服邮箱。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchants", + type: "integer", + nullable: false, + comment: "入驻状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ServicePhone", + table: "merchants", + type: "text", + nullable: true, + comment: "客服电话。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ReviewRemarks", + table: "merchants", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "审核备注或驳回原因。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Province", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "所在省份。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Longitude", + table: "merchants", + type: "double precision", + nullable: true, + comment: "经度信息。", + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LogoUrl", + table: "merchants", + type: "text", + nullable: true, + comment: "品牌 Logo。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LegalPerson", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "法人或负责人姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Latitude", + table: "merchants", + type: "double precision", + nullable: true, + comment: "纬度信息。", + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastReviewedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次审核时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "JoinedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + comment: "入驻时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "District", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "所在区县。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchants", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchants", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ContactPhone", + table: "merchants", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "联系电话。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "ContactEmail", + table: "merchants", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "联系邮箱。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "City", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "所在城市。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Category", + table: "merchants", + type: "text", + nullable: true, + comment: "品牌所属品类,如火锅、咖啡等。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BusinessLicenseNumber", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "营业执照号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BusinessLicenseImageUrl", + table: "merchants", + type: "text", + nullable: true, + comment: "营业执照扫描件地址。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BrandName", + table: "merchants", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "品牌名称(对外展示)。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "BrandAlias", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "品牌简称或别名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Address", + table: "merchants", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "详细地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchants", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchant_staff", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchant_staff", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchant_staff", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "merchant_staff", + type: "uuid", + nullable: true, + comment: "可选的关联门店 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchant_staff", + type: "integer", + nullable: false, + comment: "员工状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "RoleType", + table: "merchant_staff", + type: "integer", + nullable: false, + comment: "员工角色类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "merchant_staff", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "手机号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "PermissionsJson", + table: "merchant_staff", + type: "text", + nullable: true, + comment: "自定义权限(JSON)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "merchant_staff", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "员工姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "merchant_staff", + type: "uuid", + nullable: false, + comment: "所属商户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "IdentityUserId", + table: "merchant_staff", + type: "uuid", + nullable: true, + comment: "登录账号 ID(指向统一身份体系)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Email", + table: "merchant_staff", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "邮箱地址。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchant_staff", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchant_staff", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchant_staff", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchant_staff", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchant_staff", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchant_documents", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchant_documents", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchant_documents", + type: "integer", + nullable: false, + comment: "审核状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "merchant_documents", + type: "text", + nullable: true, + comment: "审核备注或驳回原因。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "merchant_documents", + type: "uuid", + nullable: false, + comment: "所属商户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "IssuedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + comment: "签发日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FileUrl", + table: "merchant_documents", + type: "character varying(512)", + maxLength: 512, + nullable: false, + comment: "证照文件链接。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512); + + migrationBuilder.AlterColumn( + name: "ExpiresAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + comment: "到期日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DocumentType", + table: "merchant_documents", + type: "integer", + nullable: false, + comment: "证照类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DocumentNumber", + table: "merchant_documents", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "证照编号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchant_documents", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchant_documents", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchant_documents", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchant_contracts", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TerminationReason", + table: "merchant_contracts", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "终止原因。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TerminatedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + comment: "终止时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchant_contracts", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchant_contracts", + type: "integer", + nullable: false, + comment: "合同状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: false, + comment: "合同开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "SignedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + comment: "签署时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "merchant_contracts", + type: "uuid", + nullable: false, + comment: "所属商户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "FileUrl", + table: "merchant_contracts", + type: "character varying(512)", + maxLength: 512, + nullable: false, + comment: "合同文件存储地址。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: false, + comment: "合同结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchant_contracts", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchant_contracts", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ContractNumber", + table: "merchant_contracts", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "合同编号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchant_contracts", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_tiers", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_tiers", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_tiers", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "member_tiers", + type: "integer", + nullable: false, + comment: "排序值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "RequiredGrowth", + table: "member_tiers", + type: "integer", + nullable: false, + comment: "所需成长值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "member_tiers", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "等级名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_tiers", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_tiers", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_tiers", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_tiers", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "BenefitsJson", + table: "member_tiers", + type: "text", + nullable: false, + comment: "等级权益(JSON)。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_tiers", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "member_profiles", + type: "uuid", + nullable: false, + comment: "用户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_profiles", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_profiles", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "member_profiles", + type: "integer", + nullable: false, + comment: "会员状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "PointsBalance", + table: "member_profiles", + type: "integer", + nullable: false, + comment: "会员积分余额。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Nickname", + table: "member_profiles", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "昵称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Mobile", + table: "member_profiles", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "手机号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "MemberTierId", + table: "member_profiles", + type: "uuid", + nullable: true, + comment: "当前会员等级 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "JoinedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: false, + comment: "注册时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "GrowthValue", + table: "member_profiles", + type: "integer", + nullable: false, + comment: "成长值/经验值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_profiles", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_profiles", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "BirthDate", + table: "member_profiles", + type: "timestamp with time zone", + nullable: true, + comment: "生日。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AvatarUrl", + table: "member_profiles", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "头像。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_profiles", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_point_ledgers", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SourceId", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + comment: "来源 ID(订单、活动等)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "member_point_ledgers", + type: "integer", + nullable: false, + comment: "变动原因。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: false, + comment: "发生时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "MemberId", + table: "member_point_ledgers", + type: "uuid", + nullable: false, + comment: "会员标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ExpireAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: true, + comment: "过期时间(如适用)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ChangeAmount", + table: "member_point_ledgers", + type: "integer", + nullable: false, + comment: "变动数量,可为负值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "BalanceAfterChange", + table: "member_point_ledgers", + type: "integer", + nullable: false, + comment: "变动后余额。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_point_ledgers", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_growth_logs", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_growth_logs", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: false, + comment: "发生时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "member_growth_logs", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MemberId", + table: "member_growth_logs", + type: "uuid", + nullable: false, + comment: "会员标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_growth_logs", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CurrentValue", + table: "member_growth_logs", + type: "integer", + nullable: false, + comment: "当前成长值。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_growth_logs", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ChangeValue", + table: "member_growth_logs", + type: "integer", + nullable: false, + comment: "变动数量。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_growth_logs", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "map_locations", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "map_locations", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "map_locations", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "map_locations", + type: "uuid", + nullable: true, + comment: "关联门店 ID,可空表示独立 POI。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "map_locations", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "Longitude", + table: "map_locations", + type: "double precision", + nullable: false, + comment: "经度。", + oldClrType: typeof(double), + oldType: "double precision"); + + migrationBuilder.AlterColumn( + name: "Latitude", + table: "map_locations", + type: "double precision", + nullable: false, + comment: "纬度。", + oldClrType: typeof(double), + oldType: "double precision"); + + migrationBuilder.AlterColumn( + name: "Landmark", + table: "map_locations", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "打车/导航落点描述。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "map_locations", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "map_locations", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "map_locations", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "map_locations", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Address", + table: "map_locations", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "Id", + table: "map_locations", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "inventory_items", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "inventory_items", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "inventory_items", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "inventory_items", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SafetyStock", + table: "inventory_items", + type: "integer", + nullable: true, + comment: "安全库存阈值。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "QuantityReserved", + table: "inventory_items", + type: "integer", + nullable: false, + comment: "已锁定库存(订单占用)。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "QuantityOnHand", + table: "inventory_items", + type: "integer", + nullable: false, + comment: "可用库存。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductSkuId", + table: "inventory_items", + type: "uuid", + nullable: false, + comment: "SKU 标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Location", + table: "inventory_items", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "储位或仓位信息。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ExpireDate", + table: "inventory_items", + type: "timestamp with time zone", + nullable: true, + comment: "过期日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "inventory_items", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "inventory_items", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "inventory_items", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "inventory_items", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "BatchNumber", + table: "inventory_items", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "批次编号,可为空表示混批。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "inventory_items", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "inventory_batches", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "inventory_batches", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "inventory_batches", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "RemainingQuantity", + table: "inventory_batches", + type: "integer", + nullable: false, + comment: "剩余数量。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "inventory_batches", + type: "integer", + nullable: false, + comment: "入库数量。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductionDate", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + comment: "生产日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ProductSkuId", + table: "inventory_batches", + type: "uuid", + nullable: false, + comment: "SKU 标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ExpireDate", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + comment: "过期日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "inventory_batches", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "inventory_batches", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "BatchNumber", + table: "inventory_batches", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "批次编号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Id", + table: "inventory_batches", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "inventory_adjustments", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "inventory_adjustments", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "原因说明。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "inventory_adjustments", + type: "integer", + nullable: false, + comment: "调整数量,正数增加,负数减少。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "OperatorId", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + comment: "操作人标识。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: false, + comment: "发生时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "InventoryItemId", + table: "inventory_adjustments", + type: "uuid", + nullable: false, + comment: "对应的库存记录标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AdjustmentType", + table: "inventory_adjustments", + type: "integer", + nullable: false, + comment: "调整类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "inventory_adjustments", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "group_participants", + type: "uuid", + nullable: false, + comment: "用户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "group_participants", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "group_participants", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "group_participants", + type: "integer", + nullable: false, + comment: "参与状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "group_participants", + type: "uuid", + nullable: false, + comment: "对应订单标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "JoinedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: false, + comment: "参与时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "GroupOrderId", + table: "group_participants", + type: "uuid", + nullable: false, + comment: "拼单活动标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "group_participants", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "group_participants", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "group_participants", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "group_orders", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "group_orders", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "TargetCount", + table: "group_orders", + type: "integer", + nullable: false, + comment: "成团需要的人数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SucceededAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + comment: "成团时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "group_orders", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "group_orders", + type: "integer", + nullable: false, + comment: "拼团状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StartAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: false, + comment: "开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "group_orders", + type: "uuid", + nullable: false, + comment: "关联商品或套餐。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "LeaderUserId", + table: "group_orders", + type: "uuid", + nullable: false, + comment: "团长用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "GroupPrice", + table: "group_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "拼团价格。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "GroupOrderNo", + table: "group_orders", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "拼单编号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "EndAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: false, + comment: "结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "group_orders", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CurrentCount", + table: "group_orders", + type: "integer", + nullable: false, + comment: "当前已参与人数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "group_orders", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + comment: "取消时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "group_orders", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "delivery_orders", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "delivery_orders", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "delivery_orders", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProviderOrderId", + table: "delivery_orders", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "第三方配送单号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Provider", + table: "delivery_orders", + type: "integer", + nullable: false, + comment: "配送服务商。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "PickedUpAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + comment: "取餐时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FailureReason", + table: "delivery_orders", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "异常原因。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DispatchedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + comment: "下发时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeliveryFee", + table: "delivery_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + comment: "配送费。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeliveredAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + comment: "完成时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "delivery_orders", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "delivery_orders", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CourierPhone", + table: "delivery_orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "骑手电话。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CourierName", + table: "delivery_orders", + type: "character varying(64)", + maxLength: 64, + nullable: true, + comment: "骑手姓名。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "delivery_orders", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "delivery_events", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "delivery_events", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Payload", + table: "delivery_events", + type: "text", + nullable: true, + comment: "原始数据 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: false, + comment: "发生时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Message", + table: "delivery_events", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "事件描述。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "EventType", + table: "delivery_events", + type: "integer", + nullable: false, + comment: "事件类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeliveryOrderId", + table: "delivery_events", + type: "uuid", + nullable: false, + comment: "配送单标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "delivery_events", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "delivery_events", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "delivery_events", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "coupons", + type: "uuid", + nullable: false, + comment: "归属用户。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UsedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: true, + comment: "使用时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "coupons", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "coupons", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "coupons", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "coupons", + type: "uuid", + nullable: true, + comment: "订单 ID(已使用时记录)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IssuedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: false, + comment: "发放时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ExpireAt", + table: "coupons", + type: "timestamp with time zone", + nullable: false, + comment: "到期时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "coupons", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "coupons", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CouponTemplateId", + table: "coupons", + type: "uuid", + nullable: false, + comment: "模板标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "coupons", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "券码或序列号。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "Id", + table: "coupons", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Value", + table: "coupon_templates", + type: "numeric", + nullable: false, + comment: "面值或折扣额度。", + oldClrType: typeof(decimal), + oldType: "numeric"); + + migrationBuilder.AlterColumn( + name: "ValidTo", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + comment: "可用结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ValidFrom", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + comment: "可用开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "coupon_templates", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TotalQuantity", + table: "coupon_templates", + type: "integer", + nullable: true, + comment: "总发放数量上限。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "coupon_templates", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreScopeJson", + table: "coupon_templates", + type: "text", + nullable: true, + comment: "适用门店 ID 集合(JSON)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Status", + table: "coupon_templates", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "RelativeValidDays", + table: "coupon_templates", + type: "integer", + nullable: true, + comment: "有效天数(相对发放时间)。", + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ProductScopeJson", + table: "coupon_templates", + type: "text", + nullable: true, + comment: "适用品类或商品范围(JSON)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "coupon_templates", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "模板名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "MinimumSpend", + table: "coupon_templates", + type: "numeric", + nullable: true, + comment: "最低消费门槛。", + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DiscountCap", + table: "coupon_templates", + type: "numeric", + nullable: true, + comment: "折扣上限(针对折扣券)。", + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "coupon_templates", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "coupon_templates", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "coupon_templates", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CouponType", + table: "coupon_templates", + type: "integer", + nullable: false, + comment: "券类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ClaimedQuantity", + table: "coupon_templates", + type: "integer", + nullable: false, + comment: "已领取数量。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ChannelsJson", + table: "coupon_templates", + type: "text", + nullable: true, + comment: "发放渠道(JSON)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AllowStack", + table: "coupon_templates", + type: "boolean", + nullable: false, + comment: "是否允许叠加其他优惠。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "coupon_templates", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "community_reactions", + type: "uuid", + nullable: false, + comment: "用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "community_reactions", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "community_reactions", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ReactionType", + table: "community_reactions", + type: "integer", + nullable: false, + comment: "反应类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ReactedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: false, + comment: "时间戳。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "PostId", + table: "community_reactions", + type: "uuid", + nullable: false, + comment: "动态 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "community_reactions", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "community_reactions", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "community_reactions", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "community_posts", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "community_posts", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Title", + table: "community_posts", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "标题。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "community_posts", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "community_posts", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "MediaJson", + table: "community_posts", + type: "text", + nullable: true, + comment: "媒体资源 JSON。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LikeCount", + table: "community_posts", + type: "integer", + nullable: false, + comment: "点赞数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "community_posts", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "community_posts", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "community_posts", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "community_posts", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "community_posts", + type: "text", + nullable: false, + comment: "内容。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "CommentCount", + table: "community_posts", + type: "integer", + nullable: false, + comment: "评论数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "AuthorUserId", + table: "community_posts", + type: "uuid", + nullable: false, + comment: "作者用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "community_posts", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "community_comments", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "community_comments", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "community_comments", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "PostId", + table: "community_comments", + type: "uuid", + nullable: false, + comment: "动态标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ParentId", + table: "community_comments", + type: "uuid", + nullable: true, + comment: "父级评论 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IsDeleted", + table: "community_comments", + type: "boolean", + nullable: false, + comment: "状态。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "community_comments", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "community_comments", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "community_comments", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "community_comments", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "community_comments", + type: "character varying(512)", + maxLength: 512, + nullable: false, + comment: "评论内容。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512); + + migrationBuilder.AlterColumn( + name: "AuthorUserId", + table: "community_comments", + type: "uuid", + nullable: false, + comment: "评论人。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "community_comments", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "ValidationResultJson", + table: "checkout_sessions", + type: "text", + nullable: false, + comment: "校验结果明细 JSON。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "checkout_sessions", + type: "uuid", + nullable: false, + comment: "用户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "checkout_sessions", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "checkout_sessions", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "checkout_sessions", + type: "uuid", + nullable: false, + comment: "门店标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "checkout_sessions", + type: "integer", + nullable: false, + comment: "会话状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SessionToken", + table: "checkout_sessions", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "会话 Token。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "ExpiresAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: false, + comment: "过期时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "checkout_sessions", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "checkout_sessions", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "checkout_sessions", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "checkin_records", + type: "uuid", + nullable: false, + comment: "用户标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "checkin_records", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "checkin_records", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "checkin_records", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "RewardJson", + table: "checkin_records", + type: "text", + nullable: false, + comment: "获得奖励 JSON。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "IsMakeup", + table: "checkin_records", + type: "boolean", + nullable: false, + comment: "是否补签。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "checkin_records", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "checkin_records", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "checkin_records", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "checkin_records", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CheckInDate", + table: "checkin_records", + type: "timestamp with time zone", + nullable: false, + comment: "签到日期(本地)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CheckInCampaignId", + table: "checkin_records", + type: "uuid", + nullable: false, + comment: "活动标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "checkin_records", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "checkin_campaigns", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "checkin_campaigns", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "checkin_campaigns", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: false, + comment: "开始日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "RewardsJson", + table: "checkin_campaigns", + type: "text", + nullable: false, + comment: "连签奖励 JSON。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "checkin_campaigns", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "活动名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: false, + comment: "结束日期。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "checkin_campaigns", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "活动描述。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "checkin_campaigns", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "checkin_campaigns", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AllowMakeupCount", + table: "checkin_campaigns", + type: "integer", + nullable: false, + comment: "支持补签次数。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "checkin_campaigns", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "chat_sessions", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "chat_sessions", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "chat_sessions", + type: "uuid", + nullable: true, + comment: "所属门店(可空为平台)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Status", + table: "chat_sessions", + type: "integer", + nullable: false, + comment: "会话状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "StartedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: false, + comment: "开始时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "SessionCode", + table: "chat_sessions", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "会话编号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "IsBotActive", + table: "chat_sessions", + type: "boolean", + nullable: false, + comment: "是否机器人接待中。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "EndedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: true, + comment: "结束时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "chat_sessions", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CustomerUserId", + table: "chat_sessions", + type: "uuid", + nullable: false, + comment: "顾客用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "chat_sessions", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AgentUserId", + table: "chat_sessions", + type: "uuid", + nullable: true, + comment: "当前客服员工 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "chat_sessions", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "chat_messages", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "chat_messages", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SenderUserId", + table: "chat_messages", + type: "uuid", + nullable: true, + comment: "发送方用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SenderType", + table: "chat_messages", + type: "integer", + nullable: false, + comment: "发送方类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ReadAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: true, + comment: "读取时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "IsRead", + table: "chat_messages", + type: "boolean", + nullable: false, + comment: "是否已读。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "chat_messages", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "chat_messages", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ContentType", + table: "chat_messages", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "消息类型(文字/图片/语音等)。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Content", + table: "chat_messages", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + comment: "消息内容。", + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024); + + migrationBuilder.AlterColumn( + name: "ChatSessionId", + table: "chat_messages", + type: "uuid", + nullable: false, + comment: "会话标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "chat_messages", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "cart_items", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "cart_items", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UnitPrice", + table: "cart_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "单价快照。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "cart_items", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "cart_items", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ShoppingCartId", + table: "cart_items", + type: "uuid", + nullable: false, + comment: "所属购物车标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "cart_items", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "自定义备注(口味要求)。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "cart_items", + type: "integer", + nullable: false, + comment: "数量。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "ProductSkuId", + table: "cart_items", + type: "uuid", + nullable: true, + comment: "SKU 标识。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ProductName", + table: "cart_items", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "商品名称快照。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "cart_items", + type: "uuid", + nullable: false, + comment: "商品或 SKU 标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "cart_items", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "cart_items", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "cart_items", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "cart_items", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "AttributesJson", + table: "cart_items", + type: "text", + nullable: true, + comment: "扩展 JSON(规格、加料选项等)。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "cart_items", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "cart_item_addons", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "cart_item_addons", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "cart_item_addons", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OptionId", + table: "cart_item_addons", + type: "uuid", + nullable: true, + comment: "选项 ID(可对应 ProductAddonOption)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "cart_item_addons", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "选项名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "ExtraPrice", + table: "cart_item_addons", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "附加价格。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "cart_item_addons", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "cart_item_addons", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "cart_item_addons", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "cart_item_addons", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CartItemId", + table: "cart_item_addons", + type: "uuid", + nullable: false, + comment: "所属购物车条目。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "cart_item_addons", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "affiliate_payouts", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "affiliate_payouts", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "affiliate_payouts", + type: "integer", + nullable: false, + comment: "状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "affiliate_payouts", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Period", + table: "affiliate_payouts", + type: "character varying(32)", + maxLength: 32, + nullable: false, + comment: "结算周期描述。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32); + + migrationBuilder.AlterColumn( + name: "PaidAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: true, + comment: "打款时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "affiliate_payouts", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "affiliate_payouts", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "affiliate_payouts", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "结算金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "AffiliatePartnerId", + table: "affiliate_payouts", + type: "uuid", + nullable: false, + comment: "合作伙伴标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "affiliate_payouts", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "affiliate_partners", + type: "uuid", + nullable: true, + comment: "用户 ID(如绑定平台账号)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "affiliate_partners", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "affiliate_partners", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "affiliate_partners", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "affiliate_partners", + type: "integer", + nullable: false, + comment: "当前状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "affiliate_partners", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "审核备注。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "affiliate_partners", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "联系电话。", + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "affiliate_partners", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "昵称或渠道名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "affiliate_partners", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "affiliate_partners", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "affiliate_partners", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "affiliate_partners", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CommissionRate", + table: "affiliate_partners", + type: "numeric", + nullable: false, + comment: "分成比例(0-1)。", + oldClrType: typeof(decimal), + oldType: "numeric"); + + migrationBuilder.AlterColumn( + name: "ChannelType", + table: "affiliate_partners", + type: "integer", + nullable: false, + comment: "渠道类型。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "affiliate_partners", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "affiliate_orders", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "affiliate_orders", + type: "integer", + nullable: false, + comment: "当前状态。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "SettledAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: true, + comment: "结算完成时间。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + comment: "关联订单。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OrderAmount", + table: "affiliate_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "订单金额。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "EstimatedCommission", + table: "affiliate_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + comment: "预计佣金。", + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "affiliate_orders", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "affiliate_orders", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "BuyerUserId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + comment: "用户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "AffiliatePartnerId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + comment: "推广人标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "affiliate_orders", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterTable( + name: "ticket_comments", + oldComment: "工单评论/流转记录。"); + + migrationBuilder.AlterTable( + name: "tenants", + oldComment: "平台租户信息,描述租户的生命周期与基础资料。"); + + migrationBuilder.AlterTable( + name: "tenant_subscriptions", + oldComment: "租户套餐订阅记录。"); + + migrationBuilder.AlterTable( + name: "tenant_quota_usages", + oldComment: "租户配额使用情况快照。"); + + migrationBuilder.AlterTable( + name: "tenant_packages", + oldComment: "平台提供的租户套餐定义。"); + + migrationBuilder.AlterTable( + name: "tenant_notifications", + oldComment: "面向租户的站内通知或消息推送。"); + + migrationBuilder.AlterTable( + name: "tenant_billing_statements", + oldComment: "租户账单,用于呈现周期性收费。"); + + migrationBuilder.AlterTable( + name: "support_tickets", + oldComment: "客服工单。"); + + migrationBuilder.AlterTable( + name: "stores", + oldComment: "门店信息,承载营业配置与能力。"); + + migrationBuilder.AlterTable( + name: "store_tables", + oldComment: "桌台信息与二维码绑定。"); + + migrationBuilder.AlterTable( + name: "store_table_areas", + oldComment: "门店桌台区域配置。"); + + migrationBuilder.AlterTable( + name: "store_holidays", + oldComment: "门店休息日或特殊营业日。"); + + migrationBuilder.AlterTable( + name: "store_employee_shifts", + oldComment: "门店员工排班记录。"); + + migrationBuilder.AlterTable( + name: "store_delivery_zones", + oldComment: "门店配送范围配置。"); + + migrationBuilder.AlterTable( + name: "store_business_hours", + oldComment: "门店营业时段配置。"); + + migrationBuilder.AlterTable( + name: "shopping_carts", + oldComment: "用户购物车,按租户/门店隔离。"); + + migrationBuilder.AlterTable( + name: "reservations", + oldComment: "预约/预订记录。"); + + migrationBuilder.AlterTable( + name: "refund_requests", + oldComment: "售后/退款申请。"); + + migrationBuilder.AlterTable( + name: "queue_tickets", + oldComment: "排队叫号。"); + + migrationBuilder.AlterTable( + name: "promotion_campaigns", + oldComment: "营销活动配置。"); + + migrationBuilder.AlterTable( + name: "products", + oldComment: "商品(SPU)信息。"); + + migrationBuilder.AlterTable( + name: "product_skus", + oldComment: "商品 SKU,记录具体规格组合价格。"); + + migrationBuilder.AlterTable( + name: "product_pricing_rules", + oldComment: "商品价格策略,支持会员价/时段价等。"); + + migrationBuilder.AlterTable( + name: "product_media_assets", + oldComment: "商品媒资素材。"); + + migrationBuilder.AlterTable( + name: "product_categories", + oldComment: "商品分类。"); + + migrationBuilder.AlterTable( + name: "product_attribute_options", + oldComment: "商品规格选项。"); + + migrationBuilder.AlterTable( + name: "product_attribute_groups", + oldComment: "商品规格/属性分组。"); + + migrationBuilder.AlterTable( + name: "product_addon_options", + oldComment: "加料选项。"); + + migrationBuilder.AlterTable( + name: "product_addon_groups", + oldComment: "加料/做法分组。"); + + migrationBuilder.AlterTable( + name: "payment_refund_records", + oldComment: "支付渠道退款流水。"); + + migrationBuilder.AlterTable( + name: "payment_records", + oldComment: "支付流水。"); + + migrationBuilder.AlterTable( + name: "orders", + oldComment: "交易订单。"); + + migrationBuilder.AlterTable( + name: "order_status_histories", + oldComment: "订单状态流转记录。"); + + migrationBuilder.AlterTable( + name: "order_items", + oldComment: "订单明细。"); + + migrationBuilder.AlterTable( + name: "navigation_requests", + oldComment: "用户发起的导航请求日志。"); + + migrationBuilder.AlterTable( + name: "metric_snapshots", + oldComment: "指标快照,用于大盘展示。"); + + migrationBuilder.AlterTable( + name: "metric_definitions", + oldComment: "指标定义,描述可观测的数据点。"); + + migrationBuilder.AlterTable( + name: "metric_alert_rules", + oldComment: "指标告警规则。"); + + migrationBuilder.AlterTable( + name: "merchants", + oldComment: "商户主体信息,承载入驻和资质审核结果。"); + + migrationBuilder.AlterTable( + name: "merchant_staff", + oldComment: "商户员工账号,支持门店维度分配。"); + + migrationBuilder.AlterTable( + name: "merchant_documents", + oldComment: "商户提交的资质或证照材料。"); + + migrationBuilder.AlterTable( + name: "merchant_contracts", + oldComment: "商户合同记录。"); + + migrationBuilder.AlterTable( + name: "member_tiers", + oldComment: "会员等级定义。"); + + migrationBuilder.AlterTable( + name: "member_profiles", + oldComment: "会员档案。"); + + migrationBuilder.AlterTable( + name: "member_point_ledgers", + oldComment: "积分变动流水。"); + + migrationBuilder.AlterTable( + name: "member_growth_logs", + oldComment: "成长值变动日志。"); + + migrationBuilder.AlterTable( + name: "map_locations", + oldComment: "地图 POI 信息,用于门店定位和推荐。"); + + migrationBuilder.AlterTable( + name: "inventory_items", + oldComment: "SKU 在门店的库存信息。"); + + migrationBuilder.AlterTable( + name: "inventory_batches", + oldComment: "SKU 批次信息。"); + + migrationBuilder.AlterTable( + name: "inventory_adjustments", + oldComment: "库存调整记录。"); + + migrationBuilder.AlterTable( + name: "group_participants", + oldComment: "拼单参与者。"); + + migrationBuilder.AlterTable( + name: "group_orders", + oldComment: "拼单活动。"); + + migrationBuilder.AlterTable( + name: "delivery_orders", + oldComment: "配送单。"); + + migrationBuilder.AlterTable( + name: "delivery_events", + oldComment: "配送状态事件流水。"); + + migrationBuilder.AlterTable( + name: "coupons", + oldComment: "用户领取的券。"); + + migrationBuilder.AlterTable( + name: "coupon_templates", + oldComment: "优惠券模板。"); + + migrationBuilder.AlterTable( + name: "community_reactions", + oldComment: "社区互动反馈。"); + + migrationBuilder.AlterTable( + name: "community_posts", + oldComment: "社区动态。"); + + migrationBuilder.AlterTable( + name: "community_comments", + oldComment: "社区评论。"); + + migrationBuilder.AlterTable( + name: "checkout_sessions", + oldComment: "结账会话,记录校验上下文。"); + + migrationBuilder.AlterTable( + name: "checkin_records", + oldComment: "用户签到记录。"); + + migrationBuilder.AlterTable( + name: "checkin_campaigns", + oldComment: "签到活动配置。"); + + migrationBuilder.AlterTable( + name: "chat_sessions", + oldComment: "客服会话。"); + + migrationBuilder.AlterTable( + name: "chat_messages", + oldComment: "会话消息。"); + + migrationBuilder.AlterTable( + name: "cart_items", + oldComment: "购物车条目。"); + + migrationBuilder.AlterTable( + name: "cart_item_addons", + oldComment: "购物车条目的加料/附加项。"); + + migrationBuilder.AlterTable( + name: "affiliate_payouts", + oldComment: "佣金结算记录。"); + + migrationBuilder.AlterTable( + name: "affiliate_partners", + oldComment: "分销/推广合作伙伴。"); + + migrationBuilder.AlterTable( + name: "affiliate_orders", + oldComment: "分销订单记录。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "ticket_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ticket_comments", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "ticket_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SupportTicketId", + table: "ticket_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "工单标识。"); + + migrationBuilder.AlterColumn( + name: "IsInternal", + table: "ticket_comments", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否内部备注。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "ticket_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ticket_comments", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "ticket_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ticket_comments", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "ticket_comments", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024, + oldComment: "评论内容。"); + + migrationBuilder.AlterColumn( + name: "AuthorUserId", + table: "ticket_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "评论人 ID。"); + + migrationBuilder.AlterColumn( + name: "AttachmentsJson", + table: "ticket_comments", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "附件 JSON。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "ticket_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "Website", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "官网或主要宣传链接。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "Tags", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "业务标签集合(逗号分隔)。"); + + migrationBuilder.AlterColumn( + name: "SuspensionReason", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "暂停或终止的原因说明。"); + + migrationBuilder.AlterColumn( + name: "SuspendedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次暂停服务时间。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "tenants", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "租户当前状态,涵盖审核、启用、停用等场景。"); + + migrationBuilder.AlterColumn( + name: "ShortName", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "对外展示的简称。"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "tenants", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "备注信息,用于运营记录特殊说明。"); + + migrationBuilder.AlterColumn( + name: "Province", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "所在省份或州。"); + + migrationBuilder.AlterColumn( + name: "PrimaryOwnerUserId", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "系统内对应的租户所有者账号 ID。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "tenants", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "租户全称或品牌名称。"); + + migrationBuilder.AlterColumn( + name: "LogoUrl", + table: "tenants", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "LOGO 图片地址。"); + + migrationBuilder.AlterColumn( + name: "LegalEntityName", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "法人或公司主体名称。"); + + migrationBuilder.AlterColumn( + name: "Industry", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "所属行业,如餐饮、零售等。"); + + migrationBuilder.AlterColumn( + name: "EffectiveTo", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "服务到期时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "EffectiveFrom", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "服务生效时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenants", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CoverImageUrl", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "品牌海报或封面图。"); + + migrationBuilder.AlterColumn( + name: "Country", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "所在国家/地区。"); + + migrationBuilder.AlterColumn( + name: "ContactPhone", + table: "tenants", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "主联系人电话。"); + + migrationBuilder.AlterColumn( + name: "ContactName", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "主联系人姓名。"); + + migrationBuilder.AlterColumn( + name: "ContactEmail", + table: "tenants", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "主联系人邮箱。"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "tenants", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "租户短编码,作为跨系统引用的唯一标识。"); + + migrationBuilder.AlterColumn( + name: "City", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "所在城市。"); + + migrationBuilder.AlterColumn( + name: "Address", + table: "tenants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "详细地址信息。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantPackageId", + table: "tenant_subscriptions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "当前订阅关联的套餐标识。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_subscriptions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "tenant_subscriptions", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "订阅当前状态。"); + + migrationBuilder.AlterColumn( + name: "ScheduledPackageId", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "若已排期升降配,对应的新套餐 ID。"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "tenant_subscriptions", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "运营备注信息。"); + + migrationBuilder.AlterColumn( + name: "NextBillingDate", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "下一个计费时间,配合自动续费使用。"); + + migrationBuilder.AlterColumn( + name: "EffectiveTo", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "订阅到期时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "EffectiveFrom", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "订阅生效时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_subscriptions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_subscriptions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AutoRenew", + table: "tenant_subscriptions", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否开启自动续费。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_subscriptions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UsedValue", + table: "tenant_quota_usages", + type: "numeric", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric", + oldComment: "已消耗的数量。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_quota_usages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_quota_usages", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "ResetCycle", + table: "tenant_quota_usages", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "配额刷新周期描述(如月、年)。"); + + migrationBuilder.AlterColumn( + name: "QuotaType", + table: "tenant_quota_usages", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "配额类型,例如门店数、短信条数等。"); + + migrationBuilder.AlterColumn( + name: "LimitValue", + table: "tenant_quota_usages", + type: "numeric", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric", + oldComment: "当前配额上限。"); + + migrationBuilder.AlterColumn( + name: "LastResetAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次重置时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_quota_usages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_quota_usages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_quota_usages", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_quota_usages", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "YearlyPrice", + table: "tenant_packages", + type: "numeric", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true, + oldComment: "年付价格,单位:人民币元。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_packages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_packages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "PackageType", + table: "tenant_packages", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "套餐分类(试用、标准、旗舰等)。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "tenant_packages", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "套餐名称,展示给租户的简称。"); + + migrationBuilder.AlterColumn( + name: "MonthlyPrice", + table: "tenant_packages", + type: "numeric", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true, + oldComment: "月付价格,单位:人民币元。"); + + migrationBuilder.AlterColumn( + name: "MaxStoreCount", + table: "tenant_packages", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "允许的最大门店数。"); + + migrationBuilder.AlterColumn( + name: "MaxStorageGb", + table: "tenant_packages", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "存储容量上限(GB)。"); + + migrationBuilder.AlterColumn( + name: "MaxSmsCredits", + table: "tenant_packages", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "每月短信额度上限。"); + + migrationBuilder.AlterColumn( + name: "MaxDeliveryOrders", + table: "tenant_packages", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "每月可调用的配送单数量上限。"); + + migrationBuilder.AlterColumn( + name: "MaxAccountCount", + table: "tenant_packages", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "允许创建的最大账号数。"); + + migrationBuilder.AlterColumn( + name: "IsActive", + table: "tenant_packages", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否仍可售卖。"); + + migrationBuilder.AlterColumn( + name: "FeaturePoliciesJson", + table: "tenant_packages", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "权益明细 JSON,记录自定义特性开关。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "tenant_packages", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "套餐描述,包含适用场景、权益等。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_packages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_packages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_packages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_packages", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_packages", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_notifications", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "Title", + table: "tenant_notifications", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "通知标题。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_notifications", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Severity", + table: "tenant_notifications", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "通知重要级别。"); + + migrationBuilder.AlterColumn( + name: "SentAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "推送时间。"); + + migrationBuilder.AlterColumn( + name: "ReadAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "租户是否已阅读。"); + + migrationBuilder.AlterColumn( + name: "MetadataJson", + table: "tenant_notifications", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "附加元数据 JSON。"); + + migrationBuilder.AlterColumn( + name: "Message", + table: "tenant_notifications", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024, + oldComment: "通知正文。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_notifications", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_notifications", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_notifications", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Channel", + table: "tenant_notifications", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "发布通道(站内、邮件、短信等)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_notifications", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "tenant_billing_statements", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "tenant_billing_statements", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "tenant_billing_statements", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前付款状态。"); + + migrationBuilder.AlterColumn( + name: "StatementNo", + table: "tenant_billing_statements", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "账单编号,供对账查询。"); + + migrationBuilder.AlterColumn( + name: "PeriodStart", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "账单周期开始时间。"); + + migrationBuilder.AlterColumn( + name: "PeriodEnd", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "账单周期结束时间。"); + + migrationBuilder.AlterColumn( + name: "LineItemsJson", + table: "tenant_billing_statements", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "账单明细 JSON,记录各项费用。"); + + migrationBuilder.AlterColumn( + name: "DueDate", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "到期日。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "tenant_billing_statements", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "tenant_billing_statements", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "tenant_billing_statements", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AmountPaid", + table: "tenant_billing_statements", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "实付金额。"); + + migrationBuilder.AlterColumn( + name: "AmountDue", + table: "tenant_billing_statements", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "应付金额。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "tenant_billing_statements", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "support_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TicketNo", + table: "support_tickets", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "工单编号。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "support_tickets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Subject", + table: "support_tickets", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "工单主题。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "support_tickets", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "Priority", + table: "support_tickets", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "优先级。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "support_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "关联订单(如有)。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "support_tickets", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "工单详情。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "support_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CustomerUserId", + table: "support_tickets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "客户用户 ID。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "support_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ClosedAt", + table: "support_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "关闭时间。"); + + migrationBuilder.AlterColumn( + name: "AssignedAgentId", + table: "support_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "指派的客服。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "support_tickets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "stores", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "stores", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "stores", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Tags", + table: "stores", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "门店标签(逗号分隔)。"); + + migrationBuilder.AlterColumn( + name: "SupportsReservation", + table: "stores", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "支持预约。"); + + migrationBuilder.AlterColumn( + name: "SupportsQueueing", + table: "stores", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "支持排队叫号。"); + + migrationBuilder.AlterColumn( + name: "SupportsPickup", + table: "stores", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否支持自提。"); + + migrationBuilder.AlterColumn( + name: "SupportsDineIn", + table: "stores", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否支持堂食。"); + + migrationBuilder.AlterColumn( + name: "SupportsDelivery", + table: "stores", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否支持配送。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "stores", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "门店当前运营状态。"); + + migrationBuilder.AlterColumn( + name: "Province", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "所在省份。"); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "stores", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "联系电话。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "stores", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "门店名称。"); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "stores", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商户标识。"); + + migrationBuilder.AlterColumn( + name: "ManagerName", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "门店负责人姓名。"); + + migrationBuilder.AlterColumn( + name: "Longitude", + table: "stores", + type: "double precision", + nullable: true, + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true, + oldComment: "高德/腾讯地图经度。"); + + migrationBuilder.AlterColumn( + name: "Latitude", + table: "stores", + type: "double precision", + nullable: true, + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true, + oldComment: "纬度。"); + + migrationBuilder.AlterColumn( + name: "District", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "区县信息。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "stores", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "门店描述或公告。"); + + migrationBuilder.AlterColumn( + name: "DeliveryRadiusKm", + table: "stores", + type: "numeric(6,2)", + precision: 6, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(6,2)", + oldPrecision: 6, + oldScale: 2, + oldComment: "默认配送半径(公里)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "stores", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "stores", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "stores", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "stores", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CoverImageUrl", + table: "stores", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "门店海报。"); + + migrationBuilder.AlterColumn( + name: "Country", + table: "stores", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "所在国家或地区。"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "stores", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "门店编码,便于扫码及外部对接。"); + + migrationBuilder.AlterColumn( + name: "City", + table: "stores", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "所在城市。"); + + migrationBuilder.AlterColumn( + name: "BusinessHours", + table: "stores", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "门店营业时段描述(备用字符串)。"); + + migrationBuilder.AlterColumn( + name: "Announcement", + table: "stores", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "门店公告。"); + + migrationBuilder.AlterColumn( + name: "Address", + table: "stores", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "详细地址。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "stores", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_tables", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_tables", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_tables", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Tags", + table: "store_tables", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "桌台标签(堂食、快餐等)。"); + + migrationBuilder.AlterColumn( + name: "TableCode", + table: "store_tables", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "桌码。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_tables", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "store_tables", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前桌台状态。"); + + migrationBuilder.AlterColumn( + name: "QrCodeUrl", + table: "store_tables", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "桌码二维码地址。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_tables", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_tables", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_tables", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_tables", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Capacity", + table: "store_tables", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "可容纳人数。"); + + migrationBuilder.AlterColumn( + name: "AreaId", + table: "store_tables", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "所在区域 ID。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_tables", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_table_areas", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_table_areas", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_table_areas", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_table_areas", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "store_table_areas", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "区域名称。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "store_table_areas", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "区域描述。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_table_areas", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_table_areas", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_table_areas", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_table_areas", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_table_areas", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_holidays", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_holidays", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_holidays", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_holidays", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "store_holidays", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "说明内容。"); + + migrationBuilder.AlterColumn( + name: "IsClosed", + table: "store_holidays", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否全天闭店。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_holidays", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_holidays", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "Date", + table: "store_holidays", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "日期。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_holidays", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_holidays", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_holidays", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_employee_shifts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "store_employee_shifts", + type: "interval", + nullable: false, + oldClrType: typeof(TimeSpan), + oldType: "interval", + oldComment: "开始时间。"); + + migrationBuilder.AlterColumn( + name: "StaffId", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "员工标识。"); + + migrationBuilder.AlterColumn( + name: "ShiftDate", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "班次日期。"); + + migrationBuilder.AlterColumn( + name: "RoleType", + table: "store_employee_shifts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排班角色。"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "store_employee_shifts", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "store_employee_shifts", + type: "interval", + nullable: false, + oldClrType: typeof(TimeSpan), + oldType: "interval", + oldComment: "结束时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_employee_shifts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_employee_shifts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_employee_shifts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_employee_shifts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "ZoneName", + table: "store_delivery_zones", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "区域名称。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_delivery_zones", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_delivery_zones", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_delivery_zones", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_delivery_zones", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "PolygonGeoJson", + table: "store_delivery_zones", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "GeoJSON 表示的多边形范围。"); + + migrationBuilder.AlterColumn( + name: "MinimumOrderAmount", + table: "store_delivery_zones", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "起送价。"); + + migrationBuilder.AlterColumn( + name: "EstimatedMinutes", + table: "store_delivery_zones", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "预计送达分钟。"); + + migrationBuilder.AlterColumn( + name: "DeliveryFee", + table: "store_delivery_zones", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "配送费。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_delivery_zones", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_delivery_zones", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_delivery_zones", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_delivery_zones", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_delivery_zones", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "store_business_hours", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "store_business_hours", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "store_business_hours", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "store_business_hours", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "store_business_hours", + type: "interval", + nullable: false, + oldClrType: typeof(TimeSpan), + oldType: "interval", + oldComment: "开始时间(本地时间)。"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "store_business_hours", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "HourType", + table: "store_business_hours", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "时段类型(正常营业、休息、预约等)。"); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "store_business_hours", + type: "interval", + nullable: false, + oldClrType: typeof(TimeSpan), + oldType: "interval", + oldComment: "结束时间(本地时间)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "store_business_hours", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "store_business_hours", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DayOfWeek", + table: "store_business_hours", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "星期几,0 表示周日。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "store_business_hours", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "store_business_hours", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CapacityLimit", + table: "store_business_hours", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "最大接待容量或单量限制。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "store_business_hours", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "shopping_carts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "shopping_carts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "shopping_carts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "TableContext", + table: "shopping_carts", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "桌码或场景标识(扫码点餐)。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "shopping_carts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "shopping_carts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "购物车状态,包含正常/锁定。"); + + migrationBuilder.AlterColumn( + name: "LastModifiedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "最近一次修改时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "DeliveryPreference", + table: "shopping_carts", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "履约方式(堂食/自提/配送)缓存。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "shopping_carts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "shopping_carts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "shopping_carts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "shopping_carts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "reservations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "reservations", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "TablePreference", + table: "reservations", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "桌型/标签。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "reservations", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "reservations", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "ReservationTime", + table: "reservations", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "预约时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ReservationNo", + table: "reservations", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "预约号。"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "reservations", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "PeopleCount", + table: "reservations", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "用餐人数。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "reservations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CustomerPhone", + table: "reservations", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "联系电话。"); + + migrationBuilder.AlterColumn( + name: "CustomerName", + table: "reservations", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "客户姓名。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "reservations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "reservations", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CheckedInAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "实际签到时间。"); + + migrationBuilder.AlterColumn( + name: "CheckInCode", + table: "reservations", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "核销码/到店码。"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "reservations", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "取消时间。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "reservations", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "refund_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "refund_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "refund_requests", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "退款状态。"); + + migrationBuilder.AlterColumn( + name: "ReviewNotes", + table: "refund_requests", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "审核备注。"); + + migrationBuilder.AlterColumn( + name: "RequestedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "用户提交时间。"); + + migrationBuilder.AlterColumn( + name: "RefundNo", + table: "refund_requests", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "退款单号。"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "refund_requests", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "申请原因。"); + + migrationBuilder.AlterColumn( + name: "ProcessedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "审核完成时间。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "refund_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联订单标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "refund_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "refund_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "refund_requests", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "refund_requests", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "申请金额。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "refund_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "queue_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TicketNumber", + table: "queue_tickets", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "排队编号。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "queue_tickets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "queue_tickets", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "queue_tickets", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "PartySize", + table: "queue_tickets", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "就餐人数。"); + + migrationBuilder.AlterColumn( + name: "ExpiredAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "过号时间。"); + + migrationBuilder.AlterColumn( + name: "EstimatedWaitMinutes", + table: "queue_tickets", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "预计等待分钟。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "queue_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "queue_tickets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "取消时间。"); + + migrationBuilder.AlterColumn( + name: "CalledAt", + table: "queue_tickets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "叫号时间。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "queue_tickets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "promotion_campaigns", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "promotion_campaigns", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "promotion_campaigns", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "活动状态。"); + + migrationBuilder.AlterColumn( + name: "StartAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "开始时间。"); + + migrationBuilder.AlterColumn( + name: "RulesJson", + table: "promotion_campaigns", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "活动规则 JSON。"); + + migrationBuilder.AlterColumn( + name: "PromotionType", + table: "promotion_campaigns", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "活动类型。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "promotion_campaigns", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "活动名称。"); + + migrationBuilder.AlterColumn( + name: "EndAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "结束时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "promotion_campaigns", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "promotion_campaigns", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "promotion_campaigns", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Budget", + table: "promotion_campaigns", + type: "numeric", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true, + oldComment: "预算金额。"); + + migrationBuilder.AlterColumn( + name: "BannerUrl", + table: "promotion_campaigns", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "营销素材(如 banner)。"); + + migrationBuilder.AlterColumn( + name: "AudienceDescription", + table: "promotion_campaigns", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "目标人群描述。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "promotion_campaigns", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "products", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "products", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "Unit", + table: "products", + type: "character varying(16)", + maxLength: 16, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(16)", + oldMaxLength: 16, + oldNullable: true, + oldComment: "售卖单位(份/杯等)。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "products", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Subtitle", + table: "products", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "副标题/卖点。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "products", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属门店。"); + + migrationBuilder.AlterColumn( + name: "StockQuantity", + table: "products", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "库存数量(可选)。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "products", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "商品状态。"); + + migrationBuilder.AlterColumn( + name: "SpuCode", + table: "products", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "商品编码。"); + + migrationBuilder.AlterColumn( + name: "Price", + table: "products", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "现价。"); + + migrationBuilder.AlterColumn( + name: "OriginalPrice", + table: "products", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "原价。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "products", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "商品名称。"); + + migrationBuilder.AlterColumn( + name: "MaxQuantityPerOrder", + table: "products", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "最大每单限购。"); + + migrationBuilder.AlterColumn( + name: "IsFeatured", + table: "products", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否热门推荐。"); + + migrationBuilder.AlterColumn( + name: "GalleryImages", + table: "products", + type: "character varying(1024)", + maxLength: 1024, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024, + oldNullable: true, + oldComment: "Gallery 图片逗号分隔。"); + + migrationBuilder.AlterColumn( + name: "EnablePickup", + table: "products", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "支持自提。"); + + migrationBuilder.AlterColumn( + name: "EnableDineIn", + table: "products", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "支持堂食。"); + + migrationBuilder.AlterColumn( + name: "EnableDelivery", + table: "products", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "支持配送。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "products", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "商品描述。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "products", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "products", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "products", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "products", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CoverImage", + table: "products", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "主图。"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "products", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属分类。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "products", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "Weight", + table: "product_skus", + type: "numeric(10,3)", + precision: 10, + scale: 3, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(10,3)", + oldPrecision: 10, + oldScale: 3, + oldNullable: true, + oldComment: "重量(千克)。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_skus", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_skus", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_skus", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StockQuantity", + table: "product_skus", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "可售库存。"); + + migrationBuilder.AlterColumn( + name: "SkuCode", + table: "product_skus", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "SKU 编码。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_skus", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商品标识。"); + + migrationBuilder.AlterColumn( + name: "Price", + table: "product_skus", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "售价。"); + + migrationBuilder.AlterColumn( + name: "OriginalPrice", + table: "product_skus", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "原价。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_skus", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_skus", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_skus", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_skus", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Barcode", + table: "product_skus", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "条形码。"); + + migrationBuilder.AlterColumn( + name: "AttributesJson", + table: "product_skus", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "规格属性 JSON(记录选项 ID)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_skus", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "WeekdaysJson", + table: "product_pricing_rules", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "生效星期(JSON 数组)。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_pricing_rules", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_pricing_rules", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StartTime", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "生效开始时间。"); + + migrationBuilder.AlterColumn( + name: "RuleType", + table: "product_pricing_rules", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "策略类型。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_pricing_rules", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商品。"); + + migrationBuilder.AlterColumn( + name: "Price", + table: "product_pricing_rules", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "特殊价格。"); + + migrationBuilder.AlterColumn( + name: "EndTime", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "生效结束时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_pricing_rules", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_pricing_rules", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_pricing_rules", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ConditionsJson", + table: "product_pricing_rules", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "条件描述(JSON),如会员等级、渠道等。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_pricing_rules", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "Url", + table: "product_media_assets", + type: "character varying(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldComment: "媒资链接。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_media_assets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_media_assets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_media_assets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_media_assets", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排序。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_media_assets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "商品标识。"); + + migrationBuilder.AlterColumn( + name: "MediaType", + table: "product_media_assets", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "媒体类型。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_media_assets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_media_assets", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_media_assets", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_media_assets", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Caption", + table: "product_media_assets", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "描述或标题。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_media_assets", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_categories", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_categories", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "product_categories", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属门店。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_categories", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排序值。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_categories", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "分类名称。"); + + migrationBuilder.AlterColumn( + name: "IsEnabled", + table: "product_categories", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否启用。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "product_categories", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "分类描述。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_categories", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_categories", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_categories", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_attribute_options", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_attribute_options", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_attribute_options", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_attribute_options", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排序。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_attribute_options", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "选项名称。"); + + migrationBuilder.AlterColumn( + name: "IsDefault", + table: "product_attribute_options", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否默认选中。"); + + migrationBuilder.AlterColumn( + name: "ExtraPrice", + table: "product_attribute_options", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "附加价格。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_attribute_options", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_attribute_options", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_attribute_options", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_attribute_options", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AttributeGroupId", + table: "product_attribute_options", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属规格组。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_attribute_options", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_attribute_groups", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_attribute_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "关联门店,可为空表示所有门店共享。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_attribute_groups", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "显示排序。"); + + migrationBuilder.AlterColumn( + name: "SelectionType", + table: "product_attribute_groups", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "选择类型(单选/多选)。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_attribute_groups", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "分组名称,例如“辣度”“份量”。"); + + migrationBuilder.AlterColumn( + name: "IsRequired", + table: "product_attribute_groups", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否必选。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_attribute_groups", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_attribute_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_attribute_groups", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_attribute_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_addon_options", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_addon_options", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_addon_options", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_addon_options", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排序。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_addon_options", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "选项名称。"); + + migrationBuilder.AlterColumn( + name: "IsDefault", + table: "product_addon_options", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否默认选项。"); + + migrationBuilder.AlterColumn( + name: "ExtraPrice", + table: "product_addon_options", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "附加价格。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_addon_options", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_addon_options", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_addon_options", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_addon_options", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AddonGroupId", + table: "product_addon_options", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属加料分组。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_addon_options", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "product_addon_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "product_addon_groups", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "product_addon_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "product_addon_groups", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排序值。"); + + migrationBuilder.AlterColumn( + name: "SelectionType", + table: "product_addon_groups", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "选择类型。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "product_addon_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商品。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "product_addon_groups", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "分组名称。"); + + migrationBuilder.AlterColumn( + name: "MinSelect", + table: "product_addon_groups", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "最小选择数量。"); + + migrationBuilder.AlterColumn( + name: "MaxSelect", + table: "product_addon_groups", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "最大选择数量。"); + + migrationBuilder.AlterColumn( + name: "IsRequired", + table: "product_addon_groups", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否必选。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "product_addon_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "product_addon_groups", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "product_addon_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "product_addon_groups", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "product_addon_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "payment_refund_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "payment_refund_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "payment_refund_records", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "退款状态。"); + + migrationBuilder.AlterColumn( + name: "RequestedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "退款请求时间。"); + + migrationBuilder.AlterColumn( + name: "PaymentRecordId", + table: "payment_refund_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "原支付记录标识。"); + + migrationBuilder.AlterColumn( + name: "Payload", + table: "payment_refund_records", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "渠道返回的原始数据 JSON。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "payment_refund_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联订单标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "payment_refund_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "payment_refund_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CompletedAt", + table: "payment_refund_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "完成时间。"); + + migrationBuilder.AlterColumn( + name: "ChannelRefundId", + table: "payment_refund_records", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "渠道退款流水号。"); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "payment_refund_records", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "退款金额。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "payment_refund_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "payment_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TradeNo", + table: "payment_records", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "平台交易号。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "payment_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "payment_records", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "支付状态。"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "payment_records", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "错误/备注。"); + + migrationBuilder.AlterColumn( + name: "Payload", + table: "payment_records", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "原始回调内容。"); + + migrationBuilder.AlterColumn( + name: "PaidAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "支付完成时间。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "payment_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联订单。"); + + migrationBuilder.AlterColumn( + name: "Method", + table: "payment_records", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "支付方式。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "payment_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "payment_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "payment_records", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ChannelTransactionId", + table: "payment_records", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "第三方渠道单号。"); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "payment_records", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "支付金额。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "payment_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "TableNo", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "就餐桌号。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前状态。"); + + migrationBuilder.AlterColumn( + name: "ReservationId", + table: "orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "预约 ID。"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "orders", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "QueueNumber", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "排队号(如有)。"); + + migrationBuilder.AlterColumn( + name: "PaymentStatus", + table: "orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "支付状态。"); + + migrationBuilder.AlterColumn( + name: "PayableAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "应付金额。"); + + migrationBuilder.AlterColumn( + name: "PaidAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "支付时间。"); + + migrationBuilder.AlterColumn( + name: "PaidAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "实付金额。"); + + migrationBuilder.AlterColumn( + name: "OrderNo", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "订单号。"); + + migrationBuilder.AlterColumn( + name: "ItemsAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "商品总额。"); + + migrationBuilder.AlterColumn( + name: "FinishedAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "完成时间。"); + + migrationBuilder.AlterColumn( + name: "DiscountAmount", + table: "orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "优惠金额。"); + + migrationBuilder.AlterColumn( + name: "DeliveryType", + table: "orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "履约类型。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CustomerPhone", + table: "orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "顾客手机号。"); + + migrationBuilder.AlterColumn( + name: "CustomerName", + table: "orders", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "顾客姓名。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "orders", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Channel", + table: "orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "下单渠道。"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "取消时间。"); + + migrationBuilder.AlterColumn( + name: "CancelReason", + table: "orders", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "取消原因。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "order_status_histories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "order_status_histories", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "order_status_histories", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "变更后的状态。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "order_status_histories", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "订单标识。"); + + migrationBuilder.AlterColumn( + name: "OperatorId", + table: "order_status_histories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "操作人标识(可为空表示系统)。"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "发生时间。"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "order_status_histories", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "备注信息。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "order_status_histories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "order_status_histories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "order_status_histories", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "order_status_histories", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "order_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "order_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "UnitPrice", + table: "order_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "单价。"); + + migrationBuilder.AlterColumn( + name: "Unit", + table: "order_items", + type: "character varying(16)", + maxLength: 16, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(16)", + oldMaxLength: 16, + oldNullable: true, + oldComment: "单位。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "order_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SubTotal", + table: "order_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "小计。"); + + migrationBuilder.AlterColumn( + name: "SkuName", + table: "order_items", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "SKU/规格描述。"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "order_items", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "数量。"); + + migrationBuilder.AlterColumn( + name: "ProductName", + table: "order_items", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "商品名称。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "order_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "商品 ID。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "order_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "订单 ID。"); + + migrationBuilder.AlterColumn( + name: "DiscountAmount", + table: "order_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "折扣金额。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "order_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "order_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "order_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "order_items", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AttributesJson", + table: "order_items", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "自定义属性 JSON。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "order_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "navigation_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户 ID。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "navigation_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "navigation_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "TargetApp", + table: "navigation_requests", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "跳转的地图应用。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "navigation_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店 ID。"); + + migrationBuilder.AlterColumn( + name: "RequestedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "请求时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "navigation_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "navigation_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "navigation_requests", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Channel", + table: "navigation_requests", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "来源通道(小程序、H5 等)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "navigation_requests", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "WindowStart", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "统计时间窗口开始。"); + + migrationBuilder.AlterColumn( + name: "WindowEnd", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "统计时间窗口结束。"); + + migrationBuilder.AlterColumn( + name: "Value", + table: "metric_snapshots", + type: "numeric(18,4)", + precision: 18, + scale: 4, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,4)", + oldPrecision: 18, + oldScale: 4, + oldComment: "数值。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "metric_snapshots", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "metric_snapshots", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "MetricDefinitionId", + table: "metric_snapshots", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "指标定义 ID。"); + + migrationBuilder.AlterColumn( + name: "DimensionKey", + table: "metric_snapshots", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "维度键(JSON)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "metric_snapshots", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "metric_snapshots", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "metric_snapshots", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "metric_snapshots", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "metric_definitions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "metric_definitions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "metric_definitions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "metric_definitions", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "指标名称。"); + + migrationBuilder.AlterColumn( + name: "DimensionsJson", + table: "metric_definitions", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "维度描述 JSON。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "metric_definitions", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "说明。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "metric_definitions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "metric_definitions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DefaultAggregation", + table: "metric_definitions", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "默认聚合方式。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "metric_definitions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "metric_definitions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "metric_definitions", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "指标编码。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "metric_definitions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "metric_alert_rules", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "metric_alert_rules", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "metric_alert_rules", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Severity", + table: "metric_alert_rules", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "告警级别。"); + + migrationBuilder.AlterColumn( + name: "NotificationChannels", + table: "metric_alert_rules", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "通知渠道。"); + + migrationBuilder.AlterColumn( + name: "MetricDefinitionId", + table: "metric_alert_rules", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联指标。"); + + migrationBuilder.AlterColumn( + name: "Enabled", + table: "metric_alert_rules", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否启用。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "metric_alert_rules", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "metric_alert_rules", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "metric_alert_rules", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "metric_alert_rules", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ConditionJson", + table: "metric_alert_rules", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "触发条件 JSON。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "metric_alert_rules", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "TaxNumber", + table: "merchants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "税号/统一社会信用代码。"); + + migrationBuilder.AlterColumn( + name: "SupportEmail", + table: "merchants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "客服邮箱。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchants", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "入驻状态。"); + + migrationBuilder.AlterColumn( + name: "ServicePhone", + table: "merchants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "客服电话。"); + + migrationBuilder.AlterColumn( + name: "ReviewRemarks", + table: "merchants", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "审核备注或驳回原因。"); + + migrationBuilder.AlterColumn( + name: "Province", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "所在省份。"); + + migrationBuilder.AlterColumn( + name: "Longitude", + table: "merchants", + type: "double precision", + nullable: true, + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true, + oldComment: "经度信息。"); + + migrationBuilder.AlterColumn( + name: "LogoUrl", + table: "merchants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "品牌 Logo。"); + + migrationBuilder.AlterColumn( + name: "LegalPerson", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "法人或负责人姓名。"); + + migrationBuilder.AlterColumn( + name: "Latitude", + table: "merchants", + type: "double precision", + nullable: true, + oldClrType: typeof(double), + oldType: "double precision", + oldNullable: true, + oldComment: "纬度信息。"); + + migrationBuilder.AlterColumn( + name: "LastReviewedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次审核时间。"); + + migrationBuilder.AlterColumn( + name: "JoinedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "入驻时间。"); + + migrationBuilder.AlterColumn( + name: "District", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "所在区县。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchants", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ContactPhone", + table: "merchants", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "联系电话。"); + + migrationBuilder.AlterColumn( + name: "ContactEmail", + table: "merchants", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "联系邮箱。"); + + migrationBuilder.AlterColumn( + name: "City", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "所在城市。"); + + migrationBuilder.AlterColumn( + name: "Category", + table: "merchants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "品牌所属品类,如火锅、咖啡等。"); + + migrationBuilder.AlterColumn( + name: "BusinessLicenseNumber", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "营业执照号。"); + + migrationBuilder.AlterColumn( + name: "BusinessLicenseImageUrl", + table: "merchants", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "营业执照扫描件地址。"); + + migrationBuilder.AlterColumn( + name: "BrandName", + table: "merchants", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "品牌名称(对外展示)。"); + + migrationBuilder.AlterColumn( + name: "BrandAlias", + table: "merchants", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "品牌简称或别名。"); + + migrationBuilder.AlterColumn( + name: "Address", + table: "merchants", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "详细地址。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchant_staff", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchant_staff", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchant_staff", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "merchant_staff", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "可选的关联门店 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchant_staff", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "员工状态。"); + + migrationBuilder.AlterColumn( + name: "RoleType", + table: "merchant_staff", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "员工角色类型。"); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "merchant_staff", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "手机号。"); + + migrationBuilder.AlterColumn( + name: "PermissionsJson", + table: "merchant_staff", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "自定义权限(JSON)。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "merchant_staff", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "员工姓名。"); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "merchant_staff", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商户标识。"); + + migrationBuilder.AlterColumn( + name: "IdentityUserId", + table: "merchant_staff", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "登录账号 ID(指向统一身份体系)。"); + + migrationBuilder.AlterColumn( + name: "Email", + table: "merchant_staff", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "邮箱地址。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchant_staff", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchant_staff", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchant_staff", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchant_staff", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchant_staff", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchant_documents", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchant_documents", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchant_documents", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "审核状态。"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "merchant_documents", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "审核备注或驳回原因。"); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "merchant_documents", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商户标识。"); + + migrationBuilder.AlterColumn( + name: "IssuedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "签发日期。"); + + migrationBuilder.AlterColumn( + name: "FileUrl", + table: "merchant_documents", + type: "character varying(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldComment: "证照文件链接。"); + + migrationBuilder.AlterColumn( + name: "ExpiresAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "到期日期。"); + + migrationBuilder.AlterColumn( + name: "DocumentType", + table: "merchant_documents", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "证照类型。"); + + migrationBuilder.AlterColumn( + name: "DocumentNumber", + table: "merchant_documents", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "证照编号。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchant_documents", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchant_documents", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchant_documents", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchant_documents", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "merchant_contracts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TerminationReason", + table: "merchant_contracts", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "终止原因。"); + + migrationBuilder.AlterColumn( + name: "TerminatedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "终止时间。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "merchant_contracts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "merchant_contracts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "合同状态。"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "合同开始时间。"); + + migrationBuilder.AlterColumn( + name: "SignedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "签署时间。"); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "merchant_contracts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属商户标识。"); + + migrationBuilder.AlterColumn( + name: "FileUrl", + table: "merchant_contracts", + type: "character varying(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldComment: "合同文件存储地址。"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "合同结束时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "merchant_contracts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "merchant_contracts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "merchant_contracts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ContractNumber", + table: "merchant_contracts", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "合同编号。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "merchant_contracts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_tiers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_tiers", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_tiers", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "member_tiers", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "排序值。"); + + migrationBuilder.AlterColumn( + name: "RequiredGrowth", + table: "member_tiers", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "所需成长值。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "member_tiers", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "等级名称。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_tiers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_tiers", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_tiers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_tiers", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "BenefitsJson", + table: "member_tiers", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "等级权益(JSON)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_tiers", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "member_profiles", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_profiles", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_profiles", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "member_profiles", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "会员状态。"); + + migrationBuilder.AlterColumn( + name: "PointsBalance", + table: "member_profiles", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "会员积分余额。"); + + migrationBuilder.AlterColumn( + name: "Nickname", + table: "member_profiles", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "昵称。"); + + migrationBuilder.AlterColumn( + name: "Mobile", + table: "member_profiles", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "手机号。"); + + migrationBuilder.AlterColumn( + name: "MemberTierId", + table: "member_profiles", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "当前会员等级 ID。"); + + migrationBuilder.AlterColumn( + name: "JoinedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "注册时间。"); + + migrationBuilder.AlterColumn( + name: "GrowthValue", + table: "member_profiles", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "成长值/经验值。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_profiles", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_profiles", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_profiles", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "BirthDate", + table: "member_profiles", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "生日。"); + + migrationBuilder.AlterColumn( + name: "AvatarUrl", + table: "member_profiles", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "头像。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_profiles", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_point_ledgers", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SourceId", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "来源 ID(订单、活动等)。"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "member_point_ledgers", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "变动原因。"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "发生时间。"); + + migrationBuilder.AlterColumn( + name: "MemberId", + table: "member_point_ledgers", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "会员标识。"); + + migrationBuilder.AlterColumn( + name: "ExpireAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "过期时间(如适用)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_point_ledgers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_point_ledgers", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ChangeAmount", + table: "member_point_ledgers", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "变动数量,可为负值。"); + + migrationBuilder.AlterColumn( + name: "BalanceAfterChange", + table: "member_point_ledgers", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "变动后余额。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_point_ledgers", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "member_growth_logs", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "member_growth_logs", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "发生时间。"); + + migrationBuilder.AlterColumn( + name: "Notes", + table: "member_growth_logs", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "MemberId", + table: "member_growth_logs", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "会员标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "member_growth_logs", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CurrentValue", + table: "member_growth_logs", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前成长值。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "member_growth_logs", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "member_growth_logs", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ChangeValue", + table: "member_growth_logs", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "变动数量。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "member_growth_logs", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "map_locations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "map_locations", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "map_locations", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "map_locations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "关联门店 ID,可空表示独立 POI。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "map_locations", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "名称。"); + + migrationBuilder.AlterColumn( + name: "Longitude", + table: "map_locations", + type: "double precision", + nullable: false, + oldClrType: typeof(double), + oldType: "double precision", + oldComment: "经度。"); + + migrationBuilder.AlterColumn( + name: "Latitude", + table: "map_locations", + type: "double precision", + nullable: false, + oldClrType: typeof(double), + oldType: "double precision", + oldComment: "纬度。"); + + migrationBuilder.AlterColumn( + name: "Landmark", + table: "map_locations", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "打车/导航落点描述。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "map_locations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "map_locations", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "map_locations", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "map_locations", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Address", + table: "map_locations", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "地址。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "map_locations", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "inventory_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "inventory_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "inventory_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "inventory_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "SafetyStock", + table: "inventory_items", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "安全库存阈值。"); + + migrationBuilder.AlterColumn( + name: "QuantityReserved", + table: "inventory_items", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "已锁定库存(订单占用)。"); + + migrationBuilder.AlterColumn( + name: "QuantityOnHand", + table: "inventory_items", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "可用库存。"); + + migrationBuilder.AlterColumn( + name: "ProductSkuId", + table: "inventory_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "SKU 标识。"); + + migrationBuilder.AlterColumn( + name: "Location", + table: "inventory_items", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "储位或仓位信息。"); + + migrationBuilder.AlterColumn( + name: "ExpireDate", + table: "inventory_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "过期日期。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "inventory_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "inventory_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "inventory_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "inventory_items", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "BatchNumber", + table: "inventory_items", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "批次编号,可为空表示混批。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "inventory_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "inventory_batches", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "inventory_batches", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "inventory_batches", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "RemainingQuantity", + table: "inventory_batches", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "剩余数量。"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "inventory_batches", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "入库数量。"); + + migrationBuilder.AlterColumn( + name: "ProductionDate", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "生产日期。"); + + migrationBuilder.AlterColumn( + name: "ProductSkuId", + table: "inventory_batches", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "SKU 标识。"); + + migrationBuilder.AlterColumn( + name: "ExpireDate", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "过期日期。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "inventory_batches", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "inventory_batches", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "inventory_batches", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "BatchNumber", + table: "inventory_batches", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "批次编号。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "inventory_batches", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "inventory_adjustments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Reason", + table: "inventory_adjustments", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "原因说明。"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "inventory_adjustments", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "调整数量,正数增加,负数减少。"); + + migrationBuilder.AlterColumn( + name: "OperatorId", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "操作人标识。"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "发生时间。"); + + migrationBuilder.AlterColumn( + name: "InventoryItemId", + table: "inventory_adjustments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "对应的库存记录标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "inventory_adjustments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "inventory_adjustments", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AdjustmentType", + table: "inventory_adjustments", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "调整类型。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "inventory_adjustments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "group_participants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "group_participants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "group_participants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "group_participants", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "参与状态。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "group_participants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "对应订单标识。"); + + migrationBuilder.AlterColumn( + name: "JoinedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "参与时间。"); + + migrationBuilder.AlterColumn( + name: "GroupOrderId", + table: "group_participants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "拼单活动标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "group_participants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "group_participants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "group_participants", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "group_participants", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "group_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "group_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "TargetCount", + table: "group_orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "成团需要的人数。"); + + migrationBuilder.AlterColumn( + name: "SucceededAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "成团时间。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "group_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "group_orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "拼团状态。"); + + migrationBuilder.AlterColumn( + name: "StartAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "开始时间。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "group_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联商品或套餐。"); + + migrationBuilder.AlterColumn( + name: "LeaderUserId", + table: "group_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "团长用户 ID。"); + + migrationBuilder.AlterColumn( + name: "GroupPrice", + table: "group_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "拼团价格。"); + + migrationBuilder.AlterColumn( + name: "GroupOrderNo", + table: "group_orders", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "拼单编号。"); + + migrationBuilder.AlterColumn( + name: "EndAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "结束时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "group_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CurrentCount", + table: "group_orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前已参与人数。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "group_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CancelledAt", + table: "group_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "取消时间。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "group_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "delivery_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "delivery_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "delivery_orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "ProviderOrderId", + table: "delivery_orders", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "第三方配送单号。"); + + migrationBuilder.AlterColumn( + name: "Provider", + table: "delivery_orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "配送服务商。"); + + migrationBuilder.AlterColumn( + name: "PickedUpAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "取餐时间。"); + + migrationBuilder.AlterColumn( + name: "FailureReason", + table: "delivery_orders", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "异常原因。"); + + migrationBuilder.AlterColumn( + name: "DispatchedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "下发时间。"); + + migrationBuilder.AlterColumn( + name: "DeliveryFee", + table: "delivery_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldNullable: true, + oldComment: "配送费。"); + + migrationBuilder.AlterColumn( + name: "DeliveredAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "完成时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "delivery_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "delivery_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "delivery_orders", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CourierPhone", + table: "delivery_orders", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "骑手电话。"); + + migrationBuilder.AlterColumn( + name: "CourierName", + table: "delivery_orders", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true, + oldComment: "骑手姓名。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "delivery_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "delivery_events", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "delivery_events", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Payload", + table: "delivery_events", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "原始数据 JSON。"); + + migrationBuilder.AlterColumn( + name: "OccurredAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "发生时间。"); + + migrationBuilder.AlterColumn( + name: "Message", + table: "delivery_events", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "事件描述。"); + + migrationBuilder.AlterColumn( + name: "EventType", + table: "delivery_events", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "事件类型。"); + + migrationBuilder.AlterColumn( + name: "DeliveryOrderId", + table: "delivery_events", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "配送单标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "delivery_events", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "delivery_events", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "delivery_events", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "delivery_events", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "coupons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "归属用户。"); + + migrationBuilder.AlterColumn( + name: "UsedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "使用时间。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "coupons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "coupons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "coupons", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "coupons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "订单 ID(已使用时记录)。"); + + migrationBuilder.AlterColumn( + name: "IssuedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "发放时间。"); + + migrationBuilder.AlterColumn( + name: "ExpireAt", + table: "coupons", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "到期时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "coupons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "coupons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "coupons", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CouponTemplateId", + table: "coupons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "模板标识。"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "coupons", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "券码或序列号。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "coupons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "Value", + table: "coupon_templates", + type: "numeric", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric", + oldComment: "面值或折扣额度。"); + + migrationBuilder.AlterColumn( + name: "ValidTo", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "可用结束时间。"); + + migrationBuilder.AlterColumn( + name: "ValidFrom", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "可用开始时间。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "coupon_templates", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TotalQuantity", + table: "coupon_templates", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "总发放数量上限。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "coupon_templates", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreScopeJson", + table: "coupon_templates", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "适用门店 ID 集合(JSON)。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "coupon_templates", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "RelativeValidDays", + table: "coupon_templates", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true, + oldComment: "有效天数(相对发放时间)。"); + + migrationBuilder.AlterColumn( + name: "ProductScopeJson", + table: "coupon_templates", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "适用品类或商品范围(JSON)。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "coupon_templates", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "模板名称。"); + + migrationBuilder.AlterColumn( + name: "MinimumSpend", + table: "coupon_templates", + type: "numeric", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true, + oldComment: "最低消费门槛。"); + + migrationBuilder.AlterColumn( + name: "DiscountCap", + table: "coupon_templates", + type: "numeric", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric", + oldNullable: true, + oldComment: "折扣上限(针对折扣券)。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "coupon_templates", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "coupon_templates", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "coupon_templates", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "coupon_templates", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CouponType", + table: "coupon_templates", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "券类型。"); + + migrationBuilder.AlterColumn( + name: "ClaimedQuantity", + table: "coupon_templates", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "已领取数量。"); + + migrationBuilder.AlterColumn( + name: "ChannelsJson", + table: "coupon_templates", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "发放渠道(JSON)。"); + + migrationBuilder.AlterColumn( + name: "AllowStack", + table: "coupon_templates", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否允许叠加其他优惠。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "coupon_templates", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "community_reactions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户 ID。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "community_reactions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "community_reactions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "ReactionType", + table: "community_reactions", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "反应类型。"); + + migrationBuilder.AlterColumn( + name: "ReactedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "时间戳。"); + + migrationBuilder.AlterColumn( + name: "PostId", + table: "community_reactions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "动态 ID。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "community_reactions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "community_reactions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "community_reactions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "community_reactions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "community_posts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "community_posts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "Title", + table: "community_posts", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "标题。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "community_posts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "community_posts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "MediaJson", + table: "community_posts", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "媒体资源 JSON。"); + + migrationBuilder.AlterColumn( + name: "LikeCount", + table: "community_posts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "点赞数。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "community_posts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "community_posts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "community_posts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "community_posts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "community_posts", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "内容。"); + + migrationBuilder.AlterColumn( + name: "CommentCount", + table: "community_posts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "评论数。"); + + migrationBuilder.AlterColumn( + name: "AuthorUserId", + table: "community_posts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "作者用户 ID。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "community_posts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "community_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "community_comments", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "community_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "PostId", + table: "community_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "动态标识。"); + + migrationBuilder.AlterColumn( + name: "ParentId", + table: "community_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "父级评论 ID。"); + + migrationBuilder.AlterColumn( + name: "IsDeleted", + table: "community_comments", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "community_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "community_comments", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "community_comments", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "community_comments", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "community_comments", + type: "character varying(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldComment: "评论内容。"); + + migrationBuilder.AlterColumn( + name: "AuthorUserId", + table: "community_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "评论人。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "community_comments", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "ValidationResultJson", + table: "checkout_sessions", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "校验结果明细 JSON。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "checkout_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "checkout_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "checkout_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "checkout_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "门店标识。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "checkout_sessions", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "会话状态。"); + + migrationBuilder.AlterColumn( + name: "SessionToken", + table: "checkout_sessions", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "会话 Token。"); + + migrationBuilder.AlterColumn( + name: "ExpiresAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "过期时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "checkout_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "checkout_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "checkout_sessions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "checkout_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "checkin_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "checkin_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "checkin_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "checkin_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "RewardJson", + table: "checkin_records", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "获得奖励 JSON。"); + + migrationBuilder.AlterColumn( + name: "IsMakeup", + table: "checkin_records", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否补签。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "checkin_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "checkin_records", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "checkin_records", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "checkin_records", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CheckInDate", + table: "checkin_records", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "签到日期(本地)。"); + + migrationBuilder.AlterColumn( + name: "CheckInCampaignId", + table: "checkin_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "活动标识。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "checkin_records", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "checkin_campaigns", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "checkin_campaigns", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "checkin_campaigns", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "开始日期。"); + + migrationBuilder.AlterColumn( + name: "RewardsJson", + table: "checkin_campaigns", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "连签奖励 JSON。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "checkin_campaigns", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "活动名称。"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "结束日期。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "checkin_campaigns", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "活动描述。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "checkin_campaigns", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "checkin_campaigns", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "checkin_campaigns", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AllowMakeupCount", + table: "checkin_campaigns", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "支持补签次数。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "checkin_campaigns", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "chat_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "chat_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "StoreId", + table: "chat_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "所属门店(可空为平台)。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "chat_sessions", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "会话状态。"); + + migrationBuilder.AlterColumn( + name: "StartedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "开始时间。"); + + migrationBuilder.AlterColumn( + name: "SessionCode", + table: "chat_sessions", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "会话编号。"); + + migrationBuilder.AlterColumn( + name: "IsBotActive", + table: "chat_sessions", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否机器人接待中。"); + + migrationBuilder.AlterColumn( + name: "EndedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "结束时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "chat_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CustomerUserId", + table: "chat_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "顾客用户 ID。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "chat_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "chat_sessions", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AgentUserId", + table: "chat_sessions", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "当前客服员工 ID。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "chat_sessions", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "chat_messages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "chat_messages", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SenderUserId", + table: "chat_messages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "发送方用户 ID。"); + + migrationBuilder.AlterColumn( + name: "SenderType", + table: "chat_messages", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "发送方类型。"); + + migrationBuilder.AlterColumn( + name: "ReadAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "读取时间。"); + + migrationBuilder.AlterColumn( + name: "IsRead", + table: "chat_messages", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否已读。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "chat_messages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "chat_messages", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "chat_messages", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "ContentType", + table: "chat_messages", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "消息类型(文字/图片/语音等)。"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "chat_messages", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(1024)", + oldMaxLength: 1024, + oldComment: "消息内容。"); + + migrationBuilder.AlterColumn( + name: "ChatSessionId", + table: "chat_messages", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "会话标识。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "chat_messages", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "cart_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "cart_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "UnitPrice", + table: "cart_items", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "单价快照。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "cart_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "cart_items", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "ShoppingCartId", + table: "cart_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属购物车标识。"); + + migrationBuilder.AlterColumn( + name: "Remark", + table: "cart_items", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "自定义备注(口味要求)。"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "cart_items", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "数量。"); + + migrationBuilder.AlterColumn( + name: "ProductSkuId", + table: "cart_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "SKU 标识。"); + + migrationBuilder.AlterColumn( + name: "ProductName", + table: "cart_items", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "商品名称快照。"); + + migrationBuilder.AlterColumn( + name: "ProductId", + table: "cart_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "商品或 SKU 标识。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "cart_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "cart_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "cart_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "cart_items", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "AttributesJson", + table: "cart_items", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "扩展 JSON(规格、加料选项等)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "cart_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "cart_item_addons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "cart_item_addons", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "cart_item_addons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "OptionId", + table: "cart_item_addons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "选项 ID(可对应 ProductAddonOption)。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "cart_item_addons", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "选项名称。"); + + migrationBuilder.AlterColumn( + name: "ExtraPrice", + table: "cart_item_addons", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "附加价格。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "cart_item_addons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "cart_item_addons", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "cart_item_addons", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "cart_item_addons", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CartItemId", + table: "cart_item_addons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属购物车条目。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "cart_item_addons", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "affiliate_payouts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "affiliate_payouts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "affiliate_payouts", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "状态。"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "affiliate_payouts", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "备注。"); + + migrationBuilder.AlterColumn( + name: "Period", + table: "affiliate_payouts", + type: "character varying(32)", + maxLength: 32, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldComment: "结算周期描述。"); + + migrationBuilder.AlterColumn( + name: "PaidAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "打款时间。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "affiliate_payouts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "affiliate_payouts", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "affiliate_payouts", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Amount", + table: "affiliate_payouts", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "结算金额。"); + + migrationBuilder.AlterColumn( + name: "AffiliatePartnerId", + table: "affiliate_payouts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "合作伙伴标识。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "affiliate_payouts", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "affiliate_partners", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "用户 ID(如绑定平台账号)。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "affiliate_partners", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "affiliate_partners", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "affiliate_partners", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "affiliate_partners", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前状态。"); + + migrationBuilder.AlterColumn( + name: "Remarks", + table: "affiliate_partners", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "审核备注。"); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "affiliate_partners", + type: "character varying(32)", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(32)", + oldMaxLength: 32, + oldNullable: true, + oldComment: "联系电话。"); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "affiliate_partners", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "昵称或渠道名称。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "affiliate_partners", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "affiliate_partners", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "affiliate_partners", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "affiliate_partners", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "CommissionRate", + table: "affiliate_partners", + type: "numeric", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric", + oldComment: "分成比例(0-1)。"); + + migrationBuilder.AlterColumn( + name: "ChannelType", + table: "affiliate_partners", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "渠道类型。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "affiliate_partners", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "affiliate_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "affiliate_orders", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "当前状态。"); + + migrationBuilder.AlterColumn( + name: "SettledAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "结算完成时间。"); + + migrationBuilder.AlterColumn( + name: "OrderId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联订单。"); + + migrationBuilder.AlterColumn( + name: "OrderAmount", + table: "affiliate_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "订单金额。"); + + migrationBuilder.AlterColumn( + name: "EstimatedCommission", + table: "affiliate_orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldPrecision: 18, + oldScale: 2, + oldComment: "预计佣金。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "affiliate_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "affiliate_orders", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "affiliate_orders", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "BuyerUserId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "用户 ID。"); + + migrationBuilder.AlterColumn( + name: "AffiliatePartnerId", + table: "affiliate_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "推广人标识。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "affiliate_orders", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs index d0c482a..50ffaf5 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs @@ -22,263 +22,3175 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ConditionJson") + .IsRequired() + .HasColumnType("text") + .HasComment("触发条件 JSON。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("MetricDefinitionId") + .HasColumnType("uuid") + .HasComment("关联指标。"); + + b.Property("NotificationChannels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("通知渠道。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("告警级别。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "Severity"); + + b.ToTable("metric_alert_rules", null, t => + { + t.HasComment("指标告警规则。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("指标编码。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultAggregation") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("默认聚合方式。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("说明。"); + + b.Property("DimensionsJson") + .HasColumnType("text") + .HasComment("维度描述 JSON。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("指标名称。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("metric_definitions", null, t => + { + t.HasComment("指标定义,描述可观测的数据点。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DimensionKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("维度键(JSON)。"); + + b.Property("MetricDefinitionId") + .HasColumnType("uuid") + .HasComment("指标定义 ID。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)") + .HasComment("数值。"); + + b.Property("WindowEnd") + .HasColumnType("timestamp with time zone") + .HasComment("统计时间窗口结束。"); + + b.Property("WindowStart") + .HasColumnType("timestamp with time zone") + .HasComment("统计时间窗口开始。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd") + .IsUnique(); + + b.ToTable("metric_snapshots", null, t => + { + t.HasComment("指标快照,用于大盘展示。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("券码或序列号。"); + + b.Property("CouponTemplateId") + .HasColumnType("uuid") + .HasComment("模板标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("发放时间。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("订单 ID(已使用时记录)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("归属用户。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("coupons", null, t => + { + t.HasComment("用户领取的券。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AllowStack") + .HasColumnType("boolean") + .HasComment("是否允许叠加其他优惠。"); + + b.Property("ChannelsJson") + .HasColumnType("text") + .HasComment("发放渠道(JSON)。"); + + b.Property("ClaimedQuantity") + .HasColumnType("integer") + .HasComment("已领取数量。"); + + b.Property("CouponType") + .HasColumnType("integer") + .HasComment("券类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("DiscountCap") + .HasColumnType("numeric") + .HasComment("折扣上限(针对折扣券)。"); + + b.Property("MinimumSpend") + .HasColumnType("numeric") + .HasComment("最低消费门槛。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("模板名称。"); + + b.Property("ProductScopeJson") + .HasColumnType("text") + .HasComment("适用品类或商品范围(JSON)。"); + + b.Property("RelativeValidDays") + .HasColumnType("integer") + .HasComment("有效天数(相对发放时间)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreScopeJson") + .HasColumnType("text") + .HasComment("适用门店 ID 集合(JSON)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TotalQuantity") + .HasColumnType("integer") + .HasComment("总发放数量上限。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasComment("可用开始时间。"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasComment("可用结束时间。"); + + b.Property("Value") + .HasColumnType("numeric") + .HasComment("面值或折扣额度。"); + + b.HasKey("Id"); + + b.ToTable("coupon_templates", null, t => + { + t.HasComment("优惠券模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AudienceDescription") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("目标人群描述。"); + + b.Property("BannerUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("营销素材(如 banner)。"); + + b.Property("Budget") + .HasColumnType("numeric") + .HasComment("预算金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("PromotionType") + .HasColumnType("integer") + .HasComment("活动类型。"); + + b.Property("RulesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("活动规则 JSON。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("活动状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("promotion_campaigns", null, t => + { + t.HasComment("营销活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ChatSessionId") + .HasColumnType("uuid") + .HasComment("会话标识。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("消息内容。"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("消息类型(文字/图片/语音等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRead") + .HasColumnType("boolean") + .HasComment("是否已读。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("读取时间。"); + + b.Property("SenderType") + .HasColumnType("integer") + .HasComment("发送方类型。"); + + b.Property("SenderUserId") + .HasColumnType("uuid") + .HasComment("发送方用户 ID。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ChatSessionId", "CreatedAt"); + + b.ToTable("chat_messages", null, t => + { + t.HasComment("会话消息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AgentUserId") + .HasColumnType("uuid") + .HasComment("当前客服员工 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("uuid") + .HasComment("顾客用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("IsBotActive") + .HasColumnType("boolean") + .HasComment("是否机器人接待中。"); + + b.Property("SessionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话编号。"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("所属门店(可空为平台)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionCode") + .IsUnique(); + + b.ToTable("chat_sessions", null, t => + { + t.HasComment("客服会话。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AssignedAgentId") + .HasColumnType("uuid") + .HasComment("指派的客服。"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("关闭时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("uuid") + .HasComment("客户用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("工单详情。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单(如有)。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("优先级。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("工单主题。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TicketNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("工单编号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TicketNo") + .IsUnique(); + + b.ToTable("support_tickets", null, t => + { + t.HasComment("客服工单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttachmentsJson") + .HasColumnType("text") + .HasComment("附件 JSON。"); + + b.Property("AuthorUserId") + .HasColumnType("uuid") + .HasComment("评论人 ID。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("评论内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsInternal") + .HasColumnType("boolean") + .HasComment("是否内部备注。"); + + b.Property("SupportTicketId") + .HasColumnType("uuid") + .HasComment("工单标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SupportTicketId"); + + b.ToTable("ticket_comments", null, t => + { + t.HasComment("工单评论/流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryOrderId") + .HasColumnType("uuid") + .HasComment("配送单标识。"); + + b.Property("EventType") + .HasColumnType("integer") + .HasComment("事件类型。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("事件描述。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始数据 JSON。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DeliveryOrderId", "EventType"); + + b.ToTable("delivery_events", null, t => + { + t.HasComment("配送状态事件流水。"); + }); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CourierName") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("骑手姓名。"); b.Property("CourierPhone") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("骑手电话。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveredAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); b.Property("DeliveryFee") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); b.Property("DispatchedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("下发时间。"); b.Property("FailureReason") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("异常原因。"); b.Property("OrderId") .HasColumnType("uuid"); b.Property("PickedUpAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("取餐时间。"); b.Property("Provider") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("配送服务商。"); b.Property("ProviderOrderId") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("第三方配送单号。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("状态。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); b.HasIndex("TenantId", "OrderId") .IsUnique(); - b.ToTable("delivery_orders", (string)null); + b.ToTable("delivery_orders", null, t => + { + t.HasComment("配送单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AffiliatePartnerId") + .HasColumnType("uuid") + .HasComment("推广人标识。"); + + b.Property("BuyerUserId") + .HasColumnType("uuid") + .HasComment("用户 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EstimatedCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("预计佣金。"); + + b.Property("OrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("订单金额。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单。"); + + b.Property("SettledAt") + .HasColumnType("timestamp with time zone") + .HasComment("结算完成时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "OrderId") + .IsUnique(); + + b.ToTable("affiliate_orders", null, t => + { + t.HasComment("分销订单记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ChannelType") + .HasColumnType("integer") + .HasComment("渠道类型。"); + + b.Property("CommissionRate") + .HasColumnType("numeric") + .HasComment("分成比例(0-1)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称或渠道名称。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户 ID(如绑定平台账号)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DisplayName"); + + b.ToTable("affiliate_partners", null, t => + { + t.HasComment("分销/推广合作伙伴。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AffiliatePartnerId") + .HasColumnType("uuid") + .HasComment("合作伙伴标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("结算金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("打款时间。"); + + b.Property("Period") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("结算周期描述。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "Period") + .IsUnique(); + + b.ToTable("affiliate_payouts", null, t => + { + t.HasComment("佣金结算记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AllowMakeupCount") + .HasColumnType("integer") + .HasComment("支持补签次数。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("活动描述。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("结束日期。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("RewardsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("连签奖励 JSON。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("开始日期。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("checkin_campaigns", null, t => + { + t.HasComment("签到活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CheckInCampaignId") + .HasColumnType("uuid") + .HasComment("活动标识。"); + + b.Property("CheckInDate") + .HasColumnType("timestamp with time zone") + .HasComment("签到日期(本地)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsMakeup") + .HasColumnType("boolean") + .HasComment("是否补签。"); + + b.Property("RewardJson") + .IsRequired() + .HasColumnType("text") + .HasComment("获得奖励 JSON。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CheckInCampaignId", "UserId", "CheckInDate") + .IsUnique(); + + b.ToTable("checkin_records", null, t => + { + t.HasComment("用户签到记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AuthorUserId") + .HasColumnType("uuid") + .HasComment("评论人。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("评论内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasComment("状态。"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasComment("父级评论 ID。"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasComment("动态标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "CreatedAt"); + + b.ToTable("community_comments", null, t => + { + t.HasComment("社区评论。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AuthorUserId") + .HasColumnType("uuid") + .HasComment("作者用户 ID。"); + + b.Property("CommentCount") + .HasColumnType("integer") + .HasComment("评论数。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LikeCount") + .HasColumnType("integer") + .HasComment("点赞数。"); + + b.Property("MediaJson") + .HasColumnType("text") + .HasComment("媒体资源 JSON。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AuthorUserId", "CreatedAt"); + + b.ToTable("community_posts", null, t => + { + t.HasComment("社区动态。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasComment("动态 ID。"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone") + .HasComment("时间戳。"); + + b.Property("ReactionType") + .HasColumnType("integer") + .HasComment("反应类型。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "UserId") + .IsUnique(); + + b.ToTable("community_reactions", null, t => + { + t.HasComment("社区互动反馈。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentCount") + .HasColumnType("integer") + .HasComment("当前已参与人数。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("GroupOrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("拼单编号。"); + + b.Property("GroupPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("拼团价格。"); + + b.Property("LeaderUserId") + .HasColumnType("uuid") + .HasComment("团长用户 ID。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("关联商品或套餐。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("拼团状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasComment("成团时间。"); + + b.Property("TargetCount") + .HasColumnType("integer") + .HasComment("成团需要的人数。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderNo") + .IsUnique(); + + b.ToTable("group_orders", null, t => + { + t.HasComment("拼单活动。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GroupOrderId") + .HasColumnType("uuid") + .HasComment("拼单活动标识。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("参与时间。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("对应订单标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("参与状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderId", "UserId") + .IsUnique(); + + b.ToTable("group_participants", null, t => + { + t.HasComment("拼单参与者。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AdjustmentType") + .HasColumnType("integer") + .HasComment("调整类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("InventoryItemId") + .HasColumnType("uuid") + .HasComment("对应的库存记录标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasComment("操作人标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("调整数量,正数增加,负数减少。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("原因说明。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "InventoryItemId", "OccurredAt"); + + b.ToTable("inventory_adjustments", null, t => + { + t.HasComment("库存调整记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BatchNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("批次编号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("ProductSkuId") + .HasColumnType("uuid") + .HasComment("SKU 标识。"); + + b.Property("ProductionDate") + .HasColumnType("timestamp with time zone") + .HasComment("生产日期。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("入库数量。"); + + b.Property("RemainingQuantity") + .HasColumnType("integer") + .HasComment("剩余数量。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber") + .IsUnique(); + + b.ToTable("inventory_batches", null, t => + { + t.HasComment("SKU 批次信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BatchNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("批次编号,可为空表示混批。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("Location") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("储位或仓位信息。"); + + b.Property("ProductSkuId") + .HasColumnType("uuid") + .HasComment("SKU 标识。"); + + b.Property("QuantityOnHand") + .HasColumnType("integer") + .HasComment("可用库存。"); + + b.Property("QuantityReserved") + .HasColumnType("integer") + .HasComment("已锁定库存(订单占用)。"); + + b.Property("SafetyStock") + .HasColumnType("integer") + .HasComment("安全库存阈值。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber"); + + b.ToTable("inventory_items", null, t => + { + t.HasComment("SKU 在门店的库存信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ChangeValue") + .HasColumnType("integer") + .HasComment("变动数量。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentValue") + .HasColumnType("integer") + .HasComment("当前成长值。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MemberId") + .HasColumnType("uuid") + .HasComment("会员标识。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_growth_logs", null, t => + { + t.HasComment("成长值变动日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BalanceAfterChange") + .HasColumnType("integer") + .HasComment("变动后余额。"); + + b.Property("ChangeAmount") + .HasColumnType("integer") + .HasComment("变动数量,可为负值。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(如适用)。"); + + b.Property("MemberId") + .HasColumnType("uuid") + .HasComment("会员标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Reason") + .HasColumnType("integer") + .HasComment("变动原因。"); + + b.Property("SourceId") + .HasColumnType("uuid") + .HasComment("来源 ID(订单、活动等)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_point_ledgers", null, t => + { + t.HasComment("积分变动流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AvatarUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像。"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone") + .HasComment("生日。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GrowthValue") + .HasColumnType("integer") + .HasComment("成长值/经验值。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("注册时间。"); + + b.Property("MemberTierId") + .HasColumnType("uuid") + .HasComment("当前会员等级 ID。"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("Nickname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("PointsBalance") + .HasColumnType("integer") + .HasComment("会员积分余额。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会员状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Mobile") + .IsUnique(); + + b.ToTable("member_profiles", null, t => + { + t.HasComment("会员档案。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("BenefitsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("等级权益(JSON)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("等级名称。"); + + b.Property("RequiredGrowth") + .HasColumnType("integer") + .HasComment("所需成长值。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.ToTable("member_tiers", null, t => + { + t.HasComment("会员等级定义。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("Address") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); b.Property("BrandAlias") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("品牌简称或别名。"); b.Property("BrandName") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("品牌名称(对外展示)。"); + + b.Property("BusinessLicenseImageUrl") + .HasColumnType("text") + .HasComment("营业执照扫描件地址。"); b.Property("BusinessLicenseNumber") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("营业执照号。"); + + b.Property("Category") + .HasColumnType("text") + .HasComment("品牌所属品类,如火锅、咖啡等。"); b.Property("City") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); b.Property("ContactEmail") .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("联系邮箱。"); b.Property("ContactPhone") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("District") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("所在区县。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("入驻时间。"); + + b.Property("LastReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次审核时间。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度信息。"); b.Property("LegalPerson") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("法人或负责人姓名。"); - b.Property("OnboardedAt") - .HasColumnType("timestamp with time zone"); + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("品牌 Logo。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度信息。"); b.Property("Province") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); b.Property("ReviewRemarks") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("审核备注或驳回原因。"); + + b.Property("ServicePhone") + .HasColumnType("text") + .HasComment("客服电话。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("入驻状态。"); + + b.Property("SupportEmail") + .HasColumnType("text") + .HasComment("客服邮箱。"); + + b.Property("TaxNumber") + .HasColumnType("text") + .HasComment("税号/统一社会信用代码。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); b.HasIndex("TenantId"); - b.ToTable("merchants", (string)null); + b.ToTable("merchants", null, t => + { + t.HasComment("商户主体信息,承载入驻和资质审核结果。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ContractNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("合同编号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同结束时间。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("合同文件存储地址。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签署时间。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("合同状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TerminatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("终止时间。"); + + b.Property("TerminationReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("终止原因。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "ContractNumber") + .IsUnique(); + + b.ToTable("merchant_contracts", null, t => + { + t.HasComment("商户合同记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DocumentNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("证照编号。"); + + b.Property("DocumentType") + .HasColumnType("integer") + .HasComment("证照类型。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期日期。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("证照文件链接。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签发日期。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("Remarks") + .HasColumnType("text") + .HasComment("审核备注或驳回原因。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("审核状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "DocumentType"); + + b.ToTable("merchant_documents", null, t => + { + t.HasComment("商户提交的资质或证照材料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("邮箱地址。"); + + b.Property("IdentityUserId") + .HasColumnType("uuid") + .HasComment("登录账号 ID(指向统一身份体系)。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("员工姓名。"); + + b.Property("PermissionsJson") + .HasColumnType("text") + .HasComment("自定义权限(JSON)。"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("员工角色类型。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("员工状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("可选的关联门店 ID。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "Phone"); + + b.ToTable("merchant_staff", null, t => + { + t.HasComment("商户员工账号,支持门店维度分配。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Landmark") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("打车/导航落点描述。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("名称。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("关联门店 ID,可空表示独立 POI。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("map_locations", null, t => + { + t.HasComment("地图 POI 信息,用于门店定位和推荐。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("来源通道(小程序、H5 等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("请求时间。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店 ID。"); + + b.Property("TargetApp") + .HasColumnType("integer") + .HasComment("跳转的地图应用。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId", "RequestedAt"); + + b.ToTable("navigation_requests", null, t => + { + t.HasComment("用户发起的导航请求日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("扩展 JSON(规格、加料选项等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("商品或 SKU 标识。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称快照。"); + + b.Property("ProductSkuId") + .HasColumnType("uuid") + .HasComment("SKU 标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("自定义备注(口味要求)。"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid") + .HasComment("所属购物车标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价快照。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ShoppingCartId"); + + b.ToTable("cart_items", null, t => + { + t.HasComment("购物车条目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CartItemId") + .HasColumnType("uuid") + .HasComment("所属购物车条目。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("OptionId") + .HasColumnType("uuid") + .HasComment("选项 ID(可对应 ProductAddonOption)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("cart_item_addons", null, t => + { + t.HasComment("购物车条目的加料/附加项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话 Token。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.Property("ValidationResultJson") + .IsRequired() + .HasColumnType("text") + .HasComment("校验结果明细 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionToken") + .IsUnique(); + + b.ToTable("checkout_sessions", null, t => + { + t.HasComment("结账会话,记录校验上下文。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryPreference") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("履约方式(堂食/自提/配送)缓存。"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次修改时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("购物车状态,包含正常/锁定。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TableContext") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("桌码或场景标识(扫码点餐)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId") + .IsUnique(); + + b.ToTable("shopping_carts", null, t => + { + t.HasComment("用户购物车,按租户/门店隔离。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CancelReason") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("取消原因。"); b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); b.Property("Channel") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("下单渠道。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CustomerName") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("顾客姓名。"); b.Property("CustomerPhone") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("顾客手机号。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryType") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("履约类型。"); b.Property("DiscountAmount") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("优惠金额。"); b.Property("FinishedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); b.Property("ItemsAmount") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("商品总额。"); b.Property("OrderNo") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("订单号。"); b.Property("PaidAmount") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); b.Property("PayableAmount") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("应付金额。"); b.Property("PaymentStatus") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("支付状态。"); b.Property("QueueNumber") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("排队号(如有)。"); b.Property("Remark") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("备注。"); b.Property("ReservationId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("预约 ID。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("当前状态。"); b.Property("StoreId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("门店。"); b.Property("TableNo") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("就餐桌号。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -287,72 +3199,93 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations b.HasIndex("TenantId", "StoreId", "Status"); - b.ToTable("orders", (string)null); + b.ToTable("orders", null, t => + { + t.HasComment("交易订单。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("AttributesJson") - .HasColumnType("text"); + .HasColumnType("text") + .HasComment("自定义属性 JSON。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DiscountAmount") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); b.Property("OrderId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("订单 ID。"); b.Property("ProductId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("商品 ID。"); b.Property("ProductName") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); b.Property("Quantity") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("数量。"); b.Property("SkuName") .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("SKU/规格描述。"); b.Property("SubTotal") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("小计。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("Unit") .HasMaxLength(16) - .HasColumnType("character varying(16)"); + .HasColumnType("character varying(16)") + .HasComment("单位。"); b.Property("UnitPrice") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("单价。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -360,164 +3293,442 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations b.HasIndex("TenantId", "OrderId"); - b.ToTable("order_items", (string)null); + b.ToTable("order_items", null, t => + { + t.HasComment("订单明细。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注信息。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasComment("操作人标识(可为空表示系统)。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("订单标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("变更后的状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId", "OccurredAt"); + + b.ToTable("order_status_histories", null, t => + { + t.HasComment("订单状态流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("申请金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单标识。"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核完成时间。"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("申请原因。"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("退款单号。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("用户提交时间。"); + + b.Property("ReviewNotes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("退款状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique(); + + b.ToTable("refund_requests", null, t => + { + t.HasComment("售后/退款申请。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("Amount") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); b.Property("ChannelTransactionId") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("第三方渠道单号。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Method") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("支付方式。"); b.Property("OrderId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("关联订单。"); b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("支付完成时间。"); b.Property("Payload") - .HasColumnType("text"); + .HasColumnType("text") + .HasComment("原始回调内容。"); b.Property("Remark") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("错误/备注。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("支付状态。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("TradeNo") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("平台交易号。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); b.HasIndex("TenantId", "OrderId"); - b.ToTable("payment_records", (string)null); + b.ToTable("payment_records", null, t => + { + t.HasComment("支付流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("退款金额。"); + + b.Property("ChannelRefundId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("渠道退款流水号。"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("uuid") + .HasComment("关联订单标识。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("渠道返回的原始数据 JSON。"); + + b.Property("PaymentRecordId") + .HasColumnType("uuid") + .HasComment("原支付记录标识。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款请求时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("退款状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PaymentRecordId"); + + b.ToTable("payment_refund_records", null, t => + { + t.HasComment("支付渠道退款流水。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CategoryId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属分类。"); b.Property("CoverImage") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("主图。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") - .HasColumnType("text"); + .HasColumnType("text") + .HasComment("商品描述。"); b.Property("EnableDelivery") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("支持配送。"); b.Property("EnableDineIn") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("支持堂食。"); b.Property("EnablePickup") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("支持自提。"); b.Property("GalleryImages") .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); + .HasColumnType("character varying(1024)") + .HasComment("Gallery 图片逗号分隔。"); b.Property("IsFeatured") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("是否热门推荐。"); b.Property("MaxQuantityPerOrder") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("最大每单限购。"); b.Property("Name") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); b.Property("OriginalPrice") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); b.Property("Price") .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); + .HasColumnType("numeric(18,2)") + .HasComment("现价。"); b.Property("SpuCode") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("商品编码。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("商品状态。"); b.Property("StockQuantity") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("库存数量(可选)。"); b.Property("StoreId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属门店。"); b.Property("Subtitle") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("副标题/卖点。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("Unit") .HasMaxLength(16) - .HasColumnType("character varying(16)"); + .HasColumnType("character varying(16)") + .HasComment("售卖单位(份/杯等)。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -526,117 +3737,655 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations b.HasIndex("TenantId", "StoreId"); - b.ToTable("products", (string)null); + b.ToTable("products", null, t => + { + t.HasComment("商品(SPU)信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("MaxSelect") + .HasColumnType("integer") + .HasComment("最大选择数量。"); + + b.Property("MinSelect") + .HasColumnType("integer") + .HasComment("最小选择数量。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("所属商品。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "Name"); + + b.ToTable("product_addon_groups", null, t => + { + t.HasComment("加料/做法分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AddonGroupId") + .HasColumnType("uuid") + .HasComment("所属加料分组。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选项。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("product_addon_options", null, t => + { + t.HasComment("加料选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称,例如“辣度”“份量”。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型(单选/多选)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("显示排序。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("关联门店,可为空表示所有门店共享。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("product_attribute_groups", null, t => + { + t.HasComment("商品规格/属性分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributeGroupId") + .HasColumnType("uuid") + .HasComment("所属规格组。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选中。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AttributeGroupId", "Name") + .IsUnique(); + + b.ToTable("product_attribute_options", null, t => + { + t.HasComment("商品规格选项。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("分类描述。"); b.Property("IsEnabled") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("是否启用。"); b.Property("Name") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("分类名称。"); b.Property("SortOrder") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("排序值。"); b.Property("StoreId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属门店。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); b.HasIndex("TenantId", "StoreId"); - b.ToTable("product_categories", (string)null); + b.ToTable("product_categories", null, t => + { + t.HasComment("商品分类。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Caption") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述或标题。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MediaType") + .HasColumnType("integer") + .HasComment("媒体类型。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("商品标识。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("媒资链接。"); + + b.HasKey("Id"); + + b.ToTable("product_media_assets", null, t => + { + t.HasComment("商品媒资素材。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("ConditionsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("条件描述(JSON),如会员等级、渠道等。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效结束时间。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("特殊价格。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("所属商品。"); + + b.Property("RuleType") + .HasColumnType("integer") + .HasComment("策略类型。"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效开始时间。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("WeekdaysJson") + .HasColumnType("text") + .HasComment("生效星期(JSON 数组)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "RuleType"); + + b.ToTable("product_pricing_rules", null, t => + { + t.HasComment("商品价格策略,支持会员价/时段价等。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AttributesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("规格属性 JSON(记录选项 ID)。"); + + b.Property("Barcode") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("条形码。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasComment("所属商品标识。"); + + b.Property("SkuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("SKU 编码。"); + + b.Property("StockQuantity") + .HasColumnType("integer") + .HasComment("可售库存。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Weight") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)") + .HasComment("重量(千克)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SkuCode") + .IsUnique(); + + b.ToTable("product_skus", null, t => + { + t.HasComment("商品 SKU,记录具体规格组合价格。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CalledAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("叫号时间。"); b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EstimatedWaitMinutes") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("预计等待分钟。"); b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("过号时间。"); b.Property("PartySize") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("就餐人数。"); b.Property("Remark") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("备注。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("状态。"); b.Property("StoreId") .HasColumnType("uuid"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("TicketNumber") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("排队编号。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -645,80 +4394,103 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations b.HasIndex("TenantId", "StoreId", "TicketNumber") .IsUnique(); - b.ToTable("queue_tickets", (string)null); + b.ToTable("queue_tickets", null, t => + { + t.HasComment("排队叫号。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); b.Property("CheckInCode") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("核销码/到店码。"); b.Property("CheckedInAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("实际签到时间。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CustomerName") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("客户姓名。"); b.Property("CustomerPhone") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("PeopleCount") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("用餐人数。"); b.Property("Remark") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("备注。"); b.Property("ReservationNo") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("预约号。"); b.Property("ReservationTime") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("预约时间(UTC)。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("状态。"); b.Property("StoreId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("门店。"); b.Property("TablePreference") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("桌型/标签。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -727,198 +4499,1129 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations b.HasIndex("TenantId", "StoreId"); - b.ToTable("reservations", (string)null); + b.ToTable("reservations", null, t => + { + t.HasComment("预约/预订记录。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("Address") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); b.Property("Announcement") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("门店公告。"); b.Property("BusinessHours") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("门店营业时段描述(备用字符串)。"); b.Property("City") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); b.Property("Code") .IsRequired() .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("门店编码,便于扫码及外部对接。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家或地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .HasComment("门店海报。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryRadiusKm") .HasPrecision(6, 2) - .HasColumnType("numeric(6,2)"); + .HasColumnType("numeric(6,2)") + .HasComment("默认配送半径(公里)。"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("门店描述或公告。"); b.Property("District") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("区县信息。"); b.Property("Latitude") - .HasColumnType("double precision"); + .HasColumnType("double precision") + .HasComment("纬度。"); b.Property("Longitude") - .HasColumnType("double precision"); + .HasColumnType("double precision") + .HasComment("高德/腾讯地图经度。"); b.Property("ManagerName") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("门店负责人姓名。"); b.Property("MerchantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属商户标识。"); b.Property("Name") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("门店名称。"); b.Property("Phone") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); b.Property("Province") .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("QueueEnabled") - .HasColumnType("boolean"); - - b.Property("ReservationEnabled") - .HasColumnType("boolean"); + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("门店当前运营状态。"); b.Property("SupportsDelivery") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("是否支持配送。"); b.Property("SupportsDineIn") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("是否支持堂食。"); b.Property("SupportsPickup") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("是否支持自提。"); + + b.Property("SupportsQueueing") + .HasColumnType("boolean") + .HasComment("支持排队叫号。"); + + b.Property("SupportsReservation") + .HasColumnType("boolean") + .HasComment("支持预约。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("门店标签(逗号分隔)。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); - b.HasIndex("MerchantId"); - b.HasIndex("TenantId", "Code") .IsUnique(); b.HasIndex("TenantId", "MerchantId"); - b.ToTable("stores", (string)null); + b.ToTable("stores", null, t => + { + t.HasComment("门店信息,承载营业配置与能力。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CapacityLimit") + .HasColumnType("integer") + .HasComment("最大接待容量或单量限制。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期几,0 表示周日。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(本地时间)。"); + + b.Property("HourType") + .HasColumnType("integer") + .HasComment("时段类型(正常营业、休息、预约等)。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(本地时间)。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "DayOfWeek"); + + b.ToTable("store_business_hours", null, t => + { + t.HasComment("门店营业时段配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer") + .HasComment("预计送达分钟。"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("起送价。"); + + b.Property("PolygonGeoJson") + .IsRequired() + .HasColumnType("text") + .HasComment("GeoJSON 表示的多边形范围。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("ZoneName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ZoneName"); + + b.ToTable("store_delivery_zones", null, t => + { + t.HasComment("门店配送范围配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("排班角色。"); + + b.Property("ShiftDate") + .HasColumnType("timestamp with time zone") + .HasComment("班次日期。"); + + b.Property("StaffId") + .HasColumnType("uuid") + .HasComment("员工标识。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ShiftDate", "StaffId") + .IsUnique(); + + b.ToTable("store_employee_shifts", null, t => + { + t.HasComment("门店员工排班记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasComment("日期。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsClosed") + .HasColumnType("boolean") + .HasComment("是否全天闭店。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("说明内容。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Date") + .IsUnique(); + + b.ToTable("store_holidays", null, t => + { + t.HasComment("门店休息日或特殊营业日。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AreaId") + .HasColumnType("uuid") + .HasComment("所在区域 ID。"); + + b.Property("Capacity") + .HasColumnType("integer") + .HasComment("可容纳人数。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("QrCodeUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("桌码二维码地址。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前桌台状态。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TableCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("桌码。"); + + b.Property("Tags") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("桌台标签(堂食、快餐等)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "TableCode") + .IsUnique(); + + b.ToTable("store_tables", null, t => + { + t.HasComment("桌台信息与二维码绑定。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("区域描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.Property("StoreId") + .HasColumnType("uuid") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.ToTable("store_table_areas", null, t => + { + t.HasComment("门店桌台区域配置。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Address") + .HasColumnType("text") + .HasComment("详细地址信息。"); + + b.Property("City") + .HasColumnType("text") + .HasComment("所在城市。"); b.Property("Code") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("租户短编码,作为跨系统引用的唯一标识。"); b.Property("ContactEmail") .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("主联系人邮箱。"); b.Property("ContactName") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("主联系人姓名。"); b.Property("ContactPhone") .HasMaxLength(32) - .HasColumnType("character varying(32)"); + .HasColumnType("character varying(32)") + .HasComment("主联系人电话。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家/地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .HasComment("品牌海报或封面图。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EffectiveFrom") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("服务生效时间(UTC)。"); b.Property("EffectiveTo") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("服务到期时间(UTC)。"); b.Property("Industry") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("所属行业,如餐饮、零售等。"); + + b.Property("LegalEntityName") + .HasColumnType("text") + .HasComment("法人或公司主体名称。"); b.Property("LogoUrl") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("LOGO 图片地址。"); b.Property("Name") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("租户全称或品牌名称。"); + + b.Property("PrimaryOwnerUserId") + .HasColumnType("uuid") + .HasComment("系统内对应的租户所有者账号 ID。"); + + b.Property("Province") + .HasColumnType("text") + .HasComment("所在省份或州。"); b.Property("Remarks") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("备注信息,用于运营记录特殊说明。"); b.Property("ShortName") .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("对外展示的简称。"); b.Property("Status") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("租户当前状态,涵盖审核、启用、停用等场景。"); + + b.Property("SuspendedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次暂停服务时间。"); + + b.Property("SuspensionReason") + .HasColumnType("text") + .HasComment("暂停或终止的原因说明。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("业务标签集合(逗号分隔)。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Website") + .HasColumnType("text") + .HasComment("官网或主要宣传链接。"); b.HasKey("Id"); b.HasIndex("Code") .IsUnique(); - b.ToTable("tenants", (string)null); + b.ToTable("tenants", null, t => + { + t.HasComment("平台租户信息,描述租户的生命周期与基础资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AmountDue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额。"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone") + .HasComment("到期日。"); + + b.Property("LineItemsJson") + .HasColumnType("text") + .HasComment("账单明细 JSON,记录各项费用。"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期结束时间。"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期开始时间。"); + + b.Property("StatementNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("账单编号,供对账查询。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前付款状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StatementNo") + .IsUnique(); + + b.ToTable("tenant_billing_statements", null, t => + { + t.HasComment("租户账单,用于呈现周期性收费。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("发布通道(站内、邮件、短信等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("通知正文。"); + + b.Property("MetadataJson") + .HasColumnType("text") + .HasComment("附加元数据 JSON。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("租户是否已阅读。"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasComment("推送时间。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("通知重要级别。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("通知标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Channel", "SentAt"); + + b.ToTable("tenant_notifications", null, t => + { + t.HasComment("面向租户的站内通知或消息推送。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("套餐描述,包含适用场景、权益等。"); + + b.Property("FeaturePoliciesJson") + .HasColumnType("text") + .HasComment("权益明细 JSON,记录自定义特性开关。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否仍可售卖。"); + + b.Property("MaxAccountCount") + .HasColumnType("integer") + .HasComment("允许创建的最大账号数。"); + + b.Property("MaxDeliveryOrders") + .HasColumnType("integer") + .HasComment("每月可调用的配送单数量上限。"); + + b.Property("MaxSmsCredits") + .HasColumnType("integer") + .HasComment("每月短信额度上限。"); + + b.Property("MaxStorageGb") + .HasColumnType("integer") + .HasComment("存储容量上限(GB)。"); + + b.Property("MaxStoreCount") + .HasColumnType("integer") + .HasComment("允许的最大门店数。"); + + b.Property("MonthlyPrice") + .HasColumnType("numeric") + .HasComment("月付价格,单位:人民币元。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("套餐名称,展示给租户的简称。"); + + b.Property("PackageType") + .HasColumnType("integer") + .HasComment("套餐分类(试用、标准、旗舰等)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("YearlyPrice") + .HasColumnType("numeric") + .HasComment("年付价格,单位:人民币元。"); + + b.HasKey("Id"); + + b.ToTable("tenant_packages", null, t => + { + t.HasComment("平台提供的租户套餐定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LastResetAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次重置时间。"); + + b.Property("LimitValue") + .HasColumnType("numeric") + .HasComment("当前配额上限。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型,例如门店数、短信条数等。"); + + b.Property("ResetCycle") + .HasColumnType("text") + .HasComment("配额刷新周期描述(如月、年)。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedValue") + .HasColumnType("numeric") + .HasComment("已消耗的数量。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "QuotaType") + .IsUnique(); + + b.ToTable("tenant_quota_usages", null, t => + { + t.HasComment("租户配额使用情况快照。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("AutoRenew") + .HasColumnType("boolean") + .HasComment("是否开启自动续费。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("订阅生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("订阅到期时间(UTC)。"); + + b.Property("NextBillingDate") + .HasColumnType("timestamp with time zone") + .HasComment("下一个计费时间,配合自动续费使用。"); + + b.Property("Notes") + .HasColumnType("text") + .HasComment("运营备注信息。"); + + b.Property("ScheduledPackageId") + .HasColumnType("uuid") + .HasComment("若已排期升降配,对应的新套餐 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("订阅当前状态。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("TenantPackageId") + .HasColumnType("uuid") + .HasComment("当前订阅关联的套餐标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantPackageId"); + + b.ToTable("tenant_subscriptions", null, t => + { + t.HasComment("租户套餐订阅记录。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => @@ -929,17 +5632,6 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => - { - b.HasOne("TakeoutSaaS.Domain.Merchants.Entities.Merchant", "Merchant") - .WithMany() - .HasForeignKey("MerchantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Merchant"); - }); #pragma warning restore 612, 618 } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 328a3a2..53474ea 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -1,7 +1,17 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TakeoutSaaS.Domain.Analytics.Entities; +using TakeoutSaaS.Domain.Coupons.Entities; +using TakeoutSaaS.Domain.CustomerService.Entities; using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Distribution.Entities; +using TakeoutSaaS.Domain.Engagement.Entities; +using TakeoutSaaS.Domain.GroupBuying.Entities; +using TakeoutSaaS.Domain.Inventory.Entities; +using TakeoutSaaS.Domain.Membership.Entities; using TakeoutSaaS.Domain.Merchants.Entities; +using TakeoutSaaS.Domain.Navigation.Entities; +using TakeoutSaaS.Domain.Ordering.Entities; using TakeoutSaaS.Domain.Orders.Entities; using TakeoutSaaS.Domain.Payments.Entities; using TakeoutSaaS.Domain.Products.Entities; @@ -25,16 +35,91 @@ public sealed class TakeoutAppDbContext( : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) { public DbSet Tenants => Set(); + public DbSet TenantPackages => Set(); + public DbSet TenantSubscriptions => Set(); + public DbSet TenantQuotaUsages => Set(); + public DbSet TenantBillingStatements => Set(); + public DbSet TenantNotifications => Set(); + public DbSet Merchants => Set(); + public DbSet MerchantDocuments => Set(); + public DbSet MerchantContracts => Set(); + public DbSet MerchantStaff => Set(); + public DbSet Stores => Set(); + public DbSet StoreBusinessHours => Set(); + public DbSet StoreHolidays => Set(); + public DbSet StoreDeliveryZones => Set(); + public DbSet StoreTableAreas => Set(); + public DbSet StoreTables => Set(); + public DbSet StoreEmployeeShifts => Set(); + public DbSet ProductCategories => Set(); public DbSet Products => Set(); + public DbSet ProductAttributeGroups => Set(); + public DbSet ProductAttributeOptions => Set(); + public DbSet ProductSkus => Set(); + public DbSet ProductAddonGroups => Set(); + public DbSet ProductAddonOptions => Set(); + public DbSet ProductPricingRules => Set(); + public DbSet ProductMediaAssets => Set(); + + public DbSet InventoryItems => Set(); + public DbSet InventoryAdjustments => Set(); + public DbSet InventoryBatches => Set(); + + public DbSet ShoppingCarts => Set(); + public DbSet CartItems => Set(); + public DbSet CartItemAddons => Set(); + public DbSet CheckoutSessions => Set(); + public DbSet Orders => Set(); public DbSet OrderItems => Set(); + public DbSet OrderStatusHistories => Set(); + public DbSet RefundRequests => Set(); + public DbSet PaymentRecords => Set(); + public DbSet PaymentRefundRecords => Set(); + public DbSet Reservations => Set(); public DbSet QueueTickets => Set(); + public DbSet DeliveryOrders => Set(); + public DbSet DeliveryEvents => Set(); + + public DbSet GroupOrders => Set(); + public DbSet GroupParticipants => Set(); + + public DbSet CouponTemplates => Set(); + public DbSet Coupons => Set(); + public DbSet PromotionCampaigns => Set(); + + public DbSet MemberProfiles => Set(); + public DbSet MemberTiers => Set(); + public DbSet MemberPointLedgers => Set(); + public DbSet MemberGrowthLogs => Set(); + + public DbSet ChatSessions => Set(); + public DbSet ChatMessages => Set(); + public DbSet SupportTickets => Set(); + public DbSet TicketComments => Set(); + + public DbSet AffiliatePartners => Set(); + public DbSet AffiliateOrders => Set(); + public DbSet AffiliatePayouts => Set(); + + public DbSet CheckInCampaigns => Set(); + public DbSet CheckInRecords => Set(); + public DbSet CommunityPosts => Set(); + public DbSet CommunityComments => Set(); + public DbSet CommunityReactions => Set(); + + public DbSet MapLocations => Set(); + public DbSet NavigationRequests => Set(); + + public DbSet MetricDefinitions => Set(); + public DbSet MetricSnapshots => Set(); + public DbSet MetricAlertRules => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -43,14 +128,72 @@ public sealed class TakeoutAppDbContext( ConfigureTenant(modelBuilder.Entity()); ConfigureMerchant(modelBuilder.Entity()); ConfigureStore(modelBuilder.Entity()); + ConfigureTenantPackage(modelBuilder.Entity()); + ConfigureTenantSubscription(modelBuilder.Entity()); + ConfigureTenantQuotaUsage(modelBuilder.Entity()); + ConfigureTenantBilling(modelBuilder.Entity()); + ConfigureTenantNotification(modelBuilder.Entity()); + ConfigureMerchantDocument(modelBuilder.Entity()); + ConfigureMerchantContract(modelBuilder.Entity()); + ConfigureMerchantStaff(modelBuilder.Entity()); + ConfigureStoreBusinessHour(modelBuilder.Entity()); + ConfigureStoreHoliday(modelBuilder.Entity()); + ConfigureStoreDeliveryZone(modelBuilder.Entity()); + ConfigureStoreTableArea(modelBuilder.Entity()); + ConfigureStoreTable(modelBuilder.Entity()); + ConfigureStoreEmployeeShift(modelBuilder.Entity()); ConfigureProductCategory(modelBuilder.Entity()); ConfigureProduct(modelBuilder.Entity()); + ConfigureProductAttributeGroup(modelBuilder.Entity()); + ConfigureProductAttributeOption(modelBuilder.Entity()); + ConfigureProductSku(modelBuilder.Entity()); + ConfigureProductAddonGroup(modelBuilder.Entity()); + ConfigureProductAddonOption(modelBuilder.Entity()); + ConfigureProductPricingRule(modelBuilder.Entity()); + ConfigureProductMediaAsset(modelBuilder.Entity()); + ConfigureInventoryItem(modelBuilder.Entity()); + ConfigureInventoryAdjustment(modelBuilder.Entity()); + ConfigureInventoryBatch(modelBuilder.Entity()); + ConfigureShoppingCart(modelBuilder.Entity()); + ConfigureCartItem(modelBuilder.Entity()); + ConfigureCartItemAddon(modelBuilder.Entity()); + ConfigureCheckoutSession(modelBuilder.Entity()); ConfigureOrder(modelBuilder.Entity()); ConfigureOrderItem(modelBuilder.Entity()); + ConfigureOrderStatusHistory(modelBuilder.Entity()); + ConfigureRefundRequest(modelBuilder.Entity()); ConfigurePaymentRecord(modelBuilder.Entity()); + ConfigurePaymentRefundRecord(modelBuilder.Entity()); ConfigureReservation(modelBuilder.Entity()); ConfigureQueueTicket(modelBuilder.Entity()); ConfigureDelivery(modelBuilder.Entity()); + ConfigureDeliveryEvent(modelBuilder.Entity()); + ConfigureGroupOrder(modelBuilder.Entity()); + ConfigureGroupParticipant(modelBuilder.Entity()); + ConfigureCouponTemplate(modelBuilder.Entity()); + ConfigureCoupon(modelBuilder.Entity()); + ConfigurePromotionCampaign(modelBuilder.Entity()); + ConfigureMemberProfile(modelBuilder.Entity()); + ConfigureMemberTier(modelBuilder.Entity()); + ConfigureMemberPointLedger(modelBuilder.Entity()); + ConfigureMemberGrowthLog(modelBuilder.Entity()); + ConfigureChatSession(modelBuilder.Entity()); + ConfigureChatMessage(modelBuilder.Entity()); + ConfigureSupportTicket(modelBuilder.Entity()); + ConfigureTicketComment(modelBuilder.Entity()); + ConfigureAffiliatePartner(modelBuilder.Entity()); + ConfigureAffiliateOrder(modelBuilder.Entity()); + ConfigureAffiliatePayout(modelBuilder.Entity()); + ConfigureCheckInCampaign(modelBuilder.Entity()); + ConfigureCheckInRecord(modelBuilder.Entity()); + ConfigureCommunityPost(modelBuilder.Entity()); + ConfigureCommunityComment(modelBuilder.Entity()); + ConfigureCommunityReaction(modelBuilder.Entity()); + ConfigureMapLocation(modelBuilder.Entity()); + ConfigureNavigationRequest(modelBuilder.Entity()); + ConfigureMetricDefinition(modelBuilder.Entity()); + ConfigureMetricSnapshot(modelBuilder.Entity()); + ConfigureMetricAlertRule(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } @@ -218,4 +361,632 @@ public sealed class TakeoutAppDbContext( builder.Property(x => x.FailureReason).HasMaxLength(256); builder.HasIndex(x => new { x.TenantId, x.OrderId }).IsUnique(); } + + private static void ConfigureTenantPackage(EntityTypeBuilder builder) + { + builder.ToTable("tenant_packages"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.FeaturePoliciesJson).HasColumnType("text"); + } + + private static void ConfigureTenantSubscription(EntityTypeBuilder builder) + { + builder.ToTable("tenant_subscriptions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantPackageId).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.TenantPackageId }); + } + + private static void ConfigureTenantQuotaUsage(EntityTypeBuilder builder) + { + builder.ToTable("tenant_quota_usages"); + builder.HasKey(x => x.Id); + builder.Property(x => x.QuotaType).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.QuotaType }).IsUnique(); + } + + private static void ConfigureTenantBilling(EntityTypeBuilder builder) + { + builder.ToTable("tenant_billing_statements"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StatementNo).HasMaxLength(64).IsRequired(); + builder.Property(x => x.AmountDue).HasPrecision(18, 2); + builder.Property(x => x.AmountPaid).HasPrecision(18, 2); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.LineItemsJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.StatementNo }).IsUnique(); + } + + private static void ConfigureTenantNotification(EntityTypeBuilder builder) + { + builder.ToTable("tenant_notifications"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Title).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Message).HasMaxLength(1024).IsRequired(); + builder.Property(x => x.Channel).HasConversion(); + builder.Property(x => x.Severity).HasConversion(); + builder.Property(x => x.MetadataJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.Channel, x.SentAt }); + } + + private static void ConfigureMerchantDocument(EntityTypeBuilder builder) + { + builder.ToTable("merchant_documents"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MerchantId).IsRequired(); + builder.Property(x => x.DocumentType).HasConversion(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.FileUrl).HasMaxLength(512).IsRequired(); + builder.Property(x => x.DocumentNumber).HasMaxLength(64); + builder.HasIndex(x => new { x.TenantId, x.MerchantId, x.DocumentType }); + } + + private static void ConfigureMerchantContract(EntityTypeBuilder builder) + { + builder.ToTable("merchant_contracts"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MerchantId).IsRequired(); + builder.Property(x => x.ContractNumber).HasMaxLength(64).IsRequired(); + builder.Property(x => x.FileUrl).HasMaxLength(512).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.TerminationReason).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.MerchantId, x.ContractNumber }).IsUnique(); + } + + private static void ConfigureMerchantStaff(EntityTypeBuilder builder) + { + builder.ToTable("merchant_staff"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MerchantId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Phone).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Email).HasMaxLength(128); + builder.Property(x => x.RoleType).HasConversion(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.PermissionsJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.MerchantId, x.Phone }); + } + + private static void ConfigureStoreBusinessHour(EntityTypeBuilder builder) + { + builder.ToTable("store_business_hours"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.HourType).HasConversion(); + builder.Property(x => x.Notes).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.DayOfWeek }); + } + + private static void ConfigureStoreHoliday(EntityTypeBuilder builder) + { + builder.ToTable("store_holidays"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Reason).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Date }).IsUnique(); + } + + private static void ConfigureStoreDeliveryZone(EntityTypeBuilder builder) + { + builder.ToTable("store_delivery_zones"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.ZoneName).HasMaxLength(64).IsRequired(); + builder.Property(x => x.PolygonGeoJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.MinimumOrderAmount).HasPrecision(18, 2); + builder.Property(x => x.DeliveryFee).HasPrecision(18, 2); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ZoneName }); + } + + private static void ConfigureStoreTableArea(EntityTypeBuilder builder) + { + builder.ToTable("store_table_areas"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }).IsUnique(); + } + + private static void ConfigureStoreTable(EntityTypeBuilder builder) + { + builder.ToTable("store_tables"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.TableCode).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Tags).HasMaxLength(128); + builder.Property(x => x.QrCodeUrl).HasMaxLength(512); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.TableCode }).IsUnique(); + } + + private static void ConfigureStoreEmployeeShift(EntityTypeBuilder builder) + { + builder.ToTable("store_employee_shifts"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.StaffId).IsRequired(); + builder.Property(x => x.RoleType).HasConversion(); + builder.Property(x => x.Notes).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ShiftDate, x.StaffId }).IsUnique(); + } + + private static void ConfigureProductAttributeGroup(EntityTypeBuilder builder) + { + builder.ToTable("product_attribute_groups"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.SelectionType).HasConversion(); + builder.Property(x => x.StoreId); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }); + } + + private static void ConfigureProductAttributeOption(EntityTypeBuilder builder) + { + builder.ToTable("product_attribute_options"); + builder.HasKey(x => x.Id); + builder.Property(x => x.AttributeGroupId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.ExtraPrice).HasPrecision(18, 2); + builder.HasIndex(x => new { x.TenantId, x.AttributeGroupId, x.Name }).IsUnique(); + } + + private static void ConfigureProductSku(EntityTypeBuilder builder) + { + builder.ToTable("product_skus"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ProductId).IsRequired(); + builder.Property(x => x.SkuCode).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Barcode).HasMaxLength(64); + builder.Property(x => x.Price).HasPrecision(18, 2); + builder.Property(x => x.OriginalPrice).HasPrecision(18, 2); + builder.Property(x => x.Weight).HasPrecision(10, 3); + builder.Property(x => x.AttributesJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.SkuCode }).IsUnique(); + } + + private static void ConfigureProductAddonGroup(EntityTypeBuilder builder) + { + builder.ToTable("product_addon_groups"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ProductId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.SelectionType).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.ProductId, x.Name }); + } + + private static void ConfigureProductAddonOption(EntityTypeBuilder builder) + { + builder.ToTable("product_addon_options"); + builder.HasKey(x => x.Id); + builder.Property(x => x.AddonGroupId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.ExtraPrice).HasPrecision(18, 2); + } + + private static void ConfigureProductPricingRule(EntityTypeBuilder builder) + { + builder.ToTable("product_pricing_rules"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ProductId).IsRequired(); + builder.Property(x => x.RuleType).HasConversion(); + builder.Property(x => x.ConditionsJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.Price).HasPrecision(18, 2); + builder.Property(x => x.WeekdaysJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.ProductId, x.RuleType }); + } + + private static void ConfigureProductMediaAsset(EntityTypeBuilder builder) + { + builder.ToTable("product_media_assets"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ProductId).IsRequired(); + builder.Property(x => x.MediaType).HasConversion(); + builder.Property(x => x.Url).HasMaxLength(512).IsRequired(); + builder.Property(x => x.Caption).HasMaxLength(256); + } + + private static void ConfigureInventoryItem(EntityTypeBuilder builder) + { + builder.ToTable("inventory_items"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.ProductSkuId).IsRequired(); + builder.Property(x => x.BatchNumber).HasMaxLength(64); + builder.Property(x => x.Location).HasMaxLength(64); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ProductSkuId, x.BatchNumber }); + } + + private static void ConfigureInventoryAdjustment(EntityTypeBuilder builder) + { + builder.ToTable("inventory_adjustments"); + builder.HasKey(x => x.Id); + builder.Property(x => x.InventoryItemId).IsRequired(); + builder.Property(x => x.AdjustmentType).HasConversion(); + builder.Property(x => x.Reason).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.InventoryItemId, x.OccurredAt }); + } + + private static void ConfigureInventoryBatch(EntityTypeBuilder builder) + { + builder.ToTable("inventory_batches"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.ProductSkuId).IsRequired(); + builder.Property(x => x.BatchNumber).HasMaxLength(64).IsRequired(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ProductSkuId, x.BatchNumber }).IsUnique(); + } + + private static void ConfigureShoppingCart(EntityTypeBuilder builder) + { + builder.ToTable("shopping_carts"); + builder.HasKey(x => x.Id); + builder.Property(x => x.UserId).IsRequired(); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.TableContext).HasMaxLength(64); + builder.Property(x => x.DeliveryPreference).HasMaxLength(32); + builder.HasIndex(x => new { x.TenantId, x.UserId, x.StoreId }).IsUnique(); + } + + private static void ConfigureCartItem(EntityTypeBuilder builder) + { + builder.ToTable("cart_items"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ShoppingCartId).IsRequired(); + builder.Property(x => x.ProductId).IsRequired(); + builder.Property(x => x.ProductName).HasMaxLength(128).IsRequired(); + builder.Property(x => x.UnitPrice).HasPrecision(18, 2); + builder.Property(x => x.Remark).HasMaxLength(256); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.AttributesJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.ShoppingCartId }); + } + + private static void ConfigureCartItemAddon(EntityTypeBuilder builder) + { + builder.ToTable("cart_item_addons"); + builder.HasKey(x => x.Id); + builder.Property(x => x.CartItemId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.ExtraPrice).HasPrecision(18, 2); + } + + private static void ConfigureCheckoutSession(EntityTypeBuilder builder) + { + builder.ToTable("checkout_sessions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.UserId).IsRequired(); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.SessionToken).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.ValidationResultJson).HasColumnType("text").IsRequired(); + builder.HasIndex(x => new { x.TenantId, x.SessionToken }).IsUnique(); + } + + private static void ConfigureOrderStatusHistory(EntityTypeBuilder builder) + { + builder.ToTable("order_status_histories"); + builder.HasKey(x => x.Id); + builder.Property(x => x.OrderId).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.Notes).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.OrderId, x.OccurredAt }); + } + + private static void ConfigureRefundRequest(EntityTypeBuilder builder) + { + builder.ToTable("refund_requests"); + builder.HasKey(x => x.Id); + builder.Property(x => x.OrderId).IsRequired(); + builder.Property(x => x.RefundNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Amount).HasPrecision(18, 2); + builder.Property(x => x.Reason).HasMaxLength(256).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.ReviewNotes).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.RefundNo }).IsUnique(); + } + + private static void ConfigurePaymentRefundRecord(EntityTypeBuilder builder) + { + builder.ToTable("payment_refund_records"); + builder.HasKey(x => x.Id); + builder.Property(x => x.PaymentRecordId).IsRequired(); + builder.Property(x => x.OrderId).IsRequired(); + builder.Property(x => x.Amount).HasPrecision(18, 2); + builder.Property(x => x.ChannelRefundId).HasMaxLength(64); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.Payload).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.PaymentRecordId }); + } + + private static void ConfigureDeliveryEvent(EntityTypeBuilder builder) + { + builder.ToTable("delivery_events"); + builder.HasKey(x => x.Id); + builder.Property(x => x.DeliveryOrderId).IsRequired(); + builder.Property(x => x.EventType).HasConversion(); + builder.Property(x => x.Message).HasMaxLength(256); + builder.Property(x => x.Payload).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.DeliveryOrderId, x.EventType }); + } + + private static void ConfigureGroupOrder(EntityTypeBuilder builder) + { + builder.ToTable("group_orders"); + builder.HasKey(x => x.Id); + builder.Property(x => x.GroupOrderNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.GroupPrice).HasPrecision(18, 2); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.GroupOrderNo }).IsUnique(); + } + + private static void ConfigureGroupParticipant(EntityTypeBuilder builder) + { + builder.ToTable("group_participants"); + builder.HasKey(x => x.Id); + builder.Property(x => x.GroupOrderId).IsRequired(); + builder.Property(x => x.OrderId).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.GroupOrderId, x.UserId }).IsUnique(); + } + + private static void ConfigureCouponTemplate(EntityTypeBuilder builder) + { + builder.ToTable("coupon_templates"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.CouponType).HasConversion(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.TotalQuantity); + builder.Property(x => x.StoreScopeJson).HasColumnType("text"); + builder.Property(x => x.ProductScopeJson).HasColumnType("text"); + builder.Property(x => x.ChannelsJson).HasColumnType("text"); + builder.Property(x => x.Status).HasConversion(); + } + + private static void ConfigureCoupon(EntityTypeBuilder builder) + { + builder.ToTable("coupons"); + builder.HasKey(x => x.Id); + builder.Property(x => x.CouponTemplateId).IsRequired(); + builder.Property(x => x.Code).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigurePromotionCampaign(EntityTypeBuilder builder) + { + builder.ToTable("promotion_campaigns"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.PromotionType).HasConversion(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.RulesJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.AudienceDescription).HasMaxLength(512); + builder.Property(x => x.BannerUrl).HasMaxLength(512); + } + + private static void ConfigureMemberProfile(EntityTypeBuilder builder) + { + builder.ToTable("member_profiles"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Mobile).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Nickname).HasMaxLength(64); + builder.Property(x => x.AvatarUrl).HasMaxLength(256); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.Mobile }).IsUnique(); + } + + private static void ConfigureMemberTier(EntityTypeBuilder builder) + { + builder.ToTable("member_tiers"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.BenefitsJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.Name }).IsUnique(); + } + + private static void ConfigureMemberPointLedger(EntityTypeBuilder builder) + { + builder.ToTable("member_point_ledgers"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MemberId).IsRequired(); + builder.Property(x => x.Reason).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt }); + } + + private static void ConfigureMemberGrowthLog(EntityTypeBuilder builder) + { + builder.ToTable("member_growth_logs"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MemberId).IsRequired(); + builder.Property(x => x.Notes).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt }); + } + + private static void ConfigureChatSession(EntityTypeBuilder builder) + { + builder.ToTable("chat_sessions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.SessionCode).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.SessionCode }).IsUnique(); + } + + private static void ConfigureChatMessage(EntityTypeBuilder builder) + { + builder.ToTable("chat_messages"); + builder.HasKey(x => x.Id); + builder.Property(x => x.ChatSessionId).IsRequired(); + builder.Property(x => x.SenderType).HasConversion(); + builder.Property(x => x.ContentType).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Content).HasMaxLength(1024).IsRequired(); + builder.HasIndex(x => new { x.TenantId, x.ChatSessionId, x.CreatedAt }); + } + + private static void ConfigureSupportTicket(EntityTypeBuilder builder) + { + builder.ToTable("support_tickets"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TicketNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Subject).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Description).HasColumnType("text").IsRequired(); + builder.Property(x => x.Priority).HasConversion(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.TicketNo }).IsUnique(); + } + + private static void ConfigureTicketComment(EntityTypeBuilder builder) + { + builder.ToTable("ticket_comments"); + builder.HasKey(x => x.Id); + builder.Property(x => x.SupportTicketId).IsRequired(); + builder.Property(x => x.Content).HasMaxLength(1024).IsRequired(); + builder.Property(x => x.AttachmentsJson).HasColumnType("text"); + builder.HasIndex(x => new { x.TenantId, x.SupportTicketId }); + } + + private static void ConfigureAffiliatePartner(EntityTypeBuilder builder) + { + builder.ToTable("affiliate_partners"); + builder.HasKey(x => x.Id); + builder.Property(x => x.DisplayName).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Phone).HasMaxLength(32); + builder.Property(x => x.ChannelType).HasConversion(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.Remarks).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.DisplayName }); + } + + private static void ConfigureAffiliateOrder(EntityTypeBuilder builder) + { + builder.ToTable("affiliate_orders"); + builder.HasKey(x => x.Id); + builder.Property(x => x.AffiliatePartnerId).IsRequired(); + builder.Property(x => x.OrderId).IsRequired(); + builder.Property(x => x.OrderAmount).HasPrecision(18, 2); + builder.Property(x => x.EstimatedCommission).HasPrecision(18, 2); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.AffiliatePartnerId, x.OrderId }).IsUnique(); + } + + private static void ConfigureAffiliatePayout(EntityTypeBuilder builder) + { + builder.ToTable("affiliate_payouts"); + builder.HasKey(x => x.Id); + builder.Property(x => x.AffiliatePartnerId).IsRequired(); + builder.Property(x => x.Period).HasMaxLength(32).IsRequired(); + builder.Property(x => x.Amount).HasPrecision(18, 2); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.Remarks).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.AffiliatePartnerId, x.Period }).IsUnique(); + } + + private static void ConfigureCheckInCampaign(EntityTypeBuilder builder) + { + builder.ToTable("checkin_campaigns"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.RewardsJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.Name }); + } + + private static void ConfigureCheckInRecord(EntityTypeBuilder builder) + { + builder.ToTable("checkin_records"); + builder.HasKey(x => x.Id); + builder.Property(x => x.CheckInCampaignId).IsRequired(); + builder.Property(x => x.UserId).IsRequired(); + builder.Property(x => x.RewardJson).HasColumnType("text").IsRequired(); + builder.HasIndex(x => new { x.TenantId, x.CheckInCampaignId, x.UserId, x.CheckInDate }).IsUnique(); + } + + private static void ConfigureCommunityPost(EntityTypeBuilder builder) + { + builder.ToTable("community_posts"); + builder.HasKey(x => x.Id); + builder.Property(x => x.AuthorUserId).IsRequired(); + builder.Property(x => x.Title).HasMaxLength(128); + builder.Property(x => x.Content).HasColumnType("text").IsRequired(); + builder.Property(x => x.MediaJson).HasColumnType("text"); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.AuthorUserId, x.CreatedAt }); + } + + private static void ConfigureCommunityComment(EntityTypeBuilder builder) + { + builder.ToTable("community_comments"); + builder.HasKey(x => x.Id); + builder.Property(x => x.PostId).IsRequired(); + builder.Property(x => x.Content).HasMaxLength(512).IsRequired(); + builder.HasIndex(x => new { x.TenantId, x.PostId, x.CreatedAt }); + } + + private static void ConfigureCommunityReaction(EntityTypeBuilder builder) + { + builder.ToTable("community_reactions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.PostId).IsRequired(); + builder.Property(x => x.ReactionType).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.PostId, x.UserId }).IsUnique(); + } + + private static void ConfigureMapLocation(EntityTypeBuilder builder) + { + builder.ToTable("map_locations"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Address).HasMaxLength(256).IsRequired(); + builder.Property(x => x.Landmark).HasMaxLength(128); + builder.HasIndex(x => new { x.TenantId, x.StoreId }); + } + + private static void ConfigureNavigationRequest(EntityTypeBuilder builder) + { + builder.ToTable("navigation_requests"); + builder.HasKey(x => x.Id); + builder.Property(x => x.UserId).IsRequired(); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Channel).HasConversion(); + builder.Property(x => x.TargetApp).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.UserId, x.StoreId, x.RequestedAt }); + } + + private static void ConfigureMetricDefinition(EntityTypeBuilder builder) + { + builder.ToTable("metric_definitions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.DimensionsJson).HasColumnType("text"); + builder.Property(x => x.DefaultAggregation).HasMaxLength(32).IsRequired(); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigureMetricSnapshot(EntityTypeBuilder builder) + { + builder.ToTable("metric_snapshots"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MetricDefinitionId).IsRequired(); + builder.Property(x => x.DimensionKey).HasMaxLength(256).IsRequired(); + builder.Property(x => x.Value).HasPrecision(18, 4); + builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.DimensionKey, x.WindowStart, x.WindowEnd }).IsUnique(); + } + + private static void ConfigureMetricAlertRule(EntityTypeBuilder builder) + { + builder.ToTable("metric_alert_rules"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MetricDefinitionId).IsRequired(); + builder.Property(x => x.ConditionJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.Severity).HasConversion(); + builder.Property(x => x.NotificationChannels).HasMaxLength(256); + builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.Severity }); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs index b56f2d0..e29572b 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; +using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -12,7 +13,7 @@ internal sealed class TakeoutAppDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase { public TakeoutAppDesignTimeDbContextFactory() - : base("TAKEOUTSAAS_APP_CONNECTION", "takeout_saas_app") + : base(DatabaseConstants.AppDataSource, "TAKEOUTSAAS_APP_CONNECTION") { } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs index 0a59c66..068a88f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs @@ -21,6 +21,7 @@ public abstract class AppDbContext(DbContextOptions options, ICurrentUserAccesso { base.OnModelCreating(modelBuilder); ApplySoftDeleteQueryFilters(modelBuilder); + modelBuilder.ApplyXmlComments(); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs index 88ba403..c3f59f2 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs @@ -1,25 +1,33 @@ using System; +using System.IO; +using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; -using TakeoutSaaS.Infrastructure.Common.Persistence; +using Microsoft.Extensions.Configuration; +using TakeoutSaaS.Infrastructure.Common.Options; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; /// -/// EF Core 设计时 DbContext 工厂基类,提供统一的连接串与依赖替身。 +/// EF Core 设计时 DbContext 工厂基类,统一读取 appsettings 中的数据库配置。 /// internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDbContextFactory where TContext : TenantAwareDbContext { - private readonly string _connectionStringEnvVar; - private readonly string _defaultDatabase; + private readonly string _dataSourceName; + private readonly string? _connectionStringEnvVar; - protected DesignTimeDbContextFactoryBase(string connectionStringEnvVar, string defaultDatabase) + protected DesignTimeDbContextFactoryBase(string dataSourceName, string? connectionStringEnvVar = null) { + if (string.IsNullOrWhiteSpace(dataSourceName)) + { + throw new ArgumentException("数据源名称不能为空。", nameof(dataSourceName)); + } + + _dataSourceName = dataSourceName; _connectionStringEnvVar = connectionStringEnvVar; - _defaultDatabase = defaultDatabase; } public TContext CreateDbContext(string[] args) @@ -46,15 +54,91 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb private string ResolveConnectionString() { - var env = Environment.GetEnvironmentVariable(_connectionStringEnvVar); - if (!string.IsNullOrWhiteSpace(env)) + if (!string.IsNullOrWhiteSpace(_connectionStringEnvVar)) { - return env; + var envValue = Environment.GetEnvironmentVariable(_connectionStringEnvVar); + if (!string.IsNullOrWhiteSpace(envValue)) + { + return envValue; + } } - return $"Host=localhost;Port=5432;Database={_defaultDatabase};Username=postgres;Password=postgres"; + var configuration = BuildConfiguration(); + var writeConnection = configuration[$"{DatabaseOptions.SectionName}:DataSources:{_dataSourceName}:Write"]; + if (string.IsNullOrWhiteSpace(writeConnection)) + { + throw new InvalidOperationException( + $"未在配置中找到数据源 '{_dataSourceName}' 的 Write 连接字符串,请检查 appsettings 或设置 {_connectionStringEnvVar ?? "相应"} 环境变量。"); + } + + return writeConnection; } + private static IConfigurationRoot BuildConfiguration() + { + var basePath = ResolveConfigurationDirectory(); + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; + + return new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: false) + .AddEnvironmentVariables() + .Build(); + } + + private static string ResolveConfigurationDirectory() + { + var explicitDir = Environment.GetEnvironmentVariable("TAKEOUTSAAS_APPSETTINGS_DIR"); + if (!string.IsNullOrWhiteSpace(explicitDir) && Directory.Exists(explicitDir)) + { + return explicitDir; + } + + var currentDir = Directory.GetCurrentDirectory(); + var solutionRoot = LocateSolutionRoot(currentDir); + + var candidateDirs = new[] + { + currentDir, + solutionRoot, + solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.AdminApi"), + solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.UserApi"), + solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.MiniApi") + }.Where(dir => !string.IsNullOrWhiteSpace(dir)); + + foreach (var dir in candidateDirs) + { + if (dir != null && Directory.Exists(dir) && HasAppSettings(dir)) + { + return dir; + } + } + + throw new InvalidOperationException( + "未找到 appsettings 配置文件,请设置 TAKEOUTSAAS_APPSETTINGS_DIR 环境变量指向包含 appsettings*.json 的目录。"); + } + + private static string? LocateSolutionRoot(string currentPath) + { + var directoryInfo = new DirectoryInfo(currentPath); + while (directoryInfo != null) + { + if (File.Exists(Path.Combine(directoryInfo.FullName, "TakeoutSaaS.sln"))) + { + return directoryInfo.FullName; + } + + directoryInfo = directoryInfo.Parent; + } + + return null; + } + + private static bool HasAppSettings(string directory) => + File.Exists(Path.Combine(directory, "appsettings.json")) || + Directory.GetFiles(directory, "appsettings.*.json").Length > 0; + private sealed class DesignTimeTenantProvider : ITenantProvider { public Guid GetCurrentTenantId() => Guid.Empty; diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs new file mode 100644 index 0000000..e143c8f --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs @@ -0,0 +1,136 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Xml.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace TakeoutSaaS.Infrastructure.Common.Persistence; + +/// +/// Applies XML documentation summaries to EF Core entities/columns as comments. +/// +internal static class ModelBuilderCommentExtensions +{ + public static void ApplyXmlComments(this ModelBuilder modelBuilder) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + ApplyEntityComment(entityType); + } + } + + private static void ApplyEntityComment(IMutableEntityType entityType) + { + var clrType = entityType.ClrType; + if (clrType == null) + { + return; + } + + if (XmlDocCommentProvider.TryGetSummary(clrType, out var typeComment)) + { + entityType.SetComment(typeComment); + } + + foreach (var property in entityType.GetProperties()) + { + var propertyInfo = property.PropertyInfo; + if (propertyInfo == null) + { + continue; + } + + if (XmlDocCommentProvider.TryGetSummary(propertyInfo, out var propertyComment)) + { + property.SetComment(propertyComment); + } + } + } + + private static class XmlDocCommentProvider + { + private static readonly ConcurrentDictionary> Cache = new(); + + public static bool TryGetSummary(MemberInfo member, out string? summary) + { + summary = null; + var assembly = member switch + { + Type type => type.Assembly, + _ => member.DeclaringType?.Assembly + }; + + if (assembly == null) + { + return false; + } + + var map = Cache.GetOrAdd(assembly, LoadComments); + if (map.Count == 0) + { + return false; + } + + var key = GetMemberKey(member); + if (key == null || !map.TryGetValue(key, out var text)) + { + return false; + } + + summary = text; + return true; + } + + private static IReadOnlyDictionary LoadComments(Assembly assembly) + { + var dictionary = new Dictionary(StringComparer.Ordinal); + var xmlPath = Path.ChangeExtension(assembly.Location, ".xml"); + if (string.IsNullOrWhiteSpace(xmlPath) || !File.Exists(xmlPath)) + { + return dictionary; + } + + var document = XDocument.Load(xmlPath); + foreach (var member in document.Descendants("member")) + { + var name = member.Attribute("name")?.Value; + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + var summary = member.Element("summary")?.Value; + if (string.IsNullOrWhiteSpace(summary)) + { + continue; + } + + var normalized = Normalize(summary); + if (!string.IsNullOrWhiteSpace(normalized)) + { + dictionary[name] = normalized; + } + } + + return dictionary; + } + + private static string? GetMemberKey(MemberInfo member) => + member switch + { + Type type => $"T:{GetFullName(type)}", + PropertyInfo property => $"P:{GetFullName(property.DeclaringType!)}.{property.Name}", + FieldInfo field => $"F:{GetFullName(field.DeclaringType!)}.{field.Name}", + _ => null + }; + + private static string GetFullName(Type type) => + (type.FullName ?? type.Name).Replace('+', '.'); + + private static string Normalize(string text) + { + var chars = text.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' '); + return string.Join(' ', chars.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs index be31f5a..c680f1e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs @@ -28,7 +28,7 @@ public static class DictionaryServiceCollectionExtensions public static IServiceCollection AddDictionaryInfrastructure(this IServiceCollection services, IConfiguration configuration) { services.AddDatabaseInfrastructure(configuration); - services.AddPostgresDbContext(DatabaseConstants.AppDataSource); + services.AddPostgresDbContext(DatabaseConstants.DictionaryDataSource); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs new file mode 100644 index 0000000..2f36f70 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs @@ -0,0 +1,206 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +{ + [DbContext(typeof(DictionaryDbContext))] + [Migration("20251201094456_AddEntityComments")] + partial class AddEntityComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组编码(唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("分组名称。"); + + b.Property("Scope") + .HasColumnType("integer") + .HasComment("分组作用域:系统/业务。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("dictionary_groups", null, t => + { + t.HasComment("参数字典分组(系统参数、业务参数)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasComment("关联分组 ID。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认项。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("字典项键。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("字典项值。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("GroupId", "Key") + .IsUnique(); + + b.ToTable("dictionary_items", null, t => + { + t.HasComment("参数字典项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") + .WithMany("Items") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs new file mode 100644 index 0000000..2447766 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs @@ -0,0 +1,599 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +{ + /// + public partial class AddEntityComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterTable( + name: "dictionary_items", + comment: "参数字典项。"); + + migrationBuilder.AlterTable( + name: "dictionary_groups", + comment: "参数字典分组(系统参数、业务参数)。"); + + migrationBuilder.AlterColumn( + name: "Value", + table: "dictionary_items", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "字典项值。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "dictionary_items", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "dictionary_items", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "dictionary_items", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "dictionary_items", + type: "integer", + nullable: false, + defaultValue: 100, + comment: "排序值,越小越靠前。", + oldClrType: typeof(int), + oldType: "integer", + oldDefaultValue: 100); + + migrationBuilder.AlterColumn( + name: "Key", + table: "dictionary_items", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "字典项键。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "IsEnabled", + table: "dictionary_items", + type: "boolean", + nullable: false, + defaultValue: true, + comment: "是否启用。", + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AlterColumn( + name: "IsDefault", + table: "dictionary_items", + type: "boolean", + nullable: false, + comment: "是否默认项。", + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + name: "GroupId", + table: "dictionary_items", + type: "uuid", + nullable: false, + comment: "关联分组 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "dictionary_items", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "描述信息。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "dictionary_items", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "dictionary_items", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "dictionary_items", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "dictionary_items", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "dictionary_items", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "dictionary_groups", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "dictionary_groups", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "dictionary_groups", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Scope", + table: "dictionary_groups", + type: "integer", + nullable: false, + comment: "分组作用域:系统/业务。", + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "dictionary_groups", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "分组名称。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "IsEnabled", + table: "dictionary_groups", + type: "boolean", + nullable: false, + defaultValue: true, + comment: "是否启用。", + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AlterColumn( + name: "Description", + table: "dictionary_groups", + type: "character varying(512)", + maxLength: 512, + nullable: true, + comment: "描述信息。", + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "dictionary_groups", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "dictionary_groups", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "dictionary_groups", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "dictionary_groups", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "dictionary_groups", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "分组编码(唯一)。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Id", + table: "dictionary_groups", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterTable( + name: "dictionary_items", + oldComment: "参数字典项。"); + + migrationBuilder.AlterTable( + name: "dictionary_groups", + oldComment: "参数字典分组(系统参数、业务参数)。"); + + migrationBuilder.AlterColumn( + name: "Value", + table: "dictionary_items", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "字典项值。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "dictionary_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "dictionary_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "dictionary_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "SortOrder", + table: "dictionary_items", + type: "integer", + nullable: false, + defaultValue: 100, + oldClrType: typeof(int), + oldType: "integer", + oldDefaultValue: 100, + oldComment: "排序值,越小越靠前。"); + + migrationBuilder.AlterColumn( + name: "Key", + table: "dictionary_items", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "字典项键。"); + + migrationBuilder.AlterColumn( + name: "IsEnabled", + table: "dictionary_items", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true, + oldComment: "是否启用。"); + + migrationBuilder.AlterColumn( + name: "IsDefault", + table: "dictionary_items", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldComment: "是否默认项。"); + + migrationBuilder.AlterColumn( + name: "GroupId", + table: "dictionary_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "关联分组 ID。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "dictionary_items", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "描述信息。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "dictionary_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "dictionary_items", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "dictionary_items", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "dictionary_items", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "dictionary_items", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "dictionary_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "dictionary_groups", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "dictionary_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Scope", + table: "dictionary_groups", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldComment: "分组作用域:系统/业务。"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "dictionary_groups", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "分组名称。"); + + migrationBuilder.AlterColumn( + name: "IsEnabled", + table: "dictionary_groups", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true, + oldComment: "是否启用。"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "dictionary_groups", + type: "character varying(512)", + maxLength: 512, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(512)", + oldMaxLength: 512, + oldNullable: true, + oldComment: "描述信息。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "dictionary_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "dictionary_groups", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "dictionary_groups", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "dictionary_groups", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "dictionary_groups", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "分组编码(唯一)。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "dictionary_groups", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs index 0c08edf..d55b340 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs @@ -26,50 +26,63 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("Code") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("分组编码(唯一)。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); b.Property("IsEnabled") .ValueGeneratedOnAdd() .HasColumnType("boolean") - .HasDefaultValue(true); + .HasDefaultValue(true) + .HasComment("是否启用。"); b.Property("Name") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("分组名称。"); b.Property("Scope") - .HasColumnType("integer"); + .HasColumnType("integer") + .HasComment("分组作用域:系统/业务。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -78,65 +91,83 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations b.HasIndex("TenantId", "Code") .IsUnique(); - b.ToTable("dictionary_groups", (string)null); + b.ToTable("dictionary_groups", null, t => + { + t.HasComment("参数字典分组(系统参数、业务参数)。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") .HasMaxLength(512) - .HasColumnType("character varying(512)"); + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); b.Property("GroupId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("关联分组 ID。"); b.Property("IsDefault") - .HasColumnType("boolean"); + .HasColumnType("boolean") + .HasComment("是否默认项。"); b.Property("IsEnabled") .ValueGeneratedOnAdd() .HasColumnType("boolean") - .HasDefaultValue(true); + .HasDefaultValue(true) + .HasComment("是否启用。"); b.Property("Key") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("字典项键。"); b.Property("SortOrder") .ValueGeneratedOnAdd() .HasColumnType("integer") - .HasDefaultValue(100); + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Value") .IsRequired() .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("字典项值。"); b.HasKey("Id"); @@ -145,7 +176,10 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations b.HasIndex("GroupId", "Key") .IsUnique(); - b.ToTable("dictionary_items", (string)null); + b.ToTable("dictionary_items", null, t => + { + t.HasComment("参数字典项。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs index c69074c..45d82e2 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; +using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -12,7 +13,7 @@ internal sealed class DictionaryDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase { public DictionaryDesignTimeDbContextFactory() - : base("TAKEOUTSAAS_APP_CONNECTION", "takeout_saas_app") + : base(DatabaseConstants.DictionaryDataSource, "TAKEOUTSAAS_DICTIONARY_CONNECTION") { } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs new file mode 100644 index 0000000..9f9c680 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs @@ -0,0 +1,185 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Identity.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Identity.Migrations +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20251201094410_AddEntityComments")] + partial class AddEntityComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Account") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("登录账号。"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("展示名称。"); + + b.Property("MerchantId") + .HasColumnType("uuid") + .HasComment("所属商户(平台管理员为空)。"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("密码哈希。"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("text") + .HasComment("权限集合。"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("text") + .HasComment("角色集合。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Account") + .IsUnique(); + + b.ToTable("identity_users", null, t => + { + t.HasComment("管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("OpenId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 OpenId。"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); + + b.Property("UnionId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 UnionId,可能为空。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "OpenId") + .IsUnique(); + + b.ToTable("mini_users", null, t => + { + t.HasComment("小程序用户实体。"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs new file mode 100644 index 0000000..5f0481b --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs @@ -0,0 +1,581 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Identity.Migrations +{ + /// + public partial class AddEntityComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterTable( + name: "mini_users", + comment: "小程序用户实体。"); + + migrationBuilder.AlterTable( + name: "identity_users", + comment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "mini_users", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "mini_users", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UnionId", + table: "mini_users", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "微信 UnionId,可能为空。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "mini_users", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "OpenId", + table: "mini_users", + type: "character varying(128)", + maxLength: 128, + nullable: false, + comment: "微信 OpenId。", + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "Nickname", + table: "mini_users", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "昵称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "mini_users", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "mini_users", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "mini_users", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "mini_users", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Avatar", + table: "mini_users", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "头像地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Id", + table: "mini_users", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "identity_users", + type: "uuid", + nullable: true, + comment: "最后更新人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "identity_users", + type: "timestamp with time zone", + nullable: true, + comment: "最近一次更新时间(UTC),从未更新时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "identity_users", + type: "uuid", + nullable: false, + comment: "所属租户 ID。", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Roles", + table: "identity_users", + type: "text", + nullable: false, + comment: "角色集合。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Permissions", + table: "identity_users", + type: "text", + nullable: false, + comment: "权限集合。", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "identity_users", + type: "character varying(256)", + maxLength: 256, + nullable: false, + comment: "密码哈希。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "identity_users", + type: "uuid", + nullable: true, + comment: "所属商户(平台管理员为空)。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "identity_users", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "展示名称。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "identity_users", + type: "uuid", + nullable: true, + comment: "删除人用户标识(软删除),未删除时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "identity_users", + type: "timestamp with time zone", + nullable: true, + comment: "软删除时间(UTC),未删除时为 null。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "identity_users", + type: "uuid", + nullable: true, + comment: "创建人用户标识,匿名或系统操作时为 null。", + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "identity_users", + type: "timestamp with time zone", + nullable: false, + comment: "创建时间(UTC)。", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Avatar", + table: "identity_users", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "头像地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Account", + table: "identity_users", + type: "character varying(64)", + maxLength: 64, + nullable: false, + comment: "登录账号。", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AlterColumn( + name: "Id", + table: "identity_users", + type: "uuid", + nullable: false, + comment: "实体唯一标识。", + oldClrType: typeof(Guid), + oldType: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterTable( + name: "mini_users", + oldComment: "小程序用户实体。"); + + migrationBuilder.AlterTable( + name: "identity_users", + oldComment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "mini_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "mini_users", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "UnionId", + table: "mini_users", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true, + oldComment: "微信 UnionId,可能为空。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "mini_users", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "OpenId", + table: "mini_users", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldComment: "微信 OpenId。"); + + migrationBuilder.AlterColumn( + name: "Nickname", + table: "mini_users", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "昵称。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "mini_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "mini_users", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "mini_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "mini_users", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Avatar", + table: "mini_users", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "头像地址。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "mini_users", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + + migrationBuilder.AlterColumn( + name: "UpdatedBy", + table: "identity_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "identity_users", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "identity_users", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "所属租户 ID。"); + + migrationBuilder.AlterColumn( + name: "Roles", + table: "identity_users", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "角色集合。"); + + migrationBuilder.AlterColumn( + name: "Permissions", + table: "identity_users", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldComment: "权限集合。"); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "identity_users", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldComment: "密码哈希。"); + + migrationBuilder.AlterColumn( + name: "MerchantId", + table: "identity_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "所属商户(平台管理员为空)。"); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "identity_users", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "展示名称。"); + + migrationBuilder.AlterColumn( + name: "DeletedBy", + table: "identity_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "删除人用户标识(软删除),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "identity_users", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true, + oldComment: "软删除时间(UTC),未删除时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + table: "identity_users", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true, + oldComment: "创建人用户标识,匿名或系统操作时为 null。"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "identity_users", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldComment: "创建时间(UTC)。"); + + migrationBuilder.AlterColumn( + name: "Avatar", + table: "identity_users", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "头像地址。"); + + migrationBuilder.AlterColumn( + name: "Account", + table: "identity_users", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldComment: "登录账号。"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "identity_users", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "实体唯一标识。"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs index 385d79c..1814980 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs @@ -26,58 +26,73 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("Account") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("登录账号。"); b.Property("Avatar") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DisplayName") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("展示名称。"); b.Property("MerchantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属商户(平台管理员为空)。"); b.Property("PasswordHash") .IsRequired() .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("密码哈希。"); b.Property("Permissions") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasComment("权限集合。"); b.Property("Roles") .IsRequired() - .HasColumnType("text"); + .HasColumnType("text") + .HasComment("角色集合。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -86,53 +101,68 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations b.HasIndex("TenantId", "Account") .IsUnique(); - b.ToTable("identity_users", (string)null); + b.ToTable("identity_users", null, t => + { + t.HasComment("管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + }); }); modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("实体唯一标识。"); b.Property("Avatar") .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); b.Property("CreatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); b.Property("DeletedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Nickname") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("character varying(64)") + .HasComment("昵称。"); b.Property("OpenId") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("微信 OpenId。"); b.Property("TenantId") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("所属租户 ID。"); b.Property("UnionId") .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("character varying(128)") + .HasComment("微信 UnionId,可能为空。"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); b.Property("UpdatedBy") - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -141,7 +171,10 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations b.HasIndex("TenantId", "OpenId") .IsUnique(); - b.ToTable("mini_users", (string)null); + b.ToTable("mini_users", null, t => + { + t.HasComment("小程序用户实体。"); + }); }); #pragma warning restore 612, 618 } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs index 48667fb..c18d179 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; +using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -12,7 +13,7 @@ internal sealed class IdentityDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase { public IdentityDesignTimeDbContextFactory() - : base("TAKEOUTSAAS_IDENTITY_CONNECTION", "takeout_saas_identity") + : base(DatabaseConstants.IdentityDataSource, "TAKEOUTSAAS_IDENTITY_CONNECTION") { } From b692a94d3cb07e11048327f34fecea479d042859 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 1 Dec 2025 19:58:07 +0800 Subject: [PATCH 14/56] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=20Redis=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8E=E8=B0=83=E5=BA=A6=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/10_TODO.md | 83 ++++++++++--------- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 4 +- .../Constants/DatabaseConstants.cs | 6 +- .../DatabaseServiceCollectionExtensions.cs | 11 ++- .../Services/RecurringJobRegistrar.cs | 25 +----- 6 files changed, 62 insertions(+), 71 deletions(-) diff --git a/Document/10_TODO.md b/Document/10_TODO.md index 48327f0..6ec07f3 100644 --- a/Document/10_TODO.md +++ b/Document/10_TODO.md @@ -1,57 +1,58 @@ # TODO Roadmap -> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。 +> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成? ## 1. 配置与基础设施(高优) -- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。 -- [x] Redis 服务部署完毕并记录配置。 -- [x] RabbitMQ 服务部署完毕并记录配置。 -- [x] COS 密钥配置补录完毕。 -- [ ] OSS 密钥配置补录完毕(待采购)。 -- [ ] SMS 平台密钥配置补录完毕(待采购)。 -- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。 -- [x] PostgreSQL 基础实例部署完毕并记录配置。 -- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。 +- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)? +- [x] Redis 服务部署完毕并记录配置? +- [x] RabbitMQ 服务部署完毕并记录配置? +- [x] COS 密钥配置补录完毕? +- [ ] OSS 密钥配置补录完毕(待采购)? +- [ ] SMS 平台密钥配置补录完毕(待采购)? +- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret?4324a7f604245301066ba7c3add488e,已同步?admin/mini 配置并登记更新人)? +- [x] PostgreSQL 基础实例部署完毕并记录配置? +- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md ?deploy/postgres|redis)? - [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). -- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。 -- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。 +- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成? +- [ ] Hangfire Dashboard 启用并新?Admin 角色验证/网关白名单? -## 2. 数据与迁移(高优) -- [ ] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 -- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 -- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 +## 2. 数据与迁移(高优? +- [x] App/Identity/Dictionary/Hangfire ĸ DbContext ɳʼ Migration ɹ update database +- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询? +- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行? ## 3. 稳定性与质量 -- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 -- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。 -- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 +- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler ?xUnit+FluentAssertions 单元测试框架搭建? +- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板? +- [ ] .editorconfig?globalconfig、Roslyn 分析器配置仓库通用规则并启?CI 检查? -## 4. 安全与合规 -- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 -- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 -- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 -- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 +## 4. 安全与合? +- [ ] RBAC 权限、租户隔离、用?权限洞察 API 完整演示并在 Swagger 中提供示例? +- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制? +- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口?Kibana Saved Search? +- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写?Git/数据库明文? -## 5. 观测与运维 -- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。 -- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 -- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 +## 5. 观测与运? +- [ ] TraceId 贯通,并在 Serilog 中输?Console/File/ELK 三种目标? +- [ ] Prometheus exporter 暴露关键指标?health 探针与告警规则同步推送? +- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告? ## 6. 业务能力补全 -- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。 -- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。 -- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。 +- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上?支付成功事件? +- [ ] 配送对?API 支持下单/取消/查询并完成签名验签中间件? +- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通? -## 7. 前后台 UI 对接 -- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。 -- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。 +## 7. 前后?UI 对接 +- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式? +- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环? -## 8. CI/CD 与发布 -- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。 -- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。 -- [ ] 版本与发布说明模板整理并在仓库中提供示例。 +## 8. CI/CD 与发? +- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移? +- [ ] Dev/Staging/Prod 多环境配置矩?+ 基础设施 IaC 脚本? +- [ ] 版本与发布说明模板整理并在仓库中提供示例? ## 9. 文档与知识库 -- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。 -- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。 -- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。 +- [ ] 接口文档、领域模型、关键约束使?Markdown ?API Portal 完整记录? +- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册? +- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格? + diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index 6762606..4eb2c0e 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false" + }, "Database": { "DataSources": { "AppDatabase": { @@ -30,7 +33,6 @@ } } }, - "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", "Identity": { "Jwt": { "Issuer": "takeout-saas", diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json index 6762606..4eb2c0e 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false" + }, "Database": { "DataSources": { "AppDatabase": { @@ -30,7 +33,6 @@ } } }, - "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", "Identity": { "Jwt": { "Issuer": "takeout-saas", diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs index cc18844..cf8a78e 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs @@ -1,7 +1,7 @@ -namespace TakeoutSaaS.Shared.Abstractions.Constants; +namespace TakeoutSaaS.Shared.Abstractions.Constants; /// -/// 数据源名称常量,统一配置键与使用。 +/// 数据源名称常量,统一配置键与使用说明。 /// public static class DatabaseConstants { @@ -16,7 +16,7 @@ public static class DatabaseConstants public const string IdentityDataSource = "IdentityDatabase"; /// - /// �����ֵ�⣨DictionaryDatabase���� + /// 字典库(DictionaryDatabase)。 /// public const string DictionaryDataSource = "DictionaryDatabase"; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs index bbc2b00..c1047d7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs @@ -42,10 +42,13 @@ public static class DatabaseServiceCollectionExtensions string dataSourceName) where TContext : DbContext { - services.AddDbContext((sp, options) => - { - ConfigureDbContextOptions(sp, options, dataSourceName, DatabaseConnectionRole.Write); - }); + services.AddDbContext( + (sp, options) => + { + ConfigureDbContextOptions(sp, options, dataSourceName, DatabaseConnectionRole.Write); + }, + contextLifetime: ServiceLifetime.Scoped, + optionsLifetime: ServiceLifetime.Singleton); services.AddDbContextFactory((sp, options) => { diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs index 1da22a0..6e3d559 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs @@ -1,4 +1,4 @@ -using Hangfire; +using Hangfire; using TakeoutSaaS.Module.Scheduler.Abstractions; using TakeoutSaaS.Module.Scheduler.Jobs; @@ -9,29 +9,12 @@ namespace TakeoutSaaS.Module.Scheduler.Services; /// public sealed class RecurringJobRegistrar : IRecurringJobRegistrar { - private readonly OrderTimeoutJob _orderTimeoutJob; - private readonly CouponExpireJob _couponExpireJob; - private readonly LogCleanupJob _logCleanupJob; - - /// - /// 初始化注册器。 - /// - public RecurringJobRegistrar( - OrderTimeoutJob orderTimeoutJob, - CouponExpireJob couponExpireJob, - LogCleanupJob logCleanupJob) - { - _orderTimeoutJob = orderTimeoutJob; - _couponExpireJob = couponExpireJob; - _logCleanupJob = logCleanupJob; - } - /// public Task RegisterAsync(CancellationToken cancellationToken = default) { - RecurringJob.AddOrUpdate("orders.timeout-cancel", () => _orderTimeoutJob.ExecuteAsync(), "*/5 * * * *"); - RecurringJob.AddOrUpdate("coupons.expire", () => _couponExpireJob.ExecuteAsync(), "0 */1 * * *"); - RecurringJob.AddOrUpdate("logs.cleanup", () => _logCleanupJob.ExecuteAsync(), "0 3 * * *"); + RecurringJob.AddOrUpdate("orders.timeout-cancel", job => job.ExecuteAsync(), "*/5 * * * *"); + RecurringJob.AddOrUpdate("coupons.expire", job => job.ExecuteAsync(), "0 */1 * * *"); + RecurringJob.AddOrUpdate("logs.cleanup", job => job.ExecuteAsync(), "0 3 * * *"); return Task.CompletedTask; } } From 7ae20c1459978a5ffddbf3ac77ae860a800e7642 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 1 Dec 2025 20:25:56 +0800 Subject: [PATCH 15/56] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E4=B9=B1=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/10_TODO.md | 83 ++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/Document/10_TODO.md b/Document/10_TODO.md index 6ec07f3..193d718 100644 --- a/Document/10_TODO.md +++ b/Document/10_TODO.md @@ -1,58 +1,57 @@ # TODO Roadmap -> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成? +> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。 ## 1. 配置与基础设施(高优) -- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)? -- [x] Redis 服务部署完毕并记录配置? -- [x] RabbitMQ 服务部署完毕并记录配置? -- [x] COS 密钥配置补录完毕? -- [ ] OSS 密钥配置补录完毕(待采购)? -- [ ] SMS 平台密钥配置补录完毕(待采购)? -- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret?4324a7f604245301066ba7c3add488e,已同步?admin/mini 配置并登记更新人)? -- [x] PostgreSQL 基础实例部署完毕并记录配置? -- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md ?deploy/postgres|redis)? +- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。 +- [x] Redis 服务部署完毕并记录配置。 +- [x] RabbitMQ 服务部署完毕并记录配置。 +- [x] COS 密钥配置补录完毕。 +- [ ] OSS 密钥配置补录完毕(待采购)。 +- [ ] SMS 平台密钥配置补录完毕(待采购)。 +- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。 +- [x] PostgreSQL 基础实例部署完毕并记录配置。 +- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。 - [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). -- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成? -- [ ] Hangfire Dashboard 启用并新?Admin 角色验证/网关白名单? +- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。 +- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。 -## 2. 数据与迁移(高优? -- [x] App/Identity/Dictionary/Hangfire ĸ DbContext ɳʼ Migration ɹ update database -- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询? -- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行? +## 2. 数据与迁移(高优) +- [x] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 +- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 +- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 ## 3. 稳定性与质量 -- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler ?xUnit+FluentAssertions 单元测试框架搭建? -- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板? -- [ ] .editorconfig?globalconfig、Roslyn 分析器配置仓库通用规则并启?CI 检查? +- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 +- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。 +- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 -## 4. 安全与合? -- [ ] RBAC 权限、租户隔离、用?权限洞察 API 完整演示并在 Swagger 中提供示例? -- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制? -- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口?Kibana Saved Search? -- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写?Git/数据库明文? +## 4. 安全与合规 +- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 +- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 +- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 +- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 -## 5. 观测与运? -- [ ] TraceId 贯通,并在 Serilog 中输?Console/File/ELK 三种目标? -- [ ] Prometheus exporter 暴露关键指标?health 探针与告警规则同步推送? -- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告? +## 5. 观测与运维 +- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。 +- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 +- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 ## 6. 业务能力补全 -- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上?支付成功事件? -- [ ] 配送对?API 支持下单/取消/查询并完成签名验签中间件? -- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通? +- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。 +- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。 +- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。 -## 7. 前后?UI 对接 -- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式? -- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环? +## 7. 前后台 UI 对接 +- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。 +- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。 -## 8. CI/CD 与发? -- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移? -- [ ] Dev/Staging/Prod 多环境配置矩?+ 基础设施 IaC 脚本? -- [ ] 版本与发布说明模板整理并在仓库中提供示例? +## 8. CI/CD 与发布 +- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。 +- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。 +- [ ] 版本与发布说明模板整理并在仓库中提供示例。 ## 9. 文档与知识库 -- [ ] 接口文档、领域模型、关键约束使?Markdown ?API Portal 完整记录? -- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册? -- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格? - +- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。 +- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。 +- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。 \ No newline at end of file From e996db2d23561fc4c198feeacd60d6c6e77f5816 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 1 Dec 2025 20:37:22 +0800 Subject: [PATCH 16/56] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0AI=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E8=A1=A5=E5=85=85=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/08_AI编程规范.md | 8 ++++++++ Document/09_AI精简开发规范.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/Document/08_AI编程规范.md b/Document/08_AI编程规范.md index c893af3..8c4e56a 100644 --- a/Document/08_AI编程规范.md +++ b/Document/08_AI编程规范.md @@ -2,6 +2,13 @@ > 本文档专门为AI编程助手准备,汇总了所有编码规范和规则,确保代码质量和一致性。 +## 0. AI交互补充约束 +1. 每次回复必须回复中文。 +2. 不要更改我的文件编码。 +3. 每次修复bug或者新增完小功能必须提交git。 +4. 新创建的文件或者修改过的文件注释部分必须保持中文。 +5. 项目中不要有乱码。 +6. 在 PowerShell 查看文件时必须指定 UTF8(例如 Get-Content -Encoding UTF8 或设置 $OutputEncoding 为 UTF8),避免输出乱码。 ## 1. 技术栈要求 ### 1.1 核心技术 @@ -1587,3 +1594,4 @@ Extensions/ **最后更新**:2025-11-22 **适用项目**:外卖SaaS系统 **目标读者**:AI编程助手、开发人员 + diff --git a/Document/09_AI精简开发规范.md b/Document/09_AI精简开发规范.md index 7447db8..f812dfe 100644 --- a/Document/09_AI精简开发规范.md +++ b/Document/09_AI精简开发规范.md @@ -2,6 +2,13 @@ 说明:本规范为AI编程助手与开发者共同遵循的统一编码规范,结合 0_Document 下文档约定(特别是 06_开发规范.md、02_技术架构.md、12.* 规范)执行。超出本文件内容的详细条目请以文档中心为准。 +## 0. AI交互补充约束 +1. 每次回复必须回复中文。 +2. 不要更改我的文件编码。 +3. 每次修复bug或者新增完小功能必须提交git。 +4. 新创建的文件或者修改过的文件注释部分必须保持中文。 +5. 项目中不要有乱码。 +6. 在 PowerShell 查看文件时必须指定 UTF8(例如 Get-Content -Encoding UTF8 或设置 $OutputEncoding 为 UTF8),避免输出乱码。 ## 1. 技术栈 - .NET 10 + ASP.NET Core Web API - EF Core 10(复杂关系/事务)+ Dapper(统计/批量)+ PostgreSQL 16+ @@ -77,3 +84,4 @@ - 同步阻塞异步 以上规范将随着文档中心的演进不断完善;AI编程助手生成的代码必须符合本规范,并默认使用这些约束。 + From 85279180c6ec20b30c4cde5cd0dc269d56f47d90 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 1 Dec 2025 21:11:24 +0800 Subject: [PATCH 17/56] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/10_TODO.md | 57 -------------- Document/10_服务器文档.md | 78 +++++++++++++++++++ Document/11_WaitTODO.md | 53 ------------- ...配置指引.md => 11_设计期DbContext配置指引.md} | 2 +- Document/12_SystemTodo.md | 57 ++++++++++++++ Document/12_阿里云网关服务器.md | 17 ---- Document/13_BusinessTodo.md | 54 +++++++++++++ Document/13_腾讯云主PostgreSQL服务器.md | 18 ----- Document/14_天翼云主应用服务器.md | 18 ----- Document/15_腾讯云RedisRabbitMQ服务器.md | 18 ----- 10 files changed, 190 insertions(+), 182 deletions(-) delete mode 100644 Document/10_TODO.md create mode 100644 Document/10_服务器文档.md delete mode 100644 Document/11_WaitTODO.md rename Document/{16_设计时DbContext配置指引.md => 11_设计期DbContext配置指引.md} (98%) create mode 100644 Document/12_SystemTodo.md delete mode 100644 Document/12_阿里云网关服务器.md create mode 100644 Document/13_BusinessTodo.md delete mode 100644 Document/13_腾讯云主PostgreSQL服务器.md delete mode 100644 Document/14_天翼云主应用服务器.md delete mode 100644 Document/15_腾讯云RedisRabbitMQ服务器.md diff --git a/Document/10_TODO.md b/Document/10_TODO.md deleted file mode 100644 index 193d718..0000000 --- a/Document/10_TODO.md +++ /dev/null @@ -1,57 +0,0 @@ -# TODO Roadmap - -> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。 - -## 1. 配置与基础设施(高优) -- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。 -- [x] Redis 服务部署完毕并记录配置。 -- [x] RabbitMQ 服务部署完毕并记录配置。 -- [x] COS 密钥配置补录完毕。 -- [ ] OSS 密钥配置补录完毕(待采购)。 -- [ ] SMS 平台密钥配置补录完毕(待采购)。 -- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。 -- [x] PostgreSQL 基础实例部署完毕并记录配置。 -- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。 -- [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). -- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。 -- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。 - -## 2. 数据与迁移(高优) -- [x] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 -- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 -- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 - -## 3. 稳定性与质量 -- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 -- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。 -- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 - -## 4. 安全与合规 -- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 -- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 -- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 -- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 - -## 5. 观测与运维 -- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。 -- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 -- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 - -## 6. 业务能力补全 -- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。 -- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。 -- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。 - -## 7. 前后台 UI 对接 -- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。 -- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。 - -## 8. CI/CD 与发布 -- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。 -- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。 -- [ ] 版本与发布说明模板整理并在仓库中提供示例。 - -## 9. 文档与知识库 -- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。 -- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。 -- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。 \ No newline at end of file diff --git a/Document/10_服务器文档.md b/Document/10_服务器文档.md new file mode 100644 index 0000000..6d60349 --- /dev/null +++ b/Document/10_服务器文档.md @@ -0,0 +1,78 @@ +# 服务器文档 + +> 汇总原 12~15 号服务器记录,统一追踪账号、密码、用途与到期时间,便于统一维护。 + +## 1. 阿里云网关服务器 + +### 基础信息 +- IP: 47.94.199.87 +- 账户: root +- 密码: cJ5q2k2iW7XnMA^! +- 配置: 2 核 CPU / 2 GB 内存(阿里云轻量应用服务器) +- 地点: 北京 +- 用途: 网关 +- 到期时间: 2026-12-18 + +### 建议补充 +- 系统版本: 待补充(执行 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充 +- 备份与监控: 待补充 +- 变更记录: 待补充 + +## 2. 腾讯云主机 PostgreSQL 服务器 + +### 基础信息 +- IP: 120.53.222.17 +- 账户: ubuntu +- 密码: P3y$nJt#zaa4%fh5 +- 配置: 2 核 CPU / 4 GB 内存 +- 地点: 北京 +- 用途: 主 PostgreSQL / 数据库服务器 +- 到期时间: 2026-11-26 11:22:01 + +### 建议补充 +- 系统版本: 待补充(执行 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 数据目录: 待补充(示例 `/var/lib/postgresql`) +- 数据备份/监控: 待补充 +- 安全组/开放端口: 待补充 +- 变更记录: 待补充 + +## 3. 天翼云主机 PostgreSQL 服务器 + +### 基础信息 +- IP: 49.7.179.246 +- 账户: root +- 密码: 7zE&84XI6~w57W7N +- 配置: 4 核 CPU / 8 GB 内存(天翼云) +- 地点: 北京 +- 用途: PostgreSQL 服务器 +- 到期时间: 2027-10-04 17:17:57 + +### 建议补充 +- 系统版本: 待补充(执行 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 数据目录: 待补充(示例 `/var/lib/postgresql`) +- 数据备份/监控: 待补充 +- 安全组/开放端口: 待补充 +- 变更记录: 待补充 + +## 4. 腾讯云 Redis/RabbitMQ 服务器 + +### 基础信息 +- IP: 49.232.6.45 +- 账户: ubuntu +- 密码: Z7NsRjT&XnWg7%7X +- 配置: 2 核 CPU / 4 GB 内存 +- 地点: 北京 +- 用途: Redis 与 RabbitMQ +- 到期时间: 2028-11-26 + +### 建议补充 +- 系统版本: 待补充(执行 `cat /etc/os-release`) +- 带宽/磁盘: 待补充 +- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) +- 数据持久化与备份: 待补充 +- 监控与告警: 待补充 +- 变更记录: 待补充 diff --git a/Document/11_WaitTODO.md b/Document/11_WaitTODO.md deleted file mode 100644 index 700f366..0000000 --- a/Document/11_WaitTODO.md +++ /dev/null @@ -1,53 +0,0 @@ -# 里程碑待办追踪 - -> 按“小程序版模块规划”划分四个里程碑;每个里程碑只含对应范围的任务,便于分阶段推进。 - ---- -## Phase 1(当前阶段):租户/商家入驻、门店与菜品、扫码堂食、基础下单支付、预购自提、第三方配送骨架 -- [ ] 管理端租户 API:注册、实名认证、套餐订阅/续费/升降配、审核流,Swagger ≥6 个端点,含审核日志。 -- [ ] 商家入驻 API:证照上传、合同管理、类目选择,驱动待审/审核/驳回/通过状态机,文件持久在 COS。 -- [ ] RBAC 模板:平台管理员、租户管理员、店长、店员四角色模板;API 可复制并允许租户自定义扩展。 -- [ ] 配额与套餐:TenantPackage CRUD、订阅/续费/配额校验(门店/账号/短信/配送单量),超额返回 409 并记录 TenantQuotaUsage。 -- [ ] 租户运营面板:欠费/到期告警、账单列表、公告通知接口,支持已读状态并在 Admin UI 展示。 -- [ ] 门店管理:Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。 -- [ ] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 -- [ ] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 -- [ ] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。 -- [ ] 菜品建模:分类、SPU、SKU、规格/加料组、价格策略、媒资 CRUD + 上下架流程;Mini 端可拉取完整 JSON。 -- [ ] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。 -- [ ] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。 -- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。 -- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。 -- [ ] 桌台账单:合单/拆单、结账、电子小票、桌台释放,完成结账后恢复 Idle 并生成票据 URL。 -- [ ] 自配送骨架:骑手管理、取送件信息录入、费用补贴记录,Admin 端可派单并更新 DeliveryOrder。 -- [ ] 第三方配送抽象:统一下单/取消/加价/查询接口,支持达达、美团、闪送等,含回调验签与异常补偿骨架。 -- [ ] 预购自提核销:提货码生成、手机号/二维码核销、自提柜/前台流程,超时自动取消或退款,记录操作者与时间。 -- [ ] 指标与日志:Prometheus 输出订单创建、支付成功率、配送回调耗时等,Grafana ≥8 个图表;关键流程日志记录 TraceId + 业务 ID。 -- [ ] 测试:Phase 1 核心 API 具备 ≥30 条自动化用例(单元 + 集成),覆盖租户→商户→下单链路。 - ---- -## Phase 2(下一阶段):拼单、优惠券与基础营销、会员积分/会员日、客服聊天、同城自配送调度、搜索 -- [ ] 拼单引擎:GroupOrder/Participant CRUD、发起/加入/成团条件、自动解散与退款、团内消息与提醒。 -- [ ] 优惠券与基础营销:模板管理、领券、核销、库存/有效期/叠加规则,基础抽奖/秒杀/满减活动。 -- [ ] 会员与积分:会员档案、等级/成长值、会员日通知;积分获取/消耗、有效期、黑名单。 -- [ ] 客服聊天:实时会话、机器人/人工切换、排队/转接、消息模板、敏感词审查、工单流转与评价。 -- [ ] 同城自配送调度:骑手智能指派、路线估时、无接触配送、费用补贴策略、调度看板。 -- [ ] 搜索:门店/菜品/活动/优惠券搜索,过滤/排序、热门/历史记录、联想与纠错。 - ---- -## Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、高阶营销、风控与补偿 -- [ ] 分销返利:AffiliatePartner/Order/Payout 管理,佣金阶梯、结算周期、税务信息、违规处理。 -- [ ] 签到打卡:CheckInCampaign/Record、连签奖励、补签、积分/券/成长值奖励、反作弊机制。 -- [ ] 预约预订:档期/资源占用、预约下单/支付、提醒/改期/取消、到店核销与履约记录。 -- [ ] 地图导航扩展:附近门店/推荐、距离/路线规划、跳转原生导航、导航请求埋点。 -- [ ] 社区:动态发布、评论、点赞、话题/标签、图片/视频审核、举报与风控,店铺口碑展示。 -- [ ] 高阶营销:秒杀/抽奖/裂变、裂变海报、爆款推荐位、多渠道投放分析。 -- [ ] 风控与审计:黑名单、频率限制、异常行为监控、审计日志、补偿与告警体系。 - ---- -## Phase 4:性能优化、缓存、运营大盘、测试与文档、上线与监控 -- [ ] 性能与缓存:热点接口缓存、慢查询治理、批处理优化、异步化改造。 -- [ ] 可靠性:幂等与重试策略、任务调度补偿、链路追踪、告警联动。 -- [ ] 运营大盘:交易/营销/履约/用户维度的细分报表、GMV/成本/毛利分析。 -- [ ] 文档与测试:完整测试矩阵、性能测试报告、上线手册、回滚方案。 -- [ ] 监控与运维:上线发布流程、灰度/回滚策略、系统稳定性指标、24x7 监控与告警。 diff --git a/Document/16_设计时DbContext配置指引.md b/Document/11_设计期DbContext配置指引.md similarity index 98% rename from Document/16_设计时DbContext配置指引.md rename to Document/11_设计期DbContext配置指引.md index d1fa968..5baba69 100644 --- a/Document/16_设计时DbContext配置指引.md +++ b/Document/11_设计期DbContext配置指引.md @@ -45,7 +45,7 @@ $env:TAKEOUTSAAS_APP_CONNECTION = \"Host=120.53.222.17;Port=5432;Database=takeou #(可选)覆盖 IdentityDatabase 连接串 $env:TAKEOUTSAAS_IDENTITY_CONNECTION = \"Host=...;Database=takeout_identity_db;Username=...;Password=...\" -#����ѡ������ DictionaryDatabase ���Ӵ� +#(可选)覆盖 DictionaryDatabase 连接串 $env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=...;Database=takeout_dictionary_db;Username=...;Password=..." ``` diff --git a/Document/12_SystemTodo.md b/Document/12_SystemTodo.md new file mode 100644 index 0000000..d6e5114 --- /dev/null +++ b/Document/12_SystemTodo.md @@ -0,0 +1,57 @@ +# System-Level TODOs + +> Infrastructure / platform backlog migrated from the former 11th document. Unless noted explicitly, all checklist items remain pending. + +## 1. 閰嶇疆涓庡熀纭€璁炬柦锛堥珮浼橈級 +- [x] Development/Production 鏁版嵁搴撹繛鎺ヤ笌 Secret 钀藉湴锛圫taging 鏆備笉闇€瑕侊級銆? +- [x] Redis 鏈嶅姟閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€? +- [x] RabbitMQ 鏈嶅姟閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€? +- [x] COS 瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯銆? +- [ ] OSS 瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛堝緟閲囪喘锛夈€? +- [ ] SMS 骞冲彴瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛堝緟閲囪喘锛夈€? +- [x] WeChat Mini 绋嬪簭瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛圓ppID锛歸x30f91e6afe79f405锛孉ppSecret锛?4324a7f604245301066ba7c3add488e锛屽凡鍚屾鍒?admin/mini 閰嶇疆骞剁櫥璁版洿鏂颁汉锛夈€? +- [x] PostgreSQL 鍩虹瀹炰緥閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€? +- [x] Postgres/Redis 鎺ュ叆鏂囨。 + IaC/鑴氭湰琛ラ綈锛堣 Document/infra/postgres_redis.md 涓?deploy/postgres|redis锛夈€? +- [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). +- [ ] admin/mini/user/gateway 缃戝叧鍩熷悕銆佽瘉涔︺€丆ORS 鍒楄〃鏁寸悊瀹屾垚銆? +- [ ] Hangfire Dashboard 鍚敤骞舵柊澧?Admin 瑙掕壊楠岃瘉/缃戝叧鐧藉悕鍗曘€? + +## 2. 鏁版嵁涓庤縼绉伙紙楂樹紭锛? +- [x] App/Identity/Dictionary/Hangfire 鍥涗釜 DbContext 鍧囩敓鎴愬垵濮?Migration 骞舵垚鍔?update database銆? +- [ ] 鍟嗘埛/闂ㄥ簵/鍟嗗搧/璁㈠崟/鏀粯/閰嶉€佺瓑瀹炰綋涓庝粨鍌ㄥ疄鐜板畬鎴愶紝鎻愪緵 CRUD + 鏌ヨ銆? +- [ ] 绯荤粺鍙傛暟銆侀粯璁ょ鎴枫€佺鐞嗗憳璐﹀彿銆佸熀纭€瀛楀吀鐨勭瀛愯剼鏈彲閲嶅鎵ц銆? + +## 3. 绋冲畾鎬т笌璐ㄩ噺 +- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 鐨?xUnit+FluentAssertions 鍗曞厓娴嬭瘯妗嗘灦鎼缓銆? +- [ ] WebApplicationFactory + Testcontainers 鎷夎捣 Postgres/Redis/RabbitMQ/MinIO 鐨勯泦鎴愭祴璇曟ā鏉裤€? +- [ ] .editorconfig銆?globalconfig銆丷oslyn 鍒嗘瀽鍣ㄩ厤缃粨搴撻€氱敤瑙勫垯骞跺惎鐢?CI 妫€鏌ャ€? + +## 4. 瀹夊叏涓庡悎瑙? +- [ ] RBAC 鏉冮檺銆佺鎴烽殧绂汇€佺敤鎴?鏉冮檺娲炲療 API 瀹屾暣婕旂ず骞跺湪 Swagger 涓彁渚涚ず渚嬨€? +- [ ] 鐧诲綍/鍒锋柊娴佺▼澧炲姞 IP 鏍¢獙銆佺鎴烽殧绂汇€侀獙璇佺爜/棰戠巼闄愬埗銆? +- [ ] 鐧诲綍/鏉冮檺/鏁忔劅鎿嶄綔鏃ュ織鍙拷婧紝鎻愪緵鏌ヨ鎺ュ彛鎴?Kibana Saved Search銆? +- [ ] Secret Store/KeyVault/KMS 绠$悊鏁忔劅閰嶇疆锛岀姝㈠瘑閽ュ啓鍏?Git/鏁版嵁搴撴槑鏂囥€? + +## 5. 瑙傛祴涓庤繍缁? +- [ ] TraceId 璐€氾紝骞跺湪 Serilog 涓緭鍑?Console/File/ELK 涓夌鐩爣銆? +- [ ] Prometheus exporter 鏆撮湶鍏抽敭鎸囨爣锛?health 鎺㈤拡涓庡憡璀﹁鍒欏悓姝ユ帹閫併€? +- [ ] PostgreSQL 鍏ㄩ噺/澧為噺澶囦唤鑴氭湰鍙婁竴娆$湡瀹炴仮澶嶆紨缁冩姤鍛娿€? + +## 6. 涓氬姟鑳藉姏琛ュ叏 +- [ ] 鍟嗘埛/闂ㄥ簵/鑿滃搧 API 瀹屾垚骞跺湪 MQ 涓姇閫掍笂鏋?鏀粯鎴愬姛浜嬩欢銆? +- [ ] 閰嶉€佸鎺?API 鏀寔涓嬪崟/鍙栨秷/鏌ヨ骞跺畬鎴愮鍚嶉獙绛句腑闂翠欢銆? +- [ ] 灏忕▼搴忕鍟嗗搧娴忚銆佷笅鍗曘€佹敮浠樸€佽瘎浠枫€佸浘鐗囩洿浼犵瓑 API 鍙棴鐜窇閫氥€? + +## 7. 鍓嶅悗鍙?UI 瀵规帴 +- [ ] Admin UI 閫氳繃 OpenAPI 鐢熸垚鎴栨墜鍐欑晫闈紝鎺ュ叆 Hangfire Dashboard/MQ 鐩戞帶鍙妯″紡銆? +- [ ] 灏忕▼搴忕瀹屾垚鐧诲綍銆佽彍鍗曟祻瑙堛€佷笅鍗曘€佹敮浠樸€佺墿娴佽建杩广€佺礌鏉愮洿浼犻棴鐜€? + +## 8. CI/CD 涓庡彂甯? +- [ ] CI/CD 娴佹按绾胯鐩栨瀯寤恒€佸彂甯冦€侀潤鎬佹壂鎻忋€佹暟鎹簱杩佺Щ銆? +- [ ] Dev/Staging/Prod 澶氱幆澧冮厤缃煩闃?+ 鍩虹璁炬柦 IaC 鑴氭湰銆? +- [ ] 鐗堟湰涓庡彂甯冭鏄庢ā鏉挎暣鐞嗗苟鍦ㄤ粨搴撲腑鎻愪緵绀轰緥銆? + +## 9. 鏂囨。涓庣煡璇嗗簱 +- [ ] 鎺ュ彛鏂囨。銆侀鍩熸ā鍨嬨€佸叧閿害鏉熶娇鐢?Markdown 鎴?API Portal 瀹屾暣璁板綍銆? +- [ ] 杩愯鎵嬪唽鍖呭惈閮ㄧ讲姝ラ銆佽祫婧愭嫇鎵戙€佹晠闅滄帓鏌ユ墜鍐屻€? +- [ ] 瀹夊叏鍚堣妯℃澘瑕嗙洊鏁版嵁鍒嗙骇銆佸瘑閽ョ鐞嗐€佸璁℃祦绋嬪苟褰㈡垚鍙鐢ㄨ〃鏍笺€ diff --git a/Document/12_阿里云网关服务器.md b/Document/12_阿里云网关服务器.md deleted file mode 100644 index 8fcea29..0000000 --- a/Document/12_阿里云网关服务器.md +++ /dev/null @@ -1,17 +0,0 @@ -# 阿里云网关服务器 - -## 基础信息 -- IP: 47.94.199.87 -- 账户: root -- 密码: cJ5q2k2iW7XnMA^! -- 配置: 2 核 CPU / 2 GB 内存(阿里云) -- 地点: 北京 -- 用途: 网关 -- 到期时间: 2026-12-18 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充 -- 备份与监控: 待补充 -- 变更记录: 待补充 diff --git a/Document/13_BusinessTodo.md b/Document/13_BusinessTodo.md new file mode 100644 index 0000000..e59a6dc --- /dev/null +++ b/Document/13_BusinessTodo.md @@ -0,0 +1,54 @@ +# Business-Level TODOs + +> Product & business capability roadmap grouped by milestones; each phase only tracks the scoped backlog to enable staged delivery. + +--- +## Phase 1锛堝綋鍓嶉樁娈碉級锛氱鎴?鍟嗗鍏ラ┗銆侀棬搴椾笌鑿滃搧銆佹壂鐮佸爞椋熴€佸熀纭€涓嬪崟鏀粯銆侀璐嚜鎻愩€佺涓夋柟閰嶉€侀鏋? +- [ ] 绠$悊绔鎴?API锛氭敞鍐屻€佸疄鍚嶈璇併€佸椁愯闃?缁垂/鍗囬檷閰嶃€佸鏍告祦锛孲wagger 鈮? 涓鐐癸紝鍚鏍告棩蹇椼€? +- [ ] 鍟嗗鍏ラ┗ API锛氳瘉鐓т笂浼犮€佸悎鍚岀鐞嗐€佺被鐩€夋嫨锛岄┍鍔ㄥ緟瀹?瀹℃牳/椹冲洖/閫氳繃鐘舵€佹満锛屾枃浠舵寔涔呭湪 COS銆? +- [ ] RBAC 妯℃澘锛氬钩鍙扮鐞嗗憳銆佺鎴风鐞嗗憳銆佸簵闀裤€佸簵鍛樺洓瑙掕壊妯℃澘锛汚PI 鍙鍒跺苟鍏佽绉熸埛鑷畾涔夋墿灞曘€? +- [ ] 閰嶉涓庡椁愶細TenantPackage CRUD銆佽闃?缁垂/閰嶉鏍¢獙锛堥棬搴?璐﹀彿/鐭俊/閰嶉€佸崟閲忥級锛岃秴棰濊繑鍥?409 骞惰褰?TenantQuotaUsage銆? +- [ ] 绉熸埛杩愯惀闈㈡澘锛氭瑺璐?鍒版湡鍛婅銆佽处鍗曞垪琛ㄣ€佸叕鍛婇€氱煡鎺ュ彛锛屾敮鎸佸凡璇荤姸鎬佸苟鍦?Admin UI 灞曠ず銆? +- [ ] 闂ㄥ簵绠$悊锛歋tore/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 瀹屾暣锛屽惈 GeoJSON 閰嶉€佽寖鍥村強鑳藉姏寮€鍏炽€? +- [ ] 妗岀爜绠$悊锛氭壒閲忕敓鎴愭鐮併€佺粦瀹氬尯鍩?瀹归噺銆佸鍑轰簩缁寸爜 ZIP锛圥OST /api/admin/stores/{id}/tables 鍙笅杞斤級銆? +- [ ] 鍛樺伐鎺掔彮锛氬垱寤哄憳宸ャ€佺粦瀹氶棬搴楄鑹层€佺淮鎶?StoreEmployeeShift锛屽彲鏌ヨ鏈潵 7 鏃ユ帓鐝€? +- [ ] 妗岀爜鎵爜鍏ュ彛锛歁ini 绔В鏋愪簩缁寸爜锛孏ET /api/mini/tables/{code}/context 杩斿洖闂ㄥ簵銆佹鍙般€佸叕鍛娿€? +- [ ] 鑿滃搧寤烘ā锛氬垎绫汇€丼PU銆丼KU銆佽鏍?鍔犳枡缁勩€佷环鏍肩瓥鐣ャ€佸獟璧?CRUD + 涓婁笅鏋舵祦绋嬶紱Mini 绔彲鎷夊彇瀹屾暣 JSON銆? +- [ ] 搴撳瓨浣撶郴锛歋KU 搴撳瓨銆佹壒娆°€佽皟鏁淬€佸敭缃勭鐞嗭紝鏀寔棰勫敭/妗f湡閿佸畾骞跺湪璁㈠崟涓墸鍑?閲婃斁銆? +- [ ] 鑷彁妗f湡锛氶棬搴楅厤缃嚜鎻愭椂闂寸獥銆佸閲忋€佹埅鍗曟椂闂达紱Mini 绔嵁姝ら檺鍒朵笅鍗曟椂闂淬€? +- [ ] 璐墿杞︽湇鍔★細ShoppingCart/CartItem/CartItemAddon API 鏀寔骞跺彂閿併€侀檺璐€佸埜/绉垎棰勬牎楠岋紝淇濊瘉骞跺彂鏃犺剰鏁版嵁銆? +- [ ] 璁㈠崟涓庢敮浠橈細鍫傞/鑷彁/閰嶉€佷笅鍗曘€佸井淇?鏀粯瀹濇敮浠樸€佷紭鎯犲埜/绉垎鎶垫墸銆佽鍗曠姸鎬佹満涓庨€氱煡閾捐矾榻愬叏銆? +- [ ] 妗屽彴璐﹀崟锛氬悎鍗?鎷嗗崟銆佺粨璐︺€佺數瀛愬皬绁ㄣ€佹鍙伴噴鏀撅紝瀹屾垚缁撹处鍚庢仮澶?Idle 骞剁敓鎴愮エ鎹?URL銆? +- [ ] 鑷厤閫侀鏋讹細楠戞墜绠$悊銆佸彇閫佷欢淇℃伅褰曞叆銆佽垂鐢ㄨˉ璐磋褰曪紝Admin 绔彲娲惧崟骞舵洿鏂?DeliveryOrder銆? +- [ ] 绗笁鏂归厤閫佹娊璞★細缁熶竴涓嬪崟/鍙栨秷/鍔犱环/鏌ヨ鎺ュ彛锛屾敮鎸佽揪杈俱€佺編鍥€侀棯閫佺瓑锛屽惈鍥炶皟楠岀涓庡紓甯歌ˉ鍋块鏋躲€? +- [ ] 棰勮喘鑷彁鏍搁攢锛氭彁璐х爜鐢熸垚銆佹墜鏈哄彿/浜岀淮鐮佹牳閿€銆佽嚜鎻愭煖/鍓嶅彴娴佺▼锛岃秴鏃惰嚜鍔ㄥ彇娑堟垨閫€娆撅紝璁板綍鎿嶄綔鑰呬笌鏃堕棿銆? +- [ ] 鎸囨爣涓庢棩蹇楋細Prometheus 杈撳嚭璁㈠崟鍒涘缓銆佹敮浠樻垚鍔熺巼銆侀厤閫佸洖璋冭€楁椂绛夛紝Grafana 鈮? 涓浘琛紱鍏抽敭娴佺▼鏃ュ織璁板綍 TraceId + 涓氬姟 ID銆? +- [ ] 娴嬭瘯锛歅hase 1 鏍稿績 API 鍏峰 鈮?0 鏉¤嚜鍔ㄥ寲鐢ㄤ緥锛堝崟鍏?+ 闆嗘垚锛夛紝瑕嗙洊绉熸埛鈫掑晢鎴封啋涓嬪崟閾捐矾銆? + +--- +## Phase 2锛堜笅涓€闃舵锛夛細鎷煎崟銆佷紭鎯犲埜涓庡熀纭€钀ラ攢銆佷細鍛樼Н鍒?浼氬憳鏃ャ€佸鏈嶈亰澶┿€佸悓鍩庤嚜閰嶉€佽皟搴︺€佹悳绱? +- [ ] 鎷煎崟寮曟搸锛欸roupOrder/Participant CRUD銆佸彂璧?鍔犲叆/鎴愬洟鏉′欢銆佽嚜鍔ㄨВ鏁d笌閫€娆俱€佸洟鍐呮秷鎭笌鎻愰啋銆? +- [ ] 浼樻儬鍒镐笌鍩虹钀ラ攢锛氭ā鏉跨鐞嗐€侀鍒搞€佹牳閿€銆佸簱瀛?鏈夋晥鏈?鍙犲姞瑙勫垯锛屽熀纭€鎶藉/绉掓潃/婊″噺娲诲姩銆? +- [ ] 浼氬憳涓庣Н鍒嗭細浼氬憳妗f銆佺瓑绾?鎴愰暱鍊笺€佷細鍛樻棩閫氱煡锛涚Н鍒嗚幏鍙?娑堣€椼€佹湁鏁堟湡銆侀粦鍚嶅崟銆? +- [ ] 瀹㈡湇鑱婂ぉ锛氬疄鏃朵細璇濄€佹満鍣ㄤ汉/浜哄伐鍒囨崲銆佹帓闃?杞帴銆佹秷鎭ā鏉裤€佹晱鎰熻瘝瀹℃煡銆佸伐鍗曟祦杞笌璇勪环銆? +- [ ] 鍚屽煄鑷厤閫佽皟搴︼細楠戞墜鏅鸿兘鎸囨淳銆佽矾绾夸及鏃躲€佹棤鎺ヨЕ閰嶉€併€佽垂鐢ㄨˉ璐寸瓥鐣ャ€佽皟搴︾湅鏉裤€? +- [ ] 鎼滅储锛氶棬搴?鑿滃搧/娲诲姩/浼樻儬鍒告悳绱紝杩囨护/鎺掑簭銆佺儹闂?鍘嗗彶璁板綍銆佽仈鎯充笌绾犻敊銆? + +--- +## Phase 3锛氬垎閿€杩斿埄銆佺鍒版墦鍗°€侀绾﹂璁€佸湴鍥惧鑸€佺ぞ鍖恒€侀珮闃惰惀閿€銆侀鎺т笌琛ュ伩 +- [ ] 鍒嗛攢杩斿埄锛欰ffiliatePartner/Order/Payout 绠$悊锛屼剑閲戦樁姊€佺粨绠楀懆鏈熴€佺◣鍔′俊鎭€佽繚瑙勫鐞嗐€? +- [ ] 绛惧埌鎵撳崱锛欳heckInCampaign/Record銆佽繛绛惧鍔便€佽ˉ绛俱€佺Н鍒?鍒?鎴愰暱鍊煎鍔便€佸弽浣滃紛鏈哄埗銆? +- [ ] 棰勭害棰勮锛氭。鏈?璧勬簮鍗犵敤銆侀绾︿笅鍗?鏀粯銆佹彁閱?鏀规湡/鍙栨秷銆佸埌搴楁牳閿€涓庡饱绾﹁褰曘€? +- [ ] 鍦板浘瀵艰埅鎵╁睍锛氶檮杩戦棬搴?鎺ㄨ崘銆佽窛绂?璺嚎瑙勫垝銆佽烦杞師鐢熷鑸€佸鑸姹傚煁鐐广€? +- [ ] 绀惧尯锛氬姩鎬佸彂甯冦€佽瘎璁恒€佺偣璧炪€佽瘽棰?鏍囩銆佸浘鐗?瑙嗛瀹℃牳銆佷妇鎶ヤ笌椋庢帶锛屽簵閾哄彛纰戝睍绀恒€? +- [ ] 楂橀樁钀ラ攢锛氱鏉€/鎶藉/瑁傚彉銆佽鍙樻捣鎶ャ€佺垎娆炬帹鑽愪綅銆佸娓犻亾鎶曟斁鍒嗘瀽銆? +- [ ] 椋庢帶涓庡璁★細榛戝悕鍗曘€侀鐜囬檺鍒躲€佸紓甯歌涓虹洃鎺с€佸璁℃棩蹇椼€佽ˉ鍋夸笌鍛婅浣撶郴銆? + +--- +## Phase 4锛氭€ц兘浼樺寲銆佺紦瀛樸€佽繍钀ュぇ鐩樸€佹祴璇曚笌鏂囨。銆佷笂绾夸笌鐩戞帶 +- [ ] 鎬ц兘涓庣紦瀛橈細鐑偣鎺ュ彛缂撳瓨銆佹參鏌ヨ娌荤悊銆佹壒澶勭悊浼樺寲銆佸紓姝ュ寲鏀归€犮€? +- [ ] 鍙潬鎬э細骞傜瓑涓庨噸璇曠瓥鐣ャ€佷换鍔¤皟搴﹁ˉ鍋裤€侀摼璺拷韪€佸憡璀﹁仈鍔ㄣ€? +- [ ] 杩愯惀澶х洏锛氫氦鏄?钀ラ攢/灞ョ害/鐢ㄦ埛缁村害鐨勭粏鍒嗘姤琛ㄣ€丟MV/鎴愭湰/姣涘埄鍒嗘瀽銆? +- [ ] 鏂囨。涓庢祴璇曪細瀹屾暣娴嬭瘯鐭╅樀銆佹€ц兘娴嬭瘯鎶ュ憡銆佷笂绾挎墜鍐屻€佸洖婊氭柟妗堛€? +- [ ] 鐩戞帶涓庤繍缁达細涓婄嚎鍙戝竷娴佺▼銆佺伆搴?鍥炴粴绛栫暐銆佺郴缁熺ǔ瀹氭€ф寚鏍囥€?4x7 鐩戞帶涓庡憡璀︺€? + diff --git a/Document/13_腾讯云主PostgreSQL服务器.md b/Document/13_腾讯云主PostgreSQL服务器.md deleted file mode 100644 index 549647b..0000000 --- a/Document/13_腾讯云主PostgreSQL服务器.md +++ /dev/null @@ -1,18 +0,0 @@ -# 腾讯云主 PostgreSQL 服务器 - -## 基础信息 -- IP: 120.53.222.17 -- 账户: ubuntu -- 密码: P3y$nJt#zaa4%fh5 -- 配置: 2 核 CPU / 4 GB 内存 -- 地点: 北京 -- 用途: 主 PostgreSQL / 数据库服务器 -- 到期时间: 2026-11-26 11:22:01 - -## 建议补充 -- 系统版本: 待补充(如 cat /etc/os-release) -- 带宽/磁盘: 待补充 -- 数据目录: 待补充(如 /var/lib/postgresql) -- 数据备份/监控: 待补充 -- 安全组/开放端口: 待补充 -- 变更记录: 待补充 diff --git a/Document/14_天翼云主应用服务器.md b/Document/14_天翼云主应用服务器.md deleted file mode 100644 index e02f0ed..0000000 --- a/Document/14_天翼云主应用服务器.md +++ /dev/null @@ -1,18 +0,0 @@ -# 天翼云主 PostgreSQL 服务器 - -## 基础信息 -- IP: 49.7.179.246 -- 账户: root -- 密码: 7zE&84XI6~w57W7N -- 配置: 4 核 CPU / 8 GB 内存(天翼云) -- 地点: 北京 -- 用途: 主 PostgreSQL 服务器 -- 到期时间: 2027-10-04 17:17:57 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 数据目录: 待补充(如 `/var/lib/postgresql`) -- 数据备份/监控: 待补充 -- 安全组/开放端口: 待补充 -- 变更记录: 待补充 diff --git a/Document/15_腾讯云RedisRabbitMQ服务器.md b/Document/15_腾讯云RedisRabbitMQ服务器.md deleted file mode 100644 index 3877313..0000000 --- a/Document/15_腾讯云RedisRabbitMQ服务器.md +++ /dev/null @@ -1,18 +0,0 @@ -# 腾讯云 Redis/RabbitMQ 服务器 - -## 基础信息(待补充) -- IP: 49.232.6.45 -- 账户: ubuntu -- 密码: Z7NsRjT&XnWg7%7X -- 配置: 2 核 CPU / 4 GB 内存 -- 地点: 北京 -- 用途: Redis 与 RabbitMQ -- 到期时间: 2028-11-26 - -## 建议补充 -- 系统版本: 待补充(如 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) -- 数据持久化与备份: 待补充 -- 监控与告警: 待补充 -- 变更记录: 待补充 From 462e15abbb5e143473d7bf8434f377b69e8f9936 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 1 Dec 2025 22:27:33 +0800 Subject: [PATCH 18/56] docs: merge AI guidelines and renumber documents --- Document/08_AI精简开发规范.md | 145 ++ Document/08_AI编程规范.md | 1597 ----------------- Document/09_AI精简开发规范.md | 87 - .../{10_服务器文档.md => 09_服务器文档.md} | 0 ...配置指引.md => 10_设计期DbContext配置指引.md} | 0 .../{12_SystemTodo.md => 11_SystemTodo.md} | 0 ...{13_BusinessTodo.md => 12_BusinessTodo.md} | 0 7 files changed, 145 insertions(+), 1684 deletions(-) create mode 100644 Document/08_AI精简开发规范.md delete mode 100644 Document/08_AI编程规范.md delete mode 100644 Document/09_AI精简开发规范.md rename Document/{10_服务器文档.md => 09_服务器文档.md} (100%) rename Document/{11_设计期DbContext配置指引.md => 10_设计期DbContext配置指引.md} (100%) rename Document/{12_SystemTodo.md => 11_SystemTodo.md} (100%) rename Document/{13_BusinessTodo.md => 12_BusinessTodo.md} (100%) diff --git a/Document/08_AI精简开发规范.md b/Document/08_AI精简开发规范.md new file mode 100644 index 0000000..865a052 --- /dev/null +++ b/Document/08_AI精简开发规范.md @@ -0,0 +1,145 @@ +# 编程规范_FOR_AI(TakeoutSaaS) - 终极完全体 + +> **核心指令**:你是一个高级 .NET 架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。 + +## 0. AI 交互核心约束 (元规则) +1. **语言**:必须使用**中文**回复和编写注释。 +2. **文件完整性**: + * **严禁**随意删除现有代码逻辑。 + * **严禁**修改文件编码(保持 UTF-8 无 BOM)。 + * PowerShell 读取命令必须带 `-Encoding UTF8`。 +3. **Git 原子性**:每个独立的功能点或 Bug 修复完成后,必须提示用户进行 Git 提交。 +4. **无乱码承诺**:确保所有输出(控制台、日志、API响应)无乱码。 +5. **不确定的处理**:如果你通过上下文找不到某些配置(如数据库连接串格式),**请直接询问用户**,不要瞎编。 + +## 1. 技术栈详细版本 +| 组件 | 版本/选型 | 用途说明 | +| :--- | :--- | :--- | +| **Runtime** | .NET 10 | 核心运行时 | +| **API** | ASP.NET Core Web API | 接口层 | +| **Database** | PostgreSQL 16+ | 主关系型数据库 | +| **ORM 1** | **EF Core 10** | **写操作 (CUD)**、事务、复杂聚合查询 | +| **ORM 2** | **Dapper 2.1+** | **纯读操作 (R)**、复杂报表、大批量查询 | +| **Cache** | Redis 7.0+ | 分布式缓存、Session | +| **MQ** | RabbitMQ 3.12+ | 异步解耦 (MassTransit) | +| **Libs** | MediatR, Serilog, FluentValidation | CQRS, 日志, 验证 | + +## 2. 命名与风格 (严格匹配) +* **C# 代码**: + * 类/接口/方法/属性:`PascalCase` (如 `OrderService`) + * **布尔属性**:必须加 `Is` 或 `Has` 前缀 (如 `IsDeleted`, `HasPayment`) + * 私有字段:`_camelCase` (如 `_orderRepository`) + * 参数/变量:`camelCase` (如 `orderId`) +* **PostgreSQL 数据库**: + * 表名:`snake_case` + **复数** (如 `merchant_orders`) + * 列名:`snake_case` (如 `order_no`, `is_active`) + * 主键:`id` (类型 `bigint`) +* **文件规则**: + * **一个文件一个类**。文件名必须与类名完全一致。 + +## 3. 分层架构 (Clean Architecture) +**你生成的代码必须严格归类到以下目录:** +* **`src/Api`**: 仅负责路由与 DTO 转换,**禁止**包含业务逻辑。 +* **`src/Application`**: 业务编排层。必须使用 **CQRS** (`IRequestHandler`) 和 **Mediator**。 +* **`src/Domain`**: 核心领域层。包含实体、枚举、领域异常。**禁止**依赖 EF Core 等外部库。 +* **`src/Infrastructure`**: 基础设施层。实现仓储、数据库上下文、第三方服务。 + +## 4. 注释与文档 +* **强制 XML 注释**:所有 `public` 的类、方法、属性必须有 ``。 +* **步骤注释**:超过 5 行的业务逻辑,必须分步注释: + ```csharp + // 1. 验证库存 + // 2. 扣减余额 + ``` +* **Swagger**:必须开启 JWT 鉴权按钮,Request/Response 示例必须清晰。 + +## 5. 异常处理 (防御性编程) +* **禁止空 Catch**:严禁 `catch (Exception) {}`,必须记录日志或抛出。 +* **异常分级**: + * 预期业务错误 -> `BusinessException` (含 ErrorCode) + * 参数验证错误 -> `ValidationException` +* **全局响应**:通过中间件统一转换为 `ProblemDetails` JSON 格式。 + +## 6. 异步与日志 +* **全异步**:所有 I/O 操作必须 `await`。**严禁** `.Result` 或 `.Wait()`。 +* **结构化日志**: + * ❌ `_logger.LogInfo("订单 " + id + " 创建成功");` + * ✅ `_logger.LogInformation("订单 {OrderId} 创建成功", id);` +* **脱敏**:严禁打印密码、密钥、支付凭证等敏感信息。 + +## 7. 依赖注入 (DI) +* **构造函数注入**:统一使用构造函数注入。 +* **禁止项**: + * ❌ 禁止使用 `[Inject]` 属性注入。 + * ❌ 禁止使用 `ServiceLocator` (服务定位器模式)。 + * ❌ 禁止在静态类中持有 ServiceProvider。 + +## 8. 数据访问规范 (重点执行) +### 8.1 Entity Framework Core (写/事务) +1. **无跟踪查询**:只读查询**必须**加 `.AsNoTracking()`。 +2. **杜绝 N+1**:严禁在 `foreach` 循环中查询数据库。必须使用 `.Include()`。 +3. **复杂查询**:关联表超过 2 层时,考虑使用 `.AsSplitQuery()`。 + +### 8.2 Dapper (读/报表) +1. **SQL 注入防御**:**严禁**拼接 SQL 字符串。必须使用参数化查询 (`@Param`)。 +2. **字段映射**:注意 PostgreSQL (`snake_case`) 与 C# (`PascalCase`) 的映射配置。 + +## 9. 多租户与 ID 策略 +* **ID 生成**: + * **强制**使用 **雪花算法 (Snowflake ID)**。 + * 类型:C# `long` <-> DB `bigint`。 + * **禁止**使用 UUID 或 自增 INT。 +* **租户隔离**: + * 所有业务表必须包含 `tenant_id`。 + * 写入时自动填充,读取时强制过滤。 + +## 10. API 设计与序列化 (前端兼容) +* **大整数处理**: + * 所有 `long` 类型 (Snowflake ID) 在 DTO 中**必须序列化为 string**。 + * 方案:DTO 属性加 `[JsonConverter(typeof(ToStringJsonConverter))]` 或全局配置。 +* **DTO 规范**: + * 输入:`XxxRequest` + * 输出:`XxxDto` + * **禁止** Controller 直接返回 Entity。 + +## 11. 模块化与复用 +* **核心模块划分**:Identity (身份), Tenancy (租户), Dictionary (字典), Storage (存储)。 +* **公共库 (Shared)**:通用工具类、扩展方法、常量定义必须放在 `Core/Shared` 项目中,避免重复造轮子。 + +## 12. 测试规范 +* **模式**:Arrange-Act-Assert (AAA)。 +* **工具**:xUnit + Moq + FluentAssertions。 +* **覆盖率**:核心 Domain 逻辑必须 100% 覆盖;Service 层 ≥ 70%。 + +## 13. Git 工作流 +* **提交格式 (Conventional Commits)**: + * `feat`: 新功能 + * `fix`: 修复 Bug + * `refactor`: 重构 + * `docs`: 文档 + * `style`: 格式调整 +* **分支规范**:`feature/功能名`,`bugfix/问题描述`。 + +## 14. 性能优化 (显式指令) +* **投影查询**:使用 `.Select(x => new Dto { ... })` 只查询需要的字段,减少 I/O。 +* **缓存策略**:Cache-Aside 模式。数据更新后必须立即失效缓存。 +* **批量操作**: + * EF Core 10:使用 `ExecuteUpdateAsync` / `ExecuteDeleteAsync`。 + * Dapper:使用 `ExecuteAsync` 进行批量插入。 + +## 15. 安全规范 +* **SQL 注入**:已在第 8 条强制参数化。 +* **身份认证**:Admin 端使用 JWT + RBAC;小程序端使用 Session/Token。 +* **密码存储**:必须使用 PBKDF2 或 BCrypt 加盐哈希。 + +## 16. 绝对禁止事项 (AI 自检清单) +**生成代码前,请自查是否违反以下红线:** +1. [ ] **SQL 注入**:是否拼接了 SQL 字符串? +2. [ ] **架构违规**:是否在 Controller/Domain 中使用了 DbContext? +3. [ ] **数据泄露**:是否返回了 Entity 或打印了密码? +4. [ ] **同步阻塞**:是否使用了 `.Result` 或 `.Wait()`? +5. [ ] **性能陷阱**:是否在循环中查询了数据库 (N+1)? +6. [ ] **精度丢失**:Long 类型的 ID 是否转为了 String? +7. [ ] **配置硬编码**:是否直接写死了连接串或密钥? + +--- \ No newline at end of file diff --git a/Document/08_AI编程规范.md b/Document/08_AI编程规范.md deleted file mode 100644 index 8c4e56a..0000000 --- a/Document/08_AI编程规范.md +++ /dev/null @@ -1,1597 +0,0 @@ -# 外卖SaaS系统 - AI编程规范汇总 - -> 本文档专门为AI编程助手准备,汇总了所有编码规范和规则,确保代码质量和一致性。 - -## 0. AI交互补充约束 -1. 每次回复必须回复中文。 -2. 不要更改我的文件编码。 -3. 每次修复bug或者新增完小功能必须提交git。 -4. 新创建的文件或者修改过的文件注释部分必须保持中文。 -5. 项目中不要有乱码。 -6. 在 PowerShell 查看文件时必须指定 UTF8(例如 Get-Content -Encoding UTF8 或设置 $OutputEncoding 为 UTF8),避免输出乱码。 -## 1. 技术栈要求 - -### 1.1 核心技术 -- **.NET 10** + **ASP.NET Core Web API** -- **Entity Framework Core 10**(复杂查询和实体管理) -- **Dapper 2.1+**(高性能查询和批量操作) -- **PostgreSQL 16+**(主数据库) -- **Redis 7.0+**(缓存和会话) -- **RabbitMQ 3.12+**(消息队列) - -### 1.2 必用框架和库 -- **AutoMapper**:对象映射 -- **FluentValidation**:数据验证 -- **Serilog**:结构化日志 -- **MediatR**:CQRS和中介者模式 -- **Hangfire**:后台任务调度 -- **Polly**:弹性和瞬态故障处理 -- **Swagger/Swashbuckle**:API文档 - -### 1.3 测试框架 -- **xUnit**:单元测试 -- **Moq**:Mock框架 -- **FluentAssertions**:断言库 - -## 2. 命名规范(严格遵守) - -### 2.1 C#命名规范 -```csharp -// ✅ 类名:PascalCase -public class OrderService { } - -// ✅ 接口:I + PascalCase -public interface IOrderRepository { } - -// ✅ 方法:PascalCase,异步方法以Async结尾 -public async Task CreateOrderAsync() { } - -// ✅ 私有字段:_camelCase(下划线前缀) -private readonly IOrderRepository _orderRepository; - -// ✅ 公共属性:PascalCase -public string OrderNo { get; set; } - -// ✅ 局部变量:camelCase -var orderTotal = 100.00m; - -// ✅ 常量:PascalCase -public const int MaxOrderItems = 50; - -// ✅ 枚举:PascalCase,枚举值也是PascalCase -public enum OrderStatus -{ - Pending = 1, - Confirmed = 2, - Completed = 3 -} -``` - -### 2.2 数据库命名规范 -```sql --- ✅ 表名:小写,下划线分隔,复数形式 -orders -order_items -merchant_stores - --- ✅ 字段名:小写,下划线分隔 -order_no -created_at -total_amount - --- ✅ 索引:idx_表名_字段名 -idx_orders_merchant_id -idx_orders_created_at - --- ✅ 外键:fk_表名_引用表名 -fk_orders_merchants -``` - -## 3. 项目结构规范 - -### 3.1 分层架构(DDD + Clean Architecture) -``` -TakeoutSaaS/ -├── src/ -│ ├── Api/ # API层(表现层) -│ │ ├── Controllers/ # 控制器 -│ │ ├── Filters/ # 过滤器 -│ │ ├── Middleware/ # 中间件 -│ │ └── Models/ # DTO模型 -│ ├── Application/ # 应用层 -│ │ ├── Services/ # 应用服务 -│ │ ├── DTOs/ # 数据传输对象 -│ │ ├── Interfaces/ # 服务接口 -│ │ ├── Validators/ # FluentValidation验证器 -│ │ ├── Mappings/ # AutoMapper配置 -│ │ └── Commands/Queries/ # CQRS命令和查询 -│ ├── Domain/ # 领域层(核心业务) -│ │ ├── Entities/ # 实体 -│ │ ├── ValueObjects/ # 值对象 -│ │ ├── Enums/ # 枚举 -│ │ ├── Events/ # 领域事件 -│ │ └── Interfaces/ # 仓储接口 -│ ├── Infrastructure/ # 基础设施层 -│ │ ├── Data/ # 数据访问 -│ │ │ ├── EFCore/ # EF Core实现 -│ │ │ ├── Dapper/ # Dapper实现 -│ │ │ └── Repositories/ # 仓储实现 -│ │ ├── Cache/ # 缓存实现 -│ │ ├── MessageQueue/ # 消息队列 -│ │ └── ExternalServices/ # 外部服务 -│ ├── Core/ # 核心共享层 -│ │ ├── Constants/ # 常量 -│ │ ├── Exceptions/ # 异常 -│ │ ├── Extensions/ # 扩展方法 -│ │ └── Results/ # 统一返回结果 -│ └── Modules/ # 模块化(可选) -└── tests/ - ├── UnitTests/ # 单元测试 - ├── IntegrationTests/ # 集成测试 - └── PerformanceTests/ # 性能测试 -``` - -### 3.2 文件组织规则 -- 每个文件只包含一个公共类/接口 -- 文件名与类名保持一致 -- 相关的类放在同一个文件夹 -- 使用命名空间反映文件夹结构 - -## 4. 代码注释规范 - -### 4.1 XML文档注释(必须) -```csharp -/// -/// 订单服务接口 -/// -public interface IOrderService -{ - /// - /// 创建订单 - /// - /// 订单创建请求 - /// 订单信息 - /// 业务异常 - Task CreateOrderAsync(CreateOrderRequest request); -} -``` - -### 4.2 业务逻辑注释 -```csharp -// ✅ 复杂业务逻辑必须添加注释 -public async Task CalculateOrderAmount(Order order) -{ - // 1. 计算菜品总金额 - var dishAmount = order.Items.Sum(x => x.Price * x.Quantity); - - // 2. 计算配送费(距离 > 3km,每公里加收2元) - var deliveryFee = CalculateDeliveryFee(order.Distance); - - // 3. 应用优惠券折扣 - var discount = await ApplyCouponDiscountAsync(order.CouponId, dishAmount); - - // 4. 计算最终金额 - return dishAmount + deliveryFee - discount; -} -``` - -## 5. 异常处理规范 - -### 5.1 自定义异常 -```csharp -// ✅ 业务异常 -public class BusinessException : Exception -{ - public int ErrorCode { get; } - - public BusinessException(int errorCode, string message) - : base(message) - { - ErrorCode = errorCode; - } -} - -// ✅ 验证异常 -public class ValidationException : Exception -{ - public IDictionary Errors { get; } - - public ValidationException(IDictionary errors) - : base("一个或多个验证错误") - { - Errors = errors; - } -} -``` - -### 5.2 全局异常处理中间件(必须实现) -```csharp -public class ExceptionHandlingMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public async Task InvokeAsync(HttpContext context) - { - try - { - await _next(context); - } - catch (BusinessException ex) - { - _logger.LogWarning(ex, "业务异常:{Message}", ex.Message); - await HandleBusinessExceptionAsync(context, ex); - } - catch (ValidationException ex) - { - _logger.LogWarning(ex, "验证异常:{Errors}", ex.Errors); - await HandleValidationExceptionAsync(context, ex); - } - catch (Exception ex) - { - _logger.LogError(ex, "系统异常:{Message}", ex.Message); - await HandleSystemExceptionAsync(context, ex); - } - } - - private static Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex) - { - context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; - return context.Response.WriteAsJsonAsync(new - { - success = false, - code = ex.ErrorCode, - message = ex.Message - }); - } -} -``` - -### 5.3 异常使用示例 -```csharp -// ✅ 正确的异常抛出 -public async Task GetOrderAsync(Guid orderId) -{ - var order = await _orderRepository.GetByIdAsync(orderId); - if (order == null) - { - throw new BusinessException(404, "订单不存在"); - } - return order; -} - -// ❌ 错误:不要吞掉异常 -try -{ - // ... -} -catch (Exception) -{ - // 什么都不做 - 这是错误的! -} -``` - -## 6. 服务层编码规范 - -### 6.1 服务类结构(标准模板) -```csharp -// ✅ 好的服务实现 -public class OrderService : IOrderService -{ - private readonly IOrderRepository _orderRepository; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly IOptions _settings; - - public OrderService( - IOrderRepository orderRepository, - IUnitOfWork unitOfWork, - ILogger logger, - IMapper mapper, - IOptions settings) - { - _orderRepository = orderRepository; - _unitOfWork = unitOfWork; - _logger = logger; - _mapper = mapper; - _settings = settings; - } - - public async Task CreateOrderAsync(CreateOrderRequest request) - { - try - { - // 1. 参数验证(FluentValidation会自动验证,这里是额外检查) - if (request == null) - throw new ArgumentNullException(nameof(request)); - - // 2. 记录日志 - _logger.LogInformation("创建订单:{@Request}", request); - - // 3. 业务逻辑 - var order = new Order - { - OrderNo = GenerateOrderNo(), - TotalAmount = request.TotalAmount, - DeliveryFee = _settings.Value.DefaultDeliveryFee, - CreatedAt = DateTime.UtcNow - }; - - // 4. 数据持久化 - await _orderRepository.AddAsync(order); - await _unitOfWork.SaveChangesAsync(); - - // 5. 记录成功日志 - _logger.LogInformation("订单创建成功:{OrderId}", order.Id); - - // 6. 返回DTO - return _mapper.Map(order); - } - catch (Exception ex) - { - _logger.LogError(ex, "创建订单失败:{@Request}", request); - throw; - } - } -} - -// ❌ 错误的服务实现 -public class BadOrderService -{ - // ❌ 直接注入DbContext而不是仓储 - private readonly AppDbContext _dbContext; - - // ❌ 没有日志 - // ❌ 硬编码配置 - public Order CreateOrder(CreateOrderRequest request) - { - var order = new Order(); - order.DeliveryFee = 5.0m; // ❌ 硬编码 - _dbContext.Orders.Add(order); - _dbContext.SaveChanges(); // ❌ 同步方法 - return order; // ❌ 返回实体而不是DTO - } -} -``` - -### 6.2 依赖注入规则 -```csharp -// ✅ 使用构造函数注入 -public class OrderService -{ - private readonly IOrderRepository _orderRepository; - - public OrderService(IOrderRepository orderRepository) - { - _orderRepository = orderRepository; - } -} - -// ❌ 不要使用属性注入 -public class BadOrderService -{ - [Inject] - public IOrderRepository OrderRepository { get; set; } -} - -// ❌ 不要使用服务定位器模式 -public class BadOrderService -{ - public void DoSomething() - { - var repository = ServiceLocator.GetService(); - } -} -``` - -## 7. 数据访问规范 - -### 7.1 仓储模式(必须使用) -```csharp -// ✅ 仓储接口 -public interface IOrderRepository -{ - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task AddAsync(Order order); - Task UpdateAsync(Order order); - Task DeleteAsync(Guid id); - Task ExistsAsync(Guid id); -} - -// ✅ EF Core仓储实现 -public class OrderRepository : IOrderRepository -{ - private readonly AppDbContext _context; - - public OrderRepository(AppDbContext context) - { - _context = context; - } - - public async Task GetByIdAsync(Guid id) - { - return await _context.Orders - .Include(o => o.OrderItems) - .Include(o => o.Customer) - .FirstOrDefaultAsync(o => o.Id == id); - } - - public async Task AddAsync(Order order) - { - await _context.Orders.AddAsync(order); - return order; - } -} -``` - -### 7.2 EF Core vs Dapper 使用场景 -```csharp -// ✅ EF Core - 复杂查询和实体管理 -public async Task GetOrderWithDetailsAsync(Guid orderId) -{ - return await _dbContext.Orders - .Include(o => o.OrderItems) - .ThenInclude(oi => oi.Dish) - .Include(o => o.Customer) - .Include(o => o.Merchant) - .FirstOrDefaultAsync(o => o.Id == orderId); -} - -// ✅ Dapper - 高性能统计查询 -public async Task GetOrderStatisticsAsync(DateTime startDate, DateTime endDate) -{ - var sql = @" - SELECT - COUNT(*) as TotalOrders, - SUM(total_amount) as TotalAmount, - AVG(total_amount) as AvgAmount, - MAX(total_amount) as MaxAmount, - MIN(total_amount) as MinAmount - FROM orders - WHERE created_at BETWEEN @StartDate AND @EndDate - AND status = @Status"; - - return await _connection.QueryFirstOrDefaultAsync(sql, - new { StartDate = startDate, EndDate = endDate, Status = OrderStatus.Completed }); -} - -// ✅ Dapper - 批量插入 -public async Task BulkInsertOrdersAsync(IEnumerable orders) -{ - var sql = @" - INSERT INTO orders (id, order_no, merchant_id, total_amount, created_at) - VALUES (@Id, @OrderNo, @MerchantId, @TotalAmount, @CreatedAt)"; - - return await _connection.ExecuteAsync(sql, orders); -} -``` - -### 7.3 工作单元模式(必须使用) -```csharp -// ✅ 工作单元接口 -public interface IUnitOfWork : IDisposable -{ - Task SaveChangesAsync(CancellationToken cancellationToken = default); - Task BeginTransactionAsync(); - Task CommitTransactionAsync(); - Task RollbackTransactionAsync(); -} - -// ✅ 使用工作单元 -public async Task CreateOrderWithItemsAsync(CreateOrderRequest request) -{ - await _unitOfWork.BeginTransactionAsync(); - - try - { - // 1. 创建订单 - var order = new Order { /* ... */ }; - await _orderRepository.AddAsync(order); - - // 2. 创建订单项 - foreach (var item in request.Items) - { - var orderItem = new OrderItem { /* ... */ }; - await _orderItemRepository.AddAsync(orderItem); - } - - // 3. 提交事务 - await _unitOfWork.SaveChangesAsync(); - await _unitOfWork.CommitTransactionAsync(); - - return _mapper.Map(order); - } - catch - { - await _unitOfWork.RollbackTransactionAsync(); - throw; - } -} -``` - -## 8. CQRS模式规范 - -### 8.1 命令(Command)- 写操作 -```csharp -// ✅ 命令定义 -public class CreateOrderCommand : IRequest -{ - public Guid MerchantId { get; set; } - public Guid CustomerId { get; set; } - public List Items { get; set; } - public decimal TotalAmount { get; set; } -} - -// ✅ 命令处理器 -public class CreateOrderCommandHandler : IRequestHandler -{ - private readonly IOrderRepository _orderRepository; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - - public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken) - { - _logger.LogInformation("处理创建订单命令:{@Command}", request); - - var order = new Order - { - MerchantId = request.MerchantId, - CustomerId = request.CustomerId, - TotalAmount = request.TotalAmount - }; - - await _orderRepository.AddAsync(order); - await _unitOfWork.SaveChangesAsync(cancellationToken); - - return _mapper.Map(order); - } -} -``` - -### 8.2 查询(Query)- 读操作 -```csharp -// ✅ 查询定义 -public class GetOrderByIdQuery : IRequest -{ - public Guid OrderId { get; set; } -} - -// ✅ 查询处理器 -public class GetOrderByIdQueryHandler : IRequestHandler -{ - private readonly IOrderRepository _orderRepository; - private readonly IMapper _mapper; - - public async Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken) - { - var order = await _orderRepository.GetByIdAsync(request.OrderId); - - if (order == null) - throw new BusinessException(404, "订单不存在"); - - return _mapper.Map(order); - } -} -``` - -## 9. 验证规范(FluentValidation) - -### 9.1 验证器定义 -```csharp -// ✅ 使用FluentValidation -public class CreateOrderRequestValidator : AbstractValidator -{ - public CreateOrderRequestValidator() - { - RuleFor(x => x.MerchantId) - .NotEmpty().WithMessage("商家ID不能为空"); - - RuleFor(x => x.CustomerId) - .NotEmpty().WithMessage("客户ID不能为空"); - - RuleFor(x => x.Items) - .NotEmpty().WithMessage("订单项不能为空") - .Must(items => items.Count <= 50).WithMessage("订单项不能超过50个"); - - RuleFor(x => x.TotalAmount) - .GreaterThan(0).WithMessage("订单金额必须大于0"); - - RuleFor(x => x.DeliveryAddress) - .NotEmpty().WithMessage("配送地址不能为空") - .MaximumLength(200).WithMessage("配送地址不能超过200个字符"); - } -} -``` - -### 9.2 验证器注册 -```csharp -// ✅ 在Program.cs中注册 -builder.Services.AddValidatorsFromAssemblyContaining(); -builder.Services.AddFluentValidationAutoValidation(); -``` - -## 10. 缓存策略规范 - - -### 10.1 缓存时间策略 -```csharp -// ✅ 缓存时间常量 -public static class CacheTimeouts -{ - public static readonly TimeSpan MerchantInfo = TimeSpan.FromMinutes(30); - public static readonly TimeSpan DishInfo = TimeSpan.FromMinutes(15); - public static readonly TimeSpan UserSession = TimeSpan.FromHours(2); - public static readonly TimeSpan ConfigInfo = TimeSpan.FromHours(1); - public static readonly TimeSpan HotData = TimeSpan.FromMinutes(5); -} -``` - -### 10.2 缓存使用示例 -```csharp -// ✅ 使用分布式缓存(Redis) -public class MerchantService : IMerchantService -{ - private readonly IMerchantRepository _merchantRepository; - private readonly IDistributedCache _cache; - private readonly ILogger _logger; - - public async Task GetMerchantAsync(Guid merchantId) - { - var cacheKey = $"merchant:{merchantId}"; - - // 1. 尝试从缓存获取 - var cachedData = await _cache.GetStringAsync(cacheKey); - if (!string.IsNullOrEmpty(cachedData)) - { - _logger.LogDebug("从缓存获取商家信息:{MerchantId}", merchantId); - return JsonSerializer.Deserialize(cachedData); - } - - // 2. 缓存未命中,从数据库查询 - var merchant = await _merchantRepository.GetByIdAsync(merchantId); - if (merchant == null) - throw new BusinessException(404, "商家不存在"); - - var dto = _mapper.Map(merchant); - - // 3. 写入缓存 - var cacheOptions = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = CacheTimeouts.MerchantInfo - }; - await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), cacheOptions); - - _logger.LogDebug("商家信息已缓存:{MerchantId}", merchantId); - return dto; - } - - public async Task UpdateMerchantAsync(Guid merchantId, UpdateMerchantRequest request) - { - // 更新数据库 - var merchant = await _merchantRepository.GetByIdAsync(merchantId); - // ... 更新逻辑 - await _unitOfWork.SaveChangesAsync(); - - // ✅ 更新后清除缓存 - var cacheKey = $"merchant:{merchantId}"; - await _cache.RemoveAsync(cacheKey); - _logger.LogDebug("已清除商家缓存:{MerchantId}", merchantId); - } -} -``` - -## 11. 日志规范(Serilog) - -### 11.1 日志级别使用 -```csharp -// ✅ 正确的日志级别使用 -public class OrderService -{ - private readonly ILogger _logger; - - public async Task CreateOrderAsync(CreateOrderRequest request) - { - // Trace: 非常详细的调试信息(生产环境不记录) - _logger.LogTrace("进入CreateOrderAsync方法"); - - // Debug: 调试信息(生产环境不记录) - _logger.LogDebug("订单请求参数:{@Request}", request); - - // Information: 一般信息(重要业务流程) - _logger.LogInformation("开始创建订单,商家ID:{MerchantId},客户ID:{CustomerId}", - request.MerchantId, request.CustomerId); - - try - { - // ... 业务逻辑 - - // Information: 成功完成 - _logger.LogInformation("订单创建成功:{OrderId},订单号:{OrderNo}", - order.Id, order.OrderNo); - - return dto; - } - catch (BusinessException ex) - { - // Warning: 业务异常(预期内的错误) - _logger.LogWarning(ex, "创建订单失败(业务异常):{Message}", ex.Message); - throw; - } - catch (Exception ex) - { - // Error: 系统异常(非预期错误) - _logger.LogError(ex, "创建订单失败(系统异常):{@Request}", request); - throw; - } - } -} -``` - -### 11.2 结构化日志 -```csharp -// ✅ 使用结构化日志(推荐) -_logger.LogInformation("用户 {UserId} 创建了订单 {OrderId},金额 {Amount}", - userId, orderId, amount); - -// ✅ 记录对象(使用@符号) -_logger.LogInformation("订单详情:{@Order}", order); - -// ❌ 不要使用字符串拼接 -_logger.LogInformation("用户 " + userId + " 创建了订单 " + orderId); -``` - -### 11.3 敏感信息处理 -```csharp -// ✅ 不要记录敏感信息 -public class PaymentService -{ - public async Task ProcessPaymentAsync(PaymentRequest request) - { - // ❌ 错误:记录了密码、支付密码等敏感信息 - _logger.LogInformation("支付请求:{@Request}", request); - - // ✅ 正确:只记录非敏感信息 - _logger.LogInformation("处理支付,订单ID:{OrderId},金额:{Amount}", - request.OrderId, request.Amount); - } -} -``` - -## 12. API控制器规范 - -### 12.1 控制器结构(标准模板) -```csharp -/// -/// 订单管理API -/// -[ApiController] -[Route("api/[controller]")] -[Authorize] -public class OrdersController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public OrdersController(IMediator mediator, ILogger logger) - { - _mediator = mediator; - _logger = logger; - } - - /// - /// 创建订单 - /// - /// 订单创建请求 - /// 订单信息 - /// 创建成功 - /// 请求参数错误 - /// 业务验证失败 - [HttpPost] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)] - public async Task CreateOrder([FromBody] CreateOrderRequest request) - { - _logger.LogInformation("API调用:创建订单"); - - var command = new CreateOrderCommand - { - MerchantId = request.MerchantId, - CustomerId = request.CustomerId, - Items = request.Items, - TotalAmount = request.TotalAmount - }; - - var result = await _mediator.Send(command); - - return Ok(ApiResponse.SuccessResult(result)); - } - - /// - /// 获取订单详情 - /// - /// 订单ID - /// 订单详情 - [HttpGet("{id}")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task GetOrder(Guid id) - { - var query = new GetOrderByIdQuery { OrderId = id }; - var result = await _mediator.Send(query); - return Ok(ApiResponse.SuccessResult(result)); - } - - /// - /// 获取订单列表 - /// - /// 查询参数 - /// 订单列表 - [HttpGet] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task GetOrders([FromQuery] GetOrdersRequest request) - { - var query = new GetOrdersQuery - { - PageIndex = request.PageIndex, - PageSize = request.PageSize, - Status = request.Status - }; - - var result = await _mediator.Send(query); - return Ok(ApiResponse>.SuccessResult(result)); - } -} -``` - -### 12.2 统一返回结果(ApiResponse) -```csharp -// ✅ 统一返回结果(泛型) -public class ApiResponse -{ - public bool Success { get; set; } - public int Code { get; set; } = 200; - public string? Message { get; set; } - public T? Data { get; set; } - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - // 工厂方法:成功 - public static ApiResponse SuccessResult(T data, string? message = "操作成功") => new() - { - Success = true, - Code = 200, - Message = message, - Data = data - }; - - // 工厂方法:失败 - public static ApiResponse Failure(int code, string message) => new() - { - Success = false, - Code = code, - Message = message - }; -} - -// ✅ 分页结果 -public class PagedResult -{ - public List Items { get; set; } - public int TotalCount { get; set; } - public int PageIndex { get; set; } - public int PageSize { get; set; } - public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); - public bool HasPreviousPage => PageIndex > 1; - public bool HasNextPage => PageIndex < TotalPages; -} -``` - -### 12.3 非泛型便捷封装(推荐在仅返回消息时使用) -```csharp -public static class ApiResponse -{ - // ✅ 仅返回成功/消息(无数据载荷) - public static ApiResponse Success(string? message = "操作成功") - => new ApiResponse { Success = true, Code = 200, Message = message, Data = null }; - - // ✅ 错误(配合统一错误码) - public static ApiResponse Failure(int code, string message) - => new ApiResponse { Success = false, Code = code, Message = message, Data = null }; -} -``` - -### 12.4 统一错误码(ErrorCodes) -```csharp -public static class ErrorCodes -{ - public const int BadRequest = 400; - public const int Unauthorized = 401; - public const int Forbidden = 403; - public const int NotFound = 404; - public const int Conflict = 409; - public const int ValidationFailed = 422; // 业务/验证不通过 - public const int InternalServerError = 500; - - // 业务自定义区间(10000+) - public const int BusinessError = 10001; -} -``` - -### 12.5 错误响应的ProblemDetails映射(全局异常处理中间件) -```csharp -public class BusinessException : Exception -{ - public int ErrorCode { get; } - public BusinessException(int errorCode, string message) : base(message) => ErrorCode = errorCode; -} - -public class ValidationException : Exception -{ - public IDictionary Errors { get; } - public ValidationException(IDictionary errors) : base("一个或多个验证错误") => Errors = errors; -} - -public class ExceptionHandlingMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) - { _next = next; _logger = logger; } - - public async Task InvokeAsync(HttpContext context) - { - try - { - await _next(context); - } - catch (BusinessException ex) - { - await WriteProblemDetailsAsync(context, StatusCodes.Status422UnprocessableEntity, "业务异常", ex.Message, ex.ErrorCode); - } - catch (ValidationException ex) - { - await WriteProblemDetailsAsync(context, StatusCodes.Status422UnprocessableEntity, "验证异常", ex.Message, ErrorCodes.ValidationFailed, ex.Errors); - } - catch (Exception ex) - { - _logger.LogError(ex, "系统异常"); - await WriteProblemDetailsAsync(context, StatusCodes.Status500InternalServerError, "系统异常", "服务器发生错误,请稍后重试", ErrorCodes.InternalServerError); - } - } - - private static Task WriteProblemDetailsAsync(HttpContext context, int status, string title, string detail, int code, object? errors = null) - { - var problem = new ProblemDetails - { - Status = status, - Title = title, - Detail = detail, - Instance = context.Request.Path, - Type = $"https://httpstatuses.com/{status}" - }; - problem.Extensions["code"] = code; - if (errors != null) problem.Extensions["errors"] = errors; - - context.Response.StatusCode = status; - context.Response.ContentType = "application/problem+json"; - return context.Response.WriteAsJsonAsync(problem); - } -} - -// Program.cs 中注册 -app.UseMiddleware(); -``` - - -## 13. 实体和DTO规范 - -### 13.1 实体类(Domain Entity) -```csharp -// ✅ 领域实体 -public class Order : BaseEntity -{ - public Guid Id { get; private set; } - public string OrderNo { get; private set; } - public Guid MerchantId { get; private set; } - public Guid CustomerId { get; private set; } - public decimal TotalAmount { get; private set; } - public OrderStatus Status { get; private set; } - public DateTime CreatedAt { get; private set; } - public DateTime? UpdatedAt { get; private set; } - - // 导航属性 - public virtual Merchant Merchant { get; private set; } - public virtual Customer Customer { get; private set; } - public virtual ICollection OrderItems { get; private set; } - - // 私有构造函数(用于EF Core) - private Order() { } - - // 工厂方法 - public static Order Create(Guid merchantId, Guid customerId, decimal totalAmount) - { - return new Order - { - Id = Guid.NewGuid(), - OrderNo = GenerateOrderNo(), - MerchantId = merchantId, - CustomerId = customerId, - TotalAmount = totalAmount, - Status = OrderStatus.Pending, - CreatedAt = DateTime.UtcNow - }; - } - - // 业务方法 - public void Confirm() - { - if (Status != OrderStatus.Pending) - throw new BusinessException(400, "只有待确认的订单才能确认"); - - Status = OrderStatus.Confirmed; - UpdatedAt = DateTime.UtcNow; - } - - public void Cancel(string reason) - { - if (Status == OrderStatus.Completed) - throw new BusinessException(400, "已完成的订单不能取消"); - - Status = OrderStatus.Cancelled; - UpdatedAt = DateTime.UtcNow; - } - - private static string GenerateOrderNo() - { - return $"ORD{DateTime.UtcNow:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}"; - } -} -``` - -### 13.2 DTO(数据传输对象) -```csharp -// ✅ 请求DTO -public class CreateOrderRequest -{ - public Guid MerchantId { get; set; } - public Guid CustomerId { get; set; } - public List Items { get; set; } - public decimal TotalAmount { get; set; } - public string DeliveryAddress { get; set; } - public string ContactPhone { get; set; } - public string Remark { get; set; } -} - -// ✅ 响应DTO -public class OrderDto -{ - public Guid Id { get; set; } - public string OrderNo { get; set; } - public Guid MerchantId { get; set; } - public string MerchantName { get; set; } - public Guid CustomerId { get; set; } - public string CustomerName { get; set; } - public decimal TotalAmount { get; set; } - public OrderStatus Status { get; set; } - public string StatusText { get; set; } - public List Items { get; set; } - public DateTime CreatedAt { get; set; } -} -``` - -### 13.3 AutoMapper配置 -```csharp -// ✅ AutoMapper Profile -public class OrderMappingProfile : Profile -{ - public OrderMappingProfile() - { - CreateMap() - .ForMember(dest => dest.MerchantName, opt => opt.MapFrom(src => src.Merchant.Name)) - .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)) - .ForMember(dest => dest.StatusText, opt => opt.MapFrom(src => src.Status.ToString())); - - CreateMap() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(dest => dest.OrderNo, opt => opt.Ignore()) - .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()); - } -} -``` - - -## 14. Git工作流规范 - -### 14.1 分支管理 -``` -main # 主分支,生产环境代码 -├── develop # 开发分支 -│ ├── feature/order-management # 功能分支 -│ ├── feature/payment-integration # 功能分支 -│ └── bugfix/order-calculation # 修复分支 -└── hotfix/critical-bug # 紧急修复分支 -``` - -### 14.2 分支命名规范 -- **功能分支**:`feature/功能名称`(如:`feature/order-management`) -- **修复分支**:`bugfix/问题描述`(如:`bugfix/order-calculation`) -- **紧急修复**:`hotfix/问题描述`(如:`hotfix/payment-error`) -- **发布分支**:`release/版本号`(如:`release/v1.0.0`) - -### 14.3 提交信息规范(严格遵守) -```bash -# 格式:(): - -# type类型(必须使用以下之一): -# feat: 新功能 -# fix: 修复bug -# docs: 文档更新 -# style: 代码格式调整(不影响代码运行) -# refactor: 重构(既不是新功能也不是修复bug) -# perf: 性能优化 -# test: 测试相关 -# chore: 构建/工具相关 - -# 示例(必须遵循): -git commit -m "feat(order): 添加订单创建功能" -git commit -m "fix(payment): 修复支付回调处理错误" -git commit -m "docs(api): 更新API文档" -git commit -m "refactor(service): 重构订单服务" -git commit -m "perf(query): 优化订单查询性能" -``` - -## 15. 单元测试规范 - -### 15.1 测试命名规范 -```csharp -// ✅ 测试命名格式:MethodName_Scenario_ExpectedResult -[Fact] -public async Task CreateOrder_ValidRequest_ReturnsOrderDto() -{ - // Arrange(准备) - var request = new CreateOrderRequest - { - MerchantId = Guid.NewGuid(), - CustomerId = Guid.NewGuid(), - Items = new List - { - new OrderItemDto { DishId = Guid.NewGuid(), Quantity = 2, Price = 50.00m } - }, - TotalAmount = 100.00m - }; - - // Act(执行) - var result = await _orderService.CreateOrderAsync(request); - - // Assert(断言) - result.Should().NotBeNull(); - result.OrderNo.Should().NotBeNullOrEmpty(); - result.TotalAmount.Should().Be(100.00m); -} - -[Fact] -public async Task CreateOrder_InvalidMerchantId_ThrowsBusinessException() -{ - // Arrange - var request = new CreateOrderRequest - { - MerchantId = Guid.Empty, // 无效的商家ID - CustomerId = Guid.NewGuid(), - TotalAmount = 100.00m - }; - - // Act & Assert - await Assert.ThrowsAsync( - async () => await _orderService.CreateOrderAsync(request)); -} -``` - -### 15.2 Mock使用 -```csharp -// ✅ 使用Moq进行Mock -public class OrderServiceTests -{ - private readonly Mock _orderRepositoryMock; - private readonly Mock _unitOfWorkMock; - private readonly Mock> _loggerMock; - private readonly Mock _mapperMock; - private readonly OrderService _orderService; - - public OrderServiceTests() - { - _orderRepositoryMock = new Mock(); - _unitOfWorkMock = new Mock(); - _loggerMock = new Mock>(); - _mapperMock = new Mock(); - - _orderService = new OrderService( - _orderRepositoryMock.Object, - _unitOfWorkMock.Object, - _loggerMock.Object, - _mapperMock.Object, - Options.Create(new OrderSettings()) - ); - } - - [Fact] - public async Task GetOrder_ExistingId_ReturnsOrder() - { - // Arrange - var orderId = Guid.NewGuid(); - var order = new Order { Id = orderId, OrderNo = "ORD001" }; - var orderDto = new OrderDto { Id = orderId, OrderNo = "ORD001" }; - - _orderRepositoryMock - .Setup(x => x.GetByIdAsync(orderId)) - .ReturnsAsync(order); - - _mapperMock - .Setup(x => x.Map(order)) - .Returns(orderDto); - - // Act - var result = await _orderService.GetOrderAsync(orderId); - - // Assert - result.Should().NotBeNull(); - result.Id.Should().Be(orderId); - _orderRepositoryMock.Verify(x => x.GetByIdAsync(orderId), Times.Once); - } -} -``` - -### 15.3 测试覆盖率要求 -- **核心业务逻辑**:>= 80% -- **服务层**:>= 70% -- **仓储层**:>= 60% -- **控制器层**:>= 50% - -## 16. 性能优化规范 - -### 16.1 数据库查询优化 -```csharp -// ❌ 错误:N+1查询问题 -public async Task> GetOrdersAsync() -{ - var orders = await _context.Orders.ToListAsync(); - - foreach (var order in orders) - { - // 每次循环都会查询数据库! - order.Customer = await _context.Customers.FindAsync(order.CustomerId); - order.Merchant = await _context.Merchants.FindAsync(order.MerchantId); - } - - return _mapper.Map>(orders); -} - -// ✅ 正确:使用Include预加载 -public async Task> GetOrdersAsync() -{ - var orders = await _context.Orders - .Include(o => o.Customer) - .Include(o => o.Merchant) - .Include(o => o.OrderItems) - .ThenInclude(oi => oi.Dish) - .ToListAsync(); - - return _mapper.Map>(orders); -} - -// ✅ 更好:大数据量使用Dapper -public async Task> GetOrdersAsync() -{ - var sql = @" - SELECT - o.id, o.order_no, o.total_amount, - c.id as customer_id, c.name as customer_name, - m.id as merchant_id, m.name as merchant_name - FROM orders o - INNER JOIN customers c ON o.customer_id = c.id - INNER JOIN merchants m ON o.merchant_id = m.id - WHERE o.status = @Status - ORDER BY o.created_at DESC - LIMIT @Limit OFFSET @Offset"; - - return await _connection.QueryAsync(sql, new { Status = 1, Limit = 100, Offset = 0 }); -} -``` - -### 16.2 异步编程规范 -```csharp -// ✅ 正确:使用async/await -public async Task CreateOrderAsync(CreateOrderRequest request) -{ - var order = new Order { /* ... */ }; - await _orderRepository.AddAsync(order); - await _unitOfWork.SaveChangesAsync(); - return _mapper.Map(order); -} - -// ❌ 错误:不要使用.Result或.Wait() -public OrderDto CreateOrder(CreateOrderRequest request) -{ - var order = new Order { /* ... */ }; - _orderRepository.AddAsync(order).Wait(); // 可能导致死锁! - _unitOfWork.SaveChangesAsync().Result; // 可能导致死锁! - return _mapper.Map(order); -} - -// ❌ 错误:不要混用同步和异步 -public async Task CreateOrderAsync(CreateOrderRequest request) -{ - var order = new Order { /* ... */ }; - _orderRepository.AddAsync(order).Wait(); // 错误! - await _unitOfWork.SaveChangesAsync(); - return _mapper.Map(order); -} -``` - -### 16.3 批量操作优化 -```csharp -// ❌ 错误:逐条插入 -public async Task ImportOrdersAsync(List orders) -{ - foreach (var order in orders) - { - await _context.Orders.AddAsync(order); - await _context.SaveChangesAsync(); // 每次都保存,性能差! - } -} - -// ✅ 正确:批量插入 -public async Task ImportOrdersAsync(List orders) -{ - await _context.Orders.AddRangeAsync(orders); - await _context.SaveChangesAsync(); // 一次性保存 -} - -// ✅ 更好:使用Dapper批量插入(大数据量) -public async Task ImportOrdersAsync(List orders) -{ - var sql = @" - INSERT INTO orders (id, order_no, merchant_id, total_amount, created_at) - VALUES (@Id, @OrderNo, @MerchantId, @TotalAmount, @CreatedAt)"; - - await _connection.ExecuteAsync(sql, orders); -} -``` - -## 17. 安全规范 - -### 17.1 SQL注入防护 -```csharp -// ❌ 错误:字符串拼接SQL(SQL注入风险) -public async Task GetOrderByNoAsync(string orderNo) -{ - var sql = $"SELECT * FROM orders WHERE order_no = '{orderNo}'"; // 危险! - return await _connection.QueryFirstOrDefaultAsync(sql); -} - -// ✅ 正确:使用参数化查询 -public async Task GetOrderByNoAsync(string orderNo) -{ - var sql = "SELECT * FROM orders WHERE order_no = @OrderNo"; - return await _connection.QueryFirstOrDefaultAsync(sql, new { OrderNo = orderNo }); -} -``` - -### 17.2 敏感数据加密 -```csharp -// ✅ 密码加密存储 -public class UserService -{ - private readonly IPasswordHasher _passwordHasher; - - public async Task CreateUserAsync(string username, string password) - { - var user = new User { Username = username }; - - // ✅ 使用密码哈希 - user.PasswordHash = _passwordHasher.HashPassword(user, password); - - await _userRepository.AddAsync(user); - await _unitOfWork.SaveChangesAsync(); - - return user; - } - - public async Task ValidatePasswordAsync(User user, string password) - { - var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); - return result == PasswordVerificationResult.Success; - } -} -``` - -### 17.3 授权验证 -```csharp -// ✅ 使用授权特性 -[Authorize(Roles = "Admin")] -public class AdminController : ControllerBase -{ - [HttpGet("users")] - public async Task GetUsers() - { - // 只有Admin角色可以访问 - } -} - -// ✅ 基于策略的授权 -[Authorize(Policy = "MerchantOwner")] -public class MerchantController : ControllerBase -{ - [HttpPut("{id}")] - public async Task UpdateMerchant(Guid id, UpdateMerchantRequest request) - { - // 只有商家所有者可以更新 - } -} - -// ✅ 在服务层也要验证权限 -public async Task UpdateMerchantAsync(Guid merchantId, Guid userId, UpdateMerchantRequest request) -{ - var merchant = await _merchantRepository.GetByIdAsync(merchantId); - - // 验证用户是否有权限 - if (merchant.OwnerId != userId) - { - throw new BusinessException(403, "无权限操作"); - } - - // ... 更新逻辑 -} -``` - -## 18. 代码审查清单 - -### 18.1 必查项目 -- [ ] 代码符合命名规范(PascalCase、camelCase、_camelCase) -- [ ] 所有公共API有XML文档注释 -- [ ] 复杂业务逻辑有注释说明 -- [ ] 异常处理完善(try-catch、自定义异常) -- [ ] 使用异步方法(async/await) -- [ ] 使用依赖注入(构造函数注入) -- [ ] 使用仓储模式(不直接操作DbContext) -- [ ] 使用工作单元模式(事务管理) -- [ ] 日志记录完善(Information、Warning、Error) -- [ ] 参数验证(FluentValidation) -- [ ] 返回DTO而不是实体 -- [ ] 无硬编码配置(使用IOptions) -- [ ] 无SQL注入风险(参数化查询) -- [ ] 敏感数据加密 -- [ ] 权限验证完善 - -### 18.2 性能检查 -- [ ] 避免N+1查询(使用Include) -- [ ] 大数据量使用Dapper -- [ ] 合理使用缓存 -- [ ] 批量操作使用批量方法 -- [ ] 异步方法不使用.Result或.Wait() - -### 18.3 安全检查 -- [ ] 无SQL注入风险 -- [ ] 密码已加密 -- [ ] 授权验证完善 -- [ ] 敏感信息不记录日志 -- [ ] HTTPS传输 - -## 19. 禁止事项(严格禁止) - -### 19.1 绝对禁止 -```csharp -// ❌ 禁止:直接在控制器或服务中使用DbContext -public class OrderController -{ - private readonly AppDbContext _context; // 禁止! -} - -// ❌ 禁止:硬编码配置 -var deliveryFee = 5.0m; // 禁止!应该从配置读取 - -// ❌ 禁止:返回实体类 -public Order GetOrder(Guid id) // 禁止!应该返回DTO -{ - return _context.Orders.Find(id); -} - -// ❌ 禁止:字符串拼接SQL -var sql = $"SELECT * FROM orders WHERE id = '{id}'"; // 禁止!SQL注入风险 - -// ❌ 禁止:吞掉异常 -try -{ - // ... -} -catch (Exception) -{ - // 什么都不做 - 禁止! -} - -// ❌ 禁止:使用.Result或.Wait() -var result = _service.GetOrderAsync(id).Result; // 禁止!可能死锁 - -// ❌ 禁止:记录敏感信息 -_logger.LogInformation("用户密码:{Password}", password); // 禁止! - -// ❌ 禁止:不使用异步方法 -public Order CreateOrder(CreateOrderRequest request) // 禁止!应该使用async -{ - _context.Orders.Add(order); - _context.SaveChanges(); // 应该使用SaveChangesAsync -} -``` - -## 20. 最佳实践总结 - -### 20.1 核心原则 -1. **SOLID原则**:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置 -2. **DRY原则**:不要重复自己(Don't Repeat Yourself) -3. **KISS原则**:保持简单(Keep It Simple, Stupid) -4. **YAGNI原则**:你不会需要它(You Aren't Gonna Need It) - -### 20.2 编码习惯 -- ✅ 使用有意义的变量名 -- ✅ 方法保持简短(不超过50行) -- ✅ 类保持单一职责 -- ✅ 优先使用组合而不是继承 -- ✅ 编写可测试的代码 -- ✅ 先写测试再写代码(TDD) -- ✅ 持续重构,保持代码整洁 - -### 20.3 团队协作 -- ✅ 遵循统一的代码规范 -- ✅ 代码审查必须通过 -- ✅ 提交前运行测试 -- ✅ 提交信息清晰明确 -- ✅ 及时更新文档 -- ✅ 主动分享知识 - ---- - -## 附录:快速参考 - -### A. 常用命名模式 -- 类:`OrderService`、`MerchantRepository` -- 接口:`IOrderService`、`IMerchantRepository` -- 方法:`CreateOrderAsync`、`GetOrderByIdAsync` -- 字段:`_orderRepository`、`_logger` -- 属性:`OrderNo`、`TotalAmount` -- 变量:`orderTotal`、`merchantId` - -### B. 常用文件夹结构 -``` -Controllers/ -Services/ -Repositories/ -DTOs/ -Entities/ -Validators/ -Mappings/ -Exceptions/ -Constants/ -Extensions/ -``` - -### C. 必须使用的NuGet包 -- Microsoft.EntityFrameworkCore -- Dapper -- AutoMapper.Extensions.Microsoft.DependencyInjection -- FluentValidation.AspNetCore -- Serilog.AspNetCore -- MediatR -- Swashbuckle.AspNetCore -- xUnit -- Moq -- FluentAssertions - ---- - -**文档版本**:v1.0 -**最后更新**:2025-11-22 -**适用项目**:外卖SaaS系统 -**目标读者**:AI编程助手、开发人员 - diff --git a/Document/09_AI精简开发规范.md b/Document/09_AI精简开发规范.md deleted file mode 100644 index f812dfe..0000000 --- a/Document/09_AI精简开发规范.md +++ /dev/null @@ -1,87 +0,0 @@ -# 编程规范_FOR_AI(TakeoutSaaS) - -说明:本规范为AI编程助手与开发者共同遵循的统一编码规范,结合 0_Document 下文档约定(特别是 06_开发规范.md、02_技术架构.md、12.* 规范)执行。超出本文件内容的详细条目请以文档中心为准。 - -## 0. AI交互补充约束 -1. 每次回复必须回复中文。 -2. 不要更改我的文件编码。 -3. 每次修复bug或者新增完小功能必须提交git。 -4. 新创建的文件或者修改过的文件注释部分必须保持中文。 -5. 项目中不要有乱码。 -6. 在 PowerShell 查看文件时必须指定 UTF8(例如 Get-Content -Encoding UTF8 或设置 $OutputEncoding 为 UTF8),避免输出乱码。 -## 1. 技术栈 -- .NET 10 + ASP.NET Core Web API -- EF Core 10(复杂关系/事务)+ Dapper(统计/批量)+ PostgreSQL 16+ -- Redis、RabbitMQ、Swagger、MediatR、Serilog、FluentValidation、AutoMapper、Hangfire、Polly - -## 2. 命名与风格 -- 类/方法/属性:PascalCase;接口:I前缀;私有字段:_camelCase;变量:camelCase;常量:PascalCase -- 每文件仅1个公共类型,文件名与类型名一致 -- 命名空间与目录结构一致 - -## 3. 分层与结构 -- 物理结构:Api(AdminApi/MiniApi/UserApi)+ Application + Domain + Infrastructure + Core(Shared.*) + Modules + Gateway -- 不允许在Controller/Service中直接操作DbContext,必须通过仓储/应用服务 -- 返回DTO,禁止直接返回实体 - -## 4. 注释与文档 -- 所有公共API、接口、复杂逻辑必须有XML注释 -- 控制器、服务方法提供简要说明与异常声明 - -## 5. 异常与错误码 -- 使用 BusinessException(含ErrorCode)/ ValidationException;禁止吞异常 -- 全局异常中间件输出 ProblemDetails(扩展code与errors) -- 错误码:400/401/403/404/409/422/500 + 业务10001+ - -## 6. 异步与日志 -- 全面使用 async/await,禁止 .Result/.Wait() -- 使用 Serilog 记录结构化日志,避免记录敏感数据 - -## 7. 依赖注入 -- 统一使用构造函数注入,禁止服务定位器 -- 业务逻辑在应用层,仓储在基础设施层 - -## 8. 数据访问 -- EF Core 10 负责关系/事务/迁移;Dapper 负责统计和大批量 -- 使用工作单元与仓储模式;避免N+1;只读查询使用AsNoTracking -- 参数化查询,禁止字符串拼接SQL - -## 9. 多租户 -- 通过 Header:X-Tenant-Id 或 Token Claim: tenant_id 解析租户 -- EF Core 全局过滤(tenant_id);写入数据时自动填充租户 - -## 10. 安全 -- HTTPS、Security Headers、CORS按端配置 -- 授权:AdminApi 使用JWT+RBAC;MiniApi 小程序登录态+JWT -- 严禁日志打印密码/支付信息等敏感数据 - -## 11. API 设计 -- RESTful,统一 /api/{area}/v{version} -- 统一返回:ApiResponse;分页返回使用 PagedResult -- Swagger 按版本与端分组,开启鉴权按钮 - -## 12. 模块化 -- 独立模块抽象:Identity、Authorization、Tenancy、Dictionary、Storage、Sms、Messaging、Scheduler、Delivery -- 公共横切能力抽到 Shared.* 复用 - -## 13. 测试 -- xUnit + Moq + FluentAssertions;命名:Method_Scenario_Expected -- 核心业务覆盖率≥80% - -## 14. Git 提交 -- 使用 Conventional Commits:feat/fix/docs/style/refactor/perf/test/chore - -## 15. 性能 -- 投影查询、编译查询、批量操作(ExecuteUpdate/ExecuteDelete) -- 缓存优先:Cache-Aside;更新后清缓存 - -## 16. 禁止事项 -- 直接使用DbContext(绕过仓储/工作单元) -- 硬编码配置(使用IOptions) -- 返回实体类 -- SQL拼接注入风险 -- 吞异常或静默失败 -- 同步阻塞异步 - -以上规范将随着文档中心的演进不断完善;AI编程助手生成的代码必须符合本规范,并默认使用这些约束。 - diff --git a/Document/10_服务器文档.md b/Document/09_服务器文档.md similarity index 100% rename from Document/10_服务器文档.md rename to Document/09_服务器文档.md diff --git a/Document/11_设计期DbContext配置指引.md b/Document/10_设计期DbContext配置指引.md similarity index 100% rename from Document/11_设计期DbContext配置指引.md rename to Document/10_设计期DbContext配置指引.md diff --git a/Document/12_SystemTodo.md b/Document/11_SystemTodo.md similarity index 100% rename from Document/12_SystemTodo.md rename to Document/11_SystemTodo.md diff --git a/Document/13_BusinessTodo.md b/Document/12_BusinessTodo.md similarity index 100% rename from Document/13_BusinessTodo.md rename to Document/12_BusinessTodo.md From 148475fa436ca6efa50ccc1dcf5118e502c4d05a Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 09:04:37 +0800 Subject: [PATCH 19/56] feat: migrate snowflake ids and refresh migrations --- AGENTS.md | 150 + Document/10_设计期DbContext配置指引.md | 42 + Document/11_SystemTodo.md | 92 +- Document/12_BusinessTodo.md | 89 +- Document/13_AppSeed说明.md | 52 + .../Controllers/AuthController.cs | 2 +- .../Controllers/DictionaryController.cs | 20 +- .../Controllers/MerchantsController.cs | 72 + src/Api/TakeoutSaaS.AdminApi/Program.cs | 4 + .../TakeoutSaaS.AdminApi.csproj | 4 + .../appsettings.Development.json | 34 + .../Controllers/MeController.cs | 2 +- ...pApplicationServiceCollectionExtensions.cs | 23 + .../Commands/CreateMerchantCommand.cs | 53 + .../App/Merchants/Dto/MerchantDto.cs | 63 + .../Handlers/CreateMerchantCommandHandler.cs | 54 + .../Handlers/GetMerchantByIdQueryHandler.cs | 42 + .../Handlers/SearchMerchantsQueryHandler.cs | 42 + .../Merchants/Queries/GetMerchantByIdQuery.cs | 15 + .../Merchants/Queries/SearchMerchantsQuery.cs | 16 + .../Abstractions/IDictionaryAppService.cs | 8 +- .../Abstractions/IDictionaryCache.cs | 6 +- .../Contracts/CreateDictionaryItemRequest.cs | 5 +- .../Dictionary/Models/DictionaryGroupDto.cs | 6 +- .../Dictionary/Models/DictionaryItemDto.cs | 9 +- .../Services/DictionaryAppService.cs | 38 +- .../Abstractions/IAdminAuthService.cs | 2 +- .../Identity/Abstractions/IMiniAuthService.cs | 2 +- .../Abstractions/IRefreshTokenStore.cs | 2 +- .../Identity/Contracts/CurrentUserProfile.cs | 6 +- .../Identity/Models/RefreshTokenDescriptor.cs | 2 +- .../Identity/Services/AdminAuthService.cs | 2 +- .../Identity/Services/MiniAuthService.cs | 6 +- .../Messaging/Events/OrderCreatedEvent.cs | 4 +- .../Messaging/Events/PaymentSucceededEvent.cs | 4 +- .../Sms/Services/VerificationCodeService.cs | 4 +- .../Storage/Services/FileStorageService.cs | 2 +- .../TakeoutSaaS.Application.csproj | 1 + .../Entities/AuditableEntityBase.cs | 6 +- .../Entities/EntityBase.cs | 2 +- .../Entities/IAuditableEntity.cs | 6 +- .../Entities/IMultiTenantEntity.cs | 2 +- .../Entities/MultiTenantEntityBase.cs | 2 +- .../Ids/IIdGenerator.cs | 13 + .../Ids/IdGeneratorOptions.cs | 26 + .../Results/ApiResponse.cs | 61 +- .../Security/ICurrentUserAccessor.cs | 2 +- .../Serialization/SnowflakeIdJsonConverter.cs | 52 + .../Tenancy/ITenantProvider.cs | 2 +- .../Tenancy/TenantContext.cs | 8 +- .../Ids/SnowflakeIdGenerator.cs | 111 + .../Middleware/CorrelationIdMiddleware.cs | 9 +- .../Security/ClaimsPrincipalExtensions.cs | 10 +- .../HttpContextCurrentUserAccessor.cs | 8 +- .../Analytics/Entities/MetricAlertRule.cs | 2 +- .../Analytics/Entities/MetricSnapshot.cs | 2 +- .../Coupons/Entities/Coupon.cs | 6 +- .../CustomerService/Entities/ChatMessage.cs | 4 +- .../CustomerService/Entities/ChatSession.cs | 6 +- .../CustomerService/Entities/SupportTicket.cs | 6 +- .../CustomerService/Entities/TicketComment.cs | 4 +- .../Deliveries/Entities/DeliveryEvent.cs | 2 +- .../Deliveries/Entities/DeliveryOrder.cs | 2 +- .../Repositories/IDeliveryRepository.cs | 42 + .../Dictionary/Entities/DictionaryItem.cs | 2 +- .../Repositories/IDictionaryRepository.cs | 8 +- .../Distribution/Entities/AffiliateOrder.cs | 6 +- .../Distribution/Entities/AffiliatePartner.cs | 2 +- .../Distribution/Entities/AffiliatePayout.cs | 2 +- .../Engagement/Entities/CheckInRecord.cs | 4 +- .../Engagement/Entities/CommunityComment.cs | 6 +- .../Engagement/Entities/CommunityPost.cs | 2 +- .../Engagement/Entities/CommunityReaction.cs | 4 +- .../GroupBuying/Entities/GroupOrder.cs | 6 +- .../GroupBuying/Entities/GroupParticipant.cs | 6 +- .../Identity/Entities/IdentityUser.cs | 2 +- .../Repositories/IIdentityUserRepository.cs | 2 +- .../Repositories/IMiniUserRepository.cs | 4 +- .../Inventory/Entities/InventoryAdjustment.cs | 4 +- .../Inventory/Entities/InventoryBatch.cs | 4 +- .../Inventory/Entities/InventoryItem.cs | 4 +- .../Membership/Entities/MemberGrowthLog.cs | 2 +- .../Membership/Entities/MemberPointLedger.cs | 4 +- .../Membership/Entities/MemberProfile.cs | 4 +- .../Merchants/Entities/MerchantContract.cs | 2 +- .../Merchants/Entities/MerchantDocument.cs | 2 +- .../Merchants/Entities/MerchantStaff.cs | 6 +- .../Repositories/IMerchantRepository.cs | 63 + .../Navigation/Entities/MapLocation.cs | 2 +- .../Navigation/Entities/NavigationRequest.cs | 4 +- .../Ordering/Entities/CartItem.cs | 6 +- .../Ordering/Entities/CartItemAddon.cs | 4 +- .../Ordering/Entities/CheckoutSession.cs | 4 +- .../Ordering/Entities/ShoppingCart.cs | 4 +- .../Orders/Entities/Order.cs | 4 +- .../Orders/Entities/OrderItem.cs | 4 +- .../Orders/Entities/OrderStatusHistory.cs | 4 +- .../Orders/Entities/RefundRequest.cs | 2 +- .../Orders/Repositories/IOrderRepository.cs | 69 + .../Payments/Entities/PaymentRecord.cs | 2 +- .../Payments/Entities/PaymentRefundRecord.cs | 4 +- .../Repositories/IPaymentRepository.cs | 42 + .../Products/Entities/Product.cs | 4 +- .../Products/Entities/ProductAddonGroup.cs | 2 +- .../Products/Entities/ProductAddonOption.cs | 2 +- .../Entities/ProductAttributeGroup.cs | 7 +- .../Entities/ProductAttributeOption.cs | 2 +- .../Products/Entities/ProductCategory.cs | 2 +- .../Products/Entities/ProductMediaAsset.cs | 2 +- .../Products/Entities/ProductPricingRule.cs | 7 +- .../Products/Entities/ProductSku.cs | 7 +- .../Repositories/IProductRepository.cs | 103 + .../Queues/Entities/QueueTicket.cs | 2 +- .../Reservations/Entities/Reservation.cs | 2 +- .../Stores/Entities/Store.cs | 2 +- .../Stores/Entities/StoreBusinessHour.cs | 2 +- .../Stores/Entities/StoreDeliveryZone.cs | 7 +- .../Stores/Entities/StoreEmployeeShift.cs | 4 +- .../Stores/Entities/StoreHoliday.cs | 2 +- .../Stores/Entities/StoreTable.cs | 4 +- .../Stores/Entities/StoreTableArea.cs | 7 +- .../Stores/Repositories/IStoreRepository.cs | 93 + .../Tenants/Entities/Tenant.cs | 2 +- .../Tenants/Entities/TenantSubscription.cs | 4 +- .../AppServiceCollectionExtensions.cs | 48 + .../20251201044927_InitialApp.Designer.cs | 949 - .../Migrations/20251201044927_InitialApp.cs | 497 - ...51201055852_ExpandDomainSchema.Designer.cs | 4330 --- .../20251201055852_ExpandDomainSchema.cs | 2206 -- .../20251201094254_AddEntityComments.cs | 22401 ---------------- .../App/Options/AppSeedOptions.cs | 29 + .../App/Options/DictionarySeedGroupOptions.cs | 51 + .../App/Options/DictionarySeedItemOptions.cs | 39 + .../App/Options/TenantSeedOptions.cs | 46 + .../App/Persistence/AppDataSeeder.cs | 301 + .../App/Persistence/TakeoutAppDbContext.cs | 6 +- .../App/Repositories/EfDeliveryRepository.cs | 71 + .../App/Repositories/EfMerchantRepository.cs | 116 + .../App/Repositories/EfOrderRepository.cs | 133 + .../App/Repositories/EfPaymentRepository.cs | 71 + .../App/Repositories/EfProductRepository.cs | 227 + .../App/Repositories/EfStoreRepository.cs | 174 + .../DatabaseServiceCollectionExtensions.cs | 13 + .../Common/Persistence/AppDbContext.cs | 38 +- .../DesignTimeDbContextFactoryBase.cs | 4 +- .../Persistence/TenantAwareDbContext.cs | 8 +- ...251201042346_InitialDictionary.Designer.cs | 172 - .../20251201042346_InitialDictionary.cs | 101 - .../20251201094456_AddEntityComments.cs | 599 - .../Persistence/DictionaryDbContext.cs | 6 +- .../Repositories/EfDictionaryRepository.cs | 10 +- .../Services/DistributedDictionaryCache.cs | 10 +- ...20251201042324_InitialIdentity.Designer.cs | 152 - .../20251201042324_InitialIdentity.cs | 94 - .../20251201094410_AddEntityComments.cs | 581 - .../Identity/Options/AdminSeedOptions.cs | 4 +- .../Persistence/EfIdentityUserRepository.cs | 2 +- .../Persistence/EfMiniUserRepository.cs | 6 +- .../Persistence/IdentityDataSeeder.cs | 4 +- .../Identity/Persistence/IdentityDbContext.cs | 6 +- .../Services/RedisRefreshTokenStore.cs | 2 +- ...51202005208_InitSnowflake_App.Designer.cs} | 1928 +- .../20251202005208_InitSnowflake_App.cs | 2550 ++ ...5247_InitSnowflake_Dictionary.Designer.cs} | 54 +- ...20251202005247_InitSnowflake_Dictionary.cs | 106 + .../DictionaryDbContextModelSnapshot.cs | 50 +- ...005226_InitSnowflake_Identity.Designer.cs} | 54 +- .../20251202005226_InitSnowflake_Identity.cs | 99 + .../IdentityDbContextModelSnapshot.cs | 50 +- .../TakeoutAppDbContextModelSnapshot.cs | 1924 +- .../TakeoutSaaS.Infrastructure.csproj | 1 + .../TenantProvider.cs | 4 +- .../TenantResolutionMiddleware.cs | 8 +- .../TenantResolutionOptions.cs | 12 +- 174 files changed, 8020 insertions(+), 34278 deletions(-) create mode 100644 AGENTS.md create mode 100644 Document/13_AppSeed说明.md create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/CreateMerchantCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs create mode 100644 src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedGroupOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedItemOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/TenantSeedOptions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs rename src/Infrastructure/TakeoutSaaS.Infrastructure/{App/Migrations/20251201094254_AddEntityComments.Designer.cs => Migrations/20251202005208_InitSnowflake_App.Designer.cs} (79%) create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.cs rename src/Infrastructure/TakeoutSaaS.Infrastructure/{Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs => Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.Designer.cs} (83%) create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.cs rename src/Infrastructure/TakeoutSaaS.Infrastructure/{Dictionary/Migrations => Migrations/DictionaryDb}/DictionaryDbContextModelSnapshot.cs (84%) rename src/Infrastructure/TakeoutSaaS.Infrastructure/{Identity/Migrations/20251201094410_AddEntityComments.Designer.cs => Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.Designer.cs} (81%) create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.cs rename src/Infrastructure/TakeoutSaaS.Infrastructure/{Identity/Migrations => Migrations/IdentityDb}/IdentityDbContextModelSnapshot.cs (82%) rename src/Infrastructure/TakeoutSaaS.Infrastructure/{App => }/Migrations/TakeoutAppDbContextModelSnapshot.cs (79%) diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6ff2f4f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,150 @@ +# Repository expectations +# 编程规范_FOR_AI(TakeoutSaaS) - 终极完全体 + +> **核心指令**:你是一个高级 .NET 架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。 + +## 0. AI 交互核心约束 (元规则) +1. **语言**:必须使用**中文**回复和编写注释。 +2. **文件完整性**: + * **严禁**随意删除现有代码逻辑。 + * **严禁**修改文件编码(保持 UTF-8 无 BOM)。 + * PowerShell 读取命令必须带 `-Encoding UTF8`。 +3. **Git 原子性**:每个独立的功能点或 Bug 修复完成后,必须提示用户进行 Git 提交。 +4. **无乱码承诺**:确保所有输出(控制台、日志、API响应)无乱码。 +5. **不确定的处理**:如果你通过上下文找不到某些配置(如数据库连接串格式),**请直接询问用户**,不要瞎编。 + +## 1. 技术栈详细版本 +| 组件 | 版本/选型 | 用途说明 | +| :--- | :--- | :--- | +| **Runtime** | .NET 10 | 核心运行时 | +| **API** | ASP.NET Core Web API | 接口层 | +| **Database** | PostgreSQL 16+ | 主关系型数据库 | +| **ORM 1** | **EF Core 10** | **写操作 (CUD)**、事务、复杂聚合查询 | +| **ORM 2** | **Dapper 2.1+** | **纯读操作 (R)**、复杂报表、大批量查询 | +| **Cache** | Redis 7.0+ | 分布式缓存、Session | +| **MQ** | RabbitMQ 3.12+ | 异步解耦 (MassTransit) | +| **Libs** | MediatR, Serilog, FluentValidation | CQRS, 日志, 验证 | + +## 2. 命名与风格 (严格匹配) +* **C# 代码**: + * 类/接口/方法/属性:`PascalCase` (如 `OrderService`) + * **布尔属性**:必须加 `Is` 或 `Has` 前缀 (如 `IsDeleted`, `HasPayment`) + * 私有字段:`_camelCase` (如 `_orderRepository`) + * 参数/变量:`camelCase` (如 `orderId`) +* **PostgreSQL 数据库**: + * 表名:`snake_case` + **复数** (如 `merchant_orders`) + * 列名:`snake_case` (如 `order_no`, `is_active`) + * 主键:`id` (类型 `bigint`) +* **文件规则**: + * **一个文件一个类**。文件名必须与类名完全一致。 + +## 3. 分层架构 (Clean Architecture) +**你生成的代码必须严格归类到以下目录:** +* **`src/Api`**: 仅负责路由与 DTO 转换,**禁止**包含业务逻辑。 +* **`src/Application`**: 业务编排层。必须使用 **CQRS** (`IRequestHandler`) 和 **Mediator**。 +* **`src/Domain`**: 核心领域层。包含实体、枚举、领域异常。**禁止**依赖 EF Core 等外部库。 +* **`src/Infrastructure`**: 基础设施层。实现仓储、数据库上下文、第三方服务。 + +## 4. 注释与文档 +* **强制 XML 注释**:所有 `public` 的类、方法、属性必须有 ``。 +* **步骤注释**:超过 5 行的业务逻辑,必须分步注释: + ```csharp + // 1. 验证库存 + // 2. 扣减余额 + ``` +* **Swagger**:必须开启 JWT 鉴权按钮,Request/Response 示例必须清晰。 + +## 5. 异常处理 (防御性编程) +* **禁止空 Catch**:严禁 `catch (Exception) {}`,必须记录日志或抛出。 +* **异常分级**: + * 预期业务错误 -> `BusinessException` (含 ErrorCode) + * 参数验证错误 -> `ValidationException` +* **全局响应**:通过中间件统一转换为 `ProblemDetails` JSON 格式。 + +## 6. 异步与日志 +* **全异步**:所有 I/O 操作必须 `await`。**严禁** `.Result` 或 `.Wait()`。 +* **结构化日志**: + * ❌ `_logger.LogInfo("订单 " + id + " 创建成功");` + * ✅ `_logger.LogInformation("订单 {OrderId} 创建成功", id);` +* **脱敏**:严禁打印密码、密钥、支付凭证等敏感信息。 + +## 7. 依赖注入 (DI) +* **构造函数注入**:统一使用构造函数注入。 +* **禁止项**: + * ❌ 禁止使用 `[Inject]` 属性注入。 + * ❌ 禁止使用 `ServiceLocator` (服务定位器模式)。 + * ❌ 禁止在静态类中持有 ServiceProvider。 + +## 8. 数据访问规范 (重点执行) +### 8.1 Entity Framework Core (写/事务) +1. **无跟踪查询**:只读查询**必须**加 `.AsNoTracking()`。 +2. **杜绝 N+1**:严禁在 `foreach` 循环中查询数据库。必须使用 `.Include()`。 +3. **复杂查询**:关联表超过 2 层时,考虑使用 `.AsSplitQuery()`。 + +### 8.2 Dapper (读/报表) +1. **SQL 注入防御**:**严禁**拼接 SQL 字符串。必须使用参数化查询 (`@Param`)。 +2. **字段映射**:注意 PostgreSQL (`snake_case`) 与 C# (`PascalCase`) 的映射配置。 + +## 9. 多租户与 ID 策略 +* **ID 生成**: + * **强制**使用 **雪花算法 (Snowflake ID)**。 + * 类型:C# `long` <-> DB `bigint`。 + * **禁止**使用 UUID 或 自增 INT。 +* **租户隔离**: + * 所有业务表必须包含 `tenant_id`。 + * 写入时自动填充,读取时强制过滤。 + +## 10. API 设计与序列化 (前端兼容) +* **大整数处理**: + * 所有 `long` 类型 (Snowflake ID) 在 DTO 中**必须序列化为 string**。 + * 方案:DTO 属性加 `[JsonConverter(typeof(ToStringJsonConverter))]` 或全局配置。 +* **DTO 规范**: + * 输入:`XxxRequest` + * 输出:`XxxDto` + * **禁止** Controller 直接返回 Entity。 + +## 11. 模块化与复用 +* **核心模块划分**:Identity (身份), Tenancy (租户), Dictionary (字典), Storage (存储)。 +* **公共库 (Shared)**:通用工具类、扩展方法、常量定义必须放在 `Core/Shared` 项目中,避免重复造轮子。 + +## 12. 测试规范 +* **模式**:Arrange-Act-Assert (AAA)。 +* **工具**:xUnit + Moq + FluentAssertions。 +* **覆盖率**:核心 Domain 逻辑必须 100% 覆盖;Service 层 ≥ 70%。 + +## 13. Git 工作流 +* **提交格式 (Conventional Commits)**: + * `feat`: 新功能 + * `fix`: 修复 Bug + * `refactor`: 重构 + * `docs`: 文档 + * `style`: 格式调整 +* **分支规范**:`feature/功能名`,`bugfix/问题描述`。 + +## 14. 性能优化 (显式指令) +* **投影查询**:使用 `.Select(x => new Dto { ... })` 只查询需要的字段,减少 I/O。 +* **缓存策略**:Cache-Aside 模式。数据更新后必须立即失效缓存。 +* **批量操作**: + * EF Core 10:使用 `ExecuteUpdateAsync` / `ExecuteDeleteAsync`。 + * Dapper:使用 `ExecuteAsync` 进行批量插入。 + +## 15. 安全规范 +* **SQL 注入**:已在第 8 条强制参数化。 +* **身份认证**:Admin 端使用 JWT + RBAC;小程序端使用 Session/Token。 +* **密码存储**:必须使用 PBKDF2 或 BCrypt 加盐哈希。 + +## 16. 绝对禁止事项 (AI 自检清单) +**生成代码前,请自查是否违反以下红线:** +1. [ ] **SQL 注入**:是否拼接了 SQL 字符串? +2. [ ] **架构违规**:是否在 Controller/Domain 中使用了 DbContext? +3. [ ] **数据泄露**:是否返回了 Entity 或打印了密码? +4. [ ] **同步阻塞**:是否使用了 `.Result` 或 `.Wait()`? +5. [ ] **性能陷阱**:是否在循环中查询了数据库 (N+1)? +6. [ ] **精度丢失**:Long 类型的 ID 是否转为了 String? +7. [ ] **配置硬编码**:是否直接写死了连接串或密钥? + +--- + + +# Working agreements +- 严格遵循上述技术栈和命名规范。 \ No newline at end of file diff --git a/Document/10_设计期DbContext配置指引.md b/Document/10_设计期DbContext配置指引.md index 5baba69..2da0fd9 100644 --- a/Document/10_设计期DbContext配置指引.md +++ b/Document/10_设计期DbContext配置指引.md @@ -2,6 +2,48 @@ > 目的:在执行 `dotnet ef` 命令时无需硬编码数据库连接,可根据 appsettings 与环境变量自动加载。本文覆盖环境变量设置、配置目录指定等细节。 +## 三库迁移命令 只需更改 SnowflakeIds_App 迁移关键字 +> 先生成迁移,再执行数据库更新。启动项目统一用 AdminApi 确保加载最新配置。 + +### 生成迁移 +```bash +# App 主库 +dotnet tool run dotnet-ef migrations add SnowflakeIds_App ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` + --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext + +# Identity 库 +dotnet tool run dotnet-ef migrations add SnowflakeIds_Identity ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` + --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext + +# Dictionary 库 +dotnet tool run dotnet-ef migrations add SnowflakeIds_Dictionary ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` + --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext +``` + +### 更新数据库 +```bash +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` + --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext + +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` + --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext + +dotnet tool run dotnet-ef database update ` + --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` + --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` + --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext +``` + ## 一、设计时工厂读取逻辑概述 设计时工厂(`DesignTimeDbContextFactoryBase`)按下面顺序解析连接串: 1. 若设置了 `TAKEOUTSAAS_APP_CONNECTION` / `TAKEOUTSAAS_IDENTITY_CONNECTION` / `TAKEOUTSAAS_DICTIONARY_CONNECTION` 等环境变量,则优先使用。 diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index d6e5114..f715bbc 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -1,57 +1,57 @@ -# System-Level TODOs +# TODO Roadmap -> Infrastructure / platform backlog migrated from the former 11th document. Unless noted explicitly, all checklist items remain pending. +> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。 -## 1. 閰嶇疆涓庡熀纭€璁炬柦锛堥珮浼橈級 -- [x] Development/Production 鏁版嵁搴撹繛鎺ヤ笌 Secret 钀藉湴锛圫taging 鏆備笉闇€瑕侊級銆? -- [x] Redis 鏈嶅姟閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€? -- [x] RabbitMQ 鏈嶅姟閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€? -- [x] COS 瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯銆? -- [ ] OSS 瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛堝緟閲囪喘锛夈€? -- [ ] SMS 骞冲彴瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛堝緟閲囪喘锛夈€? -- [x] WeChat Mini 绋嬪簭瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛圓ppID锛歸x30f91e6afe79f405锛孉ppSecret锛?4324a7f604245301066ba7c3add488e锛屽凡鍚屾鍒?admin/mini 閰嶇疆骞剁櫥璁版洿鏂颁汉锛夈€? -- [x] PostgreSQL 鍩虹瀹炰緥閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€? -- [x] Postgres/Redis 鎺ュ叆鏂囨。 + IaC/鑴氭湰琛ラ綈锛堣 Document/infra/postgres_redis.md 涓?deploy/postgres|redis锛夈€? +## 1. 配置与基础设施(高优) +- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。 +- [x] Redis 服务部署完毕并记录配置。 +- [x] RabbitMQ 服务部署完毕并记录配置。 +- [x] COS 密钥配置补录完毕。 +- [ ] OSS 密钥配置补录完毕(已忽略,待采购后再补录)。 +- [ ] SMS 平台密钥配置补录完毕(已忽略,待采购后再补录)。 +- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。 +- [x] PostgreSQL 基础实例部署完毕并记录配置。 +- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。 - [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). -- [ ] admin/mini/user/gateway 缃戝叧鍩熷悕銆佽瘉涔︺€丆ORS 鍒楄〃鏁寸悊瀹屾垚銆? -- [ ] Hangfire Dashboard 鍚敤骞舵柊澧?Admin 瑙掕壊楠岃瘉/缃戝叧鐧藉悕鍗曘€? +- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。(忽略,暂时不用完成) +- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。(忽略,暂时不用完成) -## 2. 鏁版嵁涓庤縼绉伙紙楂樹紭锛? -- [x] App/Identity/Dictionary/Hangfire 鍥涗釜 DbContext 鍧囩敓鎴愬垵濮?Migration 骞舵垚鍔?update database銆? -- [ ] 鍟嗘埛/闂ㄥ簵/鍟嗗搧/璁㈠崟/鏀粯/閰嶉€佺瓑瀹炰綋涓庝粨鍌ㄥ疄鐜板畬鎴愶紝鎻愪緵 CRUD + 鏌ヨ銆? -- [ ] 绯荤粺鍙傛暟銆侀粯璁ょ鎴枫€佺鐞嗗憳璐﹀彿銆佸熀纭€瀛楀吀鐨勭瀛愯剼鏈彲閲嶅鎵ц銆? +## 2. 数据与迁移(高优) +- [x] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 +- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 +- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 -## 3. 绋冲畾鎬т笌璐ㄩ噺 -- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 鐨?xUnit+FluentAssertions 鍗曞厓娴嬭瘯妗嗘灦鎼缓銆? -- [ ] WebApplicationFactory + Testcontainers 鎷夎捣 Postgres/Redis/RabbitMQ/MinIO 鐨勯泦鎴愭祴璇曟ā鏉裤€? -- [ ] .editorconfig銆?globalconfig銆丷oslyn 鍒嗘瀽鍣ㄩ厤缃粨搴撻€氱敤瑙勫垯骞跺惎鐢?CI 妫€鏌ャ€? +## 3. 稳定性与质量 +- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 +- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。 +- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 -## 4. 瀹夊叏涓庡悎瑙? -- [ ] RBAC 鏉冮檺銆佺鎴烽殧绂汇€佺敤鎴?鏉冮檺娲炲療 API 瀹屾暣婕旂ず骞跺湪 Swagger 涓彁渚涚ず渚嬨€? -- [ ] 鐧诲綍/鍒锋柊娴佺▼澧炲姞 IP 鏍¢獙銆佺鎴烽殧绂汇€侀獙璇佺爜/棰戠巼闄愬埗銆? -- [ ] 鐧诲綍/鏉冮檺/鏁忔劅鎿嶄綔鏃ュ織鍙拷婧紝鎻愪緵鏌ヨ鎺ュ彛鎴?Kibana Saved Search銆? -- [ ] Secret Store/KeyVault/KMS 绠$悊鏁忔劅閰嶇疆锛岀姝㈠瘑閽ュ啓鍏?Git/鏁版嵁搴撴槑鏂囥€? +## 4. 安全与合规 +- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 +- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 +- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 +- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 -## 5. 瑙傛祴涓庤繍缁? -- [ ] TraceId 璐€氾紝骞跺湪 Serilog 涓緭鍑?Console/File/ELK 涓夌鐩爣銆? -- [ ] Prometheus exporter 鏆撮湶鍏抽敭鎸囨爣锛?health 鎺㈤拡涓庡憡璀﹁鍒欏悓姝ユ帹閫併€? -- [ ] PostgreSQL 鍏ㄩ噺/澧為噺澶囦唤鑴氭湰鍙婁竴娆$湡瀹炴仮澶嶆紨缁冩姤鍛娿€? +## 5. 观测与运维 +- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。 +- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 +- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 -## 6. 涓氬姟鑳藉姏琛ュ叏 -- [ ] 鍟嗘埛/闂ㄥ簵/鑿滃搧 API 瀹屾垚骞跺湪 MQ 涓姇閫掍笂鏋?鏀粯鎴愬姛浜嬩欢銆? -- [ ] 閰嶉€佸鎺?API 鏀寔涓嬪崟/鍙栨秷/鏌ヨ骞跺畬鎴愮鍚嶉獙绛句腑闂翠欢銆? -- [ ] 灏忕▼搴忕鍟嗗搧娴忚銆佷笅鍗曘€佹敮浠樸€佽瘎浠枫€佸浘鐗囩洿浼犵瓑 API 鍙棴鐜窇閫氥€? +## 6. 业务能力补全 +- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。 +- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。 +- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。 -## 7. 鍓嶅悗鍙?UI 瀵规帴 -- [ ] Admin UI 閫氳繃 OpenAPI 鐢熸垚鎴栨墜鍐欑晫闈紝鎺ュ叆 Hangfire Dashboard/MQ 鐩戞帶鍙妯″紡銆? -- [ ] 灏忕▼搴忕瀹屾垚鐧诲綍銆佽彍鍗曟祻瑙堛€佷笅鍗曘€佹敮浠樸€佺墿娴佽建杩广€佺礌鏉愮洿浼犻棴鐜€? +## 7. 前后台 UI 对接 +- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。 +- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。 -## 8. CI/CD 涓庡彂甯? -- [ ] CI/CD 娴佹按绾胯鐩栨瀯寤恒€佸彂甯冦€侀潤鎬佹壂鎻忋€佹暟鎹簱杩佺Щ銆? -- [ ] Dev/Staging/Prod 澶氱幆澧冮厤缃煩闃?+ 鍩虹璁炬柦 IaC 鑴氭湰銆? -- [ ] 鐗堟湰涓庡彂甯冭鏄庢ā鏉挎暣鐞嗗苟鍦ㄤ粨搴撲腑鎻愪緵绀轰緥銆? +## 8. CI/CD 与发布 +- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。 +- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。 +- [ ] 版本与发布说明模板整理并在仓库中提供示例。 -## 9. 鏂囨。涓庣煡璇嗗簱 -- [ ] 鎺ュ彛鏂囨。銆侀鍩熸ā鍨嬨€佸叧閿害鏉熶娇鐢?Markdown 鎴?API Portal 瀹屾暣璁板綍銆? -- [ ] 杩愯鎵嬪唽鍖呭惈閮ㄧ讲姝ラ銆佽祫婧愭嫇鎵戙€佹晠闅滄帓鏌ユ墜鍐屻€? -- [ ] 瀹夊叏鍚堣妯℃澘瑕嗙洊鏁版嵁鍒嗙骇銆佸瘑閽ョ鐞嗐€佸璁℃祦绋嬪苟褰㈡垚鍙鐢ㄨ〃鏍笺€ +## 9. 文档与知识库 +- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。 +- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。 +- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。 diff --git a/Document/12_BusinessTodo.md b/Document/12_BusinessTodo.md index e59a6dc..bd3da81 100644 --- a/Document/12_BusinessTodo.md +++ b/Document/12_BusinessTodo.md @@ -1,54 +1,53 @@ -# Business-Level TODOs +# 里程碑待办追踪 -> Product & business capability roadmap grouped by milestones; each phase only tracks the scoped backlog to enable staged delivery. +> 按“小程序版模块规划”划分四个里程碑;每个里程碑只含对应范围的任务,便于分阶段推进。 --- -## Phase 1锛堝綋鍓嶉樁娈碉級锛氱鎴?鍟嗗鍏ラ┗銆侀棬搴椾笌鑿滃搧銆佹壂鐮佸爞椋熴€佸熀纭€涓嬪崟鏀粯銆侀璐嚜鎻愩€佺涓夋柟閰嶉€侀鏋? -- [ ] 绠$悊绔鎴?API锛氭敞鍐屻€佸疄鍚嶈璇併€佸椁愯闃?缁垂/鍗囬檷閰嶃€佸鏍告祦锛孲wagger 鈮? 涓鐐癸紝鍚鏍告棩蹇椼€? -- [ ] 鍟嗗鍏ラ┗ API锛氳瘉鐓т笂浼犮€佸悎鍚岀鐞嗐€佺被鐩€夋嫨锛岄┍鍔ㄥ緟瀹?瀹℃牳/椹冲洖/閫氳繃鐘舵€佹満锛屾枃浠舵寔涔呭湪 COS銆? -- [ ] RBAC 妯℃澘锛氬钩鍙扮鐞嗗憳銆佺鎴风鐞嗗憳銆佸簵闀裤€佸簵鍛樺洓瑙掕壊妯℃澘锛汚PI 鍙鍒跺苟鍏佽绉熸埛鑷畾涔夋墿灞曘€? -- [ ] 閰嶉涓庡椁愶細TenantPackage CRUD銆佽闃?缁垂/閰嶉鏍¢獙锛堥棬搴?璐﹀彿/鐭俊/閰嶉€佸崟閲忥級锛岃秴棰濊繑鍥?409 骞惰褰?TenantQuotaUsage銆? -- [ ] 绉熸埛杩愯惀闈㈡澘锛氭瑺璐?鍒版湡鍛婅銆佽处鍗曞垪琛ㄣ€佸叕鍛婇€氱煡鎺ュ彛锛屾敮鎸佸凡璇荤姸鎬佸苟鍦?Admin UI 灞曠ず銆? -- [ ] 闂ㄥ簵绠$悊锛歋tore/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 瀹屾暣锛屽惈 GeoJSON 閰嶉€佽寖鍥村強鑳藉姏寮€鍏炽€? -- [ ] 妗岀爜绠$悊锛氭壒閲忕敓鎴愭鐮併€佺粦瀹氬尯鍩?瀹归噺銆佸鍑轰簩缁寸爜 ZIP锛圥OST /api/admin/stores/{id}/tables 鍙笅杞斤級銆? -- [ ] 鍛樺伐鎺掔彮锛氬垱寤哄憳宸ャ€佺粦瀹氶棬搴楄鑹层€佺淮鎶?StoreEmployeeShift锛屽彲鏌ヨ鏈潵 7 鏃ユ帓鐝€? -- [ ] 妗岀爜鎵爜鍏ュ彛锛歁ini 绔В鏋愪簩缁寸爜锛孏ET /api/mini/tables/{code}/context 杩斿洖闂ㄥ簵銆佹鍙般€佸叕鍛娿€? -- [ ] 鑿滃搧寤烘ā锛氬垎绫汇€丼PU銆丼KU銆佽鏍?鍔犳枡缁勩€佷环鏍肩瓥鐣ャ€佸獟璧?CRUD + 涓婁笅鏋舵祦绋嬶紱Mini 绔彲鎷夊彇瀹屾暣 JSON銆? -- [ ] 搴撳瓨浣撶郴锛歋KU 搴撳瓨銆佹壒娆°€佽皟鏁淬€佸敭缃勭鐞嗭紝鏀寔棰勫敭/妗f湡閿佸畾骞跺湪璁㈠崟涓墸鍑?閲婃斁銆? -- [ ] 鑷彁妗f湡锛氶棬搴楅厤缃嚜鎻愭椂闂寸獥銆佸閲忋€佹埅鍗曟椂闂达紱Mini 绔嵁姝ら檺鍒朵笅鍗曟椂闂淬€? -- [ ] 璐墿杞︽湇鍔★細ShoppingCart/CartItem/CartItemAddon API 鏀寔骞跺彂閿併€侀檺璐€佸埜/绉垎棰勬牎楠岋紝淇濊瘉骞跺彂鏃犺剰鏁版嵁銆? -- [ ] 璁㈠崟涓庢敮浠橈細鍫傞/鑷彁/閰嶉€佷笅鍗曘€佸井淇?鏀粯瀹濇敮浠樸€佷紭鎯犲埜/绉垎鎶垫墸銆佽鍗曠姸鎬佹満涓庨€氱煡閾捐矾榻愬叏銆? -- [ ] 妗屽彴璐﹀崟锛氬悎鍗?鎷嗗崟銆佺粨璐︺€佺數瀛愬皬绁ㄣ€佹鍙伴噴鏀撅紝瀹屾垚缁撹处鍚庢仮澶?Idle 骞剁敓鎴愮エ鎹?URL銆? -- [ ] 鑷厤閫侀鏋讹細楠戞墜绠$悊銆佸彇閫佷欢淇℃伅褰曞叆銆佽垂鐢ㄨˉ璐磋褰曪紝Admin 绔彲娲惧崟骞舵洿鏂?DeliveryOrder銆? -- [ ] 绗笁鏂归厤閫佹娊璞★細缁熶竴涓嬪崟/鍙栨秷/鍔犱环/鏌ヨ鎺ュ彛锛屾敮鎸佽揪杈俱€佺編鍥€侀棯閫佺瓑锛屽惈鍥炶皟楠岀涓庡紓甯歌ˉ鍋块鏋躲€? -- [ ] 棰勮喘鑷彁鏍搁攢锛氭彁璐х爜鐢熸垚銆佹墜鏈哄彿/浜岀淮鐮佹牳閿€銆佽嚜鎻愭煖/鍓嶅彴娴佺▼锛岃秴鏃惰嚜鍔ㄥ彇娑堟垨閫€娆撅紝璁板綍鎿嶄綔鑰呬笌鏃堕棿銆? -- [ ] 鎸囨爣涓庢棩蹇楋細Prometheus 杈撳嚭璁㈠崟鍒涘缓銆佹敮浠樻垚鍔熺巼銆侀厤閫佸洖璋冭€楁椂绛夛紝Grafana 鈮? 涓浘琛紱鍏抽敭娴佺▼鏃ュ織璁板綍 TraceId + 涓氬姟 ID銆? -- [ ] 娴嬭瘯锛歅hase 1 鏍稿績 API 鍏峰 鈮?0 鏉¤嚜鍔ㄥ寲鐢ㄤ緥锛堝崟鍏?+ 闆嗘垚锛夛紝瑕嗙洊绉熸埛鈫掑晢鎴封啋涓嬪崟閾捐矾銆? - +## Phase 1(当前阶段):租户/商家入驻、门店与菜品、扫码堂食、基础下单支付、预购自提、第三方配送骨架 +- [ ] 管理端租户 API:注册、实名认证、套餐订阅/续费/升降配、审核流,Swagger ≥6 个端点,含审核日志。 +- [ ] 商家入驻 API:证照上传、合同管理、类目选择,驱动待审/审核/驳回/通过状态机,文件持久在 COS。 +- [ ] RBAC 模板:平台管理员、租户管理员、店长、店员四角色模板;API 可复制并允许租户自定义扩展。 +- [ ] 配额与套餐:TenantPackage CRUD、订阅/续费/配额校验(门店/账号/短信/配送单量),超额返回 409 并记录 TenantQuotaUsage。 +- [ ] 租户运营面板:欠费/到期告警、账单列表、公告通知接口,支持已读状态并在 Admin UI 展示。 +- [ ] 门店管理:Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。 +- [ ] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 +- [ ] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 +- [ ] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。 +- [ ] 菜品建模:分类、SPU、SKU、规格/加料组、价格策略、媒资 CRUD + 上下架流程;Mini 端可拉取完整 JSON。 +- [ ] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。 +- [ ] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。 +- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。 +- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。 +- [ ] 桌台账单:合单/拆单、结账、电子小票、桌台释放,完成结账后恢复 Idle 并生成票据 URL。 +- [ ] 自配送骨架:骑手管理、取送件信息录入、费用补贴记录,Admin 端可派单并更新 DeliveryOrder。 +- [ ] 第三方配送抽象:统一下单/取消/加价/查询接口,支持达达、美团、闪送等,含回调验签与异常补偿骨架。 +- [ ] 预购自提核销:提货码生成、手机号/二维码核销、自提柜/前台流程,超时自动取消或退款,记录操作者与时间。 +- [ ] 指标与日志:Prometheus 输出订单创建、支付成功率、配送回调耗时等,Grafana ≥8 个图表;关键流程日志记录 TraceId + 业务 ID。 +- [ ] 测试:Phase 1 核心 API 具备 ≥30 条自动化用例(单元 + 集成),覆盖租户→商户→下单链路。 --- -## Phase 2锛堜笅涓€闃舵锛夛細鎷煎崟銆佷紭鎯犲埜涓庡熀纭€钀ラ攢銆佷細鍛樼Н鍒?浼氬憳鏃ャ€佸鏈嶈亰澶┿€佸悓鍩庤嚜閰嶉€佽皟搴︺€佹悳绱? -- [ ] 鎷煎崟寮曟搸锛欸roupOrder/Participant CRUD銆佸彂璧?鍔犲叆/鎴愬洟鏉′欢銆佽嚜鍔ㄨВ鏁d笌閫€娆俱€佸洟鍐呮秷鎭笌鎻愰啋銆? -- [ ] 浼樻儬鍒镐笌鍩虹钀ラ攢锛氭ā鏉跨鐞嗐€侀鍒搞€佹牳閿€銆佸簱瀛?鏈夋晥鏈?鍙犲姞瑙勫垯锛屽熀纭€鎶藉/绉掓潃/婊″噺娲诲姩銆? -- [ ] 浼氬憳涓庣Н鍒嗭細浼氬憳妗f銆佺瓑绾?鎴愰暱鍊笺€佷細鍛樻棩閫氱煡锛涚Н鍒嗚幏鍙?娑堣€椼€佹湁鏁堟湡銆侀粦鍚嶅崟銆? -- [ ] 瀹㈡湇鑱婂ぉ锛氬疄鏃朵細璇濄€佹満鍣ㄤ汉/浜哄伐鍒囨崲銆佹帓闃?杞帴銆佹秷鎭ā鏉裤€佹晱鎰熻瘝瀹℃煡銆佸伐鍗曟祦杞笌璇勪环銆? -- [ ] 鍚屽煄鑷厤閫佽皟搴︼細楠戞墜鏅鸿兘鎸囨淳銆佽矾绾夸及鏃躲€佹棤鎺ヨЕ閰嶉€併€佽垂鐢ㄨˉ璐寸瓥鐣ャ€佽皟搴︾湅鏉裤€? -- [ ] 鎼滅储锛氶棬搴?鑿滃搧/娲诲姩/浼樻儬鍒告悳绱紝杩囨护/鎺掑簭銆佺儹闂?鍘嗗彶璁板綍銆佽仈鎯充笌绾犻敊銆? +## Phase 2(下一阶段):拼单、优惠券与基础营销、会员积分/会员日、客服聊天、同城自配送调度、搜索 +- [ ] 拼单引擎:GroupOrder/Participant CRUD、发起/加入/成团条件、自动解散与退款、团内消息与提醒。 +- [ ] 优惠券与基础营销:模板管理、领券、核销、库存/有效期/叠加规则,基础抽奖/秒杀/满减活动。 +- [ ] 会员与积分:会员档案、等级/成长值、会员日通知;积分获取/消耗、有效期、黑名单。 +- [ ] 客服聊天:实时会话、机器人/人工切换、排队/转接、消息模板、敏感词审查、工单流转与评价。 +- [ ] 同城自配送调度:骑手智能指派、路线估时、无接触配送、费用补贴策略、调度看板。 +- [ ] 搜索:门店/菜品/活动/优惠券搜索,过滤/排序、热门/历史记录、联想与纠错。 --- -## Phase 3锛氬垎閿€杩斿埄銆佺鍒版墦鍗°€侀绾﹂璁€佸湴鍥惧鑸€佺ぞ鍖恒€侀珮闃惰惀閿€銆侀鎺т笌琛ュ伩 -- [ ] 鍒嗛攢杩斿埄锛欰ffiliatePartner/Order/Payout 绠$悊锛屼剑閲戦樁姊€佺粨绠楀懆鏈熴€佺◣鍔′俊鎭€佽繚瑙勫鐞嗐€? -- [ ] 绛惧埌鎵撳崱锛欳heckInCampaign/Record銆佽繛绛惧鍔便€佽ˉ绛俱€佺Н鍒?鍒?鎴愰暱鍊煎鍔便€佸弽浣滃紛鏈哄埗銆? -- [ ] 棰勭害棰勮锛氭。鏈?璧勬簮鍗犵敤銆侀绾︿笅鍗?鏀粯銆佹彁閱?鏀规湡/鍙栨秷銆佸埌搴楁牳閿€涓庡饱绾﹁褰曘€? -- [ ] 鍦板浘瀵艰埅鎵╁睍锛氶檮杩戦棬搴?鎺ㄨ崘銆佽窛绂?璺嚎瑙勫垝銆佽烦杞師鐢熷鑸€佸鑸姹傚煁鐐广€? -- [ ] 绀惧尯锛氬姩鎬佸彂甯冦€佽瘎璁恒€佺偣璧炪€佽瘽棰?鏍囩銆佸浘鐗?瑙嗛瀹℃牳銆佷妇鎶ヤ笌椋庢帶锛屽簵閾哄彛纰戝睍绀恒€? -- [ ] 楂橀樁钀ラ攢锛氱鏉€/鎶藉/瑁傚彉銆佽鍙樻捣鎶ャ€佺垎娆炬帹鑽愪綅銆佸娓犻亾鎶曟斁鍒嗘瀽銆? -- [ ] 椋庢帶涓庡璁★細榛戝悕鍗曘€侀鐜囬檺鍒躲€佸紓甯歌涓虹洃鎺с€佸璁℃棩蹇椼€佽ˉ鍋夸笌鍛婅浣撶郴銆? +## Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、高阶营销、风控与补偿 +- [ ] 分销返利:AffiliatePartner/Order/Payout 管理,佣金阶梯、结算周期、税务信息、违规处理。 +- [ ] 签到打卡:CheckInCampaign/Record、连签奖励、补签、积分/券/成长值奖励、反作弊机制。 +- [ ] 预约预订:档期/资源占用、预约下单/支付、提醒/改期/取消、到店核销与履约记录。 +- [ ] 地图导航扩展:附近门店/推荐、距离/路线规划、跳转原生导航、导航请求埋点。 +- [ ] 社区:动态发布、评论、点赞、话题/标签、图片/视频审核、举报与风控,店铺口碑展示。 +- [ ] 高阶营销:秒杀/抽奖/裂变、裂变海报、爆款推荐位、多渠道投放分析。 +- [ ] 风控与审计:黑名单、频率限制、异常行为监控、审计日志、补偿与告警体系。 --- -## Phase 4锛氭€ц兘浼樺寲銆佺紦瀛樸€佽繍钀ュぇ鐩樸€佹祴璇曚笌鏂囨。銆佷笂绾夸笌鐩戞帶 -- [ ] 鎬ц兘涓庣紦瀛橈細鐑偣鎺ュ彛缂撳瓨銆佹參鏌ヨ娌荤悊銆佹壒澶勭悊浼樺寲銆佸紓姝ュ寲鏀归€犮€? -- [ ] 鍙潬鎬э細骞傜瓑涓庨噸璇曠瓥鐣ャ€佷换鍔¤皟搴﹁ˉ鍋裤€侀摼璺拷韪€佸憡璀﹁仈鍔ㄣ€? -- [ ] 杩愯惀澶х洏锛氫氦鏄?钀ラ攢/灞ョ害/鐢ㄦ埛缁村害鐨勭粏鍒嗘姤琛ㄣ€丟MV/鎴愭湰/姣涘埄鍒嗘瀽銆? -- [ ] 鏂囨。涓庢祴璇曪細瀹屾暣娴嬭瘯鐭╅樀銆佹€ц兘娴嬭瘯鎶ュ憡銆佷笂绾挎墜鍐屻€佸洖婊氭柟妗堛€? -- [ ] 鐩戞帶涓庤繍缁达細涓婄嚎鍙戝竷娴佺▼銆佺伆搴?鍥炴粴绛栫暐銆佺郴缁熺ǔ瀹氭€ф寚鏍囥€?4x7 鐩戞帶涓庡憡璀︺€? +## Phase 4:性能优化、缓存、运营大盘、测试与文档、上线与监控 +- [ ] 性能与缓存:热点接口缓存、慢查询治理、批处理优化、异步化改造。 +- [ ] 可靠性:幂等与重试策略、任务调度补偿、链路追踪、告警联动。 +- [ ] 运营大盘:交易/营销/履约/用户维度的细分报表、GMV/成本/毛利分析。 +- [ ] 文档与测试:完整测试矩阵、性能测试报告、上线手册、回滚方案。 +- [ ] 监控与运维:上线发布流程、灰度/回滚策略、系统稳定性指标、24x7 监控与告警。 \ No newline at end of file diff --git a/Document/13_AppSeed说明.md b/Document/13_AppSeed说明.md new file mode 100644 index 0000000..8ab02e7 --- /dev/null +++ b/Document/13_AppSeed说明.md @@ -0,0 +1,52 @@ +# App 数据种子使用说明(App:Seed) + +> 作用:在启动时自动创建默认租户与基础字典,便于本地/测试环境快速落地必备数据。由 `AppDataSeeder` 执行,支持幂等多次运行。 + +## 配置入口 +- 文件位置:`appsettings.{Environment}.json`(示例已写入 AdminApi 的 Development 配置)。 +- 配置节:`App:Seed`。 + +示例(已写入 `src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json`): +```json +"App": { + "Seed": { + "Enabled": true, + "DefaultTenant": { + "TenantId": 1000000000001, + "Code": "demo", + "Name": "Demo租户", + "ShortName": "Demo", + "ContactName": "DemoAdmin", + "ContactPhone": "13800000000" + }, + "DictionaryGroups": [ + { + "Code": "order_status", + "Name": "订单状态", + "Scope": "Business", + "Items": [ + { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, + { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, + { "Key": "finished", "Value": "已完成", "SortOrder": 30 } + ] + } + ] + } +} +``` + +字段说明: +- `Enabled`: 是否启用种子。 +- `DefaultTenant`: 默认租户(使用雪花 long ID,0 表示让雪花生成)。 +- `DictionaryGroups`: 基础字典,`Scope` 可选 `System`/`Business`,`Items` 支持重复执行更新。 + +## 运行方式 +1. 确保 Admin API 已调用 `AddAppInfrastructure`(已在 Program.cs 中注册,会启动 `AppDataSeeder`)。 +2. 修改 `appsettings.{Environment}.json` 的 `App:Seed` 后,启动 Admin API,即会自动执行种子逻辑(幂等)。 +3. 查看日志:`AppSeed` 前缀会输出创建/更新结果。 + +## 注意事项 +- ID 必须为 long(雪花),不要再使用 Guid/自增。 +- 系统租户使用 `TenantId = 0`;业务租户请填写实际雪花 ID。 +- 字典分组编码需唯一;重复运行会按编码合并更新。 +- 生产环境请按需开启 `Enabled`,避免误写入。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index 164d0c3..962ee57 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -67,7 +67,7 @@ public sealed class AuthController : BaseApiController public async Task> GetProfile(CancellationToken cancellationToken) { var userId = User.GetUserId(); - if (userId == Guid.Empty) + if (userId == 0) { return ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs index 7232ed2..f63c273 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs @@ -55,10 +55,10 @@ public sealed class DictionaryController : BaseApiController /// /// 更新字典分组。 /// - [HttpPut("{groupId:guid}")] + [HttpPut("{groupId:long}")] [PermissionAuthorize("dictionary:group:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> UpdateGroup(Guid groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken) + public async Task> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken) { var group = await _dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken); return ApiResponse.Ok(group); @@ -67,10 +67,10 @@ public sealed class DictionaryController : BaseApiController /// /// 删除字典分组。 /// - [HttpDelete("{groupId:guid}")] + [HttpDelete("{groupId:long}")] [PermissionAuthorize("dictionary:group:delete")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> DeleteGroup(Guid groupId, CancellationToken cancellationToken) + public async Task> DeleteGroup(long groupId, CancellationToken cancellationToken) { await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken); return ApiResponse.Success(); @@ -79,10 +79,10 @@ public sealed class DictionaryController : BaseApiController /// /// 创建字典项。 /// - [HttpPost("{groupId:guid}/items")] + [HttpPost("{groupId:long}/items")] [PermissionAuthorize("dictionary:item:create")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> CreateItem(Guid groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken) + public async Task> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken) { request.GroupId = groupId; var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken); @@ -92,10 +92,10 @@ public sealed class DictionaryController : BaseApiController /// /// 更新字典项。 /// - [HttpPut("items/{itemId:guid}")] + [HttpPut("items/{itemId:long}")] [PermissionAuthorize("dictionary:item:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> UpdateItem(Guid itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken) + public async Task> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken) { var item = await _dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken); return ApiResponse.Ok(item); @@ -104,10 +104,10 @@ public sealed class DictionaryController : BaseApiController /// /// 删除字典项。 /// - [HttpDelete("items/{itemId:guid}")] + [HttpDelete("items/{itemId:long}")] [PermissionAuthorize("dictionary:item:delete")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> DeleteItem(Guid itemId, CancellationToken cancellationToken) + public async Task> DeleteItem(long itemId, CancellationToken cancellationToken) { await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken); return ApiResponse.Success(); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs new file mode 100644 index 0000000..cc11ec1 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs @@ -0,0 +1,72 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Merchants.Commands; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Application.App.Merchants.Queries; +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 商户管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/merchants")] +public sealed class MerchantsController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public MerchantsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建商户。 + /// + [HttpPost] + [PermissionAuthorize("merchant:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateMerchantCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询商户列表。 + /// + [HttpGet] + [PermissionAuthorize("merchant:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] MerchantStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchMerchantsQuery { Status = status }, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 获取商户详情。 + /// + [HttpGet("{merchantId:long}")] + [PermissionAuthorize("merchant:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long merchantId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商户不存在") + : ApiResponse.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 91a6c72..14eb59d 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -6,10 +6,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; +using TakeoutSaaS.Application.App.Extensions; using TakeoutSaaS.Application.Identity.Extensions; using TakeoutSaaS.Application.Messaging.Extensions; using TakeoutSaaS.Application.Sms.Extensions; using TakeoutSaaS.Application.Storage.Extensions; +using TakeoutSaaS.Infrastructure.App.Extensions; using TakeoutSaaS.Infrastructure.Identity.Extensions; using TakeoutSaaS.Module.Authorization.Extensions; using TakeoutSaaS.Module.Dictionary.Extensions; @@ -40,6 +42,8 @@ builder.Services.AddSharedSwagger(options => }); builder.Services.AddIdentityApplication(); builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true); +builder.Services.AddAppInfrastructure(builder.Configuration); +builder.Services.AddAppApplication(); builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddAuthorization(); builder.Services.AddPermissionAuthorization(); diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index 8a896da..c8258af 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index 4eb2c0e..01c5725 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -149,5 +149,39 @@ "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" + }, + "App": { + "Seed": { + "Enabled": true, + "DefaultTenant": { + "TenantId": 1000000000001, + "Code": "demo", + "Name": "Demo租户", + "ShortName": "Demo", + "ContactName": "DemoAdmin", + "ContactPhone": "13800000000" + }, + "DictionaryGroups": [ + { + "Code": "order_status", + "Name": "订单状态", + "Scope": "Business", + "Items": [ + { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, + { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, + { "Key": "finished", "Value": "已完成", "SortOrder": 30 } + ] + }, + { + "Code": "store_tags", + "Name": "门店标签", + "Scope": "Business", + "Items": [ + { "Key": "hot", "Value": "热门", "SortOrder": 10 }, + { "Key": "new", "Value": "新店", "SortOrder": 20 } + ] + } + ] + } } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs index 795116f..4dd1fa8 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs @@ -41,7 +41,7 @@ public sealed class MeController : BaseApiController public async Task> Get(CancellationToken cancellationToken) { var userId = User.GetUserId(); - if (userId == Guid.Empty) + if (userId == 0) { return ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); } diff --git a/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs new file mode 100644 index 0000000..61d259f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace TakeoutSaaS.Application.App.Extensions; + +/// +/// 业务应用层服务注册。 +/// +public static class AppApplicationServiceCollectionExtensions +{ + /// + /// 注册业务应用层(MediatR 处理器等)。 + /// + /// 服务集合。 + /// 服务集合。 + public static IServiceCollection AddAppApplication(this IServiceCollection services) + { + services.AddMediatR(Assembly.GetExecutingAssembly()); + + return services; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/CreateMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/CreateMerchantCommand.cs new file mode 100644 index 0000000..ba98a8c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/CreateMerchantCommand.cs @@ -0,0 +1,53 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Merchants.Commands; + +/// +/// 创建商户命令。 +/// +public sealed class CreateMerchantCommand : IRequest +{ + /// + /// 品牌名称。 + /// + [Required, MaxLength(128)] + public string BrandName { get; init; } = string.Empty; + + /// + /// 品牌简称。 + /// + [MaxLength(64)] + public string? BrandAlias { get; init; } + + /// + /// 品牌 Logo。 + /// + [MaxLength(256)] + public string? LogoUrl { get; init; } + + /// + /// 品类。 + /// + [MaxLength(64)] + public string? Category { get; init; } + + /// + /// 联系电话。 + /// + [Required, MaxLength(32)] + public string ContactPhone { get; init; } = string.Empty; + + /// + /// 联系邮箱。 + /// + [MaxLength(128)] + public string? ContactEmail { get; init; } + + /// + /// 状态,可用于直接设为审核通过等场景。 + /// + public MerchantStatus Status { get; init; } = MerchantStatus.Pending; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs new file mode 100644 index 0000000..d1552c8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Merchants.Dto; + +/// +/// 商户 DTO。 +/// +public sealed class MerchantDto +{ + /// + /// 商户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 品牌名称。 + /// + public string BrandName { get; init; } = string.Empty; + + /// + /// 品牌简称。 + /// + public string? BrandAlias { get; init; } + + /// + /// 品牌 Logo。 + /// + public string? LogoUrl { get; init; } + + /// + /// 品类。 + /// + public string? Category { get; init; } + + /// + /// 联系电话。 + /// + public string ContactPhone { get; init; } = string.Empty; + + /// + /// 联系邮箱。 + /// + public string? ContactEmail { get; init; } + + /// + /// 入驻状态。 + /// + public MerchantStatus Status { get; init; } + + /// + /// 入驻时间。 + /// + public DateTime? JoinedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs new file mode 100644 index 0000000..4b32e70 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs @@ -0,0 +1,54 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Merchants.Commands; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Entities; +using TakeoutSaaS.Domain.Merchants.Repositories; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 创建商户命令处理器。 +/// +public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRepository, ILogger logger) + : IRequestHandler +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateMerchantCommand request, CancellationToken cancellationToken) + { + var merchant = new Merchant + { + BrandName = request.BrandName.Trim(), + BrandAlias = request.BrandAlias?.Trim(), + LogoUrl = request.LogoUrl?.Trim(), + Category = request.Category?.Trim(), + ContactPhone = request.ContactPhone.Trim(), + ContactEmail = request.ContactEmail?.Trim(), + Status = request.Status, + JoinedAt = DateTime.UtcNow + }; + + await _merchantRepository.AddMerchantAsync(merchant, cancellationToken); + await _merchantRepository.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("创建商户 {MerchantId} - {BrandName}", merchant.Id, merchant.BrandName); + return MapToDto(merchant); + } + + private static MerchantDto MapToDto(Merchant merchant) => new() + { + Id = merchant.Id, + TenantId = merchant.TenantId, + BrandName = merchant.BrandName, + BrandAlias = merchant.BrandAlias, + LogoUrl = merchant.LogoUrl, + Category = merchant.Category, + ContactPhone = merchant.ContactPhone, + ContactEmail = merchant.ContactEmail, + Status = merchant.Status, + JoinedAt = merchant.JoinedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs new file mode 100644 index 0000000..f689743 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs @@ -0,0 +1,42 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Application.App.Merchants.Queries; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 获取商户详情查询处理器。 +/// +public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepository, ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetMerchantByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken); + if (merchant == null) + { + return null; + } + + return new MerchantDto + { + Id = merchant.Id, + TenantId = merchant.TenantId, + BrandName = merchant.BrandName, + BrandAlias = merchant.BrandAlias, + LogoUrl = merchant.LogoUrl, + Category = merchant.Category, + ContactPhone = merchant.ContactPhone, + ContactEmail = merchant.ContactEmail, + Status = merchant.Status, + JoinedAt = merchant.JoinedAt + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs new file mode 100644 index 0000000..bfd2f9b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs @@ -0,0 +1,42 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Application.App.Merchants.Queries; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 商户列表查询处理器。 +/// +public sealed class SearchMerchantsQueryHandler( + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken); + + return merchants + .Select(merchant => new MerchantDto + { + Id = merchant.Id, + TenantId = merchant.TenantId, + BrandName = merchant.BrandName, + BrandAlias = merchant.BrandAlias, + LogoUrl = merchant.LogoUrl, + Category = merchant.Category, + ContactPhone = merchant.ContactPhone, + ContactEmail = merchant.ContactEmail, + Status = merchant.Status, + JoinedAt = merchant.JoinedAt + }) + .ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantByIdQuery.cs new file mode 100644 index 0000000..fce904d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; + +namespace TakeoutSaaS.Application.App.Merchants.Queries; + +/// +/// 按 ID 获取商户。 +/// +public sealed class GetMerchantByIdQuery : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs new file mode 100644 index 0000000..3ec7561 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs @@ -0,0 +1,16 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Merchants.Queries; + +/// +/// 搜索商户列表。 +/// +public sealed class SearchMerchantsQuery : IRequest> +{ + /// + /// 按状态过滤。 + /// + public MerchantStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs index dc51fe3..60e5f61 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryAppService.cs @@ -10,17 +10,17 @@ public interface IDictionaryAppService { Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default); - Task UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default); + Task UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default); - Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default); + Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default); Task> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default); Task CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default); - Task UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default); + Task UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default); - Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default); + Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default); Task>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs index 4bbf169..ebdc59f 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Abstractions/IDictionaryCache.cs @@ -13,15 +13,15 @@ public interface IDictionaryCache /// /// 获取缓存。 /// - Task?> GetAsync(Guid tenantId, string code, CancellationToken cancellationToken = default); + Task?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default); /// /// 写入缓存。 /// - Task SetAsync(Guid tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default); + Task SetAsync(long tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default); /// /// 移除缓存。 /// - Task RemoveAsync(Guid tenantId, string code, CancellationToken cancellationToken = default); + Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs index 553401a..668d369 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryItemRequest.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; using System.ComponentModel.DataAnnotations; namespace TakeoutSaaS.Application.Dictionary.Contracts; @@ -11,7 +13,8 @@ public sealed class CreateDictionaryItemRequest /// 所属分组 ID。 /// [Required] - public Guid GroupId { get; set; } + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long GroupId { get; set; } /// /// 字典项键。 diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs index 528167f..95a81f0 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryGroupDto.cs @@ -1,5 +1,8 @@ using TakeoutSaaS.Domain.Dictionary.Enums; +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + namespace TakeoutSaaS.Application.Dictionary.Models; /// @@ -7,7 +10,8 @@ namespace TakeoutSaaS.Application.Dictionary.Models; /// public sealed class DictionaryGroupDto { - public Guid Id { get; init; } + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } public string Code { get; init; } = string.Empty; diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs index 89faaf7..f154f3e 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Models/DictionaryItemDto.cs @@ -1,3 +1,6 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + namespace TakeoutSaaS.Application.Dictionary.Models; /// @@ -5,9 +8,11 @@ namespace TakeoutSaaS.Application.Dictionary.Models; /// public sealed class DictionaryItemDto { - public Guid Id { get; init; } + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } - public Guid GroupId { get; init; } + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long GroupId { get; init; } public string Key { get; init; } = string.Empty; diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs index 93d92c2..8ecfc79 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs @@ -47,7 +47,7 @@ public sealed class DictionaryAppService : IDictionaryAppService var group = new DictionaryGroup { - Id = Guid.NewGuid(), + Id = 0, TenantId = targetTenant, Code = normalizedCode, Name = request.Name.Trim(), @@ -62,7 +62,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return MapGroup(group, includeItems: false); } - public async Task UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default) + public async Task UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(groupId, cancellationToken); EnsureScopePermission(group.Scope); @@ -77,7 +77,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return MapGroup(group, includeItems: false); } - public async Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default) + public async Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(groupId, cancellationToken); EnsureScopePermission(group.Scope); @@ -120,7 +120,7 @@ public sealed class DictionaryAppService : IDictionaryAppService var item = new DictionaryItem { - Id = Guid.NewGuid(), + Id = 0, TenantId = group.TenantId, GroupId = group.Id, Key = request.Key.Trim(), @@ -138,7 +138,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return MapItem(item); } - public async Task UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default) + public async Task UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default) { var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); @@ -156,7 +156,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return MapItem(item); } - public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default) + public async Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default) { var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); @@ -186,8 +186,8 @@ public sealed class DictionaryAppService : IDictionaryAppService foreach (var code in normalizedCodes) { - var systemItems = await GetOrLoadCacheAsync(Guid.Empty, code, cancellationToken); - if (tenantId == Guid.Empty) + var systemItems = await GetOrLoadCacheAsync(0, code, cancellationToken); + if (tenantId == 0) { result[code] = systemItems; continue; @@ -200,7 +200,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return result; } - private async Task RequireGroupAsync(Guid groupId, CancellationToken cancellationToken) + private async Task RequireGroupAsync(long groupId, CancellationToken cancellationToken) { var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken); if (group == null) @@ -211,7 +211,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return group; } - private async Task RequireItemAsync(Guid itemId, CancellationToken cancellationToken) + private async Task RequireItemAsync(long itemId, CancellationToken cancellationToken) { var item = await _repository.FindItemByIdAsync(itemId, cancellationToken); if (item == null) @@ -222,16 +222,16 @@ public sealed class DictionaryAppService : IDictionaryAppService return item; } - private Guid ResolveTargetTenant(DictionaryScope scope) + private long ResolveTargetTenant(DictionaryScope scope) { var tenantId = _tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System) { EnsurePlatformTenant(tenantId); - return Guid.Empty; + return 0; } - if (tenantId == Guid.Empty) + if (tenantId == 0) { throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户"); } @@ -241,28 +241,28 @@ public sealed class DictionaryAppService : IDictionaryAppService private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant(); - private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, Guid tenantId) + private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, long tenantId) { if (requestedScope.HasValue) { return requestedScope.Value; } - return tenantId == Guid.Empty ? DictionaryScope.System : DictionaryScope.Business; + return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business; } private void EnsureScopePermission(DictionaryScope scope) { var tenantId = _tenantProvider.GetCurrentTenantId(); - if (scope == DictionaryScope.System && tenantId != Guid.Empty) + if (scope == DictionaryScope.System && tenantId != 0) { throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); } } - private void EnsurePlatformTenant(Guid tenantId) + private void EnsurePlatformTenant(long tenantId) { - if (tenantId != Guid.Empty) + if (tenantId != 0) { throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); } @@ -279,7 +279,7 @@ public sealed class DictionaryAppService : IDictionaryAppService // 系统参数更新需要逐租户重新合并,由调用方在下一次请求时重新加载 } - private async Task> GetOrLoadCacheAsync(Guid tenantId, string code, CancellationToken cancellationToken) + private async Task> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken) { var cached = await _cache.GetAsync(tenantId, code, cancellationToken); if (cached != null) diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs index f60dffb..28510e3 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -12,5 +12,5 @@ public interface IAdminAuthService { Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); - Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default); + Task GetProfileAsync(long userId, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs index 11efdb4..c7a509e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs @@ -12,5 +12,5 @@ public interface IMiniAuthService { Task LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default); Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); - Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default); + Task GetProfileAsync(long userId, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs index d966ca2..29a1bf6 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRefreshTokenStore.cs @@ -10,7 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Abstractions; /// public interface IRefreshTokenStore { - Task IssueAsync(Guid userId, DateTime expiresAt, CancellationToken cancellationToken = default); + Task IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default); Task GetAsync(string refreshToken, CancellationToken cancellationToken = default); Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs index e93e2bc..922b7d3 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs @@ -8,7 +8,7 @@ public sealed class CurrentUserProfile /// /// 用户 ID。 /// - public Guid UserId { get; init; } + public long UserId { get; init; } /// /// 登录账号。 @@ -23,12 +23,12 @@ public sealed class CurrentUserProfile /// /// 所属租户 ID。 /// - public Guid TenantId { get; init; } + public long TenantId { get; init; } /// /// 所属商户 ID(平台管理员为空)。 /// - public Guid? MerchantId { get; init; } + public long? MerchantId { get; init; } /// /// 角色集合。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs b/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs index 68e07e6..8a968bb 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Models/RefreshTokenDescriptor.cs @@ -9,6 +9,6 @@ namespace TakeoutSaaS.Application.Identity.Models; /// 是否已撤销 public sealed record RefreshTokenDescriptor( string Token, - Guid UserId, + long UserId, DateTime ExpiresAt, bool Revoked); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index ee0fbeb..42418a4 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -77,7 +77,7 @@ public sealed class AdminAuthService( /// 取消令牌 /// 用户档案 /// 用户不存在时抛出 - public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default) + public async Task GetProfileAsync(long userId, CancellationToken cancellationToken = default) { var user = await userRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs index 25efbf9..5d83289 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs @@ -44,7 +44,7 @@ public sealed class MiniAuthService( // 3. 获取当前租户 ID(多租户支持) var tenantId = tenantProvider.GetCurrentTenantId(); - if (tenantId == Guid.Empty) + if (tenantId == 0) { throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); } @@ -95,7 +95,7 @@ public sealed class MiniAuthService( /// 取消令牌 /// 用户档案 /// 用户不存在时抛出 - public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default) + public async Task GetProfileAsync(long userId, CancellationToken cancellationToken = default) { var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); @@ -113,7 +113,7 @@ public sealed class MiniAuthService( /// 租户 ID /// 取消令牌 /// 用户实体和是否为新用户的元组 - private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken) + private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken) { // 检查用户是否已存在 var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs index be61584..2a84f05 100644 --- a/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs +++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/OrderCreatedEvent.cs @@ -8,7 +8,7 @@ public sealed class OrderCreatedEvent /// /// 订单标识。 /// - public Guid OrderId { get; init; } + public long OrderId { get; init; } /// /// 订单编号。 @@ -23,7 +23,7 @@ public sealed class OrderCreatedEvent /// /// 所属租户。 /// - public Guid TenantId { get; init; } + public long TenantId { get; init; } /// /// 创建时间(UTC)。 diff --git a/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs b/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs index f62a88e..b0094f7 100644 --- a/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs +++ b/src/Application/TakeoutSaaS.Application/Messaging/Events/PaymentSucceededEvent.cs @@ -8,7 +8,7 @@ public sealed class PaymentSucceededEvent /// /// 订单标识。 /// - public Guid OrderId { get; init; } + public long OrderId { get; init; } /// /// 支付流水号。 @@ -23,7 +23,7 @@ public sealed class PaymentSucceededEvent /// /// 所属租户。 /// - public Guid TenantId { get; init; } + public long TenantId { get; init; } /// /// 支付时间(UTC)。 diff --git a/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs b/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs index 042410a..88806c9 100644 --- a/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs +++ b/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs @@ -44,7 +44,7 @@ public sealed class VerificationCodeService( var codeOptions = codeOptionsMonitor.CurrentValue; var templateCode = ResolveTemplate(request.Scene, smsOptions); var phone = NormalizePhoneNumber(request.PhoneNumber); - var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N"); + var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString(); var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; var cooldownKey = $"{cacheKey}:cooldown"; @@ -90,7 +90,7 @@ public sealed class VerificationCodeService( var codeOptions = codeOptionsMonitor.CurrentValue; var phone = NormalizePhoneNumber(request.PhoneNumber); - var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N"); + var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString(); var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false); diff --git a/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs b/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs index 05a02f2..f2105c5 100644 --- a/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs +++ b/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs @@ -212,7 +212,7 @@ public sealed class FileStorageService( private string BuildObjectKey(UploadFileType type, string extension) { var tenantId = tenantProvider.GetCurrentTenantId(); - var tenantSegment = tenantId == Guid.Empty ? "platform" : tenantId.ToString("N"); + var tenantSegment = tenantId == 0 ? "platform" : tenantId.ToString(); var folder = type.ToFolderName(); var now = DateTime.UtcNow; var fileName = $"{Guid.NewGuid():N}{extension}"; diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index 15da3e6..e320f29 100644 --- a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj +++ b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs index 3aaedf9..6dbcf9c 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs @@ -23,15 +23,15 @@ public abstract class AuditableEntityBase : EntityBase, IAuditableEntity /// /// 创建人用户标识,匿名或系统操作时为 null。 /// - public Guid? CreatedBy { get; set; } + public long? CreatedBy { get; set; } /// /// 最后更新人用户标识,匿名或系统操作时为 null。 /// - public Guid? UpdatedBy { get; set; } + public long? UpdatedBy { get; set; } /// /// 删除人用户标识(软删除),未删除时为 null。 /// - public Guid? DeletedBy { get; set; } + public long? DeletedBy { get; set; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs index 1cd539e..e1e2aa4 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs @@ -8,5 +8,5 @@ public abstract class EntityBase /// /// 实体唯一标识。 /// - public Guid Id { get; set; } + public long Id { get; set; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs index 7168803..844ad54 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs @@ -23,15 +23,15 @@ public interface IAuditableEntity : ISoftDeleteEntity /// /// 创建人用户标识,匿名或系统操作时为 null。 /// - Guid? CreatedBy { get; set; } + long? CreatedBy { get; set; } /// /// 最后更新人用户标识,匿名或系统操作时为 null。 /// - Guid? UpdatedBy { get; set; } + long? UpdatedBy { get; set; } /// /// 删除人用户标识(软删除),未删除时为 null。 /// - Guid? DeletedBy { get; set; } + long? DeletedBy { get; set; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs index 5ea8f7d..1a0fecd 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs @@ -8,5 +8,5 @@ public interface IMultiTenantEntity /// /// 所属租户 ID。 /// - Guid TenantId { get; set; } + long TenantId { get; set; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs index 59bf1f8..df6417e 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs @@ -8,5 +8,5 @@ public abstract class MultiTenantEntityBase : AuditableEntityBase, IMultiTenantE /// /// 所属租户 ID。 /// - public Guid TenantId { get; set; } + public long TenantId { get; set; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs new file mode 100644 index 0000000..ce8dff4 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs @@ -0,0 +1,13 @@ +namespace TakeoutSaaS.Shared.Abstractions.Ids; + +/// +/// 雪花 ID 生成器接口。 +/// +public interface IIdGenerator +{ + /// + /// 生成下一个唯一长整型 ID。 + /// + /// 雪花 ID。 + long NextId(); +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs new file mode 100644 index 0000000..6d9b40f --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Shared.Abstractions.Ids; + +/// +/// 雪花 ID 生成器配置。 +/// +public sealed class IdGeneratorOptions +{ + /// + /// 配置节名称。 + /// + public const string SectionName = "IdGenerator"; + + /// + /// 工作节点标识,0-31。 + /// + [Range(0, 31)] + public int WorkerId { get; set; } + + /// + /// 机房标识,0-31。 + /// + [Range(0, 31)] + public int DatacenterId { get; set; } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs index 50401a7..b49a215 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs @@ -99,6 +99,65 @@ public sealed record ApiResponse return TraceContext.TraceId; } - return Activity.Current?.Id ?? Guid.NewGuid().ToString("N"); + if (!string.IsNullOrWhiteSpace(TraceContext.TraceId)) + { + return TraceContext.TraceId; + } + + if (Activity.Current?.Id is { } id && !string.IsNullOrWhiteSpace(id)) + { + return id; + } + + return IdFallbackGenerator.Instance.NextId().ToString(); + } +} + +internal sealed class IdFallbackGenerator +{ + private static readonly Lazy Lazy = new(() => new IdFallbackGenerator()); + public static IdFallbackGenerator Instance => Lazy.Value; + + private readonly object _sync = new(); + private long _lastTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + private long _sequence; + + private IdFallbackGenerator() + { + } + + public long NextId() + { + lock (_sync) + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (timestamp == _lastTimestamp) + { + _sequence = (_sequence + 1) & 4095; + if (_sequence == 0) + { + timestamp = WaitNextMillis(_lastTimestamp); + } + } + else + { + _sequence = 0; + } + + _lastTimestamp = timestamp; + return ((timestamp - 1577836800000L) << 22) | _sequence; + } + } + + private static long WaitNextMillis(long lastTimestamp) + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + while (timestamp <= lastTimestamp) + { + Thread.SpinWait(100); + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + return timestamp; } } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs index 9b7e7fb..ba0d7e2 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs @@ -8,7 +8,7 @@ public interface ICurrentUserAccessor /// /// 当前用户 ID,未登录时为 Guid.Empty。 /// - Guid UserId { get; } + long UserId { get; } /// /// 是否已登录。 diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs new file mode 100644 index 0000000..b7f82a0 --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace TakeoutSaaS.Shared.Abstractions.Serialization; + +/// +/// 将 long 类型的雪花 ID 以字符串形式序列化/反序列化,避免前端精度丢失。 +/// +public sealed class SnowflakeIdJsonConverter : JsonConverter +{ + /// + public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => reader.GetInt64(), + JsonTokenType.String when long.TryParse(reader.GetString(), out var value) => value, + JsonTokenType.Null => 0, + _ => throw new JsonException("无法解析雪花 ID") + }; + } + + /// + public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) + { + writer.WriteStringValue(value == 0 ? "0" : value.ToString()); + } +} + +/// +/// 可空雪花 ID 转换器。 +/// +public sealed class NullableSnowflakeIdJsonConverter : JsonConverter +{ + /// + public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => reader.GetInt64(), + JsonTokenType.String when long.TryParse(reader.GetString(), out var value) => value, + JsonTokenType.Null => null, + _ => throw new JsonException("无法解析雪花 ID") + }; + } + + /// + public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.HasValue ? value.Value.ToString() : null); + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs index 41b999f..02c818a 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs @@ -8,5 +8,5 @@ public interface ITenantProvider /// /// 获取当前租户 ID,未解析时返回 Guid.Empty。 /// - Guid GetCurrentTenantId(); + long GetCurrentTenantId(); } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs index a4686a4..a53b38f 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs @@ -8,7 +8,7 @@ public sealed class TenantContext /// /// 未解析到租户时的默认上下文。 /// - public static TenantContext Empty { get; } = new(Guid.Empty, null, "unresolved"); + public static TenantContext Empty { get; } = new(0, null, "unresolved"); /// /// 初始化租户上下文。 @@ -16,7 +16,7 @@ public sealed class TenantContext /// 租户 ID /// 租户编码(可选) /// 解析来源 - public TenantContext(Guid tenantId, string? tenantCode, string source) + public TenantContext(long tenantId, string? tenantCode, string source) { TenantId = tenantId; TenantCode = tenantCode; @@ -26,7 +26,7 @@ public sealed class TenantContext /// /// 当前租户 ID,未解析时为 Guid.Empty。 /// - public Guid TenantId { get; } + public long TenantId { get; } /// /// 当前租户编码(例如子域名或业务编码),可为空。 @@ -41,5 +41,5 @@ public sealed class TenantContext /// /// 是否已成功解析到租户。 /// - public bool IsResolved => TenantId != Guid.Empty; + public bool IsResolved => TenantId != 0; } diff --git a/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs b/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs new file mode 100644 index 0000000..533789d --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs @@ -0,0 +1,111 @@ +using System.Diagnostics; +using System.Security.Cryptography; +using System.Threading; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Shared.Kernel.Ids; + +/// +/// 基于雪花算法的长整型 ID 生成器。 +/// +public sealed class SnowflakeIdGenerator : IIdGenerator +{ + private const long Twepoch = 1577836800000L; // 2020-01-01 UTC + private const int WorkerIdBits = 5; + private const int DatacenterIdBits = 5; + private const int SequenceBits = 12; + + private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits); + private const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits); + + private const int WorkerIdShift = SequenceBits; + private const int DatacenterIdShift = SequenceBits + WorkerIdBits; + private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; + private const long SequenceMask = -1L ^ (-1L << SequenceBits); + + private readonly long _workerId; + private readonly long _datacenterId; + private long _lastTimestamp = -1L; + private long _sequence; + private readonly object _syncRoot = new(); + + /// + /// 初始化生成器。 + /// + /// 工作节点 ID。 + /// 机房 ID。 + public SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0) + { + _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId)); + _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId)); + _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask); + } + + /// + public long NextId() + { + lock (_syncRoot) + { + var timestamp = CurrentTimeMillis(); + + if (timestamp < _lastTimestamp) + { + // 时钟回拨时等待到下一毫秒。 + var wait = _lastTimestamp - timestamp; + Thread.Sleep(TimeSpan.FromMilliseconds(wait)); + timestamp = CurrentTimeMillis(); + if (timestamp < _lastTimestamp) + { + throw new InvalidOperationException($"系统时钟回拨 {_lastTimestamp - timestamp} 毫秒,无法生成 ID。"); + } + } + + if (_lastTimestamp == timestamp) + { + _sequence = (_sequence + 1) & SequenceMask; + if (_sequence == 0) + { + timestamp = WaitNextMillis(_lastTimestamp); + } + } + else + { + _sequence = 0; + } + + _lastTimestamp = timestamp; + + var id = ((timestamp - Twepoch) << TimestampLeftShift) + | (_datacenterId << DatacenterIdShift) + | (_workerId << WorkerIdShift) + | _sequence; + + Debug.Assert(id > 0); + return id; + } + } + + private static long WaitNextMillis(long lastTimestamp) + { + var timestamp = CurrentTimeMillis(); + while (timestamp <= lastTimestamp) + { + Thread.SpinWait(50); + timestamp = CurrentTimeMillis(); + } + + return timestamp; + } + + private static long CurrentTimeMillis() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + private static long Normalize(long value, long max, string name) + { + if (value < 0 || value > max) + { + throw new ArgumentOutOfRangeException(name, value, $"取值范围 0~{max}"); + } + + return value; + } +} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs index b3cf763..ddbed3c 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Shared.Abstractions.Diagnostics; +using TakeoutSaaS.Shared.Abstractions.Ids; namespace TakeoutSaaS.Shared.Web.Middleware; @@ -17,11 +18,13 @@ public sealed class CorrelationIdMiddleware private readonly RequestDelegate _next; private readonly ILogger _logger; + private readonly IIdGenerator _idGenerator; - public CorrelationIdMiddleware(RequestDelegate next, ILogger logger) + public CorrelationIdMiddleware(RequestDelegate next, ILogger logger, IIdGenerator idGenerator) { _next = next; _logger = logger; + _idGenerator = idGenerator; } public async Task InvokeAsync(HttpContext context) @@ -52,7 +55,7 @@ public sealed class CorrelationIdMiddleware } } - private static string ResolveTraceId(HttpContext context) + private string ResolveTraceId(HttpContext context) { if (TryGetHeader(context, TraceHeader, out var traceId)) { @@ -64,7 +67,7 @@ public sealed class CorrelationIdMiddleware return requestId; } - return Guid.NewGuid().ToString("N"); + return _idGenerator.NextId().ToString(); } private static bool TryGetHeader(HttpContext context, string headerName, out string value) diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs index 8502109..05a90b2 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs @@ -9,20 +9,20 @@ namespace TakeoutSaaS.Shared.Web.Security; public static class ClaimsPrincipalExtensions { /// - /// 获取当前用户 Id(不存在时返回 Guid.Empty) + /// 获取当前用户 Id(不存在时返回 0)。 /// - public static Guid GetUserId(this ClaimsPrincipal? principal) + public static long GetUserId(this ClaimsPrincipal? principal) { if (principal == null) { - return Guid.Empty; + return 0; } var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? principal.FindFirstValue("sub"); - return Guid.TryParse(identifier, out var userId) + return long.TryParse(identifier, out var userId) ? userId - : Guid.Empty; + : 0; } } diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs index d73fbbd..6256f05 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs @@ -20,23 +20,23 @@ public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor } /// - public Guid UserId + public long UserId { get { var principal = _httpContextAccessor.HttpContext?.User; if (principal == null || !principal.Identity?.IsAuthenticated == true) { - return Guid.Empty; + return 0; } var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? principal.FindFirstValue("sub"); - return Guid.TryParse(identifier, out var id) ? id : Guid.Empty; + return long.TryParse(identifier, out var id) ? id : 0; } } /// - public bool IsAuthenticated => UserId != Guid.Empty; + public bool IsAuthenticated => UserId != 0; } diff --git a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs index 694dfca..d5ae3ee 100644 --- a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs +++ b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricAlertRule.cs @@ -11,7 +11,7 @@ public sealed class MetricAlertRule : MultiTenantEntityBase /// /// 关联指标。 /// - public Guid MetricDefinitionId { get; set; } + public long MetricDefinitionId { get; set; } /// /// 触发条件 JSON。 diff --git a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs index cb0e726..234b0a0 100644 --- a/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs +++ b/src/Domain/TakeoutSaaS.Domain/Analytics/Entities/MetricSnapshot.cs @@ -10,7 +10,7 @@ public sealed class MetricSnapshot : MultiTenantEntityBase /// /// 指标定义 ID。 /// - public Guid MetricDefinitionId { get; set; } + public long MetricDefinitionId { get; set; } /// /// 维度键(JSON)。 diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs index c327f0b..7c375e1 100644 --- a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/Coupon.cs @@ -11,7 +11,7 @@ public sealed class Coupon : MultiTenantEntityBase /// /// 模板标识。 /// - public Guid CouponTemplateId { get; set; } + public long CouponTemplateId { get; set; } /// /// 券码或序列号。 @@ -21,12 +21,12 @@ public sealed class Coupon : MultiTenantEntityBase /// /// 归属用户。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 订单 ID(已使用时记录)。 /// - public Guid? OrderId { get; set; } + public long? OrderId { get; set; } /// /// 状态。 diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs index 56799b5..ba0d3e6 100644 --- a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatMessage.cs @@ -11,7 +11,7 @@ public sealed class ChatMessage : MultiTenantEntityBase /// /// 会话标识。 /// - public Guid ChatSessionId { get; set; } + public long ChatSessionId { get; set; } /// /// 发送方类型。 @@ -21,7 +21,7 @@ public sealed class ChatMessage : MultiTenantEntityBase /// /// 发送方用户 ID。 /// - public Guid? SenderUserId { get; set; } + public long? SenderUserId { get; set; } /// /// 消息内容。 diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs index d2528ea..d9ea66d 100644 --- a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/ChatSession.cs @@ -16,17 +16,17 @@ public sealed class ChatSession : MultiTenantEntityBase /// /// 顾客用户 ID。 /// - public Guid CustomerUserId { get; set; } + public long CustomerUserId { get; set; } /// /// 当前客服员工 ID。 /// - public Guid? AgentUserId { get; set; } + public long? AgentUserId { get; set; } /// /// 所属门店(可空为平台)。 /// - public Guid? StoreId { get; set; } + public long? StoreId { get; set; } /// /// 会话状态。 diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs index 931727d..39c5ae4 100644 --- a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/SupportTicket.cs @@ -16,12 +16,12 @@ public sealed class SupportTicket : MultiTenantEntityBase /// /// 客户用户 ID。 /// - public Guid CustomerUserId { get; set; } + public long CustomerUserId { get; set; } /// /// 关联订单(如有)。 /// - public Guid? OrderId { get; set; } + public long? OrderId { get; set; } /// /// 工单主题。 @@ -46,7 +46,7 @@ public sealed class SupportTicket : MultiTenantEntityBase /// /// 指派的客服。 /// - public Guid? AssignedAgentId { get; set; } + public long? AssignedAgentId { get; set; } /// /// 关闭时间。 diff --git a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs index f6abba6..fd24969 100644 --- a/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs +++ b/src/Domain/TakeoutSaaS.Domain/CustomerService/Entities/TicketComment.cs @@ -10,12 +10,12 @@ public sealed class TicketComment : MultiTenantEntityBase /// /// 工单标识。 /// - public Guid SupportTicketId { get; set; } + public long SupportTicketId { get; set; } /// /// 评论人 ID。 /// - public Guid? AuthorUserId { get; set; } + public long? AuthorUserId { get; set; } /// /// 评论内容。 diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs index 771f564..a10fd61 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryEvent.cs @@ -11,7 +11,7 @@ public sealed class DeliveryEvent : MultiTenantEntityBase /// /// 配送单标识。 /// - public Guid DeliveryOrderId { get; set; } + public long DeliveryOrderId { get; set; } /// /// 事件类型。 diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs index 31dbfc1..c547d6b 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Entities/DeliveryOrder.cs @@ -8,7 +8,7 @@ namespace TakeoutSaaS.Domain.Deliveries.Entities; /// public sealed class DeliveryOrder : MultiTenantEntityBase { - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 配送服务商。 diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs new file mode 100644 index 0000000..010793d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Deliveries.Entities; + +namespace TakeoutSaaS.Domain.Deliveries.Repositories; + +/// +/// 配送聚合仓储契约。 +/// +public interface IDeliveryRepository +{ + /// + /// 依据标识获取配送单。 + /// + Task FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 依据订单标识获取配送单。 + /// + Task FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取配送事件轨迹。 + /// + Task> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增配送单。 + /// + Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default); + + /// + /// 新增配送事件。 + /// + Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs index 4d47912..1a38aba 100644 --- a/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Entities/DictionaryItem.cs @@ -10,7 +10,7 @@ public sealed class DictionaryItem : MultiTenantEntityBase /// /// 关联分组 ID。 /// - public Guid GroupId { get; set; } + public long GroupId { get; set; } /// /// 字典项键。 diff --git a/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs index ee338f2..9a9b427 100644 --- a/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Dictionary/Repositories/IDictionaryRepository.cs @@ -14,7 +14,7 @@ public interface IDictionaryRepository /// /// 依据 ID 获取分组。 /// - Task FindGroupByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task FindGroupByIdAsync(long id, CancellationToken cancellationToken = default); /// /// 依据编码获取分组。 @@ -39,17 +39,17 @@ public interface IDictionaryRepository /// /// 依据 ID 获取字典项。 /// - Task FindItemByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task FindItemByIdAsync(long id, CancellationToken cancellationToken = default); /// /// 获取某分组下的所有字典项。 /// - Task> GetItemsByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default); + Task> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default); /// /// 按分组编码集合获取字典项(可包含系统参数)。 /// - Task> GetItemsByCodesAsync(IEnumerable codes, Guid tenantId, bool includeSystem, CancellationToken cancellationToken = default); + Task> GetItemsByCodesAsync(IEnumerable codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default); /// /// 新增字典项。 diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs index 0b1bb24..856f922 100644 --- a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliateOrder.cs @@ -11,17 +11,17 @@ public sealed class AffiliateOrder : MultiTenantEntityBase /// /// 推广人标识。 /// - public Guid AffiliatePartnerId { get; set; } + public long AffiliatePartnerId { get; set; } /// /// 关联订单。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 用户 ID。 /// - public Guid BuyerUserId { get; set; } + public long BuyerUserId { get; set; } /// /// 订单金额。 diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs index d9ceaa1..551e39d 100644 --- a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePartner.cs @@ -11,7 +11,7 @@ public sealed class AffiliatePartner : MultiTenantEntityBase /// /// 用户 ID(如绑定平台账号)。 /// - public Guid? UserId { get; set; } + public long? UserId { get; set; } /// /// 昵称或渠道名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs index 774c4e4..c9955f9 100644 --- a/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs +++ b/src/Domain/TakeoutSaaS.Domain/Distribution/Entities/AffiliatePayout.cs @@ -11,7 +11,7 @@ public sealed class AffiliatePayout : MultiTenantEntityBase /// /// 合作伙伴标识。 /// - public Guid AffiliatePartnerId { get; set; } + public long AffiliatePartnerId { get; set; } /// /// 结算周期描述。 diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs index 9f41172..011d048 100644 --- a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CheckInRecord.cs @@ -10,12 +10,12 @@ public sealed class CheckInRecord : MultiTenantEntityBase /// /// 活动标识。 /// - public Guid CheckInCampaignId { get; set; } + public long CheckInCampaignId { get; set; } /// /// 用户标识。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 签到日期(本地)。 diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs index 3888bfd..f158121 100644 --- a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityComment.cs @@ -10,12 +10,12 @@ public sealed class CommunityComment : MultiTenantEntityBase /// /// 动态标识。 /// - public Guid PostId { get; set; } + public long PostId { get; set; } /// /// 评论人。 /// - public Guid AuthorUserId { get; set; } + public long AuthorUserId { get; set; } /// /// 评论内容。 @@ -25,7 +25,7 @@ public sealed class CommunityComment : MultiTenantEntityBase /// /// 父级评论 ID。 /// - public Guid? ParentId { get; set; } + public long? ParentId { get; set; } /// /// 状态。 diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs index 73c7e21..6b77d38 100644 --- a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityPost.cs @@ -11,7 +11,7 @@ public sealed class CommunityPost : MultiTenantEntityBase /// /// 作者用户 ID。 /// - public Guid AuthorUserId { get; set; } + public long AuthorUserId { get; set; } /// /// 标题。 diff --git a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs index b7d5c19..4d1dbdc 100644 --- a/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs +++ b/src/Domain/TakeoutSaaS.Domain/Engagement/Entities/CommunityReaction.cs @@ -11,12 +11,12 @@ public sealed class CommunityReaction : MultiTenantEntityBase /// /// 动态 ID。 /// - public Guid PostId { get; set; } + public long PostId { get; set; } /// /// 用户 ID。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 反应类型。 diff --git a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs index e5412de..86d31e3 100644 --- a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs +++ b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupOrder.cs @@ -11,12 +11,12 @@ public sealed class GroupOrder : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 关联商品或套餐。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// 拼单编号。 @@ -26,7 +26,7 @@ public sealed class GroupOrder : MultiTenantEntityBase /// /// 团长用户 ID。 /// - public Guid LeaderUserId { get; set; } + public long LeaderUserId { get; set; } /// /// 成团需要的人数。 diff --git a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs index 61e169a..943ab14 100644 --- a/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs +++ b/src/Domain/TakeoutSaaS.Domain/GroupBuying/Entities/GroupParticipant.cs @@ -11,17 +11,17 @@ public sealed class GroupParticipant : MultiTenantEntityBase /// /// 拼单活动标识。 /// - public Guid GroupOrderId { get; set; } + public long GroupOrderId { get; set; } /// /// 对应订单标识。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 用户标识。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 参与状态。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs index 6ccb304..6080712 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -25,7 +25,7 @@ public sealed class IdentityUser : MultiTenantEntityBase /// /// 所属商户(平台管理员为空)。 /// - public Guid? MerchantId { get; set; } + public long? MerchantId { get; set; } /// /// 角色集合。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index 2d5eae1..cc74343 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -18,5 +18,5 @@ public interface IIdentityUserRepository /// /// 根据 ID 获取后台用户。 /// - Task FindByIdAsync(Guid userId, CancellationToken cancellationToken = default); + Task FindByIdAsync(long userId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs index 4688ea6..41594c2 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMiniUserRepository.cs @@ -21,7 +21,7 @@ public interface IMiniUserRepository /// 用户 ID /// 取消令牌 /// 小程序用户,如果不存在则返回 null - Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task FindByIdAsync(long id, CancellationToken cancellationToken = default); /// /// 创建或更新小程序用户(如果 OpenId 已存在则更新,否则创建)。 @@ -33,5 +33,5 @@ public interface IMiniUserRepository /// 租户 ID /// 取消令牌 /// 创建或更新后的小程序用户 - Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default); + Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs index 36b7d59..ef145fd 100644 --- a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryAdjustment.cs @@ -11,7 +11,7 @@ public sealed class InventoryAdjustment : MultiTenantEntityBase /// /// 对应的库存记录标识。 /// - public Guid InventoryItemId { get; set; } + public long InventoryItemId { get; set; } /// /// 调整类型。 @@ -31,7 +31,7 @@ public sealed class InventoryAdjustment : MultiTenantEntityBase /// /// 操作人标识。 /// - public Guid? OperatorId { get; set; } + public long? OperatorId { get; set; } /// /// 发生时间。 diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs index 8a764b3..eee47f4 100644 --- a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryBatch.cs @@ -10,12 +10,12 @@ public sealed class InventoryBatch : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// SKU 标识。 /// - public Guid ProductSkuId { get; set; } + public long ProductSkuId { get; set; } /// /// 批次编号。 diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs index 6432253..6aca234 100644 --- a/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Entities/InventoryItem.cs @@ -10,12 +10,12 @@ public sealed class InventoryItem : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// SKU 标识。 /// - public Guid ProductSkuId { get; set; } + public long ProductSkuId { get; set; } /// /// 批次编号,可为空表示混批。 diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs index b06f23f..b65003d 100644 --- a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberGrowthLog.cs @@ -10,7 +10,7 @@ public sealed class MemberGrowthLog : MultiTenantEntityBase /// /// 会员标识。 /// - public Guid MemberId { get; set; } + public long MemberId { get; set; } /// /// 变动数量。 diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs index 07c4fbe..3e4dd07 100644 --- a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointLedger.cs @@ -11,7 +11,7 @@ public sealed class MemberPointLedger : MultiTenantEntityBase /// /// 会员标识。 /// - public Guid MemberId { get; set; } + public long MemberId { get; set; } /// /// 变动数量,可为负值。 @@ -31,7 +31,7 @@ public sealed class MemberPointLedger : MultiTenantEntityBase /// /// 来源 ID(订单、活动等)。 /// - public Guid? SourceId { get; set; } + public long? SourceId { get; set; } /// /// 发生时间。 diff --git a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs index 7378ccd..32df115 100644 --- a/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs +++ b/src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfile.cs @@ -11,7 +11,7 @@ public sealed class MemberProfile : MultiTenantEntityBase /// /// 用户标识。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 手机号。 @@ -31,7 +31,7 @@ public sealed class MemberProfile : MultiTenantEntityBase /// /// 当前会员等级 ID。 /// - public Guid? MemberTierId { get; set; } + public long? MemberTierId { get; set; } /// /// 会员状态。 diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs index e8a21bb..cf39793 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantContract.cs @@ -11,7 +11,7 @@ public sealed class MerchantContract : MultiTenantEntityBase /// /// 所属商户标识。 /// - public Guid MerchantId { get; set; } + public long MerchantId { get; set; } /// /// 合同编号。 diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs index 572f718..433e2df 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantDocument.cs @@ -11,7 +11,7 @@ public sealed class MerchantDocument : MultiTenantEntityBase /// /// 所属商户标识。 /// - public Guid MerchantId { get; set; } + public long MerchantId { get; set; } /// /// 证照类型。 diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs index 273054a..50026e5 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantStaff.cs @@ -11,12 +11,12 @@ public sealed class MerchantStaff : MultiTenantEntityBase /// /// 所属商户标识。 /// - public Guid MerchantId { get; set; } + public long MerchantId { get; set; } /// /// 可选的关联门店 ID。 /// - public Guid? StoreId { get; set; } + public long? StoreId { get; set; } /// /// 员工姓名。 @@ -36,7 +36,7 @@ public sealed class MerchantStaff : MultiTenantEntityBase /// /// 登录账号 ID(指向统一身份体系)。 /// - public Guid? IdentityUserId { get; set; } + public long? IdentityUserId { get; set; } /// /// 员工角色类型。 diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs new file mode 100644 index 0000000..a735ff7 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Merchants.Entities; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Domain.Merchants.Repositories; + +/// +/// 商户聚合仓储契约,提供基础 CRUD 与查询能力。 +/// +public interface IMerchantRepository +{ + /// + /// 依据标识获取商户。 + /// + Task FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 按状态筛选商户列表。 + /// + Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default); + + /// + /// 获取指定商户的员工列表。 + /// + Task> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取指定商户的合同列表。 + /// + Task> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取指定商户的资质文件列表。 + /// + Task> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增商户主体。 + /// + Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default); + + /// + /// 新增商户员工。 + /// + Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default); + + /// + /// 新增商户合同。 + /// + Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default); + + /// + /// 新增商户资质文件。 + /// + Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs index 74d9e14..54524f9 100644 --- a/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs +++ b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/MapLocation.cs @@ -10,7 +10,7 @@ public sealed class MapLocation : MultiTenantEntityBase /// /// 关联门店 ID,可空表示独立 POI。 /// - public Guid? StoreId { get; set; } + public long? StoreId { get; set; } /// /// 名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs index 284f1a5..f1bdf4b 100644 --- a/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs +++ b/src/Domain/TakeoutSaaS.Domain/Navigation/Entities/NavigationRequest.cs @@ -11,12 +11,12 @@ public sealed class NavigationRequest : MultiTenantEntityBase /// /// 用户 ID。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 门店 ID。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 来源通道(小程序、H5 等)。 diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs index d19d659..655e308 100644 --- a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItem.cs @@ -11,17 +11,17 @@ public sealed class CartItem : MultiTenantEntityBase /// /// 所属购物车标识。 /// - public Guid ShoppingCartId { get; set; } + public long ShoppingCartId { get; set; } /// /// 商品或 SKU 标识。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// SKU 标识。 /// - public Guid? ProductSkuId { get; set; } + public long? ProductSkuId { get; set; } /// /// 商品名称快照。 diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs index eb99a0d..867c098 100644 --- a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CartItemAddon.cs @@ -10,7 +10,7 @@ public sealed class CartItemAddon : MultiTenantEntityBase /// /// 所属购物车条目。 /// - public Guid CartItemId { get; set; } + public long CartItemId { get; set; } /// /// 选项名称。 @@ -25,5 +25,5 @@ public sealed class CartItemAddon : MultiTenantEntityBase /// /// 选项 ID(可对应 ProductAddonOption)。 /// - public Guid? OptionId { get; set; } + public long? OptionId { get; set; } } diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs index a3beb7a..314aaa2 100644 --- a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/CheckoutSession.cs @@ -11,12 +11,12 @@ public sealed class CheckoutSession : MultiTenantEntityBase /// /// 用户标识。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 会话 Token。 diff --git a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs index 7928c1e..6c7e6ff 100644 --- a/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs +++ b/src/Domain/TakeoutSaaS.Domain/Ordering/Entities/ShoppingCart.cs @@ -11,12 +11,12 @@ public sealed class ShoppingCart : MultiTenantEntityBase /// /// 用户标识。 /// - public Guid UserId { get; set; } + public long UserId { get; set; } /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 购物车状态,包含正常/锁定。 diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs index 9a1c295..398f8f6 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs @@ -17,7 +17,7 @@ public sealed class Order : MultiTenantEntityBase /// /// 门店。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 下单渠道。 @@ -62,7 +62,7 @@ public sealed class Order : MultiTenantEntityBase /// /// 预约 ID。 /// - public Guid? ReservationId { get; set; } + public long? ReservationId { get; set; } /// /// 商品总额。 diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs index 83681ca..63c5a67 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs @@ -10,12 +10,12 @@ public sealed class OrderItem : MultiTenantEntityBase /// /// 订单 ID。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 商品 ID。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// 商品名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs index 26823f2..c9b7a55 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderStatusHistory.cs @@ -11,7 +11,7 @@ public sealed class OrderStatusHistory : MultiTenantEntityBase /// /// 订单标识。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 变更后的状态。 @@ -21,7 +21,7 @@ public sealed class OrderStatusHistory : MultiTenantEntityBase /// /// 操作人标识(可为空表示系统)。 /// - public Guid? OperatorId { get; set; } + public long? OperatorId { get; set; } /// /// 备注信息。 diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs index d18d4ab..6b333e9 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Entities/RefundRequest.cs @@ -11,7 +11,7 @@ public sealed class RefundRequest : MultiTenantEntityBase /// /// 关联订单标识。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 退款单号。 diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs new file mode 100644 index 0000000..baba30b --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Domain.Orders.Repositories; + +/// +/// 订单聚合仓储契约。 +/// +public interface IOrderRepository +{ + /// + /// 依据标识获取订单。 + /// + Task FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 依据订单号获取订单。 + /// + Task FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 按状态筛选订单列表。 + /// + Task> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default); + + /// + /// 获取订单明细行。 + /// + Task> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取订单状态流转记录。 + /// + Task> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取订单退款申请。 + /// + Task> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增订单。 + /// + Task AddOrderAsync(Order order, CancellationToken cancellationToken = default); + + /// + /// 新增订单明细。 + /// + Task AddItemsAsync(IEnumerable items, CancellationToken cancellationToken = default); + + /// + /// 新增订单状态记录。 + /// + Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default); + + /// + /// 新增退款申请。 + /// + Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs index 2d0923f..7ba0380 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRecord.cs @@ -11,7 +11,7 @@ public sealed class PaymentRecord : MultiTenantEntityBase /// /// 关联订单。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 支付方式。 diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs index 9e91973..6fa657b 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Entities/PaymentRefundRecord.cs @@ -11,12 +11,12 @@ public sealed class PaymentRefundRecord : MultiTenantEntityBase /// /// 原支付记录标识。 /// - public Guid PaymentRecordId { get; set; } + public long PaymentRecordId { get; set; } /// /// 关联订单标识。 /// - public Guid OrderId { get; set; } + public long OrderId { get; set; } /// /// 退款金额。 diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs new file mode 100644 index 0000000..c217ce2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Payments.Entities; + +namespace TakeoutSaaS.Domain.Payments.Repositories; + +/// +/// 支付记录仓储契约。 +/// +public interface IPaymentRepository +{ + /// + /// 依据标识获取支付记录。 + /// + Task FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 依据订单标识获取支付记录。 + /// + Task FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取支付对应的退款记录。 + /// + Task> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增支付记录。 + /// + Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default); + + /// + /// 新增退款记录。 + /// + Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs index c9617e2..953a322 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/Product.cs @@ -11,12 +11,12 @@ public sealed class Product : MultiTenantEntityBase /// /// 所属门店。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 所属分类。 /// - public Guid CategoryId { get; set; } + public long CategoryId { get; set; } /// /// 商品编码。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs index dfd8dd5..605005a 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonGroup.cs @@ -11,7 +11,7 @@ public sealed class ProductAddonGroup : MultiTenantEntityBase /// /// 所属商品。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// 分组名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs index 3d27185..3ce3047 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAddonOption.cs @@ -10,7 +10,7 @@ public sealed class ProductAddonOption : MultiTenantEntityBase /// /// 所属加料分组。 /// - public Guid AddonGroupId { get; set; } + public long AddonGroupId { get; set; } /// /// 选项名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs index 62a9af1..f9b3269 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeGroup.cs @@ -11,7 +11,12 @@ public sealed class ProductAttributeGroup : MultiTenantEntityBase /// /// 关联门店,可为空表示所有门店共享。 /// - public Guid? StoreId { get; set; } + public long? StoreId { get; set; } + + /// + /// 所属商品标识。 + /// + public long ProductId { get; set; } /// /// 分组名称,例如“辣度”“份量”。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs index 80332d3..1ae3c73 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductAttributeOption.cs @@ -10,7 +10,7 @@ public sealed class ProductAttributeOption : MultiTenantEntityBase /// /// 所属规格组。 /// - public Guid AttributeGroupId { get; set; } + public long AttributeGroupId { get; set; } /// /// 选项名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs index 4d055a2..ab2afb2 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductCategory.cs @@ -10,7 +10,7 @@ public sealed class ProductCategory : MultiTenantEntityBase /// /// 所属门店。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 分类名称。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs index c07ef44..a963b26 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductMediaAsset.cs @@ -11,7 +11,7 @@ public sealed class ProductMediaAsset : MultiTenantEntityBase /// /// 商品标识。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// 媒体类型。 diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs index bace92a..a92c0fe 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductPricingRule.cs @@ -11,7 +11,7 @@ public sealed class ProductPricingRule : MultiTenantEntityBase /// /// 所属商品。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// 策略类型。 @@ -42,4 +42,9 @@ public sealed class ProductPricingRule : MultiTenantEntityBase /// 生效星期(JSON 数组)。 /// public string? WeekdaysJson { get; set; } + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; } diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs index 1c1e6ce..f20fc17 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Entities/ProductSku.cs @@ -10,7 +10,7 @@ public sealed class ProductSku : MultiTenantEntityBase /// /// 所属商品标识。 /// - public Guid ProductId { get; set; } + public long ProductId { get; set; } /// /// SKU 编码。 @@ -46,4 +46,9 @@ public sealed class ProductSku : MultiTenantEntityBase /// 规格属性 JSON(记录选项 ID)。 /// public string AttributesJson { get; set; } = string.Empty; + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; } diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs b/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs new file mode 100644 index 0000000..093ee81 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Domain.Products.Repositories; + +/// +/// 商品聚合仓储契约。 +/// +public interface IProductRepository +{ + /// + /// 依据标识获取商品。 + /// + Task FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 按分类与状态筛选商品列表。 + /// + Task> SearchAsync(long tenantId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default); + + /// + /// 获取租户下的商品分类。 + /// + Task> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品 SKU。 + /// + Task> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品加料组与选项。 + /// + Task> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品加料选项。 + /// + Task> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品规格组与选项。 + /// + Task> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品规格选项。 + /// + Task> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品媒资。 + /// + Task> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取商品定价规则。 + /// + Task> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增分类。 + /// + Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default); + + /// + /// 新增商品。 + /// + Task AddProductAsync(Product product, CancellationToken cancellationToken = default); + + /// + /// 新增 SKU。 + /// + Task AddSkusAsync(IEnumerable skus, CancellationToken cancellationToken = default); + + /// + /// 新增加料组与选项。 + /// + Task AddAddonGroupsAsync(IEnumerable groups, IEnumerable options, CancellationToken cancellationToken = default); + + /// + /// 新增规格组与选项。 + /// + Task AddAttributeGroupsAsync(IEnumerable groups, IEnumerable options, CancellationToken cancellationToken = default); + + /// + /// 新增媒资。 + /// + Task AddMediaAssetsAsync(IEnumerable assets, CancellationToken cancellationToken = default); + + /// + /// 新增定价规则。 + /// + Task AddPricingRulesAsync(IEnumerable rules, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs b/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs index f09c94e..e235909 100644 --- a/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs +++ b/src/Domain/TakeoutSaaS.Domain/Queues/Entities/QueueTicket.cs @@ -8,7 +8,7 @@ namespace TakeoutSaaS.Domain.Queues.Entities; /// public sealed class QueueTicket : MultiTenantEntityBase { - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 排队编号。 diff --git a/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs b/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs index 3214ee3..1e846e8 100644 --- a/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs +++ b/src/Domain/TakeoutSaaS.Domain/Reservations/Entities/Reservation.cs @@ -11,7 +11,7 @@ public sealed class Reservation : MultiTenantEntityBase /// /// 门店。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 预约号。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs index 5c82a91..f3c8e44 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs @@ -11,7 +11,7 @@ public sealed class Store : MultiTenantEntityBase /// /// 所属商户标识。 /// - public Guid MerchantId { get; set; } + public long MerchantId { get; set; } /// /// 门店编码,便于扫码及外部对接。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs index a85b0d8..083a46f 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreBusinessHour.cs @@ -11,7 +11,7 @@ public sealed class StoreBusinessHour : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 星期几,0 表示周日。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs index e067e1f..9e72e51 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs @@ -10,7 +10,7 @@ public sealed class StoreDeliveryZone : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 区域名称。 @@ -36,4 +36,9 @@ public sealed class StoreDeliveryZone : MultiTenantEntityBase /// 预计送达分钟。 /// public int? EstimatedMinutes { get; set; } + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; } diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs index a1bc39a..a1f0b39 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreEmployeeShift.cs @@ -11,12 +11,12 @@ public sealed class StoreEmployeeShift : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 员工标识。 /// - public Guid StaffId { get; set; } + public long StaffId { get; set; } /// /// 班次日期。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs index c464849..e10edce 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreHoliday.cs @@ -10,7 +10,7 @@ public sealed class StoreHoliday : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 日期。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs index 1b55549..97dc86b 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTable.cs @@ -11,12 +11,12 @@ public sealed class StoreTable : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 所在区域 ID。 /// - public Guid? AreaId { get; set; } + public long? AreaId { get; set; } /// /// 桌码。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs index 6255266..0fb6fb6 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreTableArea.cs @@ -10,7 +10,7 @@ public sealed class StoreTableArea : MultiTenantEntityBase /// /// 门店标识。 /// - public Guid StoreId { get; set; } + public long StoreId { get; set; } /// /// 区域名称。 @@ -21,4 +21,9 @@ public sealed class StoreTableArea : MultiTenantEntityBase /// 区域描述。 /// public string? Description { get; set; } + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; } diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs new file mode 100644 index 0000000..dccde79 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Domain.Stores.Repositories; + +/// +/// 门店聚合仓储契约。 +/// +public interface IStoreRepository +{ + /// + /// 依据标识获取门店。 + /// + Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 按租户筛选门店列表。 + /// + Task> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default); + + /// + /// 获取门店营业时段。 + /// + Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取门店配送区域配置。 + /// + Task> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取门店节假日配置。 + /// + Task> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取门店桌台区域。 + /// + Task> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取门店桌台列表。 + /// + Task> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 获取门店员工排班。 + /// + Task> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增门店。 + /// + Task AddStoreAsync(Store store, CancellationToken cancellationToken = default); + + /// + /// 新增营业时段。 + /// + Task AddBusinessHoursAsync(IEnumerable hours, CancellationToken cancellationToken = default); + + /// + /// 新增配送区域。 + /// + Task AddDeliveryZonesAsync(IEnumerable zones, CancellationToken cancellationToken = default); + + /// + /// 新增节假日配置。 + /// + Task AddHolidaysAsync(IEnumerable holidays, CancellationToken cancellationToken = default); + + /// + /// 新增桌台区域。 + /// + Task AddTableAreasAsync(IEnumerable areas, CancellationToken cancellationToken = default); + + /// + /// 新增桌台。 + /// + Task AddTablesAsync(IEnumerable tables, CancellationToken cancellationToken = default); + + /// + /// 新增排班。 + /// + Task AddShiftsAsync(IEnumerable shifts, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs index fd65958..b2823e3 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs @@ -86,7 +86,7 @@ public sealed class Tenant : AuditableEntityBase /// /// 系统内对应的租户所有者账号 ID。 /// - public Guid? PrimaryOwnerUserId { get; set; } + public long? PrimaryOwnerUserId { get; set; } /// /// 租户当前状态,涵盖审核、启用、停用等场景。 diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs index a54fce2..3090be2 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantSubscription.cs @@ -11,7 +11,7 @@ public sealed class TenantSubscription : MultiTenantEntityBase /// /// 当前订阅关联的套餐标识。 /// - public Guid TenantPackageId { get; set; } + public long TenantPackageId { get; set; } /// /// 订阅生效时间(UTC)。 @@ -41,7 +41,7 @@ public sealed class TenantSubscription : MultiTenantEntityBase /// /// 若已排期升降配,对应的新套餐 ID。 /// - public Guid? ScheduledPackageId { get; set; } + public long? ScheduledPackageId { get; set; } /// /// 运营备注信息。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs new file mode 100644 index 0000000..02df728 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Infrastructure.App.Options; +using TakeoutSaaS.Infrastructure.App.Persistence; +using TakeoutSaaS.Infrastructure.App.Repositories; +using TakeoutSaaS.Infrastructure.Common.Extensions; +using TakeoutSaaS.Shared.Abstractions.Constants; + +namespace TakeoutSaaS.Infrastructure.App.Extensions; + +/// +/// 业务主库基础设施注册扩展。 +/// +public static class AppServiceCollectionExtensions +{ + /// + /// 注册业务主库 DbContext 与仓储。 + /// + /// 服务集合。 + /// 配置源。 + /// 服务集合。 + public static IServiceCollection AddAppInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + services.AddDatabaseInfrastructure(configuration); + services.AddPostgresDbContext(DatabaseConstants.AppDataSource); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddOptions() + .Bind(configuration.GetSection(AppSeedOptions.SectionName)) + .ValidateDataAnnotations(); + + services.AddHostedService(); + + return services; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs deleted file mode 100644 index 56454dc..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.Designer.cs +++ /dev/null @@ -1,949 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TakeoutSaaS.Infrastructure.App.Persistence; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.App.Migrations -{ - [DbContext(typeof(TakeoutAppDbContext))] - [Migration("20251201044927_InitialApp")] - partial class InitialApp - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CourierName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CourierPhone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeliveryFee") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("DispatchedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FailureReason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("PickedUpAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Provider") - .HasColumnType("integer"); - - b.Property("ProviderOrderId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderId") - .IsUnique(); - - b.ToTable("delivery_orders", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Address") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BrandAlias") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("BrandName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("BusinessLicenseNumber") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("City") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ContactEmail") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ContactPhone") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("District") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LegalPerson") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OnboardedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Province") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ReviewRemarks") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.ToTable("merchants", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CancelReason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Channel") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CustomerName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CustomerPhone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryType") - .HasColumnType("integer"); - - b.Property("DiscountAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("FinishedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ItemsAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("OrderNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("PaidAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PayableAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("PaymentStatus") - .HasColumnType("integer"); - - b.Property("QueueNumber") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Remark") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ReservationId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TableNo") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderNo") - .IsUnique(); - - b.HasIndex("TenantId", "StoreId", "Status"); - - b.ToTable("orders", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AttributesJson") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DiscountAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("ProductName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("SkuName") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("SubTotal") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("Unit") - .HasMaxLength(16) - .HasColumnType("character varying(16)"); - - b.Property("UnitPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("OrderId"); - - b.HasIndex("TenantId", "OrderId"); - - b.ToTable("order_items", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ChannelTransactionId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Method") - .HasColumnType("integer"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Payload") - .HasColumnType("text"); - - b.Property("Remark") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TradeNo") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderId"); - - b.ToTable("payment_records", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CategoryId") - .HasColumnType("uuid"); - - b.Property("CoverImage") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("EnableDelivery") - .HasColumnType("boolean"); - - b.Property("EnableDineIn") - .HasColumnType("boolean"); - - b.Property("EnablePickup") - .HasColumnType("boolean"); - - b.Property("GalleryImages") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("IsFeatured") - .HasColumnType("boolean"); - - b.Property("MaxQuantityPerOrder") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("OriginalPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("SpuCode") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StockQuantity") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("Subtitle") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("Unit") - .HasMaxLength(16) - .HasColumnType("character varying(16)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "SpuCode") - .IsUnique(); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("products", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IsEnabled") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("product_categories", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CalledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EstimatedWaitMinutes") - .HasColumnType("integer"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PartySize") - .HasColumnType("integer"); - - b.Property("Remark") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TicketNumber") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId"); - - b.HasIndex("TenantId", "StoreId", "TicketNumber") - .IsUnique(); - - b.ToTable("queue_tickets", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CheckInCode") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CheckedInAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CustomerName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CustomerPhone") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("PeopleCount") - .HasColumnType("integer"); - - b.Property("Remark") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ReservationNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("ReservationTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TablePreference") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "ReservationNo") - .IsUnique(); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("reservations", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Address") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Announcement") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("BusinessHours") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("City") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryRadiusKm") - .HasPrecision(6, 2) - .HasColumnType("numeric(6,2)"); - - b.Property("District") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Latitude") - .HasColumnType("double precision"); - - b.Property("Longitude") - .HasColumnType("double precision"); - - b.Property("ManagerName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MerchantId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Phone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Province") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("QueueEnabled") - .HasColumnType("boolean"); - - b.Property("ReservationEnabled") - .HasColumnType("boolean"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("SupportsDelivery") - .HasColumnType("boolean"); - - b.Property("SupportsDineIn") - .HasColumnType("boolean"); - - b.Property("SupportsPickup") - .HasColumnType("boolean"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MerchantId"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.HasIndex("TenantId", "MerchantId"); - - b.ToTable("stores", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ContactEmail") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ContactName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ContactPhone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EffectiveFrom") - .HasColumnType("timestamp with time zone"); - - b.Property("EffectiveTo") - .HasColumnType("timestamp with time zone"); - - b.Property("Industry") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LogoUrl") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Remarks") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ShortName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("Code") - .IsUnique(); - - b.ToTable("tenants", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => - { - b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) - .WithMany() - .HasForeignKey("OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => - { - b.HasOne("TakeoutSaaS.Domain.Merchants.Entities.Merchant", "Merchant") - .WithMany() - .HasForeignKey("MerchantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Merchant"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs deleted file mode 100644 index f322c41..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201044927_InitialApp.cs +++ /dev/null @@ -1,497 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.App.Migrations -{ - /// - public partial class InitialApp : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "delivery_orders", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Provider = table.Column(type: "integer", nullable: false), - ProviderOrderId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Status = table.Column(type: "integer", nullable: false), - DeliveryFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - CourierName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - CourierPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - DispatchedAt = table.Column(type: "timestamp with time zone", nullable: true), - PickedUpAt = table.Column(type: "timestamp with time zone", nullable: true), - DeliveredAt = table.Column(type: "timestamp with time zone", nullable: true), - FailureReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_delivery_orders", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "merchants", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - BrandName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - BrandAlias = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - LegalPerson = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - BusinessLicenseNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - Province = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - City = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - District = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Status = table.Column(type: "integer", nullable: false), - ReviewRemarks = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - OnboardedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_merchants", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "orders", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - Channel = table.Column(type: "integer", nullable: false), - DeliveryType = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - PaymentStatus = table.Column(type: "integer", nullable: false), - CustomerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - CustomerPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - TableNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - QueueNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - ReservationId = table.Column(type: "uuid", nullable: true), - ItemsAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - PayableAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - PaidAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - PaidAt = table.Column(type: "timestamp with time zone", nullable: true), - FinishedAt = table.Column(type: "timestamp with time zone", nullable: true), - CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), - CancelReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Remark = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_orders", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "payment_records", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Method = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - TradeNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - ChannelTransactionId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - PaidAt = table.Column(type: "timestamp with time zone", nullable: true), - Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Payload = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_payment_records", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_categories", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - SortOrder = table.Column(type: "integer", nullable: false), - IsEnabled = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_categories", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "products", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - CategoryId = table.Column(type: "uuid", nullable: false), - SpuCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Subtitle = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Unit = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), - Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - StockQuantity = table.Column(type: "integer", nullable: true), - MaxQuantityPerOrder = table.Column(type: "integer", nullable: true), - Status = table.Column(type: "integer", nullable: false), - CoverImage = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - GalleryImages = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), - Description = table.Column(type: "text", nullable: true), - EnableDineIn = table.Column(type: "boolean", nullable: false), - EnablePickup = table.Column(type: "boolean", nullable: false), - EnableDelivery = table.Column(type: "boolean", nullable: false), - IsFeatured = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_products", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "queue_tickets", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - TicketNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - PartySize = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - EstimatedWaitMinutes = table.Column(type: "integer", nullable: true), - CalledAt = table.Column(type: "timestamp with time zone", nullable: true), - ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), - CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), - Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_queue_tickets", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "reservations", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - ReservationNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - CustomerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - CustomerPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - PeopleCount = table.Column(type: "integer", nullable: false), - ReservationTime = table.Column(type: "timestamp with time zone", nullable: false), - Status = table.Column(type: "integer", nullable: false), - TablePreference = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Remark = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CheckInCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - CheckedInAt = table.Column(type: "timestamp with time zone", nullable: true), - CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_reservations", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "tenants", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - ShortName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - ContactName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - Industry = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - LogoUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: true), - EffectiveTo = table.Column(type: "timestamp with time zone", nullable: true), - Status = table.Column(type: "integer", nullable: false), - Remarks = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_tenants", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "stores", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MerchantId = table.Column(type: "uuid", nullable: false), - Code = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - ManagerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Status = table.Column(type: "integer", nullable: false), - Province = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - City = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - District = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Longitude = table.Column(type: "double precision", nullable: true), - Latitude = table.Column(type: "double precision", nullable: true), - BusinessHours = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - SupportsDineIn = table.Column(type: "boolean", nullable: false), - SupportsPickup = table.Column(type: "boolean", nullable: false), - SupportsDelivery = table.Column(type: "boolean", nullable: false), - DeliveryRadiusKm = table.Column(type: "numeric(6,2)", precision: 6, scale: 2, nullable: false), - QueueEnabled = table.Column(type: "boolean", nullable: false), - ReservationEnabled = table.Column(type: "boolean", nullable: false), - Announcement = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_stores", x => x.Id); - table.ForeignKey( - name: "FK_stores_merchants_MerchantId", - column: x => x.MerchantId, - principalTable: "merchants", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "order_items", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - SkuName = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - Unit = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), - Quantity = table.Column(type: "integer", nullable: false), - UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - SubTotal = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - AttributesJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_order_items", x => x.Id); - table.ForeignKey( - name: "FK_order_items_orders_OrderId", - column: x => x.OrderId, - principalTable: "orders", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_delivery_orders_TenantId_OrderId", - table: "delivery_orders", - columns: new[] { "TenantId", "OrderId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_merchants_TenantId", - table: "merchants", - column: "TenantId"); - - migrationBuilder.CreateIndex( - name: "IX_order_items_OrderId", - table: "order_items", - column: "OrderId"); - - migrationBuilder.CreateIndex( - name: "IX_order_items_TenantId_OrderId", - table: "order_items", - columns: new[] { "TenantId", "OrderId" }); - - migrationBuilder.CreateIndex( - name: "IX_orders_TenantId_OrderNo", - table: "orders", - columns: new[] { "TenantId", "OrderNo" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_orders_TenantId_StoreId_Status", - table: "orders", - columns: new[] { "TenantId", "StoreId", "Status" }); - - migrationBuilder.CreateIndex( - name: "IX_payment_records_TenantId_OrderId", - table: "payment_records", - columns: new[] { "TenantId", "OrderId" }); - - migrationBuilder.CreateIndex( - name: "IX_product_categories_TenantId_StoreId", - table: "product_categories", - columns: new[] { "TenantId", "StoreId" }); - - migrationBuilder.CreateIndex( - name: "IX_products_TenantId_SpuCode", - table: "products", - columns: new[] { "TenantId", "SpuCode" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_products_TenantId_StoreId", - table: "products", - columns: new[] { "TenantId", "StoreId" }); - - migrationBuilder.CreateIndex( - name: "IX_queue_tickets_TenantId_StoreId", - table: "queue_tickets", - columns: new[] { "TenantId", "StoreId" }); - - migrationBuilder.CreateIndex( - name: "IX_queue_tickets_TenantId_StoreId_TicketNumber", - table: "queue_tickets", - columns: new[] { "TenantId", "StoreId", "TicketNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_reservations_TenantId_ReservationNo", - table: "reservations", - columns: new[] { "TenantId", "ReservationNo" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_reservations_TenantId_StoreId", - table: "reservations", - columns: new[] { "TenantId", "StoreId" }); - - migrationBuilder.CreateIndex( - name: "IX_stores_MerchantId", - table: "stores", - column: "MerchantId"); - - migrationBuilder.CreateIndex( - name: "IX_stores_TenantId_Code", - table: "stores", - columns: new[] { "TenantId", "Code" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_stores_TenantId_MerchantId", - table: "stores", - columns: new[] { "TenantId", "MerchantId" }); - - migrationBuilder.CreateIndex( - name: "IX_tenants_Code", - table: "tenants", - column: "Code", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "delivery_orders"); - - migrationBuilder.DropTable( - name: "order_items"); - - migrationBuilder.DropTable( - name: "payment_records"); - - migrationBuilder.DropTable( - name: "product_categories"); - - migrationBuilder.DropTable( - name: "products"); - - migrationBuilder.DropTable( - name: "queue_tickets"); - - migrationBuilder.DropTable( - name: "reservations"); - - migrationBuilder.DropTable( - name: "stores"); - - migrationBuilder.DropTable( - name: "tenants"); - - migrationBuilder.DropTable( - name: "orders"); - - migrationBuilder.DropTable( - name: "merchants"); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs deleted file mode 100644 index c901204..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.Designer.cs +++ /dev/null @@ -1,4330 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TakeoutSaaS.Infrastructure.App.Persistence; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.App.Migrations -{ - [DbContext(typeof(TakeoutAppDbContext))] - [Migration("20251201055852_ExpandDomainSchema")] - partial class ExpandDomainSchema - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConditionJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("MetricDefinitionId") - .HasColumnType("uuid"); - - b.Property("NotificationChannels") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Severity") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MetricDefinitionId", "Severity"); - - b.ToTable("metric_alert_rules", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DefaultAggregation") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("DimensionsJson") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("metric_definitions", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DimensionKey") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("MetricDefinitionId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("Value") - .HasPrecision(18, 4) - .HasColumnType("numeric(18,4)"); - - b.Property("WindowEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("WindowStart") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd") - .IsUnique(); - - b.ToTable("metric_snapshots", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CouponTemplateId") - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExpireAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IssuedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UsedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("coupons", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AllowStack") - .HasColumnType("boolean"); - - b.Property("ChannelsJson") - .HasColumnType("text"); - - b.Property("ClaimedQuantity") - .HasColumnType("integer"); - - b.Property("CouponType") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("DiscountCap") - .HasColumnType("numeric"); - - b.Property("MinimumSpend") - .HasColumnType("numeric"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ProductScopeJson") - .HasColumnType("text"); - - b.Property("RelativeValidDays") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreScopeJson") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TotalQuantity") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("ValidFrom") - .HasColumnType("timestamp with time zone"); - - b.Property("ValidTo") - .HasColumnType("timestamp with time zone"); - - b.Property("Value") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.ToTable("coupon_templates", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudienceDescription") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("BannerUrl") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Budget") - .HasColumnType("numeric"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("PromotionType") - .HasColumnType("integer"); - - b.Property("RulesJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("promotion_campaigns", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatSessionId") - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("ContentType") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsRead") - .HasColumnType("boolean"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SenderType") - .HasColumnType("integer"); - - b.Property("SenderUserId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "ChatSessionId", "CreatedAt"); - - b.ToTable("chat_messages", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AgentUserId") - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CustomerUserId") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsBotActive") - .HasColumnType("boolean"); - - b.Property("SessionCode") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("StartedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "SessionCode") - .IsUnique(); - - b.ToTable("chat_sessions", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AssignedAgentId") - .HasColumnType("uuid"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CustomerUserId") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Subject") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TicketNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "TicketNo") - .IsUnique(); - - b.ToTable("support_tickets", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AttachmentsJson") - .HasColumnType("text"); - - b.Property("AuthorUserId") - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsInternal") - .HasColumnType("boolean"); - - b.Property("SupportTicketId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "SupportTicketId"); - - b.ToTable("ticket_comments", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryOrderId") - .HasColumnType("uuid"); - - b.Property("EventType") - .HasColumnType("integer"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OccurredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Payload") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "DeliveryOrderId", "EventType"); - - b.ToTable("delivery_events", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CourierName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CourierPhone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeliveryFee") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("DispatchedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FailureReason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("PickedUpAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Provider") - .HasColumnType("integer"); - - b.Property("ProviderOrderId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderId") - .IsUnique(); - - b.ToTable("delivery_orders", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AffiliatePartnerId") - .HasColumnType("uuid"); - - b.Property("BuyerUserId") - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EstimatedCommission") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("OrderAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("SettledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "AffiliatePartnerId", "OrderId") - .IsUnique(); - - b.ToTable("affiliate_orders", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChannelType") - .HasColumnType("integer"); - - b.Property("CommissionRate") - .HasColumnType("numeric"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Phone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Remarks") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "DisplayName"); - - b.ToTable("affiliate_partners", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AffiliatePartnerId") - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Period") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Remarks") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "AffiliatePartnerId", "Period") - .IsUnique(); - - b.ToTable("affiliate_payouts", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AllowMakeupCount") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("RewardsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Name"); - - b.ToTable("checkin_campaigns", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CheckInCampaignId") - .HasColumnType("uuid"); - - b.Property("CheckInDate") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsMakeup") - .HasColumnType("boolean"); - - b.Property("RewardJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "CheckInCampaignId", "UserId", "CheckInDate") - .IsUnique(); - - b.ToTable("checkin_records", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AuthorUserId") - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("ParentId") - .HasColumnType("uuid"); - - b.Property("PostId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "PostId", "CreatedAt"); - - b.ToTable("community_comments", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AuthorUserId") - .HasColumnType("uuid"); - - b.Property("CommentCount") - .HasColumnType("integer"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("LikeCount") - .HasColumnType("integer"); - - b.Property("MediaJson") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("Title") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "AuthorUserId", "CreatedAt"); - - b.ToTable("community_posts", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("PostId") - .HasColumnType("uuid"); - - b.Property("ReactedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ReactionType") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "PostId", "UserId") - .IsUnique(); - - b.ToTable("community_reactions", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CurrentCount") - .HasColumnType("integer"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndAt") - .HasColumnType("timestamp with time zone"); - - b.Property("GroupOrderNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("GroupPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("LeaderUserId") - .HasColumnType("uuid"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("StartAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("SucceededAt") - .HasColumnType("timestamp with time zone"); - - b.Property("TargetCount") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "GroupOrderNo") - .IsUnique(); - - b.ToTable("group_orders", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("GroupOrderId") - .HasColumnType("uuid"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "GroupOrderId", "UserId") - .IsUnique(); - - b.ToTable("group_participants", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdjustmentType") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("InventoryItemId") - .HasColumnType("uuid"); - - b.Property("OccurredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("OperatorId") - .HasColumnType("uuid"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("Reason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "InventoryItemId", "OccurredAt"); - - b.ToTable("inventory_adjustments", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BatchNumber") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExpireDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ProductSkuId") - .HasColumnType("uuid"); - - b.Property("ProductionDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("RemainingQuantity") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber") - .IsUnique(); - - b.ToTable("inventory_batches", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BatchNumber") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExpireDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Location") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ProductSkuId") - .HasColumnType("uuid"); - - b.Property("QuantityOnHand") - .HasColumnType("integer"); - - b.Property("QuantityReserved") - .HasColumnType("integer"); - - b.Property("SafetyStock") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber"); - - b.ToTable("inventory_items", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChangeValue") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CurrentValue") - .HasColumnType("integer"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("MemberId") - .HasColumnType("uuid"); - - b.Property("Notes") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OccurredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MemberId", "OccurredAt"); - - b.ToTable("member_growth_logs", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BalanceAfterChange") - .HasColumnType("integer"); - - b.Property("ChangeAmount") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExpireAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MemberId") - .HasColumnType("uuid"); - - b.Property("OccurredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Reason") - .HasColumnType("integer"); - - b.Property("SourceId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MemberId", "OccurredAt"); - - b.ToTable("member_point_ledgers", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AvatarUrl") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BirthDate") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("GrowthValue") - .HasColumnType("integer"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MemberTierId") - .HasColumnType("uuid"); - - b.Property("Mobile") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Nickname") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("PointsBalance") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Mobile") - .IsUnique(); - - b.ToTable("member_profiles", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BenefitsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("RequiredGrowth") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Name") - .IsUnique(); - - b.ToTable("member_tiers", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Address") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("BrandAlias") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("BrandName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("BusinessLicenseImageUrl") - .HasColumnType("text"); - - b.Property("BusinessLicenseNumber") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Category") - .HasColumnType("text"); - - b.Property("City") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ContactEmail") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ContactPhone") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("District") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LastReviewedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Latitude") - .HasColumnType("double precision"); - - b.Property("LegalPerson") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LogoUrl") - .HasColumnType("text"); - - b.Property("Longitude") - .HasColumnType("double precision"); - - b.Property("Province") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ReviewRemarks") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ServicePhone") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("SupportEmail") - .HasColumnType("text"); - - b.Property("TaxNumber") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.ToTable("merchants", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ContractNumber") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("FileUrl") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("MerchantId") - .HasColumnType("uuid"); - - b.Property("SignedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TerminatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("TerminationReason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MerchantId", "ContractNumber") - .IsUnique(); - - b.ToTable("merchant_contracts", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DocumentNumber") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("DocumentType") - .HasColumnType("integer"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileUrl") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("IssuedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MerchantId") - .HasColumnType("uuid"); - - b.Property("Remarks") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MerchantId", "DocumentType"); - - b.ToTable("merchant_documents", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Email") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("IdentityUserId") - .HasColumnType("uuid"); - - b.Property("MerchantId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("PermissionsJson") - .HasColumnType("text"); - - b.Property("Phone") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("RoleType") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "MerchantId", "Phone"); - - b.ToTable("merchant_staff", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Address") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Landmark") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Latitude") - .HasColumnType("double precision"); - - b.Property("Longitude") - .HasColumnType("double precision"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("map_locations", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Channel") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("RequestedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TargetApp") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "UserId", "StoreId", "RequestedAt"); - - b.ToTable("navigation_requests", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AttributesJson") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("ProductName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ProductSkuId") - .HasColumnType("uuid"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("Remark") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UnitPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "ShoppingCartId"); - - b.ToTable("cart_items", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CartItemId") - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExtraPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OptionId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("cart_item_addons", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionToken") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ValidationResultJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "SessionToken") - .IsUnique(); - - b.ToTable("checkout_sessions", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryPreference") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("LastModifiedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TableContext") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "UserId", "StoreId") - .IsUnique(); - - b.ToTable("shopping_carts", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CancelReason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Channel") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CustomerName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CustomerPhone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryType") - .HasColumnType("integer"); - - b.Property("DiscountAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("FinishedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ItemsAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("OrderNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("PaidAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PayableAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("PaymentStatus") - .HasColumnType("integer"); - - b.Property("QueueNumber") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Remark") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ReservationId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TableNo") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderNo") - .IsUnique(); - - b.HasIndex("TenantId", "StoreId", "Status"); - - b.ToTable("orders", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AttributesJson") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DiscountAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("ProductName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("SkuName") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("SubTotal") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("Unit") - .HasMaxLength(16) - .HasColumnType("character varying(16)"); - - b.Property("UnitPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("OrderId"); - - b.HasIndex("TenantId", "OrderId"); - - b.ToTable("order_items", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Notes") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("OccurredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("OperatorId") - .HasColumnType("uuid"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderId", "OccurredAt"); - - b.ToTable("order_status_histories", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("ProcessedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Reason") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("RefundNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("RequestedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ReviewNotes") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "RefundNo") - .IsUnique(); - - b.ToTable("refund_requests", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ChannelTransactionId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Method") - .HasColumnType("integer"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("PaidAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Payload") - .HasColumnType("text"); - - b.Property("Remark") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TradeNo") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "OrderId"); - - b.ToTable("payment_records", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ChannelRefundId") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Payload") - .HasColumnType("text"); - - b.Property("PaymentRecordId") - .HasColumnType("uuid"); - - b.Property("RequestedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "PaymentRecordId"); - - b.ToTable("payment_refund_records", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CategoryId") - .HasColumnType("uuid"); - - b.Property("CoverImage") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("EnableDelivery") - .HasColumnType("boolean"); - - b.Property("EnableDineIn") - .HasColumnType("boolean"); - - b.Property("EnablePickup") - .HasColumnType("boolean"); - - b.Property("GalleryImages") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("IsFeatured") - .HasColumnType("boolean"); - - b.Property("MaxQuantityPerOrder") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("OriginalPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("SpuCode") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StockQuantity") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("Subtitle") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("Unit") - .HasMaxLength(16) - .HasColumnType("character varying(16)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "SpuCode") - .IsUnique(); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("products", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsRequired") - .HasColumnType("boolean"); - - b.Property("MaxSelect") - .HasColumnType("integer"); - - b.Property("MinSelect") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("SelectionType") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "ProductId", "Name"); - - b.ToTable("product_addon_groups", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddonGroupId") - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExtraPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("product_addon_options", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsRequired") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SelectionType") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "Name"); - - b.ToTable("product_attribute_groups", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AttributeGroupId") - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("ExtraPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "AttributeGroupId", "Name") - .IsUnique(); - - b.ToTable("product_attribute_options", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IsEnabled") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("product_categories", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Caption") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("MediaType") - .HasColumnType("integer"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.HasKey("Id"); - - b.ToTable("product_media_assets", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConditionsJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("RuleType") - .HasColumnType("integer"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("WeekdaysJson") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "ProductId", "RuleType"); - - b.ToTable("product_pricing_rules", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AttributesJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("Barcode") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("OriginalPrice") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("SkuCode") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("StockQuantity") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("Weight") - .HasPrecision(10, 3) - .HasColumnType("numeric(10,3)"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "SkuCode") - .IsUnique(); - - b.ToTable("product_skus", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CalledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EstimatedWaitMinutes") - .HasColumnType("integer"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PartySize") - .HasColumnType("integer"); - - b.Property("Remark") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TicketNumber") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId"); - - b.HasIndex("TenantId", "StoreId", "TicketNumber") - .IsUnique(); - - b.ToTable("queue_tickets", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CancelledAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CheckInCode") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("CheckedInAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("CustomerName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CustomerPhone") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("PeopleCount") - .HasColumnType("integer"); - - b.Property("Remark") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ReservationNo") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("ReservationTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TablePreference") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "ReservationNo") - .IsUnique(); - - b.HasIndex("TenantId", "StoreId"); - - b.ToTable("reservations", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Address") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Announcement") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("BusinessHours") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("City") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CoverImageUrl") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryRadiusKm") - .HasPrecision(6, 2) - .HasColumnType("numeric(6,2)"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("District") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Latitude") - .HasColumnType("double precision"); - - b.Property("Longitude") - .HasColumnType("double precision"); - - b.Property("ManagerName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MerchantId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Phone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Province") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("SupportsDelivery") - .HasColumnType("boolean"); - - b.Property("SupportsDineIn") - .HasColumnType("boolean"); - - b.Property("SupportsPickup") - .HasColumnType("boolean"); - - b.Property("SupportsQueueing") - .HasColumnType("boolean"); - - b.Property("SupportsReservation") - .HasColumnType("boolean"); - - b.Property("Tags") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.HasIndex("TenantId", "MerchantId"); - - b.ToTable("stores", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CapacityLimit") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DayOfWeek") - .HasColumnType("integer"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndTime") - .HasColumnType("interval"); - - b.Property("HourType") - .HasColumnType("integer"); - - b.Property("Notes") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("StartTime") - .HasColumnType("interval"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "DayOfWeek"); - - b.ToTable("store_business_hours", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DeliveryFee") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("EstimatedMinutes") - .HasColumnType("integer"); - - b.Property("MinimumOrderAmount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("PolygonGeoJson") - .IsRequired() - .HasColumnType("text"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("ZoneName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "ZoneName"); - - b.ToTable("store_delivery_zones", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EndTime") - .HasColumnType("interval"); - - b.Property("Notes") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("RoleType") - .HasColumnType("integer"); - - b.Property("ShiftDate") - .HasColumnType("timestamp with time zone"); - - b.Property("StaffId") - .HasColumnType("uuid"); - - b.Property("StartTime") - .HasColumnType("interval"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "ShiftDate", "StaffId") - .IsUnique(); - - b.ToTable("store_employee_shifts", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("Date") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("IsClosed") - .HasColumnType("boolean"); - - b.Property("Reason") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "Date") - .IsUnique(); - - b.ToTable("store_holidays", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AreaId") - .HasColumnType("uuid"); - - b.Property("Capacity") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("QrCodeUrl") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TableCode") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Tags") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "TableCode") - .IsUnique(); - - b.ToTable("store_tables", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("StoreId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StoreId", "Name") - .IsUnique(); - - b.ToTable("store_table_areas", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ContactEmail") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ContactName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("ContactPhone") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CoverImageUrl") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EffectiveFrom") - .HasColumnType("timestamp with time zone"); - - b.Property("EffectiveTo") - .HasColumnType("timestamp with time zone"); - - b.Property("Industry") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LegalEntityName") - .HasColumnType("text"); - - b.Property("LogoUrl") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("PrimaryOwnerUserId") - .HasColumnType("uuid"); - - b.Property("Province") - .HasColumnType("text"); - - b.Property("Remarks") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("ShortName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("SuspendedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SuspensionReason") - .HasColumnType("text"); - - b.Property("Tags") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("Website") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Code") - .IsUnique(); - - b.ToTable("tenants", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AmountDue") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("AmountPaid") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DueDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LineItemsJson") - .HasColumnType("text"); - - b.Property("PeriodEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("PeriodStart") - .HasColumnType("timestamp with time zone"); - - b.Property("StatementNo") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "StatementNo") - .IsUnique(); - - b.ToTable("tenant_billing_statements", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Channel") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("MetadataJson") - .HasColumnType("text"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SentAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Severity") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "Channel", "SentAt"); - - b.ToTable("tenant_notifications", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("FeaturePoliciesJson") - .HasColumnType("text"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("MaxAccountCount") - .HasColumnType("integer"); - - b.Property("MaxDeliveryOrders") - .HasColumnType("integer"); - - b.Property("MaxSmsCredits") - .HasColumnType("integer"); - - b.Property("MaxStorageGb") - .HasColumnType("integer"); - - b.Property("MaxStoreCount") - .HasColumnType("integer"); - - b.Property("MonthlyPrice") - .HasColumnType("numeric"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("PackageType") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("YearlyPrice") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.ToTable("tenant_packages", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("LastResetAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LimitValue") - .HasColumnType("numeric"); - - b.Property("QuotaType") - .HasColumnType("integer"); - - b.Property("ResetCycle") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("UsedValue") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "QuotaType") - .IsUnique(); - - b.ToTable("tenant_quota_usages", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AutoRenew") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("EffectiveFrom") - .HasColumnType("timestamp with time zone"); - - b.Property("EffectiveTo") - .HasColumnType("timestamp with time zone"); - - b.Property("NextBillingDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Notes") - .HasColumnType("text"); - - b.Property("ScheduledPackageId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("TenantPackageId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId", "TenantPackageId"); - - b.ToTable("tenant_subscriptions", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => - { - b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) - .WithMany() - .HasForeignKey("OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs deleted file mode 100644 index fc4cb04..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201055852_ExpandDomainSchema.cs +++ /dev/null @@ -1,2206 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.App.Migrations -{ - /// - public partial class ExpandDomainSchema : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_stores_merchants_MerchantId", - table: "stores"); - - migrationBuilder.DropIndex( - name: "IX_stores_MerchantId", - table: "stores"); - - migrationBuilder.RenameColumn( - name: "ReservationEnabled", - table: "stores", - newName: "SupportsReservation"); - - migrationBuilder.RenameColumn( - name: "QueueEnabled", - table: "stores", - newName: "SupportsQueueing"); - - migrationBuilder.RenameColumn( - name: "OnboardedAt", - table: "merchants", - newName: "LastReviewedAt"); - - migrationBuilder.AddColumn( - name: "Address", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "City", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Country", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "CoverImageUrl", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "LegalEntityName", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "PrimaryOwnerUserId", - table: "tenants", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "Province", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "SuspendedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "SuspensionReason", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Tags", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Website", - table: "tenants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Country", - table: "stores", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "CoverImageUrl", - table: "stores", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Description", - table: "stores", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Tags", - table: "stores", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "BusinessLicenseImageUrl", - table: "merchants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Category", - table: "merchants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "JoinedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "Latitude", - table: "merchants", - type: "double precision", - nullable: true); - - migrationBuilder.AddColumn( - name: "LogoUrl", - table: "merchants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Longitude", - table: "merchants", - type: "double precision", - nullable: true); - - migrationBuilder.AddColumn( - name: "ServicePhone", - table: "merchants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "SupportEmail", - table: "merchants", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "TaxNumber", - table: "merchants", - type: "text", - nullable: true); - - migrationBuilder.CreateTable( - name: "affiliate_orders", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AffiliatePartnerId = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - BuyerUserId = table.Column(type: "uuid", nullable: false), - OrderAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - EstimatedCommission = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Status = table.Column(type: "integer", nullable: false), - SettledAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_affiliate_orders", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "affiliate_partners", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: true), - DisplayName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - ChannelType = table.Column(type: "integer", nullable: false), - CommissionRate = table.Column(type: "numeric", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Remarks = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_affiliate_partners", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "affiliate_payouts", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AffiliatePartnerId = table.Column(type: "uuid", nullable: false), - Period = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Status = table.Column(type: "integer", nullable: false), - PaidAt = table.Column(type: "timestamp with time zone", nullable: true), - Remarks = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_affiliate_payouts", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "cart_item_addons", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CartItemId = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - OptionId = table.Column(type: "uuid", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_cart_item_addons", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "cart_items", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - ProductSkuId = table.Column(type: "uuid", nullable: true), - ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Quantity = table.Column(type: "integer", nullable: false), - Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Status = table.Column(type: "integer", nullable: false), - AttributesJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_cart_items", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "chat_messages", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ChatSessionId = table.Column(type: "uuid", nullable: false), - SenderType = table.Column(type: "integer", nullable: false), - SenderUserId = table.Column(type: "uuid", nullable: true), - Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), - ContentType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - IsRead = table.Column(type: "boolean", nullable: false), - ReadAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_chat_messages", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "chat_sessions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - SessionCode = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - CustomerUserId = table.Column(type: "uuid", nullable: false), - AgentUserId = table.Column(type: "uuid", nullable: true), - StoreId = table.Column(type: "uuid", nullable: true), - Status = table.Column(type: "integer", nullable: false), - IsBotActive = table.Column(type: "boolean", nullable: false), - StartedAt = table.Column(type: "timestamp with time zone", nullable: false), - EndedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_chat_sessions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "checkin_campaigns", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - StartDate = table.Column(type: "timestamp with time zone", nullable: false), - EndDate = table.Column(type: "timestamp with time zone", nullable: false), - AllowMakeupCount = table.Column(type: "integer", nullable: false), - RewardsJson = table.Column(type: "text", nullable: false), - Status = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_checkin_campaigns", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "checkin_records", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CheckInCampaignId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - CheckInDate = table.Column(type: "timestamp with time zone", nullable: false), - IsMakeup = table.Column(type: "boolean", nullable: false), - RewardJson = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_checkin_records", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "checkout_sessions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - SessionToken = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Status = table.Column(type: "integer", nullable: false), - ValidationResultJson = table.Column(type: "text", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_checkout_sessions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "community_comments", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - PostId = table.Column(type: "uuid", nullable: false), - AuthorUserId = table.Column(type: "uuid", nullable: false), - Content = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - ParentId = table.Column(type: "uuid", nullable: true), - IsDeleted = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_community_comments", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "community_posts", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AuthorUserId = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - Content = table.Column(type: "text", nullable: false), - MediaJson = table.Column(type: "text", nullable: true), - Status = table.Column(type: "integer", nullable: false), - LikeCount = table.Column(type: "integer", nullable: false), - CommentCount = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_community_posts", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "community_reactions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - PostId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - ReactionType = table.Column(type: "integer", nullable: false), - ReactedAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_community_reactions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "coupon_templates", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - CouponType = table.Column(type: "integer", nullable: false), - Value = table.Column(type: "numeric", nullable: false), - DiscountCap = table.Column(type: "numeric", nullable: true), - MinimumSpend = table.Column(type: "numeric", nullable: true), - ValidFrom = table.Column(type: "timestamp with time zone", nullable: true), - ValidTo = table.Column(type: "timestamp with time zone", nullable: true), - RelativeValidDays = table.Column(type: "integer", nullable: true), - TotalQuantity = table.Column(type: "integer", nullable: true), - ClaimedQuantity = table.Column(type: "integer", nullable: false), - StoreScopeJson = table.Column(type: "text", nullable: true), - ProductScopeJson = table.Column(type: "text", nullable: true), - ChannelsJson = table.Column(type: "text", nullable: true), - AllowStack = table.Column(type: "boolean", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_coupon_templates", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "coupons", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CouponTemplateId = table.Column(type: "uuid", nullable: false), - Code = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: true), - Status = table.Column(type: "integer", nullable: false), - IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), - UsedAt = table.Column(type: "timestamp with time zone", nullable: true), - ExpireAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_coupons", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "delivery_events", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - DeliveryOrderId = table.Column(type: "uuid", nullable: false), - EventType = table.Column(type: "integer", nullable: false), - Message = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Payload = table.Column(type: "text", nullable: true), - OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_delivery_events", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "group_orders", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - GroupOrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - LeaderUserId = table.Column(type: "uuid", nullable: false), - TargetCount = table.Column(type: "integer", nullable: false), - CurrentCount = table.Column(type: "integer", nullable: false), - GroupPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - StartAt = table.Column(type: "timestamp with time zone", nullable: false), - EndAt = table.Column(type: "timestamp with time zone", nullable: false), - Status = table.Column(type: "integer", nullable: false), - SucceededAt = table.Column(type: "timestamp with time zone", nullable: true), - CancelledAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_group_orders", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "group_participants", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - GroupOrderId = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_group_participants", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "inventory_adjustments", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - InventoryItemId = table.Column(type: "uuid", nullable: false), - AdjustmentType = table.Column(type: "integer", nullable: false), - Quantity = table.Column(type: "integer", nullable: false), - Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - OperatorId = table.Column(type: "uuid", nullable: true), - OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_inventory_adjustments", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "inventory_batches", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - ProductSkuId = table.Column(type: "uuid", nullable: false), - BatchNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - ProductionDate = table.Column(type: "timestamp with time zone", nullable: true), - ExpireDate = table.Column(type: "timestamp with time zone", nullable: true), - Quantity = table.Column(type: "integer", nullable: false), - RemainingQuantity = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_inventory_batches", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "inventory_items", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - ProductSkuId = table.Column(type: "uuid", nullable: false), - BatchNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - QuantityOnHand = table.Column(type: "integer", nullable: false), - QuantityReserved = table.Column(type: "integer", nullable: false), - SafetyStock = table.Column(type: "integer", nullable: true), - Location = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - ExpireDate = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_inventory_items", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "map_locations", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: true), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Longitude = table.Column(type: "double precision", nullable: false), - Latitude = table.Column(type: "double precision", nullable: false), - Landmark = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_map_locations", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "member_growth_logs", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MemberId = table.Column(type: "uuid", nullable: false), - ChangeValue = table.Column(type: "integer", nullable: false), - CurrentValue = table.Column(type: "integer", nullable: false), - Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_member_growth_logs", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "member_point_ledgers", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MemberId = table.Column(type: "uuid", nullable: false), - ChangeAmount = table.Column(type: "integer", nullable: false), - BalanceAfterChange = table.Column(type: "integer", nullable: false), - Reason = table.Column(type: "integer", nullable: false), - SourceId = table.Column(type: "uuid", nullable: true), - OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpireAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_member_point_ledgers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "member_profiles", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Mobile = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Nickname = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - AvatarUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - MemberTierId = table.Column(type: "uuid", nullable: true), - Status = table.Column(type: "integer", nullable: false), - PointsBalance = table.Column(type: "integer", nullable: false), - GrowthValue = table.Column(type: "integer", nullable: false), - BirthDate = table.Column(type: "timestamp with time zone", nullable: true), - JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_member_profiles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "member_tiers", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - RequiredGrowth = table.Column(type: "integer", nullable: false), - BenefitsJson = table.Column(type: "text", nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_member_tiers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "merchant_contracts", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MerchantId = table.Column(type: "uuid", nullable: false), - ContractNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Status = table.Column(type: "integer", nullable: false), - StartDate = table.Column(type: "timestamp with time zone", nullable: false), - EndDate = table.Column(type: "timestamp with time zone", nullable: false), - FileUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - SignedAt = table.Column(type: "timestamp with time zone", nullable: true), - TerminatedAt = table.Column(type: "timestamp with time zone", nullable: true), - TerminationReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_merchant_contracts", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "merchant_documents", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MerchantId = table.Column(type: "uuid", nullable: false), - DocumentType = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - FileUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - DocumentNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - IssuedAt = table.Column(type: "timestamp with time zone", nullable: true), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), - Remarks = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_merchant_documents", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "merchant_staff", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MerchantId = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: true), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Email = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - IdentityUserId = table.Column(type: "uuid", nullable: true), - RoleType = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - PermissionsJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_merchant_staff", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "metric_alert_rules", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MetricDefinitionId = table.Column(type: "uuid", nullable: false), - ConditionJson = table.Column(type: "text", nullable: false), - Severity = table.Column(type: "integer", nullable: false), - NotificationChannels = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Enabled = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_metric_alert_rules", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "metric_definitions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - DimensionsJson = table.Column(type: "text", nullable: true), - DefaultAggregation = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_metric_definitions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "metric_snapshots", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MetricDefinitionId = table.Column(type: "uuid", nullable: false), - DimensionKey = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - WindowStart = table.Column(type: "timestamp with time zone", nullable: false), - WindowEnd = table.Column(type: "timestamp with time zone", nullable: false), - Value = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_metric_snapshots", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "navigation_requests", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - Channel = table.Column(type: "integer", nullable: false), - TargetApp = table.Column(type: "integer", nullable: false), - RequestedAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_navigation_requests", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "order_status_histories", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - OperatorId = table.Column(type: "uuid", nullable: true), - Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - OccurredAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_order_status_histories", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "payment_refund_records", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - PaymentRecordId = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - ChannelRefundId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Status = table.Column(type: "integer", nullable: false), - RequestedAt = table.Column(type: "timestamp with time zone", nullable: false), - CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), - Payload = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_payment_refund_records", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_addon_groups", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - SelectionType = table.Column(type: "integer", nullable: false), - MinSelect = table.Column(type: "integer", nullable: true), - MaxSelect = table.Column(type: "integer", nullable: true), - IsRequired = table.Column(type: "boolean", nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_addon_groups", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_addon_options", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AddonGroupId = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - IsDefault = table.Column(type: "boolean", nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_addon_options", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_attribute_groups", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: true), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - SelectionType = table.Column(type: "integer", nullable: false), - IsRequired = table.Column(type: "boolean", nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_attribute_groups", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_attribute_options", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AttributeGroupId = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - SortOrder = table.Column(type: "integer", nullable: false), - IsDefault = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_attribute_options", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_media_assets", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - MediaType = table.Column(type: "integer", nullable: false), - Url = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - Caption = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - SortOrder = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_media_assets", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_pricing_rules", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - RuleType = table.Column(type: "integer", nullable: false), - ConditionsJson = table.Column(type: "text", nullable: false), - Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - StartTime = table.Column(type: "timestamp with time zone", nullable: true), - EndTime = table.Column(type: "timestamp with time zone", nullable: true), - WeekdaysJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_pricing_rules", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "product_skus", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - SkuCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Barcode = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - StockQuantity = table.Column(type: "integer", nullable: true), - Weight = table.Column(type: "numeric(10,3)", precision: 10, scale: 3, nullable: true), - AttributesJson = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_product_skus", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "promotion_campaigns", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - PromotionType = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - StartAt = table.Column(type: "timestamp with time zone", nullable: false), - EndAt = table.Column(type: "timestamp with time zone", nullable: false), - Budget = table.Column(type: "numeric", nullable: true), - RulesJson = table.Column(type: "text", nullable: false), - AudienceDescription = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - BannerUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_promotion_campaigns", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "refund_requests", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - RefundNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Status = table.Column(type: "integer", nullable: false), - RequestedAt = table.Column(type: "timestamp with time zone", nullable: false), - ProcessedAt = table.Column(type: "timestamp with time zone", nullable: true), - ReviewNotes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_refund_requests", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "shopping_carts", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - TableContext = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), - DeliveryPreference = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - LastModifiedAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_shopping_carts", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "store_business_hours", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - DayOfWeek = table.Column(type: "integer", nullable: false), - HourType = table.Column(type: "integer", nullable: false), - StartTime = table.Column(type: "interval", nullable: false), - EndTime = table.Column(type: "interval", nullable: false), - CapacityLimit = table.Column(type: "integer", nullable: true), - Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_store_business_hours", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "store_delivery_zones", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - ZoneName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - PolygonGeoJson = table.Column(type: "text", nullable: false), - MinimumOrderAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - DeliveryFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), - EstimatedMinutes = table.Column(type: "integer", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_store_delivery_zones", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "store_employee_shifts", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - StaffId = table.Column(type: "uuid", nullable: false), - ShiftDate = table.Column(type: "timestamp with time zone", nullable: false), - StartTime = table.Column(type: "interval", nullable: false), - EndTime = table.Column(type: "interval", nullable: false), - RoleType = table.Column(type: "integer", nullable: false), - Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_store_employee_shifts", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "store_holidays", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - Date = table.Column(type: "timestamp with time zone", nullable: false), - IsClosed = table.Column(type: "boolean", nullable: false), - Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_store_holidays", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "store_table_areas", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_store_table_areas", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "store_tables", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StoreId = table.Column(type: "uuid", nullable: false), - AreaId = table.Column(type: "uuid", nullable: true), - TableCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Capacity = table.Column(type: "integer", nullable: false), - Tags = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - Status = table.Column(type: "integer", nullable: false), - QrCodeUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_store_tables", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "support_tickets", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - TicketNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - CustomerUserId = table.Column(type: "uuid", nullable: false), - OrderId = table.Column(type: "uuid", nullable: true), - Subject = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Description = table.Column(type: "text", nullable: false), - Priority = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - AssignedAgentId = table.Column(type: "uuid", nullable: true), - ClosedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_support_tickets", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "tenant_billing_statements", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StatementNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - PeriodStart = table.Column(type: "timestamp with time zone", nullable: false), - PeriodEnd = table.Column(type: "timestamp with time zone", nullable: false), - AmountDue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - AmountPaid = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Status = table.Column(type: "integer", nullable: false), - DueDate = table.Column(type: "timestamp with time zone", nullable: false), - LineItemsJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_tenant_billing_statements", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "tenant_notifications", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Message = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), - Channel = table.Column(type: "integer", nullable: false), - Severity = table.Column(type: "integer", nullable: false), - SentAt = table.Column(type: "timestamp with time zone", nullable: false), - ReadAt = table.Column(type: "timestamp with time zone", nullable: true), - MetadataJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_tenant_notifications", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "tenant_packages", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - PackageType = table.Column(type: "integer", nullable: false), - MonthlyPrice = table.Column(type: "numeric", nullable: true), - YearlyPrice = table.Column(type: "numeric", nullable: true), - MaxStoreCount = table.Column(type: "integer", nullable: true), - MaxAccountCount = table.Column(type: "integer", nullable: true), - MaxStorageGb = table.Column(type: "integer", nullable: true), - MaxSmsCredits = table.Column(type: "integer", nullable: true), - MaxDeliveryOrders = table.Column(type: "integer", nullable: true), - FeaturePoliciesJson = table.Column(type: "text", nullable: true), - IsActive = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_tenant_packages", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "tenant_quota_usages", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - QuotaType = table.Column(type: "integer", nullable: false), - LimitValue = table.Column(type: "numeric", nullable: false), - UsedValue = table.Column(type: "numeric", nullable: false), - ResetCycle = table.Column(type: "text", nullable: true), - LastResetAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_tenant_quota_usages", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "tenant_subscriptions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - TenantPackageId = table.Column(type: "uuid", nullable: false), - EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: false), - EffectiveTo = table.Column(type: "timestamp with time zone", nullable: false), - NextBillingDate = table.Column(type: "timestamp with time zone", nullable: true), - Status = table.Column(type: "integer", nullable: false), - AutoRenew = table.Column(type: "boolean", nullable: false), - ScheduledPackageId = table.Column(type: "uuid", nullable: true), - Notes = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_tenant_subscriptions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ticket_comments", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - SupportTicketId = table.Column(type: "uuid", nullable: false), - AuthorUserId = table.Column(type: "uuid", nullable: true), - Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), - IsInternal = table.Column(type: "boolean", nullable: false), - AttachmentsJson = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ticket_comments", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_affiliate_orders_TenantId_AffiliatePartnerId_OrderId", - table: "affiliate_orders", - columns: new[] { "TenantId", "AffiliatePartnerId", "OrderId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_affiliate_partners_TenantId_DisplayName", - table: "affiliate_partners", - columns: new[] { "TenantId", "DisplayName" }); - - migrationBuilder.CreateIndex( - name: "IX_affiliate_payouts_TenantId_AffiliatePartnerId_Period", - table: "affiliate_payouts", - columns: new[] { "TenantId", "AffiliatePartnerId", "Period" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_cart_items_TenantId_ShoppingCartId", - table: "cart_items", - columns: new[] { "TenantId", "ShoppingCartId" }); - - migrationBuilder.CreateIndex( - name: "IX_chat_messages_TenantId_ChatSessionId_CreatedAt", - table: "chat_messages", - columns: new[] { "TenantId", "ChatSessionId", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_chat_sessions_TenantId_SessionCode", - table: "chat_sessions", - columns: new[] { "TenantId", "SessionCode" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_checkin_campaigns_TenantId_Name", - table: "checkin_campaigns", - columns: new[] { "TenantId", "Name" }); - - migrationBuilder.CreateIndex( - name: "IX_checkin_records_TenantId_CheckInCampaignId_UserId_CheckInDa~", - table: "checkin_records", - columns: new[] { "TenantId", "CheckInCampaignId", "UserId", "CheckInDate" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_checkout_sessions_TenantId_SessionToken", - table: "checkout_sessions", - columns: new[] { "TenantId", "SessionToken" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_community_comments_TenantId_PostId_CreatedAt", - table: "community_comments", - columns: new[] { "TenantId", "PostId", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_community_posts_TenantId_AuthorUserId_CreatedAt", - table: "community_posts", - columns: new[] { "TenantId", "AuthorUserId", "CreatedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_community_reactions_TenantId_PostId_UserId", - table: "community_reactions", - columns: new[] { "TenantId", "PostId", "UserId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_coupons_TenantId_Code", - table: "coupons", - columns: new[] { "TenantId", "Code" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_delivery_events_TenantId_DeliveryOrderId_EventType", - table: "delivery_events", - columns: new[] { "TenantId", "DeliveryOrderId", "EventType" }); - - migrationBuilder.CreateIndex( - name: "IX_group_orders_TenantId_GroupOrderNo", - table: "group_orders", - columns: new[] { "TenantId", "GroupOrderNo" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_group_participants_TenantId_GroupOrderId_UserId", - table: "group_participants", - columns: new[] { "TenantId", "GroupOrderId", "UserId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_inventory_adjustments_TenantId_InventoryItemId_OccurredAt", - table: "inventory_adjustments", - columns: new[] { "TenantId", "InventoryItemId", "OccurredAt" }); - - migrationBuilder.CreateIndex( - name: "IX_inventory_batches_TenantId_StoreId_ProductSkuId_BatchNumber", - table: "inventory_batches", - columns: new[] { "TenantId", "StoreId", "ProductSkuId", "BatchNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_inventory_items_TenantId_StoreId_ProductSkuId_BatchNumber", - table: "inventory_items", - columns: new[] { "TenantId", "StoreId", "ProductSkuId", "BatchNumber" }); - - migrationBuilder.CreateIndex( - name: "IX_map_locations_TenantId_StoreId", - table: "map_locations", - columns: new[] { "TenantId", "StoreId" }); - - migrationBuilder.CreateIndex( - name: "IX_member_growth_logs_TenantId_MemberId_OccurredAt", - table: "member_growth_logs", - columns: new[] { "TenantId", "MemberId", "OccurredAt" }); - - migrationBuilder.CreateIndex( - name: "IX_member_point_ledgers_TenantId_MemberId_OccurredAt", - table: "member_point_ledgers", - columns: new[] { "TenantId", "MemberId", "OccurredAt" }); - - migrationBuilder.CreateIndex( - name: "IX_member_profiles_TenantId_Mobile", - table: "member_profiles", - columns: new[] { "TenantId", "Mobile" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_member_tiers_TenantId_Name", - table: "member_tiers", - columns: new[] { "TenantId", "Name" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_merchant_contracts_TenantId_MerchantId_ContractNumber", - table: "merchant_contracts", - columns: new[] { "TenantId", "MerchantId", "ContractNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_merchant_documents_TenantId_MerchantId_DocumentType", - table: "merchant_documents", - columns: new[] { "TenantId", "MerchantId", "DocumentType" }); - - migrationBuilder.CreateIndex( - name: "IX_merchant_staff_TenantId_MerchantId_Phone", - table: "merchant_staff", - columns: new[] { "TenantId", "MerchantId", "Phone" }); - - migrationBuilder.CreateIndex( - name: "IX_metric_alert_rules_TenantId_MetricDefinitionId_Severity", - table: "metric_alert_rules", - columns: new[] { "TenantId", "MetricDefinitionId", "Severity" }); - - migrationBuilder.CreateIndex( - name: "IX_metric_definitions_TenantId_Code", - table: "metric_definitions", - columns: new[] { "TenantId", "Code" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_metric_snapshots_TenantId_MetricDefinitionId_DimensionKey_W~", - table: "metric_snapshots", - columns: new[] { "TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_navigation_requests_TenantId_UserId_StoreId_RequestedAt", - table: "navigation_requests", - columns: new[] { "TenantId", "UserId", "StoreId", "RequestedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_order_status_histories_TenantId_OrderId_OccurredAt", - table: "order_status_histories", - columns: new[] { "TenantId", "OrderId", "OccurredAt" }); - - migrationBuilder.CreateIndex( - name: "IX_payment_refund_records_TenantId_PaymentRecordId", - table: "payment_refund_records", - columns: new[] { "TenantId", "PaymentRecordId" }); - - migrationBuilder.CreateIndex( - name: "IX_product_addon_groups_TenantId_ProductId_Name", - table: "product_addon_groups", - columns: new[] { "TenantId", "ProductId", "Name" }); - - migrationBuilder.CreateIndex( - name: "IX_product_attribute_groups_TenantId_StoreId_Name", - table: "product_attribute_groups", - columns: new[] { "TenantId", "StoreId", "Name" }); - - migrationBuilder.CreateIndex( - name: "IX_product_attribute_options_TenantId_AttributeGroupId_Name", - table: "product_attribute_options", - columns: new[] { "TenantId", "AttributeGroupId", "Name" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_product_pricing_rules_TenantId_ProductId_RuleType", - table: "product_pricing_rules", - columns: new[] { "TenantId", "ProductId", "RuleType" }); - - migrationBuilder.CreateIndex( - name: "IX_product_skus_TenantId_SkuCode", - table: "product_skus", - columns: new[] { "TenantId", "SkuCode" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_refund_requests_TenantId_RefundNo", - table: "refund_requests", - columns: new[] { "TenantId", "RefundNo" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_shopping_carts_TenantId_UserId_StoreId", - table: "shopping_carts", - columns: new[] { "TenantId", "UserId", "StoreId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_store_business_hours_TenantId_StoreId_DayOfWeek", - table: "store_business_hours", - columns: new[] { "TenantId", "StoreId", "DayOfWeek" }); - - migrationBuilder.CreateIndex( - name: "IX_store_delivery_zones_TenantId_StoreId_ZoneName", - table: "store_delivery_zones", - columns: new[] { "TenantId", "StoreId", "ZoneName" }); - - migrationBuilder.CreateIndex( - name: "IX_store_employee_shifts_TenantId_StoreId_ShiftDate_StaffId", - table: "store_employee_shifts", - columns: new[] { "TenantId", "StoreId", "ShiftDate", "StaffId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_store_holidays_TenantId_StoreId_Date", - table: "store_holidays", - columns: new[] { "TenantId", "StoreId", "Date" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_store_table_areas_TenantId_StoreId_Name", - table: "store_table_areas", - columns: new[] { "TenantId", "StoreId", "Name" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_store_tables_TenantId_StoreId_TableCode", - table: "store_tables", - columns: new[] { "TenantId", "StoreId", "TableCode" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_support_tickets_TenantId_TicketNo", - table: "support_tickets", - columns: new[] { "TenantId", "TicketNo" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_tenant_billing_statements_TenantId_StatementNo", - table: "tenant_billing_statements", - columns: new[] { "TenantId", "StatementNo" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_tenant_notifications_TenantId_Channel_SentAt", - table: "tenant_notifications", - columns: new[] { "TenantId", "Channel", "SentAt" }); - - migrationBuilder.CreateIndex( - name: "IX_tenant_quota_usages_TenantId_QuotaType", - table: "tenant_quota_usages", - columns: new[] { "TenantId", "QuotaType" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_tenant_subscriptions_TenantId_TenantPackageId", - table: "tenant_subscriptions", - columns: new[] { "TenantId", "TenantPackageId" }); - - migrationBuilder.CreateIndex( - name: "IX_ticket_comments_TenantId_SupportTicketId", - table: "ticket_comments", - columns: new[] { "TenantId", "SupportTicketId" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "affiliate_orders"); - - migrationBuilder.DropTable( - name: "affiliate_partners"); - - migrationBuilder.DropTable( - name: "affiliate_payouts"); - - migrationBuilder.DropTable( - name: "cart_item_addons"); - - migrationBuilder.DropTable( - name: "cart_items"); - - migrationBuilder.DropTable( - name: "chat_messages"); - - migrationBuilder.DropTable( - name: "chat_sessions"); - - migrationBuilder.DropTable( - name: "checkin_campaigns"); - - migrationBuilder.DropTable( - name: "checkin_records"); - - migrationBuilder.DropTable( - name: "checkout_sessions"); - - migrationBuilder.DropTable( - name: "community_comments"); - - migrationBuilder.DropTable( - name: "community_posts"); - - migrationBuilder.DropTable( - name: "community_reactions"); - - migrationBuilder.DropTable( - name: "coupon_templates"); - - migrationBuilder.DropTable( - name: "coupons"); - - migrationBuilder.DropTable( - name: "delivery_events"); - - migrationBuilder.DropTable( - name: "group_orders"); - - migrationBuilder.DropTable( - name: "group_participants"); - - migrationBuilder.DropTable( - name: "inventory_adjustments"); - - migrationBuilder.DropTable( - name: "inventory_batches"); - - migrationBuilder.DropTable( - name: "inventory_items"); - - migrationBuilder.DropTable( - name: "map_locations"); - - migrationBuilder.DropTable( - name: "member_growth_logs"); - - migrationBuilder.DropTable( - name: "member_point_ledgers"); - - migrationBuilder.DropTable( - name: "member_profiles"); - - migrationBuilder.DropTable( - name: "member_tiers"); - - migrationBuilder.DropTable( - name: "merchant_contracts"); - - migrationBuilder.DropTable( - name: "merchant_documents"); - - migrationBuilder.DropTable( - name: "merchant_staff"); - - migrationBuilder.DropTable( - name: "metric_alert_rules"); - - migrationBuilder.DropTable( - name: "metric_definitions"); - - migrationBuilder.DropTable( - name: "metric_snapshots"); - - migrationBuilder.DropTable( - name: "navigation_requests"); - - migrationBuilder.DropTable( - name: "order_status_histories"); - - migrationBuilder.DropTable( - name: "payment_refund_records"); - - migrationBuilder.DropTable( - name: "product_addon_groups"); - - migrationBuilder.DropTable( - name: "product_addon_options"); - - migrationBuilder.DropTable( - name: "product_attribute_groups"); - - migrationBuilder.DropTable( - name: "product_attribute_options"); - - migrationBuilder.DropTable( - name: "product_media_assets"); - - migrationBuilder.DropTable( - name: "product_pricing_rules"); - - migrationBuilder.DropTable( - name: "product_skus"); - - migrationBuilder.DropTable( - name: "promotion_campaigns"); - - migrationBuilder.DropTable( - name: "refund_requests"); - - migrationBuilder.DropTable( - name: "shopping_carts"); - - migrationBuilder.DropTable( - name: "store_business_hours"); - - migrationBuilder.DropTable( - name: "store_delivery_zones"); - - migrationBuilder.DropTable( - name: "store_employee_shifts"); - - migrationBuilder.DropTable( - name: "store_holidays"); - - migrationBuilder.DropTable( - name: "store_table_areas"); - - migrationBuilder.DropTable( - name: "store_tables"); - - migrationBuilder.DropTable( - name: "support_tickets"); - - migrationBuilder.DropTable( - name: "tenant_billing_statements"); - - migrationBuilder.DropTable( - name: "tenant_notifications"); - - migrationBuilder.DropTable( - name: "tenant_packages"); - - migrationBuilder.DropTable( - name: "tenant_quota_usages"); - - migrationBuilder.DropTable( - name: "tenant_subscriptions"); - - migrationBuilder.DropTable( - name: "ticket_comments"); - - migrationBuilder.DropColumn( - name: "Address", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "City", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "Country", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "CoverImageUrl", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "LegalEntityName", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "PrimaryOwnerUserId", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "Province", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "SuspendedAt", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "SuspensionReason", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "Tags", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "Website", - table: "tenants"); - - migrationBuilder.DropColumn( - name: "Country", - table: "stores"); - - migrationBuilder.DropColumn( - name: "CoverImageUrl", - table: "stores"); - - migrationBuilder.DropColumn( - name: "Description", - table: "stores"); - - migrationBuilder.DropColumn( - name: "Tags", - table: "stores"); - - migrationBuilder.DropColumn( - name: "BusinessLicenseImageUrl", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "Category", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "JoinedAt", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "Latitude", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "LogoUrl", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "Longitude", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "ServicePhone", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "SupportEmail", - table: "merchants"); - - migrationBuilder.DropColumn( - name: "TaxNumber", - table: "merchants"); - - migrationBuilder.RenameColumn( - name: "SupportsReservation", - table: "stores", - newName: "ReservationEnabled"); - - migrationBuilder.RenameColumn( - name: "SupportsQueueing", - table: "stores", - newName: "QueueEnabled"); - - migrationBuilder.RenameColumn( - name: "LastReviewedAt", - table: "merchants", - newName: "OnboardedAt"); - - migrationBuilder.CreateIndex( - name: "IX_stores_MerchantId", - table: "stores", - column: "MerchantId"); - - migrationBuilder.AddForeignKey( - name: "FK_stores_merchants_MerchantId", - table: "stores", - column: "MerchantId", - principalTable: "merchants", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs deleted file mode 100644 index 38504cd..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.cs +++ /dev/null @@ -1,22401 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.App.Migrations -{ - /// - public partial class AddEntityComments : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterTable( - name: "ticket_comments", - comment: "工单评论/流转记录。"); - - migrationBuilder.AlterTable( - name: "tenants", - comment: "平台租户信息,描述租户的生命周期与基础资料。"); - - migrationBuilder.AlterTable( - name: "tenant_subscriptions", - comment: "租户套餐订阅记录。"); - - migrationBuilder.AlterTable( - name: "tenant_quota_usages", - comment: "租户配额使用情况快照。"); - - migrationBuilder.AlterTable( - name: "tenant_packages", - comment: "平台提供的租户套餐定义。"); - - migrationBuilder.AlterTable( - name: "tenant_notifications", - comment: "面向租户的站内通知或消息推送。"); - - migrationBuilder.AlterTable( - name: "tenant_billing_statements", - comment: "租户账单,用于呈现周期性收费。"); - - migrationBuilder.AlterTable( - name: "support_tickets", - comment: "客服工单。"); - - migrationBuilder.AlterTable( - name: "stores", - comment: "门店信息,承载营业配置与能力。"); - - migrationBuilder.AlterTable( - name: "store_tables", - comment: "桌台信息与二维码绑定。"); - - migrationBuilder.AlterTable( - name: "store_table_areas", - comment: "门店桌台区域配置。"); - - migrationBuilder.AlterTable( - name: "store_holidays", - comment: "门店休息日或特殊营业日。"); - - migrationBuilder.AlterTable( - name: "store_employee_shifts", - comment: "门店员工排班记录。"); - - migrationBuilder.AlterTable( - name: "store_delivery_zones", - comment: "门店配送范围配置。"); - - migrationBuilder.AlterTable( - name: "store_business_hours", - comment: "门店营业时段配置。"); - - migrationBuilder.AlterTable( - name: "shopping_carts", - comment: "用户购物车,按租户/门店隔离。"); - - migrationBuilder.AlterTable( - name: "reservations", - comment: "预约/预订记录。"); - - migrationBuilder.AlterTable( - name: "refund_requests", - comment: "售后/退款申请。"); - - migrationBuilder.AlterTable( - name: "queue_tickets", - comment: "排队叫号。"); - - migrationBuilder.AlterTable( - name: "promotion_campaigns", - comment: "营销活动配置。"); - - migrationBuilder.AlterTable( - name: "products", - comment: "商品(SPU)信息。"); - - migrationBuilder.AlterTable( - name: "product_skus", - comment: "商品 SKU,记录具体规格组合价格。"); - - migrationBuilder.AlterTable( - name: "product_pricing_rules", - comment: "商品价格策略,支持会员价/时段价等。"); - - migrationBuilder.AlterTable( - name: "product_media_assets", - comment: "商品媒资素材。"); - - migrationBuilder.AlterTable( - name: "product_categories", - comment: "商品分类。"); - - migrationBuilder.AlterTable( - name: "product_attribute_options", - comment: "商品规格选项。"); - - migrationBuilder.AlterTable( - name: "product_attribute_groups", - comment: "商品规格/属性分组。"); - - migrationBuilder.AlterTable( - name: "product_addon_options", - comment: "加料选项。"); - - migrationBuilder.AlterTable( - name: "product_addon_groups", - comment: "加料/做法分组。"); - - migrationBuilder.AlterTable( - name: "payment_refund_records", - comment: "支付渠道退款流水。"); - - migrationBuilder.AlterTable( - name: "payment_records", - comment: "支付流水。"); - - migrationBuilder.AlterTable( - name: "orders", - comment: "交易订单。"); - - migrationBuilder.AlterTable( - name: "order_status_histories", - comment: "订单状态流转记录。"); - - migrationBuilder.AlterTable( - name: "order_items", - comment: "订单明细。"); - - migrationBuilder.AlterTable( - name: "navigation_requests", - comment: "用户发起的导航请求日志。"); - - migrationBuilder.AlterTable( - name: "metric_snapshots", - comment: "指标快照,用于大盘展示。"); - - migrationBuilder.AlterTable( - name: "metric_definitions", - comment: "指标定义,描述可观测的数据点。"); - - migrationBuilder.AlterTable( - name: "metric_alert_rules", - comment: "指标告警规则。"); - - migrationBuilder.AlterTable( - name: "merchants", - comment: "商户主体信息,承载入驻和资质审核结果。"); - - migrationBuilder.AlterTable( - name: "merchant_staff", - comment: "商户员工账号,支持门店维度分配。"); - - migrationBuilder.AlterTable( - name: "merchant_documents", - comment: "商户提交的资质或证照材料。"); - - migrationBuilder.AlterTable( - name: "merchant_contracts", - comment: "商户合同记录。"); - - migrationBuilder.AlterTable( - name: "member_tiers", - comment: "会员等级定义。"); - - migrationBuilder.AlterTable( - name: "member_profiles", - comment: "会员档案。"); - - migrationBuilder.AlterTable( - name: "member_point_ledgers", - comment: "积分变动流水。"); - - migrationBuilder.AlterTable( - name: "member_growth_logs", - comment: "成长值变动日志。"); - - migrationBuilder.AlterTable( - name: "map_locations", - comment: "地图 POI 信息,用于门店定位和推荐。"); - - migrationBuilder.AlterTable( - name: "inventory_items", - comment: "SKU 在门店的库存信息。"); - - migrationBuilder.AlterTable( - name: "inventory_batches", - comment: "SKU 批次信息。"); - - migrationBuilder.AlterTable( - name: "inventory_adjustments", - comment: "库存调整记录。"); - - migrationBuilder.AlterTable( - name: "group_participants", - comment: "拼单参与者。"); - - migrationBuilder.AlterTable( - name: "group_orders", - comment: "拼单活动。"); - - migrationBuilder.AlterTable( - name: "delivery_orders", - comment: "配送单。"); - - migrationBuilder.AlterTable( - name: "delivery_events", - comment: "配送状态事件流水。"); - - migrationBuilder.AlterTable( - name: "coupons", - comment: "用户领取的券。"); - - migrationBuilder.AlterTable( - name: "coupon_templates", - comment: "优惠券模板。"); - - migrationBuilder.AlterTable( - name: "community_reactions", - comment: "社区互动反馈。"); - - migrationBuilder.AlterTable( - name: "community_posts", - comment: "社区动态。"); - - migrationBuilder.AlterTable( - name: "community_comments", - comment: "社区评论。"); - - migrationBuilder.AlterTable( - name: "checkout_sessions", - comment: "结账会话,记录校验上下文。"); - - migrationBuilder.AlterTable( - name: "checkin_records", - comment: "用户签到记录。"); - - migrationBuilder.AlterTable( - name: "checkin_campaigns", - comment: "签到活动配置。"); - - migrationBuilder.AlterTable( - name: "chat_sessions", - comment: "客服会话。"); - - migrationBuilder.AlterTable( - name: "chat_messages", - comment: "会话消息。"); - - migrationBuilder.AlterTable( - name: "cart_items", - comment: "购物车条目。"); - - migrationBuilder.AlterTable( - name: "cart_item_addons", - comment: "购物车条目的加料/附加项。"); - - migrationBuilder.AlterTable( - name: "affiliate_payouts", - comment: "佣金结算记录。"); - - migrationBuilder.AlterTable( - name: "affiliate_partners", - comment: "分销/推广合作伙伴。"); - - migrationBuilder.AlterTable( - name: "affiliate_orders", - comment: "分销订单记录。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "ticket_comments", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "ticket_comments", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "ticket_comments", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SupportTicketId", - table: "ticket_comments", - type: "uuid", - nullable: false, - comment: "工单标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "IsInternal", - table: "ticket_comments", - type: "boolean", - nullable: false, - comment: "是否内部备注。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "ticket_comments", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "ticket_comments", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "ticket_comments", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "ticket_comments", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "ticket_comments", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - comment: "评论内容。", - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024); - - migrationBuilder.AlterColumn( - name: "AuthorUserId", - table: "ticket_comments", - type: "uuid", - nullable: true, - comment: "评论人 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "AttachmentsJson", - table: "ticket_comments", - type: "text", - nullable: true, - comment: "附件 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "ticket_comments", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Website", - table: "tenants", - type: "text", - nullable: true, - comment: "官网或主要宣传链接。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenants", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Tags", - table: "tenants", - type: "text", - nullable: true, - comment: "业务标签集合(逗号分隔)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SuspensionReason", - table: "tenants", - type: "text", - nullable: true, - comment: "暂停或终止的原因说明。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SuspendedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次暂停服务时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Status", - table: "tenants", - type: "integer", - nullable: false, - comment: "租户当前状态,涵盖审核、启用、停用等场景。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ShortName", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "对外展示的简称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "tenants", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "备注信息,用于运营记录特殊说明。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Province", - table: "tenants", - type: "text", - nullable: true, - comment: "所在省份或州。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PrimaryOwnerUserId", - table: "tenants", - type: "uuid", - nullable: true, - comment: "系统内对应的租户所有者账号 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "tenants", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "租户全称或品牌名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "LogoUrl", - table: "tenants", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "LOGO 图片地址。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "LegalEntityName", - table: "tenants", - type: "text", - nullable: true, - comment: "法人或公司主体名称。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Industry", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "所属行业,如餐饮、零售等。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EffectiveTo", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - comment: "服务到期时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EffectiveFrom", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - comment: "服务生效时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenants", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenants", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CoverImageUrl", - table: "tenants", - type: "text", - nullable: true, - comment: "品牌海报或封面图。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Country", - table: "tenants", - type: "text", - nullable: true, - comment: "所在国家/地区。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ContactPhone", - table: "tenants", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "主联系人电话。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ContactName", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "主联系人姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ContactEmail", - table: "tenants", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "主联系人邮箱。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Code", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "租户短编码,作为跨系统引用的唯一标识。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "City", - table: "tenants", - type: "text", - nullable: true, - comment: "所在城市。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Address", - table: "tenants", - type: "text", - nullable: true, - comment: "详细地址信息。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenants", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantPackageId", - table: "tenant_subscriptions", - type: "uuid", - nullable: false, - comment: "当前订阅关联的套餐标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_subscriptions", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "tenant_subscriptions", - type: "integer", - nullable: false, - comment: "订阅当前状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ScheduledPackageId", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - comment: "若已排期升降配,对应的新套餐 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "tenant_subscriptions", - type: "text", - nullable: true, - comment: "运营备注信息。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "NextBillingDate", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: true, - comment: "下一个计费时间,配合自动续费使用。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EffectiveTo", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: false, - comment: "订阅到期时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "EffectiveFrom", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: false, - comment: "订阅生效时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AutoRenew", - table: "tenant_subscriptions", - type: "boolean", - nullable: false, - comment: "是否开启自动续费。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_subscriptions", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UsedValue", - table: "tenant_quota_usages", - type: "numeric", - nullable: false, - comment: "已消耗的数量。", - oldClrType: typeof(decimal), - oldType: "numeric"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_quota_usages", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_quota_usages", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ResetCycle", - table: "tenant_quota_usages", - type: "text", - nullable: true, - comment: "配额刷新周期描述(如月、年)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "QuotaType", - table: "tenant_quota_usages", - type: "integer", - nullable: false, - comment: "配额类型,例如门店数、短信条数等。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "LimitValue", - table: "tenant_quota_usages", - type: "numeric", - nullable: false, - comment: "当前配额上限。", - oldClrType: typeof(decimal), - oldType: "numeric"); - - migrationBuilder.AlterColumn( - name: "LastResetAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次重置时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_quota_usages", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_quota_usages", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_quota_usages", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "YearlyPrice", - table: "tenant_packages", - type: "numeric", - nullable: true, - comment: "年付价格,单位:人民币元。", - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_packages", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_packages", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PackageType", - table: "tenant_packages", - type: "integer", - nullable: false, - comment: "套餐分类(试用、标准、旗舰等)。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "tenant_packages", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "套餐名称,展示给租户的简称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "MonthlyPrice", - table: "tenant_packages", - type: "numeric", - nullable: true, - comment: "月付价格,单位:人民币元。", - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MaxStoreCount", - table: "tenant_packages", - type: "integer", - nullable: true, - comment: "允许的最大门店数。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MaxStorageGb", - table: "tenant_packages", - type: "integer", - nullable: true, - comment: "存储容量上限(GB)。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MaxSmsCredits", - table: "tenant_packages", - type: "integer", - nullable: true, - comment: "每月短信额度上限。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MaxDeliveryOrders", - table: "tenant_packages", - type: "integer", - nullable: true, - comment: "每月可调用的配送单数量上限。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MaxAccountCount", - table: "tenant_packages", - type: "integer", - nullable: true, - comment: "允许创建的最大账号数。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IsActive", - table: "tenant_packages", - type: "boolean", - nullable: false, - comment: "是否仍可售卖。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "FeaturePoliciesJson", - table: "tenant_packages", - type: "text", - nullable: true, - comment: "权益明细 JSON,记录自定义特性开关。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Description", - table: "tenant_packages", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "套餐描述,包含适用场景、权益等。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_packages", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_packages", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_packages", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_packages", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_packages", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_notifications", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Title", - table: "tenant_notifications", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "通知标题。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_notifications", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Severity", - table: "tenant_notifications", - type: "integer", - nullable: false, - comment: "通知重要级别。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SentAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: false, - comment: "推送时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ReadAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: true, - comment: "租户是否已阅读。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MetadataJson", - table: "tenant_notifications", - type: "text", - nullable: true, - comment: "附加元数据 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Message", - table: "tenant_notifications", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - comment: "通知正文。", - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_notifications", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_notifications", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Channel", - table: "tenant_notifications", - type: "integer", - nullable: false, - comment: "发布通道(站内、邮件、短信等)。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_notifications", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_billing_statements", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_billing_statements", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "tenant_billing_statements", - type: "integer", - nullable: false, - comment: "当前付款状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StatementNo", - table: "tenant_billing_statements", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "账单编号,供对账查询。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "PeriodStart", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - comment: "账单周期开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "PeriodEnd", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - comment: "账单周期结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "LineItemsJson", - table: "tenant_billing_statements", - type: "text", - nullable: true, - comment: "账单明细 JSON,记录各项费用。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DueDate", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - comment: "到期日。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_billing_statements", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_billing_statements", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AmountPaid", - table: "tenant_billing_statements", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "实付金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "AmountDue", - table: "tenant_billing_statements", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "应付金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_billing_statements", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "support_tickets", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TicketNo", - table: "support_tickets", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "工单编号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "support_tickets", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Subject", - table: "support_tickets", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "工单主题。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "Status", - table: "support_tickets", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Priority", - table: "support_tickets", - type: "integer", - nullable: false, - comment: "优先级。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "support_tickets", - type: "uuid", - nullable: true, - comment: "关联订单(如有)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Description", - table: "support_tickets", - type: "text", - nullable: false, - comment: "工单详情。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "support_tickets", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CustomerUserId", - table: "support_tickets", - type: "uuid", - nullable: false, - comment: "客户用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "support_tickets", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ClosedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "关闭时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "AssignedAgentId", - table: "support_tickets", - type: "uuid", - nullable: true, - comment: "指派的客服。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "support_tickets", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "stores", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "stores", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "stores", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Tags", - table: "stores", - type: "text", - nullable: true, - comment: "门店标签(逗号分隔)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SupportsReservation", - table: "stores", - type: "boolean", - nullable: false, - comment: "支持预约。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "SupportsQueueing", - table: "stores", - type: "boolean", - nullable: false, - comment: "支持排队叫号。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "SupportsPickup", - table: "stores", - type: "boolean", - nullable: false, - comment: "是否支持自提。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "SupportsDineIn", - table: "stores", - type: "boolean", - nullable: false, - comment: "是否支持堂食。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "SupportsDelivery", - table: "stores", - type: "boolean", - nullable: false, - comment: "是否支持配送。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "stores", - type: "integer", - nullable: false, - comment: "门店当前运营状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Province", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "所在省份。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Phone", - table: "stores", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "联系电话。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "stores", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "门店名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "stores", - type: "uuid", - nullable: false, - comment: "所属商户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ManagerName", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "门店负责人姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Longitude", - table: "stores", - type: "double precision", - nullable: true, - comment: "高德/腾讯地图经度。", - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Latitude", - table: "stores", - type: "double precision", - nullable: true, - comment: "纬度。", - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "District", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "区县信息。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Description", - table: "stores", - type: "text", - nullable: true, - comment: "门店描述或公告。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeliveryRadiusKm", - table: "stores", - type: "numeric(6,2)", - precision: 6, - scale: 2, - nullable: false, - comment: "默认配送半径(公里)。", - oldClrType: typeof(decimal), - oldType: "numeric(6,2)", - oldPrecision: 6, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "stores", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "stores", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "stores", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "stores", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CoverImageUrl", - table: "stores", - type: "text", - nullable: true, - comment: "门店海报。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Country", - table: "stores", - type: "text", - nullable: true, - comment: "所在国家或地区。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Code", - table: "stores", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "门店编码,便于扫码及外部对接。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "City", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "所在城市。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "BusinessHours", - table: "stores", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "门店营业时段描述(备用字符串)。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Announcement", - table: "stores", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "门店公告。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Address", - table: "stores", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "详细地址。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "stores", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_tables", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_tables", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_tables", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Tags", - table: "store_tables", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "桌台标签(堂食、快餐等)。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TableCode", - table: "store_tables", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "桌码。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_tables", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "store_tables", - type: "integer", - nullable: false, - comment: "当前桌台状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "QrCodeUrl", - table: "store_tables", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "桌码二维码地址。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_tables", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_tables", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_tables", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_tables", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Capacity", - table: "store_tables", - type: "integer", - nullable: false, - comment: "可容纳人数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "AreaId", - table: "store_tables", - type: "uuid", - nullable: true, - comment: "所在区域 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_tables", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_table_areas", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_table_areas", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_table_areas", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_table_areas", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "store_table_areas", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "区域名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Description", - table: "store_table_areas", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "区域描述。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_table_areas", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_table_areas", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_table_areas", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_table_areas", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_table_areas", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_holidays", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_holidays", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_holidays", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_holidays", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "store_holidays", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "说明内容。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IsClosed", - table: "store_holidays", - type: "boolean", - nullable: false, - comment: "是否全天闭店。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_holidays", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_holidays", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Date", - table: "store_holidays", - type: "timestamp with time zone", - nullable: false, - comment: "日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_holidays", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_holidays", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_holidays", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_employee_shifts", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StartTime", - table: "store_employee_shifts", - type: "interval", - nullable: false, - comment: "开始时间。", - oldClrType: typeof(TimeSpan), - oldType: "interval"); - - migrationBuilder.AlterColumn( - name: "StaffId", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - comment: "员工标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ShiftDate", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: false, - comment: "班次日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "RoleType", - table: "store_employee_shifts", - type: "integer", - nullable: false, - comment: "排班角色。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "store_employee_shifts", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EndTime", - table: "store_employee_shifts", - type: "interval", - nullable: false, - comment: "结束时间。", - oldClrType: typeof(TimeSpan), - oldType: "interval"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_employee_shifts", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_employee_shifts", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ZoneName", - table: "store_delivery_zones", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "区域名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_delivery_zones", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_delivery_zones", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_delivery_zones", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_delivery_zones", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "PolygonGeoJson", - table: "store_delivery_zones", - type: "text", - nullable: false, - comment: "GeoJSON 表示的多边形范围。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "MinimumOrderAmount", - table: "store_delivery_zones", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "起送价。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EstimatedMinutes", - table: "store_delivery_zones", - type: "integer", - nullable: true, - comment: "预计送达分钟。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeliveryFee", - table: "store_delivery_zones", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "配送费。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_delivery_zones", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_delivery_zones", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_delivery_zones", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_delivery_zones", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_delivery_zones", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_business_hours", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_business_hours", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_business_hours", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_business_hours", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StartTime", - table: "store_business_hours", - type: "interval", - nullable: false, - comment: "开始时间(本地时间)。", - oldClrType: typeof(TimeSpan), - oldType: "interval"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "store_business_hours", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "HourType", - table: "store_business_hours", - type: "integer", - nullable: false, - comment: "时段类型(正常营业、休息、预约等)。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "EndTime", - table: "store_business_hours", - type: "interval", - nullable: false, - comment: "结束时间(本地时间)。", - oldClrType: typeof(TimeSpan), - oldType: "interval"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_business_hours", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_business_hours", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DayOfWeek", - table: "store_business_hours", - type: "integer", - nullable: false, - comment: "星期几,0 表示周日。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_business_hours", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_business_hours", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CapacityLimit", - table: "store_business_hours", - type: "integer", - nullable: true, - comment: "最大接待容量或单量限制。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_business_hours", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "shopping_carts", - type: "uuid", - nullable: false, - comment: "用户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "shopping_carts", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "shopping_carts", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TableContext", - table: "shopping_carts", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "桌码或场景标识(扫码点餐)。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "shopping_carts", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "shopping_carts", - type: "integer", - nullable: false, - comment: "购物车状态,包含正常/锁定。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "LastModifiedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: false, - comment: "最近一次修改时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeliveryPreference", - table: "shopping_carts", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "履约方式(堂食/自提/配送)缓存。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "shopping_carts", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "shopping_carts", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "shopping_carts", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "reservations", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "reservations", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TablePreference", - table: "reservations", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "桌型/标签。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "reservations", - type: "uuid", - nullable: false, - comment: "门店。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "reservations", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ReservationTime", - table: "reservations", - type: "timestamp with time zone", - nullable: false, - comment: "预约时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ReservationNo", - table: "reservations", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "预约号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "reservations", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PeopleCount", - table: "reservations", - type: "integer", - nullable: false, - comment: "用餐人数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "reservations", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CustomerPhone", - table: "reservations", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "联系电话。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "CustomerName", - table: "reservations", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "客户姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "reservations", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "reservations", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CheckedInAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - comment: "实际签到时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CheckInCode", - table: "reservations", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "核销码/到店码。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - comment: "取消时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "reservations", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "refund_requests", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "refund_requests", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "refund_requests", - type: "integer", - nullable: false, - comment: "退款状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ReviewNotes", - table: "refund_requests", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "审核备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "RequestedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: false, - comment: "用户提交时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "RefundNo", - table: "refund_requests", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "退款单号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "refund_requests", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "申请原因。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "ProcessedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: true, - comment: "审核完成时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "refund_requests", - type: "uuid", - nullable: false, - comment: "关联订单标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "refund_requests", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "refund_requests", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "refund_requests", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "申请金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "Id", - table: "refund_requests", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "queue_tickets", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TicketNumber", - table: "queue_tickets", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "排队编号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "queue_tickets", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "queue_tickets", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "queue_tickets", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PartySize", - table: "queue_tickets", - type: "integer", - nullable: false, - comment: "就餐人数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ExpiredAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "过号时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EstimatedWaitMinutes", - table: "queue_tickets", - type: "integer", - nullable: true, - comment: "预计等待分钟。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "queue_tickets", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "queue_tickets", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "取消时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CalledAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - comment: "叫号时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "queue_tickets", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "promotion_campaigns", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "promotion_campaigns", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "promotion_campaigns", - type: "integer", - nullable: false, - comment: "活动状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StartAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: false, - comment: "开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "RulesJson", - table: "promotion_campaigns", - type: "text", - nullable: false, - comment: "活动规则 JSON。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "PromotionType", - table: "promotion_campaigns", - type: "integer", - nullable: false, - comment: "活动类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "promotion_campaigns", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "活动名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "EndAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: false, - comment: "结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "promotion_campaigns", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "promotion_campaigns", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Budget", - table: "promotion_campaigns", - type: "numeric", - nullable: true, - comment: "预算金额。", - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "BannerUrl", - table: "promotion_campaigns", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "营销素材(如 banner)。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "AudienceDescription", - table: "promotion_campaigns", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "目标人群描述。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "promotion_campaigns", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "products", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "products", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Unit", - table: "products", - type: "character varying(16)", - maxLength: 16, - nullable: true, - comment: "售卖单位(份/杯等)。", - oldClrType: typeof(string), - oldType: "character varying(16)", - oldMaxLength: 16, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "products", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Subtitle", - table: "products", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "副标题/卖点。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "products", - type: "uuid", - nullable: false, - comment: "所属门店。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StockQuantity", - table: "products", - type: "integer", - nullable: true, - comment: "库存数量(可选)。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Status", - table: "products", - type: "integer", - nullable: false, - comment: "商品状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SpuCode", - table: "products", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "商品编码。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "Price", - table: "products", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "现价。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "OriginalPrice", - table: "products", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "原价。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "products", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "商品名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "MaxQuantityPerOrder", - table: "products", - type: "integer", - nullable: true, - comment: "最大每单限购。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IsFeatured", - table: "products", - type: "boolean", - nullable: false, - comment: "是否热门推荐。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "GalleryImages", - table: "products", - type: "character varying(1024)", - maxLength: 1024, - nullable: true, - comment: "Gallery 图片逗号分隔。", - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EnablePickup", - table: "products", - type: "boolean", - nullable: false, - comment: "支持自提。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "EnableDineIn", - table: "products", - type: "boolean", - nullable: false, - comment: "支持堂食。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "EnableDelivery", - table: "products", - type: "boolean", - nullable: false, - comment: "支持配送。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "products", - type: "text", - nullable: true, - comment: "商品描述。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "products", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "products", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "products", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "products", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CoverImage", - table: "products", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "主图。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CategoryId", - table: "products", - type: "uuid", - nullable: false, - comment: "所属分类。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "products", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Weight", - table: "product_skus", - type: "numeric(10,3)", - precision: 10, - scale: 3, - nullable: true, - comment: "重量(千克)。", - oldClrType: typeof(decimal), - oldType: "numeric(10,3)", - oldPrecision: 10, - oldScale: 3, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_skus", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_skus", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_skus", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StockQuantity", - table: "product_skus", - type: "integer", - nullable: true, - comment: "可售库存。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SkuCode", - table: "product_skus", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "SKU 编码。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_skus", - type: "uuid", - nullable: false, - comment: "所属商品标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Price", - table: "product_skus", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "售价。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "OriginalPrice", - table: "product_skus", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "原价。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_skus", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_skus", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_skus", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_skus", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Barcode", - table: "product_skus", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "条形码。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "AttributesJson", - table: "product_skus", - type: "text", - nullable: false, - comment: "规格属性 JSON(记录选项 ID)。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_skus", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "WeekdaysJson", - table: "product_pricing_rules", - type: "text", - nullable: true, - comment: "生效星期(JSON 数组)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_pricing_rules", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_pricing_rules", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StartTime", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - comment: "生效开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "RuleType", - table: "product_pricing_rules", - type: "integer", - nullable: false, - comment: "策略类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_pricing_rules", - type: "uuid", - nullable: false, - comment: "所属商品。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Price", - table: "product_pricing_rules", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "特殊价格。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "EndTime", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - comment: "生效结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_pricing_rules", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_pricing_rules", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ConditionsJson", - table: "product_pricing_rules", - type: "text", - nullable: false, - comment: "条件描述(JSON),如会员等级、渠道等。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_pricing_rules", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Url", - table: "product_media_assets", - type: "character varying(512)", - maxLength: 512, - nullable: false, - comment: "媒资链接。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_media_assets", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_media_assets", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_media_assets", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_media_assets", - type: "integer", - nullable: false, - comment: "排序。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_media_assets", - type: "uuid", - nullable: false, - comment: "商品标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "MediaType", - table: "product_media_assets", - type: "integer", - nullable: false, - comment: "媒体类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_media_assets", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_media_assets", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_media_assets", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_media_assets", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Caption", - table: "product_media_assets", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "描述或标题。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_media_assets", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_categories", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_categories", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_categories", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "product_categories", - type: "uuid", - nullable: false, - comment: "所属门店。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_categories", - type: "integer", - nullable: false, - comment: "排序值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_categories", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "分类名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "IsEnabled", - table: "product_categories", - type: "boolean", - nullable: false, - comment: "是否启用。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "product_categories", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "分类描述。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_categories", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_categories", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_categories", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_categories", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_categories", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_attribute_options", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_attribute_options", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_attribute_options", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_attribute_options", - type: "integer", - nullable: false, - comment: "排序。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_attribute_options", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "选项名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "IsDefault", - table: "product_attribute_options", - type: "boolean", - nullable: false, - comment: "是否默认选中。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "ExtraPrice", - table: "product_attribute_options", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "附加价格。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_attribute_options", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_attribute_options", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_attribute_options", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_attribute_options", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AttributeGroupId", - table: "product_attribute_options", - type: "uuid", - nullable: false, - comment: "所属规格组。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_attribute_options", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_attribute_groups", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_attribute_groups", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - comment: "关联门店,可为空表示所有门店共享。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_attribute_groups", - type: "integer", - nullable: false, - comment: "显示排序。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SelectionType", - table: "product_attribute_groups", - type: "integer", - nullable: false, - comment: "选择类型(单选/多选)。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_attribute_groups", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "分组名称,例如“辣度”“份量”。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "IsRequired", - table: "product_attribute_groups", - type: "boolean", - nullable: false, - comment: "是否必选。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_attribute_groups", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_attribute_groups", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_attribute_groups", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_addon_options", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_addon_options", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_addon_options", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_addon_options", - type: "integer", - nullable: false, - comment: "排序。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_addon_options", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "选项名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "IsDefault", - table: "product_addon_options", - type: "boolean", - nullable: false, - comment: "是否默认选项。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "ExtraPrice", - table: "product_addon_options", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "附加价格。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_addon_options", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_addon_options", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_addon_options", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_addon_options", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AddonGroupId", - table: "product_addon_options", - type: "uuid", - nullable: false, - comment: "所属加料分组。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_addon_options", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_addon_groups", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_addon_groups", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_addon_groups", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_addon_groups", - type: "integer", - nullable: false, - comment: "排序值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SelectionType", - table: "product_addon_groups", - type: "integer", - nullable: false, - comment: "选择类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_addon_groups", - type: "uuid", - nullable: false, - comment: "所属商品。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_addon_groups", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "分组名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "MinSelect", - table: "product_addon_groups", - type: "integer", - nullable: true, - comment: "最小选择数量。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MaxSelect", - table: "product_addon_groups", - type: "integer", - nullable: true, - comment: "最大选择数量。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IsRequired", - table: "product_addon_groups", - type: "boolean", - nullable: false, - comment: "是否必选。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_addon_groups", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_addon_groups", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_addon_groups", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_addon_groups", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_addon_groups", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "payment_refund_records", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "payment_refund_records", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "payment_refund_records", - type: "integer", - nullable: false, - comment: "退款状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "RequestedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: false, - comment: "退款请求时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "PaymentRecordId", - table: "payment_refund_records", - type: "uuid", - nullable: false, - comment: "原支付记录标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Payload", - table: "payment_refund_records", - type: "text", - nullable: true, - comment: "渠道返回的原始数据 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "payment_refund_records", - type: "uuid", - nullable: false, - comment: "关联订单标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "payment_refund_records", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "payment_refund_records", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CompletedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: true, - comment: "完成时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ChannelRefundId", - table: "payment_refund_records", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "渠道退款流水号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "payment_refund_records", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "退款金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "Id", - table: "payment_refund_records", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "payment_records", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TradeNo", - table: "payment_records", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "平台交易号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "payment_records", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "payment_records", - type: "integer", - nullable: false, - comment: "支付状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "payment_records", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "错误/备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Payload", - table: "payment_records", - type: "text", - nullable: true, - comment: "原始回调内容。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PaidAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: true, - comment: "支付完成时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "payment_records", - type: "uuid", - nullable: false, - comment: "关联订单。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Method", - table: "payment_records", - type: "integer", - nullable: false, - comment: "支付方式。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "payment_records", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "payment_records", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ChannelTransactionId", - table: "payment_records", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "第三方渠道单号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "payment_records", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "支付金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "Id", - table: "payment_records", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "orders", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "orders", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TableNo", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "就餐桌号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "orders", - type: "uuid", - nullable: false, - comment: "门店。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "orders", - type: "integer", - nullable: false, - comment: "当前状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ReservationId", - table: "orders", - type: "uuid", - nullable: true, - comment: "预约 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "orders", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "QueueNumber", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "排队号(如有)。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PaymentStatus", - table: "orders", - type: "integer", - nullable: false, - comment: "支付状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "PayableAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "应付金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "PaidAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - comment: "支付时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "PaidAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "实付金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "OrderNo", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "订单号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "ItemsAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "商品总额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "FinishedAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - comment: "完成时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DiscountAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "优惠金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "DeliveryType", - table: "orders", - type: "integer", - nullable: false, - comment: "履约类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "orders", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CustomerPhone", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "顾客手机号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CustomerName", - table: "orders", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "顾客姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "orders", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "orders", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Channel", - table: "orders", - type: "integer", - nullable: false, - comment: "下单渠道。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - comment: "取消时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CancelReason", - table: "orders", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "取消原因。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "orders", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "order_status_histories", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "order_status_histories", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "order_status_histories", - type: "integer", - nullable: false, - comment: "变更后的状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "order_status_histories", - type: "uuid", - nullable: false, - comment: "订单标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "OperatorId", - table: "order_status_histories", - type: "uuid", - nullable: true, - comment: "操作人标识(可为空表示系统)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: false, - comment: "发生时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "order_status_histories", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "备注信息。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "order_status_histories", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "order_status_histories", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "order_status_histories", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "order_items", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "order_items", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UnitPrice", - table: "order_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "单价。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "Unit", - table: "order_items", - type: "character varying(16)", - maxLength: 16, - nullable: true, - comment: "单位。", - oldClrType: typeof(string), - oldType: "character varying(16)", - oldMaxLength: 16, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "order_items", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SubTotal", - table: "order_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "小计。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "SkuName", - table: "order_items", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "SKU/规格描述。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "order_items", - type: "integer", - nullable: false, - comment: "数量。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductName", - table: "order_items", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "商品名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "order_items", - type: "uuid", - nullable: false, - comment: "商品 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "order_items", - type: "uuid", - nullable: false, - comment: "订单 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DiscountAmount", - table: "order_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "折扣金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "order_items", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "order_items", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "order_items", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "order_items", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AttributesJson", - table: "order_items", - type: "text", - nullable: true, - comment: "自定义属性 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "order_items", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "navigation_requests", - type: "uuid", - nullable: false, - comment: "用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "navigation_requests", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "navigation_requests", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TargetApp", - table: "navigation_requests", - type: "integer", - nullable: false, - comment: "跳转的地图应用。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "navigation_requests", - type: "uuid", - nullable: false, - comment: "门店 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "RequestedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: false, - comment: "请求时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "navigation_requests", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "navigation_requests", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Channel", - table: "navigation_requests", - type: "integer", - nullable: false, - comment: "来源通道(小程序、H5 等)。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "navigation_requests", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "WindowStart", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: false, - comment: "统计时间窗口开始。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "WindowEnd", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: false, - comment: "统计时间窗口结束。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Value", - table: "metric_snapshots", - type: "numeric(18,4)", - precision: 18, - scale: 4, - nullable: false, - comment: "数值。", - oldClrType: typeof(decimal), - oldType: "numeric(18,4)", - oldPrecision: 18, - oldScale: 4); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "metric_snapshots", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "metric_snapshots", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "MetricDefinitionId", - table: "metric_snapshots", - type: "uuid", - nullable: false, - comment: "指标定义 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DimensionKey", - table: "metric_snapshots", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "维度键(JSON)。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "metric_snapshots", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "metric_snapshots", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "metric_snapshots", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "metric_definitions", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "metric_definitions", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "metric_definitions", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "metric_definitions", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "指标名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "DimensionsJson", - table: "metric_definitions", - type: "text", - nullable: true, - comment: "维度描述 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Description", - table: "metric_definitions", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "说明。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "metric_definitions", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "metric_definitions", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DefaultAggregation", - table: "metric_definitions", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "默认聚合方式。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "metric_definitions", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "metric_definitions", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "metric_definitions", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "指标编码。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Id", - table: "metric_definitions", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "metric_alert_rules", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "metric_alert_rules", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "metric_alert_rules", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Severity", - table: "metric_alert_rules", - type: "integer", - nullable: false, - comment: "告警级别。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "NotificationChannels", - table: "metric_alert_rules", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "通知渠道。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "MetricDefinitionId", - table: "metric_alert_rules", - type: "uuid", - nullable: false, - comment: "关联指标。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Enabled", - table: "metric_alert_rules", - type: "boolean", - nullable: false, - comment: "是否启用。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "metric_alert_rules", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "metric_alert_rules", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "metric_alert_rules", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "metric_alert_rules", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ConditionJson", - table: "metric_alert_rules", - type: "text", - nullable: false, - comment: "触发条件 JSON。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "metric_alert_rules", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchants", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchants", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TaxNumber", - table: "merchants", - type: "text", - nullable: true, - comment: "税号/统一社会信用代码。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SupportEmail", - table: "merchants", - type: "text", - nullable: true, - comment: "客服邮箱。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchants", - type: "integer", - nullable: false, - comment: "入驻状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ServicePhone", - table: "merchants", - type: "text", - nullable: true, - comment: "客服电话。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ReviewRemarks", - table: "merchants", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "审核备注或驳回原因。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Province", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "所在省份。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Longitude", - table: "merchants", - type: "double precision", - nullable: true, - comment: "经度信息。", - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "LogoUrl", - table: "merchants", - type: "text", - nullable: true, - comment: "品牌 Logo。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "LegalPerson", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "法人或负责人姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Latitude", - table: "merchants", - type: "double precision", - nullable: true, - comment: "纬度信息。", - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "LastReviewedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次审核时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "JoinedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - comment: "入驻时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "District", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "所在区县。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchants", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchants", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ContactPhone", - table: "merchants", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "联系电话。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "ContactEmail", - table: "merchants", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "联系邮箱。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "City", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "所在城市。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Category", - table: "merchants", - type: "text", - nullable: true, - comment: "品牌所属品类,如火锅、咖啡等。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "BusinessLicenseNumber", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "营业执照号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "BusinessLicenseImageUrl", - table: "merchants", - type: "text", - nullable: true, - comment: "营业执照扫描件地址。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "BrandName", - table: "merchants", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "品牌名称(对外展示)。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "BrandAlias", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "品牌简称或别名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Address", - table: "merchants", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "详细地址。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchants", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchant_staff", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchant_staff", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchant_staff", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "merchant_staff", - type: "uuid", - nullable: true, - comment: "可选的关联门店 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchant_staff", - type: "integer", - nullable: false, - comment: "员工状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "RoleType", - table: "merchant_staff", - type: "integer", - nullable: false, - comment: "员工角色类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Phone", - table: "merchant_staff", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "手机号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "PermissionsJson", - table: "merchant_staff", - type: "text", - nullable: true, - comment: "自定义权限(JSON)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "merchant_staff", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "员工姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "merchant_staff", - type: "uuid", - nullable: false, - comment: "所属商户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "IdentityUserId", - table: "merchant_staff", - type: "uuid", - nullable: true, - comment: "登录账号 ID(指向统一身份体系)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Email", - table: "merchant_staff", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "邮箱地址。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchant_staff", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchant_staff", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchant_staff", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchant_staff", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchant_staff", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchant_documents", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchant_documents", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchant_documents", - type: "integer", - nullable: false, - comment: "审核状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "merchant_documents", - type: "text", - nullable: true, - comment: "审核备注或驳回原因。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "merchant_documents", - type: "uuid", - nullable: false, - comment: "所属商户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "IssuedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - comment: "签发日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "FileUrl", - table: "merchant_documents", - type: "character varying(512)", - maxLength: 512, - nullable: false, - comment: "证照文件链接。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512); - - migrationBuilder.AlterColumn( - name: "ExpiresAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - comment: "到期日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DocumentType", - table: "merchant_documents", - type: "integer", - nullable: false, - comment: "证照类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DocumentNumber", - table: "merchant_documents", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "证照编号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchant_documents", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchant_documents", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchant_documents", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchant_contracts", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TerminationReason", - table: "merchant_contracts", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "终止原因。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TerminatedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - comment: "终止时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchant_contracts", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchant_contracts", - type: "integer", - nullable: false, - comment: "合同状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StartDate", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: false, - comment: "合同开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "SignedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - comment: "签署时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "merchant_contracts", - type: "uuid", - nullable: false, - comment: "所属商户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "FileUrl", - table: "merchant_contracts", - type: "character varying(512)", - maxLength: 512, - nullable: false, - comment: "合同文件存储地址。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512); - - migrationBuilder.AlterColumn( - name: "EndDate", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: false, - comment: "合同结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchant_contracts", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchant_contracts", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ContractNumber", - table: "merchant_contracts", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "合同编号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchant_contracts", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_tiers", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_tiers", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_tiers", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "member_tiers", - type: "integer", - nullable: false, - comment: "排序值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "RequiredGrowth", - table: "member_tiers", - type: "integer", - nullable: false, - comment: "所需成长值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "member_tiers", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "等级名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_tiers", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_tiers", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_tiers", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_tiers", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "BenefitsJson", - table: "member_tiers", - type: "text", - nullable: false, - comment: "等级权益(JSON)。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_tiers", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "member_profiles", - type: "uuid", - nullable: false, - comment: "用户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_profiles", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_profiles", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "member_profiles", - type: "integer", - nullable: false, - comment: "会员状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "PointsBalance", - table: "member_profiles", - type: "integer", - nullable: false, - comment: "会员积分余额。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Nickname", - table: "member_profiles", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "昵称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Mobile", - table: "member_profiles", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "手机号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "MemberTierId", - table: "member_profiles", - type: "uuid", - nullable: true, - comment: "当前会员等级 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "JoinedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: false, - comment: "注册时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "GrowthValue", - table: "member_profiles", - type: "integer", - nullable: false, - comment: "成长值/经验值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_profiles", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_profiles", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "BirthDate", - table: "member_profiles", - type: "timestamp with time zone", - nullable: true, - comment: "生日。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "AvatarUrl", - table: "member_profiles", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "头像。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_profiles", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_point_ledgers", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SourceId", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - comment: "来源 ID(订单、活动等)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "member_point_ledgers", - type: "integer", - nullable: false, - comment: "变动原因。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: false, - comment: "发生时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "MemberId", - table: "member_point_ledgers", - type: "uuid", - nullable: false, - comment: "会员标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ExpireAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: true, - comment: "过期时间(如适用)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ChangeAmount", - table: "member_point_ledgers", - type: "integer", - nullable: false, - comment: "变动数量,可为负值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "BalanceAfterChange", - table: "member_point_ledgers", - type: "integer", - nullable: false, - comment: "变动后余额。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_point_ledgers", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_growth_logs", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_growth_logs", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: false, - comment: "发生时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "member_growth_logs", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MemberId", - table: "member_growth_logs", - type: "uuid", - nullable: false, - comment: "会员标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_growth_logs", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CurrentValue", - table: "member_growth_logs", - type: "integer", - nullable: false, - comment: "当前成长值。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_growth_logs", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ChangeValue", - table: "member_growth_logs", - type: "integer", - nullable: false, - comment: "变动数量。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_growth_logs", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "map_locations", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "map_locations", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "map_locations", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "map_locations", - type: "uuid", - nullable: true, - comment: "关联门店 ID,可空表示独立 POI。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "map_locations", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "Longitude", - table: "map_locations", - type: "double precision", - nullable: false, - comment: "经度。", - oldClrType: typeof(double), - oldType: "double precision"); - - migrationBuilder.AlterColumn( - name: "Latitude", - table: "map_locations", - type: "double precision", - nullable: false, - comment: "纬度。", - oldClrType: typeof(double), - oldType: "double precision"); - - migrationBuilder.AlterColumn( - name: "Landmark", - table: "map_locations", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "打车/导航落点描述。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "map_locations", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "map_locations", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "map_locations", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "map_locations", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Address", - table: "map_locations", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "地址。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "Id", - table: "map_locations", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "inventory_items", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "inventory_items", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "inventory_items", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "inventory_items", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SafetyStock", - table: "inventory_items", - type: "integer", - nullable: true, - comment: "安全库存阈值。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "QuantityReserved", - table: "inventory_items", - type: "integer", - nullable: false, - comment: "已锁定库存(订单占用)。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "QuantityOnHand", - table: "inventory_items", - type: "integer", - nullable: false, - comment: "可用库存。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductSkuId", - table: "inventory_items", - type: "uuid", - nullable: false, - comment: "SKU 标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Location", - table: "inventory_items", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "储位或仓位信息。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ExpireDate", - table: "inventory_items", - type: "timestamp with time zone", - nullable: true, - comment: "过期日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "inventory_items", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "inventory_items", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "inventory_items", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "inventory_items", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "BatchNumber", - table: "inventory_items", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "批次编号,可为空表示混批。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "inventory_items", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "inventory_batches", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "inventory_batches", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "inventory_batches", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "RemainingQuantity", - table: "inventory_batches", - type: "integer", - nullable: false, - comment: "剩余数量。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "inventory_batches", - type: "integer", - nullable: false, - comment: "入库数量。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductionDate", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - comment: "生产日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ProductSkuId", - table: "inventory_batches", - type: "uuid", - nullable: false, - comment: "SKU 标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ExpireDate", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - comment: "过期日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "inventory_batches", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "inventory_batches", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "BatchNumber", - table: "inventory_batches", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "批次编号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Id", - table: "inventory_batches", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "inventory_adjustments", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "inventory_adjustments", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "原因说明。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "inventory_adjustments", - type: "integer", - nullable: false, - comment: "调整数量,正数增加,负数减少。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "OperatorId", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - comment: "操作人标识。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: false, - comment: "发生时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "InventoryItemId", - table: "inventory_adjustments", - type: "uuid", - nullable: false, - comment: "对应的库存记录标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AdjustmentType", - table: "inventory_adjustments", - type: "integer", - nullable: false, - comment: "调整类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "inventory_adjustments", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "group_participants", - type: "uuid", - nullable: false, - comment: "用户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "group_participants", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "group_participants", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "group_participants", - type: "integer", - nullable: false, - comment: "参与状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "group_participants", - type: "uuid", - nullable: false, - comment: "对应订单标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "JoinedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: false, - comment: "参与时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "GroupOrderId", - table: "group_participants", - type: "uuid", - nullable: false, - comment: "拼单活动标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "group_participants", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "group_participants", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "group_participants", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "group_orders", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "group_orders", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "TargetCount", - table: "group_orders", - type: "integer", - nullable: false, - comment: "成团需要的人数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SucceededAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - comment: "成团时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "group_orders", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "group_orders", - type: "integer", - nullable: false, - comment: "拼团状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StartAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: false, - comment: "开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "group_orders", - type: "uuid", - nullable: false, - comment: "关联商品或套餐。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "LeaderUserId", - table: "group_orders", - type: "uuid", - nullable: false, - comment: "团长用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "GroupPrice", - table: "group_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "拼团价格。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "GroupOrderNo", - table: "group_orders", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "拼单编号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "EndAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: false, - comment: "结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "group_orders", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CurrentCount", - table: "group_orders", - type: "integer", - nullable: false, - comment: "当前已参与人数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "group_orders", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - comment: "取消时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "group_orders", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "delivery_orders", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "delivery_orders", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "delivery_orders", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProviderOrderId", - table: "delivery_orders", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "第三方配送单号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Provider", - table: "delivery_orders", - type: "integer", - nullable: false, - comment: "配送服务商。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "PickedUpAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - comment: "取餐时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "FailureReason", - table: "delivery_orders", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "异常原因。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DispatchedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - comment: "下发时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeliveryFee", - table: "delivery_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - comment: "配送费。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeliveredAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - comment: "完成时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "delivery_orders", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "delivery_orders", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CourierPhone", - table: "delivery_orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "骑手电话。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CourierName", - table: "delivery_orders", - type: "character varying(64)", - maxLength: 64, - nullable: true, - comment: "骑手姓名。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "delivery_orders", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "delivery_events", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "delivery_events", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Payload", - table: "delivery_events", - type: "text", - nullable: true, - comment: "原始数据 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: false, - comment: "发生时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Message", - table: "delivery_events", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "事件描述。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "EventType", - table: "delivery_events", - type: "integer", - nullable: false, - comment: "事件类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeliveryOrderId", - table: "delivery_events", - type: "uuid", - nullable: false, - comment: "配送单标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "delivery_events", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "delivery_events", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "delivery_events", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "coupons", - type: "uuid", - nullable: false, - comment: "归属用户。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UsedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: true, - comment: "使用时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "coupons", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "coupons", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "coupons", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "coupons", - type: "uuid", - nullable: true, - comment: "订单 ID(已使用时记录)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IssuedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: false, - comment: "发放时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ExpireAt", - table: "coupons", - type: "timestamp with time zone", - nullable: false, - comment: "到期时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "coupons", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "coupons", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CouponTemplateId", - table: "coupons", - type: "uuid", - nullable: false, - comment: "模板标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "coupons", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "券码或序列号。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "Id", - table: "coupons", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Value", - table: "coupon_templates", - type: "numeric", - nullable: false, - comment: "面值或折扣额度。", - oldClrType: typeof(decimal), - oldType: "numeric"); - - migrationBuilder.AlterColumn( - name: "ValidTo", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - comment: "可用结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ValidFrom", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - comment: "可用开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "coupon_templates", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TotalQuantity", - table: "coupon_templates", - type: "integer", - nullable: true, - comment: "总发放数量上限。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "coupon_templates", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreScopeJson", - table: "coupon_templates", - type: "text", - nullable: true, - comment: "适用门店 ID 集合(JSON)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Status", - table: "coupon_templates", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "RelativeValidDays", - table: "coupon_templates", - type: "integer", - nullable: true, - comment: "有效天数(相对发放时间)。", - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ProductScopeJson", - table: "coupon_templates", - type: "text", - nullable: true, - comment: "适用品类或商品范围(JSON)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "coupon_templates", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "模板名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "MinimumSpend", - table: "coupon_templates", - type: "numeric", - nullable: true, - comment: "最低消费门槛。", - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DiscountCap", - table: "coupon_templates", - type: "numeric", - nullable: true, - comment: "折扣上限(针对折扣券)。", - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Description", - table: "coupon_templates", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "coupon_templates", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "coupon_templates", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CouponType", - table: "coupon_templates", - type: "integer", - nullable: false, - comment: "券类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ClaimedQuantity", - table: "coupon_templates", - type: "integer", - nullable: false, - comment: "已领取数量。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ChannelsJson", - table: "coupon_templates", - type: "text", - nullable: true, - comment: "发放渠道(JSON)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "AllowStack", - table: "coupon_templates", - type: "boolean", - nullable: false, - comment: "是否允许叠加其他优惠。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "coupon_templates", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "community_reactions", - type: "uuid", - nullable: false, - comment: "用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "community_reactions", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "community_reactions", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ReactionType", - table: "community_reactions", - type: "integer", - nullable: false, - comment: "反应类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ReactedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: false, - comment: "时间戳。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "PostId", - table: "community_reactions", - type: "uuid", - nullable: false, - comment: "动态 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "community_reactions", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "community_reactions", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "community_reactions", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "community_posts", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "community_posts", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Title", - table: "community_posts", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "标题。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "community_posts", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "community_posts", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "MediaJson", - table: "community_posts", - type: "text", - nullable: true, - comment: "媒体资源 JSON。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "LikeCount", - table: "community_posts", - type: "integer", - nullable: false, - comment: "点赞数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "community_posts", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "community_posts", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "community_posts", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "community_posts", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "community_posts", - type: "text", - nullable: false, - comment: "内容。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "CommentCount", - table: "community_posts", - type: "integer", - nullable: false, - comment: "评论数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "AuthorUserId", - table: "community_posts", - type: "uuid", - nullable: false, - comment: "作者用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "community_posts", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "community_comments", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "community_comments", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "community_comments", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "PostId", - table: "community_comments", - type: "uuid", - nullable: false, - comment: "动态标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ParentId", - table: "community_comments", - type: "uuid", - nullable: true, - comment: "父级评论 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IsDeleted", - table: "community_comments", - type: "boolean", - nullable: false, - comment: "状态。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "community_comments", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "community_comments", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "community_comments", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "community_comments", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "community_comments", - type: "character varying(512)", - maxLength: 512, - nullable: false, - comment: "评论内容。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512); - - migrationBuilder.AlterColumn( - name: "AuthorUserId", - table: "community_comments", - type: "uuid", - nullable: false, - comment: "评论人。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "community_comments", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "ValidationResultJson", - table: "checkout_sessions", - type: "text", - nullable: false, - comment: "校验结果明细 JSON。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "checkout_sessions", - type: "uuid", - nullable: false, - comment: "用户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "checkout_sessions", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "checkout_sessions", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "checkout_sessions", - type: "uuid", - nullable: false, - comment: "门店标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "checkout_sessions", - type: "integer", - nullable: false, - comment: "会话状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SessionToken", - table: "checkout_sessions", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "会话 Token。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "ExpiresAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: false, - comment: "过期时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "checkout_sessions", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "checkout_sessions", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "checkout_sessions", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "checkin_records", - type: "uuid", - nullable: false, - comment: "用户标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "checkin_records", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "checkin_records", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "checkin_records", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "RewardJson", - table: "checkin_records", - type: "text", - nullable: false, - comment: "获得奖励 JSON。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "IsMakeup", - table: "checkin_records", - type: "boolean", - nullable: false, - comment: "是否补签。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "checkin_records", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "checkin_records", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "checkin_records", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "checkin_records", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CheckInDate", - table: "checkin_records", - type: "timestamp with time zone", - nullable: false, - comment: "签到日期(本地)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CheckInCampaignId", - table: "checkin_records", - type: "uuid", - nullable: false, - comment: "活动标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "checkin_records", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "checkin_campaigns", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "checkin_campaigns", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "checkin_campaigns", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StartDate", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: false, - comment: "开始日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "RewardsJson", - table: "checkin_campaigns", - type: "text", - nullable: false, - comment: "连签奖励 JSON。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "checkin_campaigns", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "活动名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "EndDate", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: false, - comment: "结束日期。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "checkin_campaigns", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "活动描述。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "checkin_campaigns", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "checkin_campaigns", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AllowMakeupCount", - table: "checkin_campaigns", - type: "integer", - nullable: false, - comment: "支持补签次数。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "checkin_campaigns", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "chat_sessions", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "chat_sessions", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "chat_sessions", - type: "uuid", - nullable: true, - comment: "所属门店(可空为平台)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Status", - table: "chat_sessions", - type: "integer", - nullable: false, - comment: "会话状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "StartedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: false, - comment: "开始时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "SessionCode", - table: "chat_sessions", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "会话编号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "IsBotActive", - table: "chat_sessions", - type: "boolean", - nullable: false, - comment: "是否机器人接待中。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "EndedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: true, - comment: "结束时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "chat_sessions", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CustomerUserId", - table: "chat_sessions", - type: "uuid", - nullable: false, - comment: "顾客用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "chat_sessions", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AgentUserId", - table: "chat_sessions", - type: "uuid", - nullable: true, - comment: "当前客服员工 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "chat_sessions", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "chat_messages", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "chat_messages", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SenderUserId", - table: "chat_messages", - type: "uuid", - nullable: true, - comment: "发送方用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "SenderType", - table: "chat_messages", - type: "integer", - nullable: false, - comment: "发送方类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ReadAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: true, - comment: "读取时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "IsRead", - table: "chat_messages", - type: "boolean", - nullable: false, - comment: "是否已读。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "chat_messages", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "chat_messages", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "ContentType", - table: "chat_messages", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "消息类型(文字/图片/语音等)。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Content", - table: "chat_messages", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - comment: "消息内容。", - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024); - - migrationBuilder.AlterColumn( - name: "ChatSessionId", - table: "chat_messages", - type: "uuid", - nullable: false, - comment: "会话标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "chat_messages", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "cart_items", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "cart_items", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UnitPrice", - table: "cart_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "单价快照。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "cart_items", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "cart_items", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ShoppingCartId", - table: "cart_items", - type: "uuid", - nullable: false, - comment: "所属购物车标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "cart_items", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "自定义备注(口味要求)。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "cart_items", - type: "integer", - nullable: false, - comment: "数量。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "ProductSkuId", - table: "cart_items", - type: "uuid", - nullable: true, - comment: "SKU 标识。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ProductName", - table: "cart_items", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "商品名称快照。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "cart_items", - type: "uuid", - nullable: false, - comment: "商品或 SKU 标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "cart_items", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "cart_items", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "cart_items", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "cart_items", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "AttributesJson", - table: "cart_items", - type: "text", - nullable: true, - comment: "扩展 JSON(规格、加料选项等)。", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "cart_items", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "cart_item_addons", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "cart_item_addons", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "cart_item_addons", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "OptionId", - table: "cart_item_addons", - type: "uuid", - nullable: true, - comment: "选项 ID(可对应 ProductAddonOption)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Name", - table: "cart_item_addons", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "选项名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "ExtraPrice", - table: "cart_item_addons", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "附加价格。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "cart_item_addons", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "cart_item_addons", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "cart_item_addons", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "cart_item_addons", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CartItemId", - table: "cart_item_addons", - type: "uuid", - nullable: false, - comment: "所属购物车条目。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "cart_item_addons", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "affiliate_payouts", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "affiliate_payouts", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "affiliate_payouts", - type: "integer", - nullable: false, - comment: "状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "affiliate_payouts", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Period", - table: "affiliate_payouts", - type: "character varying(32)", - maxLength: 32, - nullable: false, - comment: "结算周期描述。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32); - - migrationBuilder.AlterColumn( - name: "PaidAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: true, - comment: "打款时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "affiliate_payouts", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "affiliate_payouts", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "affiliate_payouts", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "结算金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "AffiliatePartnerId", - table: "affiliate_payouts", - type: "uuid", - nullable: false, - comment: "合作伙伴标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "affiliate_payouts", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "affiliate_partners", - type: "uuid", - nullable: true, - comment: "用户 ID(如绑定平台账号)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "affiliate_partners", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "affiliate_partners", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "affiliate_partners", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "affiliate_partners", - type: "integer", - nullable: false, - comment: "当前状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "affiliate_partners", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "审核备注。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Phone", - table: "affiliate_partners", - type: "character varying(32)", - maxLength: 32, - nullable: true, - comment: "联系电话。", - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DisplayName", - table: "affiliate_partners", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "昵称或渠道名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "affiliate_partners", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "affiliate_partners", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "affiliate_partners", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "affiliate_partners", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "CommissionRate", - table: "affiliate_partners", - type: "numeric", - nullable: false, - comment: "分成比例(0-1)。", - oldClrType: typeof(decimal), - oldType: "numeric"); - - migrationBuilder.AlterColumn( - name: "ChannelType", - table: "affiliate_partners", - type: "integer", - nullable: false, - comment: "渠道类型。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "affiliate_partners", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "affiliate_orders", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "affiliate_orders", - type: "integer", - nullable: false, - comment: "当前状态。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "SettledAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: true, - comment: "结算完成时间。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - comment: "关联订单。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "OrderAmount", - table: "affiliate_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "订单金额。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "EstimatedCommission", - table: "affiliate_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - comment: "预计佣金。", - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "affiliate_orders", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "affiliate_orders", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "BuyerUserId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - comment: "用户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "AffiliatePartnerId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - comment: "推广人标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "affiliate_orders", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterTable( - name: "ticket_comments", - oldComment: "工单评论/流转记录。"); - - migrationBuilder.AlterTable( - name: "tenants", - oldComment: "平台租户信息,描述租户的生命周期与基础资料。"); - - migrationBuilder.AlterTable( - name: "tenant_subscriptions", - oldComment: "租户套餐订阅记录。"); - - migrationBuilder.AlterTable( - name: "tenant_quota_usages", - oldComment: "租户配额使用情况快照。"); - - migrationBuilder.AlterTable( - name: "tenant_packages", - oldComment: "平台提供的租户套餐定义。"); - - migrationBuilder.AlterTable( - name: "tenant_notifications", - oldComment: "面向租户的站内通知或消息推送。"); - - migrationBuilder.AlterTable( - name: "tenant_billing_statements", - oldComment: "租户账单,用于呈现周期性收费。"); - - migrationBuilder.AlterTable( - name: "support_tickets", - oldComment: "客服工单。"); - - migrationBuilder.AlterTable( - name: "stores", - oldComment: "门店信息,承载营业配置与能力。"); - - migrationBuilder.AlterTable( - name: "store_tables", - oldComment: "桌台信息与二维码绑定。"); - - migrationBuilder.AlterTable( - name: "store_table_areas", - oldComment: "门店桌台区域配置。"); - - migrationBuilder.AlterTable( - name: "store_holidays", - oldComment: "门店休息日或特殊营业日。"); - - migrationBuilder.AlterTable( - name: "store_employee_shifts", - oldComment: "门店员工排班记录。"); - - migrationBuilder.AlterTable( - name: "store_delivery_zones", - oldComment: "门店配送范围配置。"); - - migrationBuilder.AlterTable( - name: "store_business_hours", - oldComment: "门店营业时段配置。"); - - migrationBuilder.AlterTable( - name: "shopping_carts", - oldComment: "用户购物车,按租户/门店隔离。"); - - migrationBuilder.AlterTable( - name: "reservations", - oldComment: "预约/预订记录。"); - - migrationBuilder.AlterTable( - name: "refund_requests", - oldComment: "售后/退款申请。"); - - migrationBuilder.AlterTable( - name: "queue_tickets", - oldComment: "排队叫号。"); - - migrationBuilder.AlterTable( - name: "promotion_campaigns", - oldComment: "营销活动配置。"); - - migrationBuilder.AlterTable( - name: "products", - oldComment: "商品(SPU)信息。"); - - migrationBuilder.AlterTable( - name: "product_skus", - oldComment: "商品 SKU,记录具体规格组合价格。"); - - migrationBuilder.AlterTable( - name: "product_pricing_rules", - oldComment: "商品价格策略,支持会员价/时段价等。"); - - migrationBuilder.AlterTable( - name: "product_media_assets", - oldComment: "商品媒资素材。"); - - migrationBuilder.AlterTable( - name: "product_categories", - oldComment: "商品分类。"); - - migrationBuilder.AlterTable( - name: "product_attribute_options", - oldComment: "商品规格选项。"); - - migrationBuilder.AlterTable( - name: "product_attribute_groups", - oldComment: "商品规格/属性分组。"); - - migrationBuilder.AlterTable( - name: "product_addon_options", - oldComment: "加料选项。"); - - migrationBuilder.AlterTable( - name: "product_addon_groups", - oldComment: "加料/做法分组。"); - - migrationBuilder.AlterTable( - name: "payment_refund_records", - oldComment: "支付渠道退款流水。"); - - migrationBuilder.AlterTable( - name: "payment_records", - oldComment: "支付流水。"); - - migrationBuilder.AlterTable( - name: "orders", - oldComment: "交易订单。"); - - migrationBuilder.AlterTable( - name: "order_status_histories", - oldComment: "订单状态流转记录。"); - - migrationBuilder.AlterTable( - name: "order_items", - oldComment: "订单明细。"); - - migrationBuilder.AlterTable( - name: "navigation_requests", - oldComment: "用户发起的导航请求日志。"); - - migrationBuilder.AlterTable( - name: "metric_snapshots", - oldComment: "指标快照,用于大盘展示。"); - - migrationBuilder.AlterTable( - name: "metric_definitions", - oldComment: "指标定义,描述可观测的数据点。"); - - migrationBuilder.AlterTable( - name: "metric_alert_rules", - oldComment: "指标告警规则。"); - - migrationBuilder.AlterTable( - name: "merchants", - oldComment: "商户主体信息,承载入驻和资质审核结果。"); - - migrationBuilder.AlterTable( - name: "merchant_staff", - oldComment: "商户员工账号,支持门店维度分配。"); - - migrationBuilder.AlterTable( - name: "merchant_documents", - oldComment: "商户提交的资质或证照材料。"); - - migrationBuilder.AlterTable( - name: "merchant_contracts", - oldComment: "商户合同记录。"); - - migrationBuilder.AlterTable( - name: "member_tiers", - oldComment: "会员等级定义。"); - - migrationBuilder.AlterTable( - name: "member_profiles", - oldComment: "会员档案。"); - - migrationBuilder.AlterTable( - name: "member_point_ledgers", - oldComment: "积分变动流水。"); - - migrationBuilder.AlterTable( - name: "member_growth_logs", - oldComment: "成长值变动日志。"); - - migrationBuilder.AlterTable( - name: "map_locations", - oldComment: "地图 POI 信息,用于门店定位和推荐。"); - - migrationBuilder.AlterTable( - name: "inventory_items", - oldComment: "SKU 在门店的库存信息。"); - - migrationBuilder.AlterTable( - name: "inventory_batches", - oldComment: "SKU 批次信息。"); - - migrationBuilder.AlterTable( - name: "inventory_adjustments", - oldComment: "库存调整记录。"); - - migrationBuilder.AlterTable( - name: "group_participants", - oldComment: "拼单参与者。"); - - migrationBuilder.AlterTable( - name: "group_orders", - oldComment: "拼单活动。"); - - migrationBuilder.AlterTable( - name: "delivery_orders", - oldComment: "配送单。"); - - migrationBuilder.AlterTable( - name: "delivery_events", - oldComment: "配送状态事件流水。"); - - migrationBuilder.AlterTable( - name: "coupons", - oldComment: "用户领取的券。"); - - migrationBuilder.AlterTable( - name: "coupon_templates", - oldComment: "优惠券模板。"); - - migrationBuilder.AlterTable( - name: "community_reactions", - oldComment: "社区互动反馈。"); - - migrationBuilder.AlterTable( - name: "community_posts", - oldComment: "社区动态。"); - - migrationBuilder.AlterTable( - name: "community_comments", - oldComment: "社区评论。"); - - migrationBuilder.AlterTable( - name: "checkout_sessions", - oldComment: "结账会话,记录校验上下文。"); - - migrationBuilder.AlterTable( - name: "checkin_records", - oldComment: "用户签到记录。"); - - migrationBuilder.AlterTable( - name: "checkin_campaigns", - oldComment: "签到活动配置。"); - - migrationBuilder.AlterTable( - name: "chat_sessions", - oldComment: "客服会话。"); - - migrationBuilder.AlterTable( - name: "chat_messages", - oldComment: "会话消息。"); - - migrationBuilder.AlterTable( - name: "cart_items", - oldComment: "购物车条目。"); - - migrationBuilder.AlterTable( - name: "cart_item_addons", - oldComment: "购物车条目的加料/附加项。"); - - migrationBuilder.AlterTable( - name: "affiliate_payouts", - oldComment: "佣金结算记录。"); - - migrationBuilder.AlterTable( - name: "affiliate_partners", - oldComment: "分销/推广合作伙伴。"); - - migrationBuilder.AlterTable( - name: "affiliate_orders", - oldComment: "分销订单记录。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "ticket_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "ticket_comments", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "ticket_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SupportTicketId", - table: "ticket_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "工单标识。"); - - migrationBuilder.AlterColumn( - name: "IsInternal", - table: "ticket_comments", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否内部备注。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "ticket_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "ticket_comments", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "ticket_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "ticket_comments", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "ticket_comments", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024, - oldComment: "评论内容。"); - - migrationBuilder.AlterColumn( - name: "AuthorUserId", - table: "ticket_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "评论人 ID。"); - - migrationBuilder.AlterColumn( - name: "AttachmentsJson", - table: "ticket_comments", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "附件 JSON。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "ticket_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "Website", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "官网或主要宣传链接。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "Tags", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "业务标签集合(逗号分隔)。"); - - migrationBuilder.AlterColumn( - name: "SuspensionReason", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "暂停或终止的原因说明。"); - - migrationBuilder.AlterColumn( - name: "SuspendedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次暂停服务时间。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "tenants", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "租户当前状态,涵盖审核、启用、停用等场景。"); - - migrationBuilder.AlterColumn( - name: "ShortName", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "对外展示的简称。"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "tenants", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "备注信息,用于运营记录特殊说明。"); - - migrationBuilder.AlterColumn( - name: "Province", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "所在省份或州。"); - - migrationBuilder.AlterColumn( - name: "PrimaryOwnerUserId", - table: "tenants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "系统内对应的租户所有者账号 ID。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "tenants", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "租户全称或品牌名称。"); - - migrationBuilder.AlterColumn( - name: "LogoUrl", - table: "tenants", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "LOGO 图片地址。"); - - migrationBuilder.AlterColumn( - name: "LegalEntityName", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "法人或公司主体名称。"); - - migrationBuilder.AlterColumn( - name: "Industry", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "所属行业,如餐饮、零售等。"); - - migrationBuilder.AlterColumn( - name: "EffectiveTo", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "服务到期时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "EffectiveFrom", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "服务生效时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenants", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CoverImageUrl", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "品牌海报或封面图。"); - - migrationBuilder.AlterColumn( - name: "Country", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "所在国家/地区。"); - - migrationBuilder.AlterColumn( - name: "ContactPhone", - table: "tenants", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "主联系人电话。"); - - migrationBuilder.AlterColumn( - name: "ContactName", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "主联系人姓名。"); - - migrationBuilder.AlterColumn( - name: "ContactEmail", - table: "tenants", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "主联系人邮箱。"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "tenants", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "租户短编码,作为跨系统引用的唯一标识。"); - - migrationBuilder.AlterColumn( - name: "City", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "所在城市。"); - - migrationBuilder.AlterColumn( - name: "Address", - table: "tenants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "详细地址信息。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantPackageId", - table: "tenant_subscriptions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "当前订阅关联的套餐标识。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_subscriptions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "tenant_subscriptions", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "订阅当前状态。"); - - migrationBuilder.AlterColumn( - name: "ScheduledPackageId", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "若已排期升降配,对应的新套餐 ID。"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "tenant_subscriptions", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "运营备注信息。"); - - migrationBuilder.AlterColumn( - name: "NextBillingDate", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "下一个计费时间,配合自动续费使用。"); - - migrationBuilder.AlterColumn( - name: "EffectiveTo", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "订阅到期时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "EffectiveFrom", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "订阅生效时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_subscriptions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_subscriptions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AutoRenew", - table: "tenant_subscriptions", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否开启自动续费。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_subscriptions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UsedValue", - table: "tenant_quota_usages", - type: "numeric", - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric", - oldComment: "已消耗的数量。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_quota_usages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_quota_usages", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "ResetCycle", - table: "tenant_quota_usages", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "配额刷新周期描述(如月、年)。"); - - migrationBuilder.AlterColumn( - name: "QuotaType", - table: "tenant_quota_usages", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "配额类型,例如门店数、短信条数等。"); - - migrationBuilder.AlterColumn( - name: "LimitValue", - table: "tenant_quota_usages", - type: "numeric", - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric", - oldComment: "当前配额上限。"); - - migrationBuilder.AlterColumn( - name: "LastResetAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次重置时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_quota_usages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_quota_usages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_quota_usages", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_quota_usages", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "YearlyPrice", - table: "tenant_packages", - type: "numeric", - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true, - oldComment: "年付价格,单位:人民币元。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_packages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_packages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "PackageType", - table: "tenant_packages", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "套餐分类(试用、标准、旗舰等)。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "tenant_packages", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "套餐名称,展示给租户的简称。"); - - migrationBuilder.AlterColumn( - name: "MonthlyPrice", - table: "tenant_packages", - type: "numeric", - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true, - oldComment: "月付价格,单位:人民币元。"); - - migrationBuilder.AlterColumn( - name: "MaxStoreCount", - table: "tenant_packages", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "允许的最大门店数。"); - - migrationBuilder.AlterColumn( - name: "MaxStorageGb", - table: "tenant_packages", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "存储容量上限(GB)。"); - - migrationBuilder.AlterColumn( - name: "MaxSmsCredits", - table: "tenant_packages", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "每月短信额度上限。"); - - migrationBuilder.AlterColumn( - name: "MaxDeliveryOrders", - table: "tenant_packages", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "每月可调用的配送单数量上限。"); - - migrationBuilder.AlterColumn( - name: "MaxAccountCount", - table: "tenant_packages", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "允许创建的最大账号数。"); - - migrationBuilder.AlterColumn( - name: "IsActive", - table: "tenant_packages", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否仍可售卖。"); - - migrationBuilder.AlterColumn( - name: "FeaturePoliciesJson", - table: "tenant_packages", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "权益明细 JSON,记录自定义特性开关。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "tenant_packages", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "套餐描述,包含适用场景、权益等。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_packages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_packages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_packages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_packages", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_packages", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_notifications", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "Title", - table: "tenant_notifications", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "通知标题。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_notifications", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Severity", - table: "tenant_notifications", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "通知重要级别。"); - - migrationBuilder.AlterColumn( - name: "SentAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "推送时间。"); - - migrationBuilder.AlterColumn( - name: "ReadAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "租户是否已阅读。"); - - migrationBuilder.AlterColumn( - name: "MetadataJson", - table: "tenant_notifications", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "附加元数据 JSON。"); - - migrationBuilder.AlterColumn( - name: "Message", - table: "tenant_notifications", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024, - oldComment: "通知正文。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_notifications", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_notifications", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_notifications", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Channel", - table: "tenant_notifications", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "发布通道(站内、邮件、短信等)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_notifications", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "tenant_billing_statements", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "tenant_billing_statements", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "tenant_billing_statements", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前付款状态。"); - - migrationBuilder.AlterColumn( - name: "StatementNo", - table: "tenant_billing_statements", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "账单编号,供对账查询。"); - - migrationBuilder.AlterColumn( - name: "PeriodStart", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "账单周期开始时间。"); - - migrationBuilder.AlterColumn( - name: "PeriodEnd", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "账单周期结束时间。"); - - migrationBuilder.AlterColumn( - name: "LineItemsJson", - table: "tenant_billing_statements", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "账单明细 JSON,记录各项费用。"); - - migrationBuilder.AlterColumn( - name: "DueDate", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "到期日。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "tenant_billing_statements", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "tenant_billing_statements", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "tenant_billing_statements", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AmountPaid", - table: "tenant_billing_statements", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "实付金额。"); - - migrationBuilder.AlterColumn( - name: "AmountDue", - table: "tenant_billing_statements", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "应付金额。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "tenant_billing_statements", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "support_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TicketNo", - table: "support_tickets", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "工单编号。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "support_tickets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Subject", - table: "support_tickets", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "工单主题。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "support_tickets", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "Priority", - table: "support_tickets", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "优先级。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "support_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "关联订单(如有)。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "support_tickets", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "工单详情。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "support_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CustomerUserId", - table: "support_tickets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "客户用户 ID。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "support_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ClosedAt", - table: "support_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "关闭时间。"); - - migrationBuilder.AlterColumn( - name: "AssignedAgentId", - table: "support_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "指派的客服。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "support_tickets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "stores", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "stores", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "stores", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Tags", - table: "stores", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "门店标签(逗号分隔)。"); - - migrationBuilder.AlterColumn( - name: "SupportsReservation", - table: "stores", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "支持预约。"); - - migrationBuilder.AlterColumn( - name: "SupportsQueueing", - table: "stores", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "支持排队叫号。"); - - migrationBuilder.AlterColumn( - name: "SupportsPickup", - table: "stores", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否支持自提。"); - - migrationBuilder.AlterColumn( - name: "SupportsDineIn", - table: "stores", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否支持堂食。"); - - migrationBuilder.AlterColumn( - name: "SupportsDelivery", - table: "stores", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否支持配送。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "stores", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "门店当前运营状态。"); - - migrationBuilder.AlterColumn( - name: "Province", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "所在省份。"); - - migrationBuilder.AlterColumn( - name: "Phone", - table: "stores", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "联系电话。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "stores", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "门店名称。"); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "stores", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商户标识。"); - - migrationBuilder.AlterColumn( - name: "ManagerName", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "门店负责人姓名。"); - - migrationBuilder.AlterColumn( - name: "Longitude", - table: "stores", - type: "double precision", - nullable: true, - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true, - oldComment: "高德/腾讯地图经度。"); - - migrationBuilder.AlterColumn( - name: "Latitude", - table: "stores", - type: "double precision", - nullable: true, - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true, - oldComment: "纬度。"); - - migrationBuilder.AlterColumn( - name: "District", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "区县信息。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "stores", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "门店描述或公告。"); - - migrationBuilder.AlterColumn( - name: "DeliveryRadiusKm", - table: "stores", - type: "numeric(6,2)", - precision: 6, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(6,2)", - oldPrecision: 6, - oldScale: 2, - oldComment: "默认配送半径(公里)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "stores", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "stores", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "stores", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "stores", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CoverImageUrl", - table: "stores", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "门店海报。"); - - migrationBuilder.AlterColumn( - name: "Country", - table: "stores", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "所在国家或地区。"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "stores", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "门店编码,便于扫码及外部对接。"); - - migrationBuilder.AlterColumn( - name: "City", - table: "stores", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "所在城市。"); - - migrationBuilder.AlterColumn( - name: "BusinessHours", - table: "stores", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "门店营业时段描述(备用字符串)。"); - - migrationBuilder.AlterColumn( - name: "Announcement", - table: "stores", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "门店公告。"); - - migrationBuilder.AlterColumn( - name: "Address", - table: "stores", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "详细地址。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "stores", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_tables", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_tables", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_tables", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Tags", - table: "store_tables", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "桌台标签(堂食、快餐等)。"); - - migrationBuilder.AlterColumn( - name: "TableCode", - table: "store_tables", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "桌码。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_tables", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "store_tables", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前桌台状态。"); - - migrationBuilder.AlterColumn( - name: "QrCodeUrl", - table: "store_tables", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "桌码二维码地址。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_tables", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_tables", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_tables", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_tables", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Capacity", - table: "store_tables", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "可容纳人数。"); - - migrationBuilder.AlterColumn( - name: "AreaId", - table: "store_tables", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "所在区域 ID。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_tables", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_table_areas", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_table_areas", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_table_areas", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_table_areas", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "store_table_areas", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "区域名称。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "store_table_areas", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "区域描述。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_table_areas", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_table_areas", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_table_areas", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_table_areas", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_table_areas", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_holidays", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_holidays", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_holidays", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_holidays", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "store_holidays", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "说明内容。"); - - migrationBuilder.AlterColumn( - name: "IsClosed", - table: "store_holidays", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否全天闭店。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_holidays", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_holidays", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "Date", - table: "store_holidays", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "日期。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_holidays", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_holidays", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_holidays", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_employee_shifts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "StartTime", - table: "store_employee_shifts", - type: "interval", - nullable: false, - oldClrType: typeof(TimeSpan), - oldType: "interval", - oldComment: "开始时间。"); - - migrationBuilder.AlterColumn( - name: "StaffId", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "员工标识。"); - - migrationBuilder.AlterColumn( - name: "ShiftDate", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "班次日期。"); - - migrationBuilder.AlterColumn( - name: "RoleType", - table: "store_employee_shifts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排班角色。"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "store_employee_shifts", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "EndTime", - table: "store_employee_shifts", - type: "interval", - nullable: false, - oldClrType: typeof(TimeSpan), - oldType: "interval", - oldComment: "结束时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_employee_shifts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_employee_shifts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_employee_shifts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_employee_shifts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "ZoneName", - table: "store_delivery_zones", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "区域名称。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_delivery_zones", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_delivery_zones", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_delivery_zones", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_delivery_zones", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "PolygonGeoJson", - table: "store_delivery_zones", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "GeoJSON 表示的多边形范围。"); - - migrationBuilder.AlterColumn( - name: "MinimumOrderAmount", - table: "store_delivery_zones", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "起送价。"); - - migrationBuilder.AlterColumn( - name: "EstimatedMinutes", - table: "store_delivery_zones", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "预计送达分钟。"); - - migrationBuilder.AlterColumn( - name: "DeliveryFee", - table: "store_delivery_zones", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "配送费。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_delivery_zones", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_delivery_zones", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_delivery_zones", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_delivery_zones", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_delivery_zones", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "store_business_hours", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "store_business_hours", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "store_business_hours", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "store_business_hours", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "StartTime", - table: "store_business_hours", - type: "interval", - nullable: false, - oldClrType: typeof(TimeSpan), - oldType: "interval", - oldComment: "开始时间(本地时间)。"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "store_business_hours", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "HourType", - table: "store_business_hours", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "时段类型(正常营业、休息、预约等)。"); - - migrationBuilder.AlterColumn( - name: "EndTime", - table: "store_business_hours", - type: "interval", - nullable: false, - oldClrType: typeof(TimeSpan), - oldType: "interval", - oldComment: "结束时间(本地时间)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "store_business_hours", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "store_business_hours", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DayOfWeek", - table: "store_business_hours", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "星期几,0 表示周日。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "store_business_hours", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "store_business_hours", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CapacityLimit", - table: "store_business_hours", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "最大接待容量或单量限制。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "store_business_hours", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "shopping_carts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "shopping_carts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "shopping_carts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "TableContext", - table: "shopping_carts", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "桌码或场景标识(扫码点餐)。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "shopping_carts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "shopping_carts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "购物车状态,包含正常/锁定。"); - - migrationBuilder.AlterColumn( - name: "LastModifiedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "最近一次修改时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "DeliveryPreference", - table: "shopping_carts", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "履约方式(堂食/自提/配送)缓存。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "shopping_carts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "shopping_carts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "shopping_carts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "shopping_carts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "reservations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "reservations", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "TablePreference", - table: "reservations", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "桌型/标签。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "reservations", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "reservations", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "ReservationTime", - table: "reservations", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "预约时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ReservationNo", - table: "reservations", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "预约号。"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "reservations", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "PeopleCount", - table: "reservations", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "用餐人数。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "reservations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CustomerPhone", - table: "reservations", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "联系电话。"); - - migrationBuilder.AlterColumn( - name: "CustomerName", - table: "reservations", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "客户姓名。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "reservations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "reservations", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CheckedInAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "实际签到时间。"); - - migrationBuilder.AlterColumn( - name: "CheckInCode", - table: "reservations", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "核销码/到店码。"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "reservations", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "取消时间。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "reservations", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "refund_requests", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "refund_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "refund_requests", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "退款状态。"); - - migrationBuilder.AlterColumn( - name: "ReviewNotes", - table: "refund_requests", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "审核备注。"); - - migrationBuilder.AlterColumn( - name: "RequestedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "用户提交时间。"); - - migrationBuilder.AlterColumn( - name: "RefundNo", - table: "refund_requests", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "退款单号。"); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "refund_requests", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "申请原因。"); - - migrationBuilder.AlterColumn( - name: "ProcessedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "审核完成时间。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "refund_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联订单标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "refund_requests", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "refund_requests", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "refund_requests", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "refund_requests", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "申请金额。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "refund_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "queue_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TicketNumber", - table: "queue_tickets", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "排队编号。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "queue_tickets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "queue_tickets", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "queue_tickets", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "PartySize", - table: "queue_tickets", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "就餐人数。"); - - migrationBuilder.AlterColumn( - name: "ExpiredAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "过号时间。"); - - migrationBuilder.AlterColumn( - name: "EstimatedWaitMinutes", - table: "queue_tickets", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "预计等待分钟。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "queue_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "queue_tickets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "取消时间。"); - - migrationBuilder.AlterColumn( - name: "CalledAt", - table: "queue_tickets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "叫号时间。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "queue_tickets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "promotion_campaigns", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "promotion_campaigns", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "promotion_campaigns", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "活动状态。"); - - migrationBuilder.AlterColumn( - name: "StartAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "开始时间。"); - - migrationBuilder.AlterColumn( - name: "RulesJson", - table: "promotion_campaigns", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "活动规则 JSON。"); - - migrationBuilder.AlterColumn( - name: "PromotionType", - table: "promotion_campaigns", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "活动类型。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "promotion_campaigns", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "活动名称。"); - - migrationBuilder.AlterColumn( - name: "EndAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "结束时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "promotion_campaigns", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "promotion_campaigns", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "promotion_campaigns", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Budget", - table: "promotion_campaigns", - type: "numeric", - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true, - oldComment: "预算金额。"); - - migrationBuilder.AlterColumn( - name: "BannerUrl", - table: "promotion_campaigns", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "营销素材(如 banner)。"); - - migrationBuilder.AlterColumn( - name: "AudienceDescription", - table: "promotion_campaigns", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "目标人群描述。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "promotion_campaigns", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "products", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "products", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "Unit", - table: "products", - type: "character varying(16)", - maxLength: 16, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(16)", - oldMaxLength: 16, - oldNullable: true, - oldComment: "售卖单位(份/杯等)。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "products", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Subtitle", - table: "products", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "副标题/卖点。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "products", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属门店。"); - - migrationBuilder.AlterColumn( - name: "StockQuantity", - table: "products", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "库存数量(可选)。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "products", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "商品状态。"); - - migrationBuilder.AlterColumn( - name: "SpuCode", - table: "products", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "商品编码。"); - - migrationBuilder.AlterColumn( - name: "Price", - table: "products", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "现价。"); - - migrationBuilder.AlterColumn( - name: "OriginalPrice", - table: "products", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "原价。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "products", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "商品名称。"); - - migrationBuilder.AlterColumn( - name: "MaxQuantityPerOrder", - table: "products", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "最大每单限购。"); - - migrationBuilder.AlterColumn( - name: "IsFeatured", - table: "products", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否热门推荐。"); - - migrationBuilder.AlterColumn( - name: "GalleryImages", - table: "products", - type: "character varying(1024)", - maxLength: 1024, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024, - oldNullable: true, - oldComment: "Gallery 图片逗号分隔。"); - - migrationBuilder.AlterColumn( - name: "EnablePickup", - table: "products", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "支持自提。"); - - migrationBuilder.AlterColumn( - name: "EnableDineIn", - table: "products", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "支持堂食。"); - - migrationBuilder.AlterColumn( - name: "EnableDelivery", - table: "products", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "支持配送。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "products", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "商品描述。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "products", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "products", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "products", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "products", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CoverImage", - table: "products", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "主图。"); - - migrationBuilder.AlterColumn( - name: "CategoryId", - table: "products", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属分类。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "products", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "Weight", - table: "product_skus", - type: "numeric(10,3)", - precision: 10, - scale: 3, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(10,3)", - oldPrecision: 10, - oldScale: 3, - oldNullable: true, - oldComment: "重量(千克)。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_skus", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_skus", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_skus", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StockQuantity", - table: "product_skus", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "可售库存。"); - - migrationBuilder.AlterColumn( - name: "SkuCode", - table: "product_skus", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "SKU 编码。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_skus", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商品标识。"); - - migrationBuilder.AlterColumn( - name: "Price", - table: "product_skus", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "售价。"); - - migrationBuilder.AlterColumn( - name: "OriginalPrice", - table: "product_skus", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "原价。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_skus", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_skus", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_skus", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_skus", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Barcode", - table: "product_skus", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "条形码。"); - - migrationBuilder.AlterColumn( - name: "AttributesJson", - table: "product_skus", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "规格属性 JSON(记录选项 ID)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_skus", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "WeekdaysJson", - table: "product_pricing_rules", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "生效星期(JSON 数组)。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_pricing_rules", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_pricing_rules", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StartTime", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "生效开始时间。"); - - migrationBuilder.AlterColumn( - name: "RuleType", - table: "product_pricing_rules", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "策略类型。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_pricing_rules", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商品。"); - - migrationBuilder.AlterColumn( - name: "Price", - table: "product_pricing_rules", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "特殊价格。"); - - migrationBuilder.AlterColumn( - name: "EndTime", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "生效结束时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_pricing_rules", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_pricing_rules", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_pricing_rules", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ConditionsJson", - table: "product_pricing_rules", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "条件描述(JSON),如会员等级、渠道等。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_pricing_rules", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "Url", - table: "product_media_assets", - type: "character varying(512)", - maxLength: 512, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldComment: "媒资链接。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_media_assets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_media_assets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_media_assets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_media_assets", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排序。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_media_assets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "商品标识。"); - - migrationBuilder.AlterColumn( - name: "MediaType", - table: "product_media_assets", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "媒体类型。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_media_assets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_media_assets", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_media_assets", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_media_assets", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Caption", - table: "product_media_assets", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "描述或标题。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_media_assets", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_categories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_categories", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_categories", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "product_categories", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属门店。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_categories", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排序值。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_categories", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "分类名称。"); - - migrationBuilder.AlterColumn( - name: "IsEnabled", - table: "product_categories", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否启用。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "product_categories", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "分类描述。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_categories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_categories", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_categories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_categories", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_categories", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_attribute_options", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_attribute_options", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_attribute_options", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_attribute_options", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排序。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_attribute_options", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "选项名称。"); - - migrationBuilder.AlterColumn( - name: "IsDefault", - table: "product_attribute_options", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否默认选中。"); - - migrationBuilder.AlterColumn( - name: "ExtraPrice", - table: "product_attribute_options", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "附加价格。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_attribute_options", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_attribute_options", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_attribute_options", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_attribute_options", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AttributeGroupId", - table: "product_attribute_options", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属规格组。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_attribute_options", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_attribute_groups", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_attribute_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "关联门店,可为空表示所有门店共享。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_attribute_groups", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "显示排序。"); - - migrationBuilder.AlterColumn( - name: "SelectionType", - table: "product_attribute_groups", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "选择类型(单选/多选)。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_attribute_groups", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "分组名称,例如“辣度”“份量”。"); - - migrationBuilder.AlterColumn( - name: "IsRequired", - table: "product_attribute_groups", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否必选。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_attribute_groups", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_attribute_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_attribute_groups", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_attribute_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_addon_options", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_addon_options", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_addon_options", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_addon_options", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排序。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_addon_options", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "选项名称。"); - - migrationBuilder.AlterColumn( - name: "IsDefault", - table: "product_addon_options", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否默认选项。"); - - migrationBuilder.AlterColumn( - name: "ExtraPrice", - table: "product_addon_options", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "附加价格。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_addon_options", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_addon_options", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_addon_options", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_addon_options", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AddonGroupId", - table: "product_addon_options", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属加料分组。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_addon_options", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "product_addon_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "product_addon_groups", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "product_addon_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "product_addon_groups", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排序值。"); - - migrationBuilder.AlterColumn( - name: "SelectionType", - table: "product_addon_groups", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "选择类型。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "product_addon_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商品。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "product_addon_groups", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "分组名称。"); - - migrationBuilder.AlterColumn( - name: "MinSelect", - table: "product_addon_groups", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "最小选择数量。"); - - migrationBuilder.AlterColumn( - name: "MaxSelect", - table: "product_addon_groups", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "最大选择数量。"); - - migrationBuilder.AlterColumn( - name: "IsRequired", - table: "product_addon_groups", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否必选。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "product_addon_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "product_addon_groups", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "product_addon_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "product_addon_groups", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "product_addon_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "payment_refund_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "payment_refund_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "payment_refund_records", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "退款状态。"); - - migrationBuilder.AlterColumn( - name: "RequestedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "退款请求时间。"); - - migrationBuilder.AlterColumn( - name: "PaymentRecordId", - table: "payment_refund_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "原支付记录标识。"); - - migrationBuilder.AlterColumn( - name: "Payload", - table: "payment_refund_records", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "渠道返回的原始数据 JSON。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "payment_refund_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联订单标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "payment_refund_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "payment_refund_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CompletedAt", - table: "payment_refund_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "完成时间。"); - - migrationBuilder.AlterColumn( - name: "ChannelRefundId", - table: "payment_refund_records", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "渠道退款流水号。"); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "payment_refund_records", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "退款金额。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "payment_refund_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "payment_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TradeNo", - table: "payment_records", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "平台交易号。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "payment_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "payment_records", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "支付状态。"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "payment_records", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "错误/备注。"); - - migrationBuilder.AlterColumn( - name: "Payload", - table: "payment_records", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "原始回调内容。"); - - migrationBuilder.AlterColumn( - name: "PaidAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "支付完成时间。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "payment_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联订单。"); - - migrationBuilder.AlterColumn( - name: "Method", - table: "payment_records", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "支付方式。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "payment_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "payment_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "payment_records", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ChannelTransactionId", - table: "payment_records", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "第三方渠道单号。"); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "payment_records", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "支付金额。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "payment_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "TableNo", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "就餐桌号。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前状态。"); - - migrationBuilder.AlterColumn( - name: "ReservationId", - table: "orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "预约 ID。"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "orders", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "QueueNumber", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "排队号(如有)。"); - - migrationBuilder.AlterColumn( - name: "PaymentStatus", - table: "orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "支付状态。"); - - migrationBuilder.AlterColumn( - name: "PayableAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "应付金额。"); - - migrationBuilder.AlterColumn( - name: "PaidAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "支付时间。"); - - migrationBuilder.AlterColumn( - name: "PaidAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "实付金额。"); - - migrationBuilder.AlterColumn( - name: "OrderNo", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "订单号。"); - - migrationBuilder.AlterColumn( - name: "ItemsAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "商品总额。"); - - migrationBuilder.AlterColumn( - name: "FinishedAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "完成时间。"); - - migrationBuilder.AlterColumn( - name: "DiscountAmount", - table: "orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "优惠金额。"); - - migrationBuilder.AlterColumn( - name: "DeliveryType", - table: "orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "履约类型。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CustomerPhone", - table: "orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "顾客手机号。"); - - migrationBuilder.AlterColumn( - name: "CustomerName", - table: "orders", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "顾客姓名。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "orders", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Channel", - table: "orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "下单渠道。"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "取消时间。"); - - migrationBuilder.AlterColumn( - name: "CancelReason", - table: "orders", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "取消原因。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "order_status_histories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "order_status_histories", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "order_status_histories", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "变更后的状态。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "order_status_histories", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "订单标识。"); - - migrationBuilder.AlterColumn( - name: "OperatorId", - table: "order_status_histories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "操作人标识(可为空表示系统)。"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "发生时间。"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "order_status_histories", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "备注信息。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "order_status_histories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "order_status_histories", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "order_status_histories", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "order_status_histories", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "order_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "order_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "UnitPrice", - table: "order_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "单价。"); - - migrationBuilder.AlterColumn( - name: "Unit", - table: "order_items", - type: "character varying(16)", - maxLength: 16, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(16)", - oldMaxLength: 16, - oldNullable: true, - oldComment: "单位。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "order_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SubTotal", - table: "order_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "小计。"); - - migrationBuilder.AlterColumn( - name: "SkuName", - table: "order_items", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "SKU/规格描述。"); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "order_items", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "数量。"); - - migrationBuilder.AlterColumn( - name: "ProductName", - table: "order_items", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "商品名称。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "order_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "商品 ID。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "order_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "订单 ID。"); - - migrationBuilder.AlterColumn( - name: "DiscountAmount", - table: "order_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "折扣金额。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "order_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "order_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "order_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "order_items", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AttributesJson", - table: "order_items", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "自定义属性 JSON。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "order_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "navigation_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户 ID。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "navigation_requests", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "navigation_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "TargetApp", - table: "navigation_requests", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "跳转的地图应用。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "navigation_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店 ID。"); - - migrationBuilder.AlterColumn( - name: "RequestedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "请求时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "navigation_requests", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "navigation_requests", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "navigation_requests", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Channel", - table: "navigation_requests", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "来源通道(小程序、H5 等)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "navigation_requests", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "WindowStart", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "统计时间窗口开始。"); - - migrationBuilder.AlterColumn( - name: "WindowEnd", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "统计时间窗口结束。"); - - migrationBuilder.AlterColumn( - name: "Value", - table: "metric_snapshots", - type: "numeric(18,4)", - precision: 18, - scale: 4, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,4)", - oldPrecision: 18, - oldScale: 4, - oldComment: "数值。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "metric_snapshots", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "metric_snapshots", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "MetricDefinitionId", - table: "metric_snapshots", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "指标定义 ID。"); - - migrationBuilder.AlterColumn( - name: "DimensionKey", - table: "metric_snapshots", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "维度键(JSON)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "metric_snapshots", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "metric_snapshots", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "metric_snapshots", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "metric_snapshots", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "metric_definitions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "metric_definitions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "metric_definitions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "metric_definitions", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "指标名称。"); - - migrationBuilder.AlterColumn( - name: "DimensionsJson", - table: "metric_definitions", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "维度描述 JSON。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "metric_definitions", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "说明。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "metric_definitions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "metric_definitions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DefaultAggregation", - table: "metric_definitions", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "默认聚合方式。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "metric_definitions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "metric_definitions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "metric_definitions", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "指标编码。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "metric_definitions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "metric_alert_rules", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "metric_alert_rules", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "metric_alert_rules", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Severity", - table: "metric_alert_rules", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "告警级别。"); - - migrationBuilder.AlterColumn( - name: "NotificationChannels", - table: "metric_alert_rules", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "通知渠道。"); - - migrationBuilder.AlterColumn( - name: "MetricDefinitionId", - table: "metric_alert_rules", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联指标。"); - - migrationBuilder.AlterColumn( - name: "Enabled", - table: "metric_alert_rules", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否启用。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "metric_alert_rules", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "metric_alert_rules", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "metric_alert_rules", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "metric_alert_rules", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ConditionJson", - table: "metric_alert_rules", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "触发条件 JSON。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "metric_alert_rules", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "TaxNumber", - table: "merchants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "税号/统一社会信用代码。"); - - migrationBuilder.AlterColumn( - name: "SupportEmail", - table: "merchants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "客服邮箱。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchants", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "入驻状态。"); - - migrationBuilder.AlterColumn( - name: "ServicePhone", - table: "merchants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "客服电话。"); - - migrationBuilder.AlterColumn( - name: "ReviewRemarks", - table: "merchants", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "审核备注或驳回原因。"); - - migrationBuilder.AlterColumn( - name: "Province", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "所在省份。"); - - migrationBuilder.AlterColumn( - name: "Longitude", - table: "merchants", - type: "double precision", - nullable: true, - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true, - oldComment: "经度信息。"); - - migrationBuilder.AlterColumn( - name: "LogoUrl", - table: "merchants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "品牌 Logo。"); - - migrationBuilder.AlterColumn( - name: "LegalPerson", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "法人或负责人姓名。"); - - migrationBuilder.AlterColumn( - name: "Latitude", - table: "merchants", - type: "double precision", - nullable: true, - oldClrType: typeof(double), - oldType: "double precision", - oldNullable: true, - oldComment: "纬度信息。"); - - migrationBuilder.AlterColumn( - name: "LastReviewedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次审核时间。"); - - migrationBuilder.AlterColumn( - name: "JoinedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "入驻时间。"); - - migrationBuilder.AlterColumn( - name: "District", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "所在区县。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchants", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ContactPhone", - table: "merchants", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "联系电话。"); - - migrationBuilder.AlterColumn( - name: "ContactEmail", - table: "merchants", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "联系邮箱。"); - - migrationBuilder.AlterColumn( - name: "City", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "所在城市。"); - - migrationBuilder.AlterColumn( - name: "Category", - table: "merchants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "品牌所属品类,如火锅、咖啡等。"); - - migrationBuilder.AlterColumn( - name: "BusinessLicenseNumber", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "营业执照号。"); - - migrationBuilder.AlterColumn( - name: "BusinessLicenseImageUrl", - table: "merchants", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "营业执照扫描件地址。"); - - migrationBuilder.AlterColumn( - name: "BrandName", - table: "merchants", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "品牌名称(对外展示)。"); - - migrationBuilder.AlterColumn( - name: "BrandAlias", - table: "merchants", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "品牌简称或别名。"); - - migrationBuilder.AlterColumn( - name: "Address", - table: "merchants", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "详细地址。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchant_staff", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchant_staff", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchant_staff", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "merchant_staff", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "可选的关联门店 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchant_staff", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "员工状态。"); - - migrationBuilder.AlterColumn( - name: "RoleType", - table: "merchant_staff", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "员工角色类型。"); - - migrationBuilder.AlterColumn( - name: "Phone", - table: "merchant_staff", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "手机号。"); - - migrationBuilder.AlterColumn( - name: "PermissionsJson", - table: "merchant_staff", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "自定义权限(JSON)。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "merchant_staff", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "员工姓名。"); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "merchant_staff", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商户标识。"); - - migrationBuilder.AlterColumn( - name: "IdentityUserId", - table: "merchant_staff", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "登录账号 ID(指向统一身份体系)。"); - - migrationBuilder.AlterColumn( - name: "Email", - table: "merchant_staff", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "邮箱地址。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchant_staff", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchant_staff", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchant_staff", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchant_staff", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchant_staff", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchant_documents", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchant_documents", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchant_documents", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "审核状态。"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "merchant_documents", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "审核备注或驳回原因。"); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "merchant_documents", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商户标识。"); - - migrationBuilder.AlterColumn( - name: "IssuedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "签发日期。"); - - migrationBuilder.AlterColumn( - name: "FileUrl", - table: "merchant_documents", - type: "character varying(512)", - maxLength: 512, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldComment: "证照文件链接。"); - - migrationBuilder.AlterColumn( - name: "ExpiresAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "到期日期。"); - - migrationBuilder.AlterColumn( - name: "DocumentType", - table: "merchant_documents", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "证照类型。"); - - migrationBuilder.AlterColumn( - name: "DocumentNumber", - table: "merchant_documents", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "证照编号。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchant_documents", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchant_documents", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchant_documents", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchant_documents", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "merchant_contracts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TerminationReason", - table: "merchant_contracts", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "终止原因。"); - - migrationBuilder.AlterColumn( - name: "TerminatedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "终止时间。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "merchant_contracts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "merchant_contracts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "合同状态。"); - - migrationBuilder.AlterColumn( - name: "StartDate", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "合同开始时间。"); - - migrationBuilder.AlterColumn( - name: "SignedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "签署时间。"); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "merchant_contracts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属商户标识。"); - - migrationBuilder.AlterColumn( - name: "FileUrl", - table: "merchant_contracts", - type: "character varying(512)", - maxLength: 512, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldComment: "合同文件存储地址。"); - - migrationBuilder.AlterColumn( - name: "EndDate", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "合同结束时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "merchant_contracts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "merchant_contracts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "merchant_contracts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ContractNumber", - table: "merchant_contracts", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "合同编号。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "merchant_contracts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_tiers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_tiers", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_tiers", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "member_tiers", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "排序值。"); - - migrationBuilder.AlterColumn( - name: "RequiredGrowth", - table: "member_tiers", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "所需成长值。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "member_tiers", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "等级名称。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_tiers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_tiers", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_tiers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_tiers", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "BenefitsJson", - table: "member_tiers", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "等级权益(JSON)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_tiers", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "member_profiles", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_profiles", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_profiles", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "member_profiles", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "会员状态。"); - - migrationBuilder.AlterColumn( - name: "PointsBalance", - table: "member_profiles", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "会员积分余额。"); - - migrationBuilder.AlterColumn( - name: "Nickname", - table: "member_profiles", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "昵称。"); - - migrationBuilder.AlterColumn( - name: "Mobile", - table: "member_profiles", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "手机号。"); - - migrationBuilder.AlterColumn( - name: "MemberTierId", - table: "member_profiles", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "当前会员等级 ID。"); - - migrationBuilder.AlterColumn( - name: "JoinedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "注册时间。"); - - migrationBuilder.AlterColumn( - name: "GrowthValue", - table: "member_profiles", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "成长值/经验值。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_profiles", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_profiles", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_profiles", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "BirthDate", - table: "member_profiles", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "生日。"); - - migrationBuilder.AlterColumn( - name: "AvatarUrl", - table: "member_profiles", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "头像。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_profiles", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_point_ledgers", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SourceId", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "来源 ID(订单、活动等)。"); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "member_point_ledgers", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "变动原因。"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "发生时间。"); - - migrationBuilder.AlterColumn( - name: "MemberId", - table: "member_point_ledgers", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "会员标识。"); - - migrationBuilder.AlterColumn( - name: "ExpireAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "过期时间(如适用)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_point_ledgers", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_point_ledgers", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ChangeAmount", - table: "member_point_ledgers", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "变动数量,可为负值。"); - - migrationBuilder.AlterColumn( - name: "BalanceAfterChange", - table: "member_point_ledgers", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "变动后余额。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_point_ledgers", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "member_growth_logs", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "member_growth_logs", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "发生时间。"); - - migrationBuilder.AlterColumn( - name: "Notes", - table: "member_growth_logs", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "MemberId", - table: "member_growth_logs", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "会员标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "member_growth_logs", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CurrentValue", - table: "member_growth_logs", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前成长值。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "member_growth_logs", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "member_growth_logs", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ChangeValue", - table: "member_growth_logs", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "变动数量。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "member_growth_logs", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "map_locations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "map_locations", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "map_locations", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "map_locations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "关联门店 ID,可空表示独立 POI。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "map_locations", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "名称。"); - - migrationBuilder.AlterColumn( - name: "Longitude", - table: "map_locations", - type: "double precision", - nullable: false, - oldClrType: typeof(double), - oldType: "double precision", - oldComment: "经度。"); - - migrationBuilder.AlterColumn( - name: "Latitude", - table: "map_locations", - type: "double precision", - nullable: false, - oldClrType: typeof(double), - oldType: "double precision", - oldComment: "纬度。"); - - migrationBuilder.AlterColumn( - name: "Landmark", - table: "map_locations", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "打车/导航落点描述。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "map_locations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "map_locations", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "map_locations", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "map_locations", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Address", - table: "map_locations", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "地址。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "map_locations", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "inventory_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "inventory_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "inventory_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "inventory_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "SafetyStock", - table: "inventory_items", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "安全库存阈值。"); - - migrationBuilder.AlterColumn( - name: "QuantityReserved", - table: "inventory_items", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "已锁定库存(订单占用)。"); - - migrationBuilder.AlterColumn( - name: "QuantityOnHand", - table: "inventory_items", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "可用库存。"); - - migrationBuilder.AlterColumn( - name: "ProductSkuId", - table: "inventory_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "SKU 标识。"); - - migrationBuilder.AlterColumn( - name: "Location", - table: "inventory_items", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "储位或仓位信息。"); - - migrationBuilder.AlterColumn( - name: "ExpireDate", - table: "inventory_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "过期日期。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "inventory_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "inventory_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "inventory_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "inventory_items", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "BatchNumber", - table: "inventory_items", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "批次编号,可为空表示混批。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "inventory_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "inventory_batches", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "inventory_batches", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "inventory_batches", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "RemainingQuantity", - table: "inventory_batches", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "剩余数量。"); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "inventory_batches", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "入库数量。"); - - migrationBuilder.AlterColumn( - name: "ProductionDate", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "生产日期。"); - - migrationBuilder.AlterColumn( - name: "ProductSkuId", - table: "inventory_batches", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "SKU 标识。"); - - migrationBuilder.AlterColumn( - name: "ExpireDate", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "过期日期。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "inventory_batches", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "inventory_batches", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "inventory_batches", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "BatchNumber", - table: "inventory_batches", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "批次编号。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "inventory_batches", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "inventory_adjustments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Reason", - table: "inventory_adjustments", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "原因说明。"); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "inventory_adjustments", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "调整数量,正数增加,负数减少。"); - - migrationBuilder.AlterColumn( - name: "OperatorId", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "操作人标识。"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "发生时间。"); - - migrationBuilder.AlterColumn( - name: "InventoryItemId", - table: "inventory_adjustments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "对应的库存记录标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "inventory_adjustments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "inventory_adjustments", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AdjustmentType", - table: "inventory_adjustments", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "调整类型。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "inventory_adjustments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "group_participants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "group_participants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "group_participants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "group_participants", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "参与状态。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "group_participants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "对应订单标识。"); - - migrationBuilder.AlterColumn( - name: "JoinedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "参与时间。"); - - migrationBuilder.AlterColumn( - name: "GroupOrderId", - table: "group_participants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "拼单活动标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "group_participants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "group_participants", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "group_participants", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "group_participants", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "group_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "group_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "TargetCount", - table: "group_orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "成团需要的人数。"); - - migrationBuilder.AlterColumn( - name: "SucceededAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "成团时间。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "group_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "group_orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "拼团状态。"); - - migrationBuilder.AlterColumn( - name: "StartAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "开始时间。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "group_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联商品或套餐。"); - - migrationBuilder.AlterColumn( - name: "LeaderUserId", - table: "group_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "团长用户 ID。"); - - migrationBuilder.AlterColumn( - name: "GroupPrice", - table: "group_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "拼团价格。"); - - migrationBuilder.AlterColumn( - name: "GroupOrderNo", - table: "group_orders", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "拼单编号。"); - - migrationBuilder.AlterColumn( - name: "EndAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "结束时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "group_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CurrentCount", - table: "group_orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前已参与人数。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "group_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CancelledAt", - table: "group_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "取消时间。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "group_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "delivery_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "delivery_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "delivery_orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "ProviderOrderId", - table: "delivery_orders", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "第三方配送单号。"); - - migrationBuilder.AlterColumn( - name: "Provider", - table: "delivery_orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "配送服务商。"); - - migrationBuilder.AlterColumn( - name: "PickedUpAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "取餐时间。"); - - migrationBuilder.AlterColumn( - name: "FailureReason", - table: "delivery_orders", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "异常原因。"); - - migrationBuilder.AlterColumn( - name: "DispatchedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "下发时间。"); - - migrationBuilder.AlterColumn( - name: "DeliveryFee", - table: "delivery_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldNullable: true, - oldComment: "配送费。"); - - migrationBuilder.AlterColumn( - name: "DeliveredAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "完成时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "delivery_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "delivery_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "delivery_orders", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CourierPhone", - table: "delivery_orders", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "骑手电话。"); - - migrationBuilder.AlterColumn( - name: "CourierName", - table: "delivery_orders", - type: "character varying(64)", - maxLength: 64, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldNullable: true, - oldComment: "骑手姓名。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "delivery_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "delivery_events", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "delivery_events", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Payload", - table: "delivery_events", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "原始数据 JSON。"); - - migrationBuilder.AlterColumn( - name: "OccurredAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "发生时间。"); - - migrationBuilder.AlterColumn( - name: "Message", - table: "delivery_events", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "事件描述。"); - - migrationBuilder.AlterColumn( - name: "EventType", - table: "delivery_events", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "事件类型。"); - - migrationBuilder.AlterColumn( - name: "DeliveryOrderId", - table: "delivery_events", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "配送单标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "delivery_events", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "delivery_events", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "delivery_events", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "delivery_events", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "coupons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "归属用户。"); - - migrationBuilder.AlterColumn( - name: "UsedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "使用时间。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "coupons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "coupons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "coupons", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "coupons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "订单 ID(已使用时记录)。"); - - migrationBuilder.AlterColumn( - name: "IssuedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "发放时间。"); - - migrationBuilder.AlterColumn( - name: "ExpireAt", - table: "coupons", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "到期时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "coupons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "coupons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "coupons", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CouponTemplateId", - table: "coupons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "模板标识。"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "coupons", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "券码或序列号。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "coupons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "Value", - table: "coupon_templates", - type: "numeric", - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric", - oldComment: "面值或折扣额度。"); - - migrationBuilder.AlterColumn( - name: "ValidTo", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "可用结束时间。"); - - migrationBuilder.AlterColumn( - name: "ValidFrom", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "可用开始时间。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "coupon_templates", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TotalQuantity", - table: "coupon_templates", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "总发放数量上限。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "coupon_templates", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreScopeJson", - table: "coupon_templates", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "适用门店 ID 集合(JSON)。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "coupon_templates", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "RelativeValidDays", - table: "coupon_templates", - type: "integer", - nullable: true, - oldClrType: typeof(int), - oldType: "integer", - oldNullable: true, - oldComment: "有效天数(相对发放时间)。"); - - migrationBuilder.AlterColumn( - name: "ProductScopeJson", - table: "coupon_templates", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "适用品类或商品范围(JSON)。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "coupon_templates", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "模板名称。"); - - migrationBuilder.AlterColumn( - name: "MinimumSpend", - table: "coupon_templates", - type: "numeric", - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true, - oldComment: "最低消费门槛。"); - - migrationBuilder.AlterColumn( - name: "DiscountCap", - table: "coupon_templates", - type: "numeric", - nullable: true, - oldClrType: typeof(decimal), - oldType: "numeric", - oldNullable: true, - oldComment: "折扣上限(针对折扣券)。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "coupon_templates", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "coupon_templates", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "coupon_templates", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "coupon_templates", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CouponType", - table: "coupon_templates", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "券类型。"); - - migrationBuilder.AlterColumn( - name: "ClaimedQuantity", - table: "coupon_templates", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "已领取数量。"); - - migrationBuilder.AlterColumn( - name: "ChannelsJson", - table: "coupon_templates", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "发放渠道(JSON)。"); - - migrationBuilder.AlterColumn( - name: "AllowStack", - table: "coupon_templates", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否允许叠加其他优惠。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "coupon_templates", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "community_reactions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户 ID。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "community_reactions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "community_reactions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "ReactionType", - table: "community_reactions", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "反应类型。"); - - migrationBuilder.AlterColumn( - name: "ReactedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "时间戳。"); - - migrationBuilder.AlterColumn( - name: "PostId", - table: "community_reactions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "动态 ID。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "community_reactions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "community_reactions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "community_reactions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "community_reactions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "community_posts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "community_posts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "Title", - table: "community_posts", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "标题。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "community_posts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "community_posts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "MediaJson", - table: "community_posts", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "媒体资源 JSON。"); - - migrationBuilder.AlterColumn( - name: "LikeCount", - table: "community_posts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "点赞数。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "community_posts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "community_posts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "community_posts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "community_posts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "community_posts", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "内容。"); - - migrationBuilder.AlterColumn( - name: "CommentCount", - table: "community_posts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "评论数。"); - - migrationBuilder.AlterColumn( - name: "AuthorUserId", - table: "community_posts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "作者用户 ID。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "community_posts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "community_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "community_comments", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "community_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "PostId", - table: "community_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "动态标识。"); - - migrationBuilder.AlterColumn( - name: "ParentId", - table: "community_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "父级评论 ID。"); - - migrationBuilder.AlterColumn( - name: "IsDeleted", - table: "community_comments", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "community_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "community_comments", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "community_comments", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "community_comments", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "community_comments", - type: "character varying(512)", - maxLength: 512, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldComment: "评论内容。"); - - migrationBuilder.AlterColumn( - name: "AuthorUserId", - table: "community_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "评论人。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "community_comments", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "ValidationResultJson", - table: "checkout_sessions", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "校验结果明细 JSON。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "checkout_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "checkout_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "checkout_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "checkout_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "门店标识。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "checkout_sessions", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "会话状态。"); - - migrationBuilder.AlterColumn( - name: "SessionToken", - table: "checkout_sessions", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "会话 Token。"); - - migrationBuilder.AlterColumn( - name: "ExpiresAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "过期时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "checkout_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "checkout_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "checkout_sessions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "checkout_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "checkin_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "checkin_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "checkin_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "checkin_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "RewardJson", - table: "checkin_records", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "获得奖励 JSON。"); - - migrationBuilder.AlterColumn( - name: "IsMakeup", - table: "checkin_records", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否补签。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "checkin_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "checkin_records", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "checkin_records", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "checkin_records", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CheckInDate", - table: "checkin_records", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "签到日期(本地)。"); - - migrationBuilder.AlterColumn( - name: "CheckInCampaignId", - table: "checkin_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "活动标识。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "checkin_records", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "checkin_campaigns", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "checkin_campaigns", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "checkin_campaigns", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "StartDate", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "开始日期。"); - - migrationBuilder.AlterColumn( - name: "RewardsJson", - table: "checkin_campaigns", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "连签奖励 JSON。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "checkin_campaigns", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "活动名称。"); - - migrationBuilder.AlterColumn( - name: "EndDate", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "结束日期。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "checkin_campaigns", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "活动描述。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "checkin_campaigns", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "checkin_campaigns", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "checkin_campaigns", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AllowMakeupCount", - table: "checkin_campaigns", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "支持补签次数。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "checkin_campaigns", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "chat_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "chat_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "StoreId", - table: "chat_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "所属门店(可空为平台)。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "chat_sessions", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "会话状态。"); - - migrationBuilder.AlterColumn( - name: "StartedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "开始时间。"); - - migrationBuilder.AlterColumn( - name: "SessionCode", - table: "chat_sessions", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "会话编号。"); - - migrationBuilder.AlterColumn( - name: "IsBotActive", - table: "chat_sessions", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否机器人接待中。"); - - migrationBuilder.AlterColumn( - name: "EndedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "结束时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "chat_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CustomerUserId", - table: "chat_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "顾客用户 ID。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "chat_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "chat_sessions", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AgentUserId", - table: "chat_sessions", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "当前客服员工 ID。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "chat_sessions", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "chat_messages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "chat_messages", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SenderUserId", - table: "chat_messages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "发送方用户 ID。"); - - migrationBuilder.AlterColumn( - name: "SenderType", - table: "chat_messages", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "发送方类型。"); - - migrationBuilder.AlterColumn( - name: "ReadAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "读取时间。"); - - migrationBuilder.AlterColumn( - name: "IsRead", - table: "chat_messages", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否已读。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "chat_messages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "chat_messages", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "chat_messages", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "ContentType", - table: "chat_messages", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "消息类型(文字/图片/语音等)。"); - - migrationBuilder.AlterColumn( - name: "Content", - table: "chat_messages", - type: "character varying(1024)", - maxLength: 1024, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(1024)", - oldMaxLength: 1024, - oldComment: "消息内容。"); - - migrationBuilder.AlterColumn( - name: "ChatSessionId", - table: "chat_messages", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "会话标识。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "chat_messages", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "cart_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "cart_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "UnitPrice", - table: "cart_items", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "单价快照。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "cart_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "cart_items", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "ShoppingCartId", - table: "cart_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属购物车标识。"); - - migrationBuilder.AlterColumn( - name: "Remark", - table: "cart_items", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "自定义备注(口味要求)。"); - - migrationBuilder.AlterColumn( - name: "Quantity", - table: "cart_items", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "数量。"); - - migrationBuilder.AlterColumn( - name: "ProductSkuId", - table: "cart_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "SKU 标识。"); - - migrationBuilder.AlterColumn( - name: "ProductName", - table: "cart_items", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "商品名称快照。"); - - migrationBuilder.AlterColumn( - name: "ProductId", - table: "cart_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "商品或 SKU 标识。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "cart_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "cart_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "cart_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "cart_items", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "AttributesJson", - table: "cart_items", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true, - oldComment: "扩展 JSON(规格、加料选项等)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "cart_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "cart_item_addons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "cart_item_addons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "cart_item_addons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "OptionId", - table: "cart_item_addons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "选项 ID(可对应 ProductAddonOption)。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "cart_item_addons", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "选项名称。"); - - migrationBuilder.AlterColumn( - name: "ExtraPrice", - table: "cart_item_addons", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "附加价格。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "cart_item_addons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "cart_item_addons", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "cart_item_addons", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "cart_item_addons", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CartItemId", - table: "cart_item_addons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属购物车条目。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "cart_item_addons", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "affiliate_payouts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "affiliate_payouts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "affiliate_payouts", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "状态。"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "affiliate_payouts", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "备注。"); - - migrationBuilder.AlterColumn( - name: "Period", - table: "affiliate_payouts", - type: "character varying(32)", - maxLength: 32, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldComment: "结算周期描述。"); - - migrationBuilder.AlterColumn( - name: "PaidAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "打款时间。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "affiliate_payouts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "affiliate_payouts", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "affiliate_payouts", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Amount", - table: "affiliate_payouts", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "结算金额。"); - - migrationBuilder.AlterColumn( - name: "AffiliatePartnerId", - table: "affiliate_payouts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "合作伙伴标识。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "affiliate_payouts", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UserId", - table: "affiliate_partners", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "用户 ID(如绑定平台账号)。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "affiliate_partners", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "affiliate_partners", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "affiliate_partners", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "affiliate_partners", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前状态。"); - - migrationBuilder.AlterColumn( - name: "Remarks", - table: "affiliate_partners", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "审核备注。"); - - migrationBuilder.AlterColumn( - name: "Phone", - table: "affiliate_partners", - type: "character varying(32)", - maxLength: 32, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(32)", - oldMaxLength: 32, - oldNullable: true, - oldComment: "联系电话。"); - - migrationBuilder.AlterColumn( - name: "DisplayName", - table: "affiliate_partners", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "昵称或渠道名称。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "affiliate_partners", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "affiliate_partners", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "affiliate_partners", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "affiliate_partners", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "CommissionRate", - table: "affiliate_partners", - type: "numeric", - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric", - oldComment: "分成比例(0-1)。"); - - migrationBuilder.AlterColumn( - name: "ChannelType", - table: "affiliate_partners", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "渠道类型。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "affiliate_partners", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "affiliate_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Status", - table: "affiliate_orders", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "当前状态。"); - - migrationBuilder.AlterColumn( - name: "SettledAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "结算完成时间。"); - - migrationBuilder.AlterColumn( - name: "OrderId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联订单。"); - - migrationBuilder.AlterColumn( - name: "OrderAmount", - table: "affiliate_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "订单金额。"); - - migrationBuilder.AlterColumn( - name: "EstimatedCommission", - table: "affiliate_orders", - type: "numeric(18,2)", - precision: 18, - scale: 2, - nullable: false, - oldClrType: typeof(decimal), - oldType: "numeric(18,2)", - oldPrecision: 18, - oldScale: 2, - oldComment: "预计佣金。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "affiliate_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "affiliate_orders", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "affiliate_orders", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "BuyerUserId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "用户 ID。"); - - migrationBuilder.AlterColumn( - name: "AffiliatePartnerId", - table: "affiliate_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "推广人标识。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "affiliate_orders", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs new file mode 100644 index 0000000..d5992f7 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace TakeoutSaaS.Infrastructure.App.Options; + +/// +/// 业务数据种子配置。 +/// +public sealed class AppSeedOptions +{ + /// + /// 配置节名称。 + /// + public const string SectionName = "App:Seed"; + + /// + /// 是否启用业务数据种子。 + /// + public bool Enabled { get; set; } + + /// + /// 默认租户配置。 + /// + public TenantSeedOptions? DefaultTenant { get; set; } + + /// + /// 基础字典分组。 + /// + public List DictionaryGroups { get; set; } = new(); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedGroupOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedGroupOptions.cs new file mode 100644 index 0000000..2cec142 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedGroupOptions.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Domain.Dictionary.Enums; + +namespace TakeoutSaaS.Infrastructure.App.Options; + +/// +/// 字典分组种子配置。 +/// +public sealed class DictionarySeedGroupOptions +{ + /// + /// 所属租户,不填则使用默认租户或系统租户。 + /// + public long? TenantId { get; set; } + + /// + /// 分组编码。 + /// + [Required] + [MaxLength(64)] + public string Code { get; set; } = string.Empty; + + /// + /// 分组名称。 + /// + [Required] + [MaxLength(128)] + public string Name { get; set; } = string.Empty; + + /// + /// 分组作用域。 + /// + public DictionaryScope Scope { get; set; } = DictionaryScope.Business; + + /// + /// 描述信息。 + /// + [MaxLength(512)] + public string? Description { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 字典项集合。 + /// + public List Items { get; set; } = new(); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedItemOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedItemOptions.cs new file mode 100644 index 0000000..6d3ea0b --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/DictionarySeedItemOptions.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.App.Options; + +/// +/// 字典项种子配置。 +/// +public sealed class DictionarySeedItemOptions +{ + /// + /// 字典项键。 + /// + [Required] + [MaxLength(64)] + public string Key { get; set; } = string.Empty; + + /// + /// 字典项值。 + /// + [Required] + [MaxLength(256)] + public string Value { get; set; } = string.Empty; + + /// + /// 描述。 + /// + [MaxLength(512)] + public string? Description { get; set; } + + /// + /// 排序。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/TenantSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/TenantSeedOptions.cs new file mode 100644 index 0000000..044e4e6 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/TenantSeedOptions.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Infrastructure.App.Options; + +/// +/// 默认租户种子配置。 +/// +public sealed class TenantSeedOptions +{ + /// + /// 自定义租户标识,不填则自动生成。 + /// + public long TenantId { get; set; } + + /// + /// 租户编码。 + /// + [Required] + [MaxLength(64)] + public string Code { get; set; } = string.Empty; + + /// + /// 租户名称。 + /// + [Required] + [MaxLength(128)] + public string Name { get; set; } = string.Empty; + + /// + /// 租户简称。 + /// + [MaxLength(128)] + public string? ShortName { get; set; } + + /// + /// 联系人姓名。 + /// + [MaxLength(64)] + public string? ContactName { get; set; } + + /// + /// 联系电话。 + /// + [MaxLength(32)] + public string? ContactPhone { get; set; } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs new file mode 100644 index 0000000..eede6a4 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs @@ -0,0 +1,301 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Infrastructure.App.Options; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Persistence; + +/// +/// 业务数据种子,确保默认租户与基础字典可重复执行。 +/// +public sealed class AppDataSeeder : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly AppSeedOptions _options; + + /// + /// 初始化种子服务。 + /// + public AppDataSeeder( + IServiceProvider serviceProvider, + ILogger logger, + IOptions options) + { + _serviceProvider = serviceProvider; + _logger = logger; + _options = options.Value; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + if (!_options.Enabled) + { + _logger.LogInformation("AppSeed 未启用,跳过业务数据初始化"); + return; + } + + using var scope = _serviceProvider.CreateScope(); + var appDbContext = scope.ServiceProvider.GetRequiredService(); + var dictionaryDbContext = scope.ServiceProvider.GetRequiredService(); + + var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken); + await EnsureDictionarySeedsAsync(dictionaryDbContext, defaultTenantId, cancellationToken); + + _logger.LogInformation("AppSeed 完成业务数据初始化"); + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// 确保默认租户存在。 + /// + private async Task EnsureDefaultTenantAsync(TakeoutAppDbContext dbContext, CancellationToken cancellationToken) + { + var tenantOptions = _options.DefaultTenant; + if (tenantOptions == null || string.IsNullOrWhiteSpace(tenantOptions.Code) || string.IsNullOrWhiteSpace(tenantOptions.Name)) + { + _logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子"); + return null; + } + + var code = tenantOptions.Code.Trim(); + var existingTenant = await dbContext.Tenants + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.Code == code, cancellationToken); + + if (existingTenant == null) + { + var tenant = new Tenant + { + Id = tenantOptions.TenantId, + Code = code, + Name = tenantOptions.Name.Trim(), + ShortName = tenantOptions.ShortName?.Trim(), + ContactName = tenantOptions.ContactName?.Trim(), + ContactPhone = tenantOptions.ContactPhone?.Trim(), + Status = TenantStatus.Active + }; + + await dbContext.Tenants.AddAsync(tenant, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + _logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code); + return tenant.Id; + } + + var updated = false; + + if (!string.Equals(existingTenant.Name, tenantOptions.Name, StringComparison.Ordinal)) + { + existingTenant.Name = tenantOptions.Name.Trim(); + updated = true; + } + + if (!string.Equals(existingTenant.ShortName, tenantOptions.ShortName, StringComparison.Ordinal)) + { + existingTenant.ShortName = tenantOptions.ShortName?.Trim(); + updated = true; + } + + if (!string.Equals(existingTenant.ContactName, tenantOptions.ContactName, StringComparison.Ordinal)) + { + existingTenant.ContactName = tenantOptions.ContactName?.Trim(); + updated = true; + } + + if (!string.Equals(existingTenant.ContactPhone, tenantOptions.ContactPhone, StringComparison.Ordinal)) + { + existingTenant.ContactPhone = tenantOptions.ContactPhone?.Trim(); + updated = true; + } + + if (existingTenant.Status != TenantStatus.Active) + { + existingTenant.Status = TenantStatus.Active; + updated = true; + } + + if (updated) + { + dbContext.Tenants.Update(existingTenant); + await dbContext.SaveChangesAsync(cancellationToken); + _logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code); + } + else + { + _logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code); + } + + return existingTenant.Id; + } + + /// + /// 确保基础字典存在。 + /// + private async Task EnsureDictionarySeedsAsync(DictionaryDbContext dbContext, long? defaultTenantId, CancellationToken cancellationToken) + { + if (_options.DictionaryGroups == null || _options.DictionaryGroups.Count == 0) + { + _logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子"); + return; + } + + foreach (var groupOptions in _options.DictionaryGroups) + { + if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name)) + { + _logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空"); + continue; + } + + var tenantId = groupOptions.TenantId ?? defaultTenantId ?? 0; + var code = groupOptions.Code.Trim(); + + var group = await dbContext.DictionaryGroups + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Code == code, cancellationToken); + + if (group == null) + { + group = new DictionaryGroup + { + Id = 0, + TenantId = tenantId, + Code = code, + Name = groupOptions.Name.Trim(), + Scope = groupOptions.Scope, + Description = groupOptions.Description?.Trim(), + IsEnabled = groupOptions.IsEnabled + }; + + await dbContext.DictionaryGroups.AddAsync(group, cancellationToken); + _logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId); + } + else + { + var groupUpdated = false; + + if (!string.Equals(group.Name, groupOptions.Name, StringComparison.Ordinal)) + { + group.Name = groupOptions.Name.Trim(); + groupUpdated = true; + } + + if (!string.Equals(group.Description, groupOptions.Description, StringComparison.Ordinal)) + { + group.Description = groupOptions.Description?.Trim(); + groupUpdated = true; + } + + if (group.Scope != groupOptions.Scope) + { + group.Scope = groupOptions.Scope; + groupUpdated = true; + } + + if (group.IsEnabled != groupOptions.IsEnabled) + { + group.IsEnabled = groupOptions.IsEnabled; + groupUpdated = true; + } + + if (groupUpdated) + { + dbContext.DictionaryGroups.Update(group); + } + } + + await UpsertDictionaryItemsAsync(dbContext, group, groupOptions.Items, tenantId, cancellationToken); + } + + await dbContext.SaveChangesAsync(cancellationToken); + } + + /// + /// 合并字典项。 + /// + private static async Task UpsertDictionaryItemsAsync( + DictionaryDbContext dbContext, + DictionaryGroup group, + IEnumerable seedItems, + long tenantId, + CancellationToken cancellationToken) + { + var materializedItems = seedItems + .Where(item => !string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value)) + .ToList(); + + if (materializedItems.Count == 0) + { + return; + } + + var existingItems = await dbContext.DictionaryItems + .IgnoreQueryFilters() + .Where(x => x.GroupId == group.Id) + .ToListAsync(cancellationToken); + + foreach (var seed in materializedItems) + { + var key = seed.Key.Trim(); + var existing = existingItems.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + + if (existing == null) + { + var newItem = new DictionaryItem + { + Id = 0, + TenantId = tenantId, + GroupId = group.Id, + Key = key, + Value = seed.Value.Trim(), + Description = seed.Description?.Trim(), + SortOrder = seed.SortOrder, + IsEnabled = seed.IsEnabled + }; + + await dbContext.DictionaryItems.AddAsync(newItem, cancellationToken); + continue; + } + + var updated = false; + + if (!string.Equals(existing.Value, seed.Value, StringComparison.Ordinal)) + { + existing.Value = seed.Value.Trim(); + updated = true; + } + + if (!string.Equals(existing.Description, seed.Description, StringComparison.Ordinal)) + { + existing.Description = seed.Description?.Trim(); + updated = true; + } + + if (existing.SortOrder != seed.SortOrder) + { + existing.SortOrder = seed.SortOrder; + updated = true; + } + + if (existing.IsEnabled != seed.IsEnabled) + { + existing.IsEnabled = seed.IsEnabled; + updated = true; + } + + if (updated) + { + dbContext.DictionaryItems.Update(existing); + } + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 53474ea..14d4ff7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -20,6 +20,7 @@ using TakeoutSaaS.Domain.Reservations.Entities; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -31,8 +32,9 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence; public sealed class TakeoutAppDbContext( DbContextOptions options, ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) + ICurrentUserAccessor? currentUserAccessor = null, + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { public DbSet Tenants => Set(); public DbSet TenantPackages => Set(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs new file mode 100644 index 0000000..45db052 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs @@ -0,0 +1,71 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 配送聚合的 EF Core 仓储实现。 +/// +public sealed class EfDeliveryRepository : IDeliveryRepository +{ + private readonly TakeoutAppDbContext _context; + + /// + /// 初始化仓储。 + /// + public EfDeliveryRepository(TakeoutAppDbContext context) + { + _context = context; + } + + /// + public Task FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.DeliveryOrders + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public Task FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.DeliveryOrders + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) + { + var events = await _context.DeliveryEvents + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId) + .OrderBy(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return events; + } + + /// + public Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) + { + return _context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask(); + } + + /// + public Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default) + { + return _context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask(); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs new file mode 100644 index 0000000..a17b212 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs @@ -0,0 +1,116 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Merchants.Entities; +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 商户聚合的 EF Core 仓储实现。 +/// +public sealed class EfMerchantRepository : IMerchantRepository +{ + private readonly TakeoutAppDbContext _context; + + /// + /// 初始化仓储。 + /// + public EfMerchantRepository(TakeoutAppDbContext context) + { + _context = context; + } + + /// + public Task FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.Merchants + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.Id == merchantId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default) + { + var query = _context.Merchants + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + return await query + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + } + + /// + public async Task> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) + { + var staffs = await _context.MerchantStaff + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) + .OrderBy(x => x.Name) + .ToListAsync(cancellationToken); + + return staffs; + } + + /// + public async Task> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) + { + var contracts = await _context.MerchantContracts + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return contracts; + } + + /// + public async Task> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) + { + var documents = await _context.MerchantDocuments + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) + .OrderBy(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return documents; + } + + /// + public Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default) + { + return _context.Merchants.AddAsync(merchant, cancellationToken).AsTask(); + } + + /// + public Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default) + { + return _context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask(); + } + + /// + public Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default) + { + return _context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask(); + } + + /// + public Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default) + { + return _context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask(); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs new file mode 100644 index 0000000..9517ef9 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 订单聚合的 EF Core 仓储实现。 +/// +public sealed class EfOrderRepository : IOrderRepository +{ + private readonly TakeoutAppDbContext _context; + + /// + /// 初始化仓储。 + /// + public EfOrderRepository(TakeoutAppDbContext context) + { + _context = context; + } + + /// + public Task FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.Orders + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.Id == orderId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public Task FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default) + { + return _context.Orders + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.OrderNo == orderNo) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default) + { + var query = _context.Orders + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + if (paymentStatus.HasValue) + { + query = query.Where(x => x.PaymentStatus == paymentStatus.Value); + } + + var orders = await query + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return orders; + } + + /// + public async Task> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + var items = await _context.OrderItems + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .OrderBy(x => x.Id) + .ToListAsync(cancellationToken); + + return items; + } + + /// + public async Task> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + var histories = await _context.OrderStatusHistories + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .OrderBy(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return histories; + } + + /// + public async Task> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + var refunds = await _context.RefundRequests + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return refunds; + } + + /// + public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default) + { + return _context.Orders.AddAsync(order, cancellationToken).AsTask(); + } + + /// + public Task AddItemsAsync(IEnumerable items, CancellationToken cancellationToken = default) + { + return _context.OrderItems.AddRangeAsync(items, cancellationToken); + } + + /// + public Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default) + { + return _context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask(); + } + + /// + public Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default) + { + return _context.RefundRequests.AddAsync(refund, cancellationToken).AsTask(); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs new file mode 100644 index 0000000..af5034d --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs @@ -0,0 +1,71 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 支付记录的 EF Core 仓储实现。 +/// +public sealed class EfPaymentRepository : IPaymentRepository +{ + private readonly TakeoutAppDbContext _context; + + /// + /// 初始化仓储。 + /// + public EfPaymentRepository(TakeoutAppDbContext context) + { + _context = context; + } + + /// + public Task FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.PaymentRecords + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.Id == paymentId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public Task FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.PaymentRecords + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) + { + var refunds = await _context.PaymentRefundRecords + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId) + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return refunds; + } + + /// + public Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default) + { + return _context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask(); + } + + /// + public Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default) + { + return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask(); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs new file mode 100644 index 0000000..fde29fe --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs @@ -0,0 +1,227 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 商品聚合的 EF Core 仓储实现。 +/// +public sealed class EfProductRepository : IProductRepository +{ + private readonly TakeoutAppDbContext _context; + + /// + /// 初始化仓储。 + /// + public EfProductRepository(TakeoutAppDbContext context) + { + _context = context; + } + + /// + public Task FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.Products + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.Id == productId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> SearchAsync(long tenantId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default) + { + var query = _context.Products + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (categoryId.HasValue) + { + query = query.Where(x => x.CategoryId == categoryId.Value); + } + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + var products = await query + .OrderBy(x => x.Name) + .ToListAsync(cancellationToken); + + return products; + } + + /// + public async Task> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default) + { + var categories = await _context.ProductCategories + .AsNoTracking() + .Where(x => x.TenantId == tenantId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return categories; + } + + /// + public async Task> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var skus = await _context.ProductSkus + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return skus; + } + + /// + public async Task> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var groups = await _context.ProductAddonGroups + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return groups; + } + + /// + public async Task> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var groupIds = await _context.ProductAddonGroups + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .Select(x => x.Id) + .ToListAsync(cancellationToken); + + if (groupIds.Count == 0) + { + return Array.Empty(); + } + + var options = await _context.ProductAddonOptions + .AsNoTracking() + .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId)) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return options; + } + + /// + public async Task> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var groups = await _context.ProductAttributeGroups + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return groups; + } + + /// + public async Task> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var groupIds = await _context.ProductAttributeGroups + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .Select(x => x.Id) + .ToListAsync(cancellationToken); + + if (groupIds.Count == 0) + { + return Array.Empty(); + } + + var options = await _context.ProductAttributeOptions + .AsNoTracking() + .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId)) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return options; + } + + /// + public async Task> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var assets = await _context.ProductMediaAssets + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return assets; + } + + /// + public async Task> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var rules = await _context.ProductPricingRules + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return rules; + } + + /// + public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default) + { + return _context.ProductCategories.AddAsync(category, cancellationToken).AsTask(); + } + + /// + public Task AddProductAsync(Product product, CancellationToken cancellationToken = default) + { + return _context.Products.AddAsync(product, cancellationToken).AsTask(); + } + + /// + public Task AddSkusAsync(IEnumerable skus, CancellationToken cancellationToken = default) + { + return _context.ProductSkus.AddRangeAsync(skus, cancellationToken); + } + + /// + public Task AddAddonGroupsAsync(IEnumerable groups, IEnumerable options, CancellationToken cancellationToken = default) + { + var addGroupsTask = _context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken); + var addOptionsTask = _context.ProductAddonOptions.AddRangeAsync(options, cancellationToken); + return Task.WhenAll(addGroupsTask, addOptionsTask); + } + + /// + public Task AddAttributeGroupsAsync(IEnumerable groups, IEnumerable options, CancellationToken cancellationToken = default) + { + var addGroupsTask = _context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken); + var addOptionsTask = _context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken); + return Task.WhenAll(addGroupsTask, addOptionsTask); + } + + /// + public Task AddMediaAssetsAsync(IEnumerable assets, CancellationToken cancellationToken = default) + { + return _context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken); + } + + /// + public Task AddPricingRulesAsync(IEnumerable rules, CancellationToken cancellationToken = default) + { + return _context.ProductPricingRules.AddRangeAsync(rules, cancellationToken); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs new file mode 100644 index 0000000..120f1d0 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -0,0 +1,174 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 门店聚合的 EF Core 仓储实现。 +/// +public sealed class EfStoreRepository : IStoreRepository +{ + private readonly TakeoutAppDbContext _context; + + /// + /// 初始化仓储。 + /// + public EfStoreRepository(TakeoutAppDbContext context) + { + _context = context; + } + + /// + public Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + return _context.Stores + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.Id == storeId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default) + { + var query = _context.Stores + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + var stores = await query + .OrderBy(x => x.Name) + .ToListAsync(cancellationToken); + + return stores; + } + + /// + public async Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var hours = await _context.StoreBusinessHours + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.DayOfWeek) + .ThenBy(x => x.StartTime) + .ToListAsync(cancellationToken); + + return hours; + } + + /// + public async Task> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var zones = await _context.StoreDeliveryZones + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return zones; + } + + /// + public async Task> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var holidays = await _context.StoreHolidays + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.Date) + .ToListAsync(cancellationToken); + + return holidays; + } + + /// + public async Task> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var areas = await _context.StoreTableAreas + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.SortOrder) + .ToListAsync(cancellationToken); + + return areas; + } + + /// + public async Task> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var tables = await _context.StoreTables + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.TableCode) + .ToListAsync(cancellationToken); + + return tables; + } + + /// + public async Task> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var shifts = await _context.StoreEmployeeShifts + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.ShiftDate) + .ThenBy(x => x.StartTime) + .ToListAsync(cancellationToken); + + return shifts; + } + + /// + public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default) + { + return _context.Stores.AddAsync(store, cancellationToken).AsTask(); + } + + /// + public Task AddBusinessHoursAsync(IEnumerable hours, CancellationToken cancellationToken = default) + { + return _context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken); + } + + /// + public Task AddDeliveryZonesAsync(IEnumerable zones, CancellationToken cancellationToken = default) + { + return _context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken); + } + + /// + public Task AddHolidaysAsync(IEnumerable holidays, CancellationToken cancellationToken = default) + { + return _context.StoreHolidays.AddRangeAsync(holidays, cancellationToken); + } + + /// + public Task AddTableAreasAsync(IEnumerable areas, CancellationToken cancellationToken = default) + { + return _context.StoreTableAreas.AddRangeAsync(areas, cancellationToken); + } + + /// + public Task AddTablesAsync(IEnumerable tables, CancellationToken cancellationToken = default) + { + return _context.StoreTables.AddRangeAsync(tables, cancellationToken); + } + + /// + public Task AddShiftsAsync(IEnumerable shifts, CancellationToken cancellationToken = default) + { + return _context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs index c1047d7..4f50569 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Extensions/DatabaseServiceCollectionExtensions.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TakeoutSaaS.Infrastructure.Common.Options; using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Data; +using TakeoutSaaS.Shared.Kernel.Ids; namespace TakeoutSaaS.Infrastructure.Common.Extensions; @@ -25,6 +27,17 @@ public static class DatabaseServiceCollectionExtensions .ValidateDataAnnotations() .ValidateOnStart(); + services.AddOptions() + .Bind(configuration.GetSection(IdGeneratorOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return new SnowflakeIdGenerator(options.WorkerId, options.DatacenterId); + }); + services.AddSingleton(); services.AddScoped(); return services; diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs index 068a88f..4733274 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/AppDbContext.cs @@ -3,15 +3,20 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Shared.Abstractions.Entities; using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Ids; namespace TakeoutSaaS.Infrastructure.Common.Persistence; /// /// 应用基础 DbContext,统一处理审计字段、软删除与全局查询过滤。 /// -public abstract class AppDbContext(DbContextOptions options, ICurrentUserAccessor? currentUserAccessor = null) : DbContext(options) +public abstract class AppDbContext( + DbContextOptions options, + ICurrentUserAccessor? currentUserAccessor = null, + IIdGenerator? idGenerator = null) : DbContext(options) { private readonly ICurrentUserAccessor? _currentUserAccessor = currentUserAccessor; + private readonly IIdGenerator? _idGenerator = idGenerator; /// /// 构建模型时应用软删除过滤器。 @@ -50,10 +55,35 @@ public abstract class AppDbContext(DbContextOptions options, ICurrentUserAccesso /// protected virtual void OnBeforeSaving() { + ApplyIdGeneration(); ApplySoftDeleteMetadata(); ApplyAuditMetadata(); } + /// + /// 为新增实体生成雪花 ID。 + /// + private void ApplyIdGeneration() + { + if (_idGenerator == null) + { + return; + } + + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State != EntityState.Added) + { + continue; + } + + if (entry.Entity.Id == 0) + { + entry.Entity.Id = _idGenerator.NextId(); + } + } + } + /// /// 将软删除实体的删除操作转换为设置 DeletedAt。 /// @@ -114,10 +144,10 @@ public abstract class AppDbContext(DbContextOptions options, ICurrentUserAccesso } } - private Guid? GetCurrentUserIdOrNull() + private long? GetCurrentUserIdOrNull() { - var userId = _currentUserAccessor?.UserId ?? Guid.Empty; - return userId == Guid.Empty ? null : userId; + var userId = _currentUserAccessor?.UserId ?? 0; + return userId == 0 ? null : userId; } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs index c3f59f2..5a2cf5f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs @@ -141,12 +141,12 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb private sealed class DesignTimeTenantProvider : ITenantProvider { - public Guid GetCurrentTenantId() => Guid.Empty; + public long GetCurrentTenantId() => 0; } private sealed class DesignTimeCurrentUserAccessor : ICurrentUserAccessor { - public Guid UserId => Guid.Empty; + public long UserId => 0; public bool IsAuthenticated => false; } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs index 8681af9..f743d24 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Shared.Abstractions.Entities; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Ids; namespace TakeoutSaaS.Infrastructure.Common.Persistence; @@ -12,14 +13,15 @@ namespace TakeoutSaaS.Infrastructure.Common.Persistence; public abstract class TenantAwareDbContext( DbContextOptions options, ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null) : AppDbContext(options, currentUserAccessor) + ICurrentUserAccessor? currentUserAccessor = null, + IIdGenerator? idGenerator = null) : AppDbContext(options, currentUserAccessor, idGenerator) { private readonly ITenantProvider _tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider)); /// /// 当前请求租户 ID。 /// - protected Guid CurrentTenantId => _tenantProvider.GetCurrentTenantId(); + protected long CurrentTenantId => _tenantProvider.GetCurrentTenantId(); /// /// 保存前填充租户元数据并执行基础处理。 @@ -71,7 +73,7 @@ public abstract class TenantAwareDbContext( foreach (var entry in ChangeTracker.Entries()) { - if (entry.State == EntityState.Added && entry.Entity.TenantId == Guid.Empty && tenantId != Guid.Empty) + if (entry.State == EntityState.Added && entry.Entity.TenantId == 0 && tenantId != 0) { entry.Entity.TenantId = tenantId; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs deleted file mode 100644 index ed76f4c..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.Designer.cs +++ /dev/null @@ -1,172 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TakeoutSaaS.Infrastructure.Dictionary.Persistence; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations -{ - [DbContext(typeof(DictionaryDbContext))] - [Migration("20251201042346_InitialDictionary")] - partial class InitialDictionary - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("IsEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Scope") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("dictionary_groups", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("GroupId") - .HasColumnType("uuid"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("IsEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Key") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(100); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("GroupId", "Key") - .IsUnique(); - - b.ToTable("dictionary_items", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => - { - b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") - .WithMany("Items") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Group"); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs deleted file mode 100644 index 3e0084a..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201042346_InitialDictionary.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations -{ - /// - public partial class InitialDictionary : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "dictionary_groups", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Scope = table.Column(type: "integer", nullable: false), - Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_dictionary_groups", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "dictionary_items", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - GroupId = table.Column(type: "uuid", nullable: false), - Key = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Value = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - IsDefault = table.Column(type: "boolean", nullable: false), - IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true), - SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100), - Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_dictionary_items", x => x.Id); - table.ForeignKey( - name: "FK_dictionary_items_dictionary_groups_GroupId", - column: x => x.GroupId, - principalTable: "dictionary_groups", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_dictionary_groups_TenantId", - table: "dictionary_groups", - column: "TenantId"); - - migrationBuilder.CreateIndex( - name: "IX_dictionary_groups_TenantId_Code", - table: "dictionary_groups", - columns: new[] { "TenantId", "Code" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_dictionary_items_GroupId_Key", - table: "dictionary_items", - columns: new[] { "GroupId", "Key" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_dictionary_items_TenantId", - table: "dictionary_items", - column: "TenantId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "dictionary_items"); - - migrationBuilder.DropTable( - name: "dictionary_groups"); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs deleted file mode 100644 index 2447766..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.cs +++ /dev/null @@ -1,599 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations -{ - /// - public partial class AddEntityComments : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterTable( - name: "dictionary_items", - comment: "参数字典项。"); - - migrationBuilder.AlterTable( - name: "dictionary_groups", - comment: "参数字典分组(系统参数、业务参数)。"); - - migrationBuilder.AlterColumn( - name: "Value", - table: "dictionary_items", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "字典项值。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "dictionary_items", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "dictionary_items", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "dictionary_items", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "dictionary_items", - type: "integer", - nullable: false, - defaultValue: 100, - comment: "排序值,越小越靠前。", - oldClrType: typeof(int), - oldType: "integer", - oldDefaultValue: 100); - - migrationBuilder.AlterColumn( - name: "Key", - table: "dictionary_items", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "字典项键。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "IsEnabled", - table: "dictionary_items", - type: "boolean", - nullable: false, - defaultValue: true, - comment: "是否启用。", - oldClrType: typeof(bool), - oldType: "boolean", - oldDefaultValue: true); - - migrationBuilder.AlterColumn( - name: "IsDefault", - table: "dictionary_items", - type: "boolean", - nullable: false, - comment: "是否默认项。", - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AlterColumn( - name: "GroupId", - table: "dictionary_items", - type: "uuid", - nullable: false, - comment: "关联分组 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "dictionary_items", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "描述信息。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "dictionary_items", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "dictionary_items", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "dictionary_items", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "dictionary_items", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "dictionary_items", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "dictionary_groups", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "dictionary_groups", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "dictionary_groups", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Scope", - table: "dictionary_groups", - type: "integer", - nullable: false, - comment: "分组作用域:系统/业务。", - oldClrType: typeof(int), - oldType: "integer"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "dictionary_groups", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "分组名称。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "IsEnabled", - table: "dictionary_groups", - type: "boolean", - nullable: false, - defaultValue: true, - comment: "是否启用。", - oldClrType: typeof(bool), - oldType: "boolean", - oldDefaultValue: true); - - migrationBuilder.AlterColumn( - name: "Description", - table: "dictionary_groups", - type: "character varying(512)", - maxLength: 512, - nullable: true, - comment: "描述信息。", - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "dictionary_groups", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "dictionary_groups", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "dictionary_groups", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "dictionary_groups", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "dictionary_groups", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "分组编码(唯一)。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Id", - table: "dictionary_groups", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterTable( - name: "dictionary_items", - oldComment: "参数字典项。"); - - migrationBuilder.AlterTable( - name: "dictionary_groups", - oldComment: "参数字典分组(系统参数、业务参数)。"); - - migrationBuilder.AlterColumn( - name: "Value", - table: "dictionary_items", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "字典项值。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "dictionary_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "dictionary_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "dictionary_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "SortOrder", - table: "dictionary_items", - type: "integer", - nullable: false, - defaultValue: 100, - oldClrType: typeof(int), - oldType: "integer", - oldDefaultValue: 100, - oldComment: "排序值,越小越靠前。"); - - migrationBuilder.AlterColumn( - name: "Key", - table: "dictionary_items", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "字典项键。"); - - migrationBuilder.AlterColumn( - name: "IsEnabled", - table: "dictionary_items", - type: "boolean", - nullable: false, - defaultValue: true, - oldClrType: typeof(bool), - oldType: "boolean", - oldDefaultValue: true, - oldComment: "是否启用。"); - - migrationBuilder.AlterColumn( - name: "IsDefault", - table: "dictionary_items", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldComment: "是否默认项。"); - - migrationBuilder.AlterColumn( - name: "GroupId", - table: "dictionary_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "关联分组 ID。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "dictionary_items", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "描述信息。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "dictionary_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "dictionary_items", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "dictionary_items", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "dictionary_items", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "dictionary_items", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "dictionary_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "dictionary_groups", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "dictionary_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Scope", - table: "dictionary_groups", - type: "integer", - nullable: false, - oldClrType: typeof(int), - oldType: "integer", - oldComment: "分组作用域:系统/业务。"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "dictionary_groups", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "分组名称。"); - - migrationBuilder.AlterColumn( - name: "IsEnabled", - table: "dictionary_groups", - type: "boolean", - nullable: false, - defaultValue: true, - oldClrType: typeof(bool), - oldType: "boolean", - oldDefaultValue: true, - oldComment: "是否启用。"); - - migrationBuilder.AlterColumn( - name: "Description", - table: "dictionary_groups", - type: "character varying(512)", - maxLength: 512, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(512)", - oldMaxLength: 512, - oldNullable: true, - oldComment: "描述信息。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "dictionary_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "dictionary_groups", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "dictionary_groups", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "dictionary_groups", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Code", - table: "dictionary_groups", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "分组编码(唯一)。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "dictionary_groups", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index a6aa6b3..6025e35 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Domain.Dictionary.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -13,8 +14,9 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; public sealed class DictionaryDbContext( DbContextOptions options, ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) + ICurrentUserAccessor? currentUserAccessor = null, + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 字典分组集。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs index 3bb86c1..358a7c7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs @@ -19,7 +19,7 @@ public sealed class EfDictionaryRepository : IDictionaryRepository _context = context; } - public Task FindGroupByIdAsync(Guid id, CancellationToken cancellationToken = default) + public Task FindGroupByIdAsync(long id, CancellationToken cancellationToken = default) => _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); public Task FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default) @@ -50,10 +50,10 @@ public sealed class EfDictionaryRepository : IDictionaryRepository return Task.CompletedTask; } - public Task FindItemByIdAsync(Guid id, CancellationToken cancellationToken = default) + public Task FindItemByIdAsync(long id, CancellationToken cancellationToken = default) => _context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); - public async Task> GetItemsByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default) + public async Task> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default) { return await _context.DictionaryItems .AsNoTracking() @@ -77,7 +77,7 @@ public sealed class EfDictionaryRepository : IDictionaryRepository public Task SaveChangesAsync(CancellationToken cancellationToken = default) => _context.SaveChangesAsync(cancellationToken); - public async Task> GetItemsByCodesAsync(IEnumerable codes, Guid tenantId, bool includeSystem, CancellationToken cancellationToken = default) + public async Task> GetItemsByCodesAsync(IEnumerable codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default) { var normalizedCodes = codes .Where(code => !string.IsNullOrWhiteSpace(code)) @@ -96,7 +96,7 @@ public sealed class EfDictionaryRepository : IDictionaryRepository .Include(item => item.Group) .Where(item => normalizedCodes.Contains(item.Group!.Code)); - query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == Guid.Empty)); + query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == 0)); return await query .OrderBy(item => item.SortOrder) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs index 9024674..d2708ba 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs @@ -22,7 +22,7 @@ public sealed class DistributedDictionaryCache : IDictionaryCache _options = options.Value; } - public async Task?> GetAsync(Guid tenantId, string code, CancellationToken cancellationToken = default) + public async Task?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default) { var cacheKey = BuildKey(tenantId, code); var payload = await _cache.GetAsync(cacheKey, cancellationToken); @@ -34,7 +34,7 @@ public sealed class DistributedDictionaryCache : IDictionaryCache return JsonSerializer.Deserialize>(payload, _serializerOptions); } - public Task SetAsync(Guid tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default) + public Task SetAsync(long tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default) { var cacheKey = BuildKey(tenantId, code); var payload = JsonSerializer.SerializeToUtf8Bytes(items, _serializerOptions); @@ -45,12 +45,12 @@ public sealed class DistributedDictionaryCache : IDictionaryCache return _cache.SetAsync(cacheKey, payload, options, cancellationToken); } - public Task RemoveAsync(Guid tenantId, string code, CancellationToken cancellationToken = default) + public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default) { var cacheKey = BuildKey(tenantId, code); return _cache.RemoveAsync(cacheKey, cancellationToken); } - private static string BuildKey(Guid tenantId, string code) - => $"dictionary:{tenantId.ToString().ToLowerInvariant()}:{code.ToLowerInvariant()}"; + private static string BuildKey(long tenantId, string code) + => $"dictionary:{tenantId}:{code.ToLowerInvariant()}"; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs deleted file mode 100644 index 9ea6285..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.Designer.cs +++ /dev/null @@ -1,152 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TakeoutSaaS.Infrastructure.Identity.Persistence; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.Identity.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20251201042324_InitialIdentity")] - partial class InitialIdentity - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Account") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Avatar") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("DisplayName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MerchantId") - .HasColumnType("uuid"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Permissions") - .IsRequired() - .HasColumnType("text"); - - b.Property("Roles") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "Account") - .IsUnique(); - - b.ToTable("identity_users", (string)null); - }); - - modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedBy") - .HasColumnType("uuid"); - - b.Property("Nickname") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OpenId") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("TenantId") - .HasColumnType("uuid"); - - b.Property("UnionId") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedBy") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "OpenId") - .IsUnique(); - - b.ToTable("mini_users", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs deleted file mode 100644 index 5a5a2c7..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201042324_InitialIdentity.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.Identity.Migrations -{ - /// - public partial class InitialIdentity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "identity_users", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Account = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - DisplayName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - PasswordHash = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - MerchantId = table.Column(type: "uuid", nullable: true), - Roles = table.Column(type: "text", nullable: false), - Permissions = table.Column(type: "text", nullable: false), - Avatar = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_identity_users", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "mini_users", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OpenId = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - UnionId = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - Nickname = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Avatar = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: true), - UpdatedBy = table.Column(type: "uuid", nullable: true), - DeletedBy = table.Column(type: "uuid", nullable: true), - TenantId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_mini_users", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_identity_users_TenantId", - table: "identity_users", - column: "TenantId"); - - migrationBuilder.CreateIndex( - name: "IX_identity_users_TenantId_Account", - table: "identity_users", - columns: new[] { "TenantId", "Account" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_mini_users_TenantId", - table: "mini_users", - column: "TenantId"); - - migrationBuilder.CreateIndex( - name: "IX_mini_users_TenantId_OpenId", - table: "mini_users", - columns: new[] { "TenantId", "OpenId" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "identity_users"); - - migrationBuilder.DropTable( - name: "mini_users"); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs deleted file mode 100644 index 5f0481b..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.cs +++ /dev/null @@ -1,581 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TakeoutSaaS.Infrastructure.Identity.Migrations -{ - /// - public partial class AddEntityComments : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterTable( - name: "mini_users", - comment: "小程序用户实体。"); - - migrationBuilder.AlterTable( - name: "identity_users", - comment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "mini_users", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "mini_users", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UnionId", - table: "mini_users", - type: "character varying(128)", - maxLength: 128, - nullable: true, - comment: "微信 UnionId,可能为空。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "mini_users", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "OpenId", - table: "mini_users", - type: "character varying(128)", - maxLength: 128, - nullable: false, - comment: "微信 OpenId。", - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); - - migrationBuilder.AlterColumn( - name: "Nickname", - table: "mini_users", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "昵称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "mini_users", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "mini_users", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "mini_users", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "mini_users", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Avatar", - table: "mini_users", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "头像地址。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Id", - table: "mini_users", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "identity_users", - type: "uuid", - nullable: true, - comment: "最后更新人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "identity_users", - type: "timestamp with time zone", - nullable: true, - comment: "最近一次更新时间(UTC),从未更新时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "identity_users", - type: "uuid", - nullable: false, - comment: "所属租户 ID。", - oldClrType: typeof(Guid), - oldType: "uuid"); - - migrationBuilder.AlterColumn( - name: "Roles", - table: "identity_users", - type: "text", - nullable: false, - comment: "角色集合。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Permissions", - table: "identity_users", - type: "text", - nullable: false, - comment: "权限集合。", - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "PasswordHash", - table: "identity_users", - type: "character varying(256)", - maxLength: 256, - nullable: false, - comment: "密码哈希。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "identity_users", - type: "uuid", - nullable: true, - comment: "所属商户(平台管理员为空)。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DisplayName", - table: "identity_users", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "展示名称。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "identity_users", - type: "uuid", - nullable: true, - comment: "删除人用户标识(软删除),未删除时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "identity_users", - type: "timestamp with time zone", - nullable: true, - comment: "软删除时间(UTC),未删除时为 null。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "identity_users", - type: "uuid", - nullable: true, - comment: "创建人用户标识,匿名或系统操作时为 null。", - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "identity_users", - type: "timestamp with time zone", - nullable: false, - comment: "创建时间(UTC)。", - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "Avatar", - table: "identity_users", - type: "character varying(256)", - maxLength: 256, - nullable: true, - comment: "头像地址。", - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Account", - table: "identity_users", - type: "character varying(64)", - maxLength: 64, - nullable: false, - comment: "登录账号。", - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64); - - migrationBuilder.AlterColumn( - name: "Id", - table: "identity_users", - type: "uuid", - nullable: false, - comment: "实体唯一标识。", - oldClrType: typeof(Guid), - oldType: "uuid"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterTable( - name: "mini_users", - oldComment: "小程序用户实体。"); - - migrationBuilder.AlterTable( - name: "identity_users", - oldComment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "mini_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "mini_users", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "UnionId", - table: "mini_users", - type: "character varying(128)", - maxLength: 128, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldNullable: true, - oldComment: "微信 UnionId,可能为空。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "mini_users", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "OpenId", - table: "mini_users", - type: "character varying(128)", - maxLength: 128, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128, - oldComment: "微信 OpenId。"); - - migrationBuilder.AlterColumn( - name: "Nickname", - table: "mini_users", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "昵称。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "mini_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "mini_users", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "mini_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "mini_users", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Avatar", - table: "mini_users", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "头像地址。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "mini_users", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - - migrationBuilder.AlterColumn( - name: "UpdatedBy", - table: "identity_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "最后更新人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "UpdatedAt", - table: "identity_users", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "最近一次更新时间(UTC),从未更新时为 null。"); - - migrationBuilder.AlterColumn( - name: "TenantId", - table: "identity_users", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "所属租户 ID。"); - - migrationBuilder.AlterColumn( - name: "Roles", - table: "identity_users", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "角色集合。"); - - migrationBuilder.AlterColumn( - name: "Permissions", - table: "identity_users", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldComment: "权限集合。"); - - migrationBuilder.AlterColumn( - name: "PasswordHash", - table: "identity_users", - type: "character varying(256)", - maxLength: 256, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldComment: "密码哈希。"); - - migrationBuilder.AlterColumn( - name: "MerchantId", - table: "identity_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "所属商户(平台管理员为空)。"); - - migrationBuilder.AlterColumn( - name: "DisplayName", - table: "identity_users", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "展示名称。"); - - migrationBuilder.AlterColumn( - name: "DeletedBy", - table: "identity_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "删除人用户标识(软删除),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "DeletedAt", - table: "identity_users", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true, - oldComment: "软删除时间(UTC),未删除时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "identity_users", - type: "uuid", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uuid", - oldNullable: true, - oldComment: "创建人用户标识,匿名或系统操作时为 null。"); - - migrationBuilder.AlterColumn( - name: "CreatedAt", - table: "identity_users", - type: "timestamp with time zone", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldComment: "创建时间(UTC)。"); - - migrationBuilder.AlterColumn( - name: "Avatar", - table: "identity_users", - type: "character varying(256)", - maxLength: 256, - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256, - oldNullable: true, - oldComment: "头像地址。"); - - migrationBuilder.AlterColumn( - name: "Account", - table: "identity_users", - type: "character varying(64)", - maxLength: 64, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(64)", - oldMaxLength: 64, - oldComment: "登录账号。"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "identity_users", - type: "uuid", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uuid", - oldComment: "实体唯一标识。"); - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs index 575f1b0..5a2f813 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs @@ -39,12 +39,12 @@ public sealed class SeedUserOptions /// /// 所属租户 ID。 /// - public Guid TenantId { get; set; } + public long TenantId { get; set; } /// /// 所属商户 ID(平台管理员为空)。 /// - public Guid? MerchantId { get; set; } + public long? MerchantId { get; set; } /// /// 角色集合。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index e90127c..f11a02d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -22,6 +22,6 @@ public sealed class EfIdentityUserRepository : IIdentityUserRepository public Task FindByAccountAsync(string account, CancellationToken cancellationToken = default) => _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); - public Task FindByIdAsync(Guid userId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(long userId, CancellationToken cancellationToken = default) => _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs index d5c372b..9f843ca 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs @@ -22,17 +22,17 @@ public sealed class EfMiniUserRepository : IMiniUserRepository public Task FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default) => _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); - public Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default) + public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) => _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); - public async Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default) + public async Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default) { var user = await _dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); if (user == null) { user = new MiniUser { - Id = Guid.NewGuid(), + Id = 0, OpenId = openId, UnionId = unionId, Nickname = nickname ?? "小程序用户", diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index f640da0..b6ce20f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -42,7 +42,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger { user = new DomainIdentityUser { - Id = Guid.NewGuid(), + Id = 0, Account = userOptions.Account, DisplayName = userOptions.DisplayName, TenantId = userOptions.TenantId, @@ -80,7 +80,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger .Select(v => v.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase)]; - private static IDisposable EnterTenantScope(ITenantContextAccessor accessor, Guid tenantId) + private static IDisposable EnterTenantScope(ITenantContextAccessor accessor, long tenantId) { var previous = accessor.Current; accessor.Current = new TenantContext(tenantId, null, "admin-seed"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 07ab279..7f98396 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; +using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -16,8 +17,9 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; public sealed class IdentityDbContext( DbContextOptions options, ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor) + ICurrentUserAccessor? currentUserAccessor = null, + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 管理后台用户集合。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs index aabd967..3a670a9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs @@ -26,7 +26,7 @@ public sealed class RedisRefreshTokenStore : IRefreshTokenStore _options = options.Value; } - public async Task IssueAsync(Guid userId, DateTime expiresAt, CancellationToken cancellationToken = default) + public async Task IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default) { var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48)); var descriptor = new RefreshTokenDescriptor(token, userId, expiresAt, false); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.Designer.cs similarity index 79% rename from src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs rename to src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.Designer.cs index 2baf184..84faa52 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/20251201094254_AddEntityComments.Designer.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.Designer.cs @@ -9,11 +9,11 @@ using TakeoutSaaS.Infrastructure.App.Persistence; #nullable disable -namespace TakeoutSaaS.Infrastructure.App.Migrations +namespace TakeoutSaaS.Infrastructure.Migrations { [DbContext(typeof(TakeoutAppDbContext))] - [Migration("20251201094254_AddEntityComments")] - partial class AddEntityComments + [Migration("20251202005208_InitSnowflake_App")] + partial class InitSnowflake_App { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -27,11 +27,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ConditionJson") .IsRequired() .HasColumnType("text") @@ -41,24 +43,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Enabled") .HasColumnType("boolean") .HasComment("是否启用。"); - b.Property("MetricDefinitionId") - .HasColumnType("uuid") + b.Property("MetricDefinitionId") + .HasColumnType("bigint") .HasComment("关联指标。"); b.Property("NotificationChannels") @@ -71,16 +73,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("告警级别。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -95,11 +97,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Code") .IsRequired() .HasMaxLength(64) @@ -110,8 +114,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DefaultAggregation") @@ -124,8 +128,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -143,16 +147,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("指标名称。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -168,25 +172,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DimensionKey") @@ -195,20 +201,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("维度键(JSON)。"); - b.Property("MetricDefinitionId") - .HasColumnType("uuid") + b.Property("MetricDefinitionId") + .HasColumnType("bigint") .HasComment("指标定义 ID。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Value") @@ -237,35 +243,37 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Code") .IsRequired() .HasMaxLength(32) .HasColumnType("character varying(32)") .HasComment("券码或序列号。"); - b.Property("CouponTemplateId") - .HasColumnType("uuid") + b.Property("CouponTemplateId") + .HasColumnType("bigint") .HasComment("模板标识。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireAt") @@ -276,32 +284,32 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("发放时间。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("订单 ID(已使用时记录)。"); b.Property("Status") .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("UsedAt") .HasColumnType("timestamp with time zone") .HasComment("使用时间。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("归属用户。"); b.HasKey("Id"); @@ -317,11 +325,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AllowStack") .HasColumnType("boolean") .HasComment("是否允许叠加其他优惠。"); @@ -342,16 +352,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -389,8 +399,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("适用门店 ID 集合(JSON)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TotalQuantity") @@ -401,8 +411,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("ValidFrom") @@ -427,11 +437,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AudienceDescription") .HasMaxLength(512) .HasColumnType("character varying(512)") @@ -450,16 +462,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndAt") @@ -489,16 +501,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("活动状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -511,13 +523,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("ChatSessionId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSessionId") + .HasColumnType("bigint") .HasComment("会话标识。"); b.Property("Content") @@ -536,16 +550,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsRead") @@ -560,20 +574,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("发送方类型。"); - b.Property("SenderUserId") - .HasColumnType("uuid") + b.Property("SenderUserId") + .HasColumnType("bigint") .HasComment("发送方用户 ID。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -588,33 +602,35 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AgentUserId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentUserId") + .HasColumnType("bigint") .HasComment("当前客服员工 ID。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); - b.Property("CustomerUserId") - .HasColumnType("uuid") + b.Property("CustomerUserId") + .HasColumnType("bigint") .HasComment("顾客用户 ID。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndedAt") @@ -639,20 +655,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("会话状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("所属门店(可空为平台)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -668,13 +684,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AssignedAgentId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAgentId") + .HasColumnType("bigint") .HasComment("指派的客服。"); b.Property("ClosedAt") @@ -685,20 +703,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); - b.Property("CustomerUserId") - .HasColumnType("uuid") + b.Property("CustomerUserId") + .HasColumnType("bigint") .HasComment("客户用户 ID。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -706,8 +724,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("工单详情。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单(如有)。"); b.Property("Priority") @@ -724,8 +742,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("工单主题。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TicketNo") @@ -738,8 +756,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -755,17 +773,19 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttachmentsJson") .HasColumnType("text") .HasComment("附件 JSON。"); - b.Property("AuthorUserId") - .HasColumnType("uuid") + b.Property("AuthorUserId") + .HasColumnType("bigint") .HasComment("评论人 ID。"); b.Property("Content") @@ -778,36 +798,36 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsInternal") .HasColumnType("boolean") .HasComment("是否内部备注。"); - b.Property("SupportTicketId") - .HasColumnType("uuid") + b.Property("SupportTicketId") + .HasColumnType("bigint") .HasComment("工单标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -822,29 +842,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("DeliveryOrderId") - .HasColumnType("uuid") + b.Property("DeliveryOrderId") + .HasColumnType("bigint") .HasComment("配送单标识。"); b.Property("EventType") @@ -865,16 +887,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("原始数据 JSON。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -889,11 +911,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CourierName") .HasMaxLength(64) .HasColumnType("character varying(64)") @@ -908,16 +932,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveredAt") @@ -938,8 +962,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("异常原因。"); - b.Property("OrderId") - .HasColumnType("uuid"); + b.Property("OrderId") + .HasColumnType("bigint"); b.Property("PickedUpAt") .HasColumnType("timestamp with time zone") @@ -958,16 +982,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -983,33 +1007,35 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AffiliatePartnerId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") .HasComment("推广人标识。"); - b.Property("BuyerUserId") - .HasColumnType("uuid") + b.Property("BuyerUserId") + .HasColumnType("bigint") .HasComment("用户 ID。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EstimatedCommission") @@ -1022,8 +1048,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("订单金额。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单。"); b.Property("SettledAt") @@ -1034,16 +1060,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1059,11 +1085,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ChannelType") .HasColumnType("integer") .HasComment("渠道类型。"); @@ -1076,16 +1104,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DisplayName") @@ -1108,20 +1136,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户 ID(如绑定平台账号)。"); b.HasKey("Id"); @@ -1136,13 +1164,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AffiliatePartnerId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") .HasComment("合作伙伴标识。"); b.Property("Amount") @@ -1154,16 +1184,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("PaidAt") @@ -1185,16 +1215,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1210,11 +1240,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AllowMakeupCount") .HasColumnType("integer") .HasComment("支持补签次数。"); @@ -1223,16 +1255,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -1263,16 +1295,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1287,13 +1319,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("CheckInCampaignId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CheckInCampaignId") + .HasColumnType("bigint") .HasComment("活动标识。"); b.Property("CheckInDate") @@ -1304,16 +1338,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsMakeup") @@ -1325,20 +1359,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("获得奖励 JSON。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -1354,13 +1388,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AuthorUserId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") .HasComment("评论人。"); b.Property("Content") @@ -1373,40 +1409,40 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsDeleted") .HasColumnType("boolean") .HasComment("状态。"); - b.Property("ParentId") - .HasColumnType("uuid") + b.Property("ParentId") + .HasColumnType("bigint") .HasComment("父级评论 ID。"); - b.Property("PostId") - .HasColumnType("uuid") + b.Property("PostId") + .HasColumnType("bigint") .HasComment("动态标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1421,13 +1457,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AuthorUserId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") .HasComment("作者用户 ID。"); b.Property("CommentCount") @@ -1443,16 +1481,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("LikeCount") @@ -1467,8 +1505,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Title") @@ -1480,8 +1518,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1496,29 +1534,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("PostId") - .HasColumnType("uuid") + b.Property("PostId") + .HasColumnType("bigint") .HasComment("动态 ID。"); b.Property("ReactedAt") @@ -1529,20 +1569,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("反应类型。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户 ID。"); b.HasKey("Id"); @@ -1558,11 +1598,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CancelledAt") .HasColumnType("timestamp with time zone") .HasComment("取消时间。"); @@ -1571,8 +1613,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CurrentCount") @@ -1583,8 +1625,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndAt") @@ -1602,12 +1644,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("拼团价格。"); - b.Property("LeaderUserId") - .HasColumnType("uuid") + b.Property("LeaderUserId") + .HasColumnType("bigint") .HasComment("团长用户 ID。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("关联商品或套餐。"); b.Property("StartAt") @@ -1618,8 +1660,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("拼团状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); b.Property("SucceededAt") @@ -1630,16 +1672,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("成团需要的人数。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1655,57 +1697,59 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("GroupOrderId") - .HasColumnType("uuid") + b.Property("GroupOrderId") + .HasColumnType("bigint") .HasComment("拼单活动标识。"); b.Property("JoinedAt") .HasColumnType("timestamp with time zone") .HasComment("参与时间。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("对应订单标识。"); b.Property("Status") .HasColumnType("integer") .HasComment("参与状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -1721,11 +1765,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AdjustmentType") .HasColumnType("integer") .HasComment("调整类型。"); @@ -1734,28 +1780,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("InventoryItemId") - .HasColumnType("uuid") + b.Property("InventoryItemId") + .HasColumnType("bigint") .HasComment("对应的库存记录标识。"); b.Property("OccurredAt") .HasColumnType("timestamp with time zone") .HasComment("发生时间。"); - b.Property("OperatorId") - .HasColumnType("uuid") + b.Property("OperatorId") + .HasColumnType("bigint") .HasComment("操作人标识。"); b.Property("Quantity") @@ -1767,16 +1813,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("原因说明。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1791,11 +1837,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BatchNumber") .IsRequired() .HasMaxLength(64) @@ -1806,24 +1854,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireDate") .HasColumnType("timestamp with time zone") .HasComment("过期日期。"); - b.Property("ProductSkuId") - .HasColumnType("uuid") + b.Property("ProductSkuId") + .HasColumnType("bigint") .HasComment("SKU 标识。"); b.Property("ProductionDate") @@ -1838,20 +1886,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("剩余数量。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1867,11 +1915,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BatchNumber") .HasMaxLength(64) .HasColumnType("character varying(64)") @@ -1881,16 +1931,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireDate") @@ -1902,8 +1952,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("储位或仓位信息。"); - b.Property("ProductSkuId") - .HasColumnType("uuid") + b.Property("ProductSkuId") + .HasColumnType("bigint") .HasComment("SKU 标识。"); b.Property("QuantityOnHand") @@ -1918,20 +1968,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("安全库存阈值。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1946,11 +1996,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ChangeValue") .HasColumnType("integer") .HasComment("变动数量。"); @@ -1959,8 +2011,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CurrentValue") @@ -1971,12 +2023,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("MemberId") - .HasColumnType("uuid") + b.Property("MemberId") + .HasColumnType("bigint") .HasComment("会员标识。"); b.Property("Notes") @@ -1988,16 +2040,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("发生时间。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2012,11 +2064,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BalanceAfterChange") .HasColumnType("integer") .HasComment("变动后余额。"); @@ -2029,24 +2083,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireAt") .HasColumnType("timestamp with time zone") .HasComment("过期时间(如适用)。"); - b.Property("MemberId") - .HasColumnType("uuid") + b.Property("MemberId") + .HasColumnType("bigint") .HasComment("会员标识。"); b.Property("OccurredAt") @@ -2057,20 +2111,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("变动原因。"); - b.Property("SourceId") - .HasColumnType("uuid") + b.Property("SourceId") + .HasColumnType("bigint") .HasComment("来源 ID(订单、活动等)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2085,11 +2139,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AvatarUrl") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -2103,16 +2159,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("GrowthValue") @@ -2123,8 +2179,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("注册时间。"); - b.Property("MemberTierId") - .HasColumnType("uuid") + b.Property("MemberTierId") + .HasColumnType("bigint") .HasComment("当前会员等级 ID。"); b.Property("Mobile") @@ -2146,20 +2202,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("会员状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -2175,11 +2231,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BenefitsJson") .IsRequired() .HasColumnType("text") @@ -2189,16 +2247,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Name") @@ -2215,16 +2273,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序值。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2240,11 +2298,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -2294,16 +2354,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("District") @@ -2362,16 +2422,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("税号/统一社会信用代码。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2386,11 +2446,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ContractNumber") .IsRequired() .HasMaxLength(64) @@ -2401,16 +2463,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndDate") @@ -2423,8 +2485,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(512)") .HasComment("合同文件存储地址。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("SignedAt") @@ -2439,8 +2501,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("合同状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TerminatedAt") @@ -2456,8 +2518,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2473,25 +2535,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DocumentNumber") @@ -2517,8 +2581,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("签发日期。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("Remarks") @@ -2529,16 +2593,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("审核状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2553,25 +2617,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Email") @@ -2579,12 +2645,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("邮箱地址。"); - b.Property("IdentityUserId") - .HasColumnType("uuid") + b.Property("IdentityUserId") + .HasColumnType("bigint") .HasComment("登录账号 ID(指向统一身份体系)。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("Name") @@ -2611,20 +2677,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("员工状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("可选的关联门店 ID。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2639,11 +2705,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .IsRequired() .HasMaxLength(256) @@ -2654,16 +2722,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Landmark") @@ -2685,20 +2753,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("名称。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("关联门店 ID,可空表示独立 POI。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2713,11 +2781,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Channel") .HasColumnType("integer") .HasComment("来源通道(小程序、H5 等)。"); @@ -2726,44 +2796,44 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("RequestedAt") .HasColumnType("timestamp with time zone") .HasComment("请求时间。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店 ID。"); b.Property("TargetApp") .HasColumnType("integer") .HasComment("跳转的地图应用。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户 ID。"); b.HasKey("Id"); @@ -2778,11 +2848,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttributesJson") .HasColumnType("text") .HasComment("扩展 JSON(规格、加料选项等)。"); @@ -2791,20 +2863,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("商品或 SKU 标识。"); b.Property("ProductName") @@ -2813,8 +2885,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("商品名称快照。"); - b.Property("ProductSkuId") - .HasColumnType("uuid") + b.Property("ProductSkuId") + .HasColumnType("bigint") .HasComment("SKU 标识。"); b.Property("Quantity") @@ -2826,16 +2898,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("自定义备注(口味要求)。"); - b.Property("ShoppingCartId") - .HasColumnType("uuid") + b.Property("ShoppingCartId") + .HasColumnType("bigint") .HasComment("所属购物车标识。"); b.Property("Status") .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UnitPrice") @@ -2847,8 +2919,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2863,29 +2935,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("CartItemId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CartItemId") + .HasColumnType("bigint") .HasComment("所属购物车条目。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExtraPrice") @@ -2899,20 +2973,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("选项名称。"); - b.Property("OptionId") - .HasColumnType("uuid") + b.Property("OptionId") + .HasColumnType("bigint") .HasComment("选项 ID(可对应 ProductAddonOption)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2925,25 +2999,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpiresAt") @@ -2960,24 +3036,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("会话状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.Property("ValidationResultJson") @@ -2998,25 +3074,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryPreference") @@ -3032,8 +3110,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("购物车状态,包含正常/锁定。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); b.Property("TableContext") @@ -3041,20 +3119,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("桌码或场景标识(扫码点餐)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -3070,11 +3148,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CancelReason") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -3092,8 +3172,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CustomerName") @@ -3110,8 +3190,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryType") @@ -3166,16 +3246,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(512)") .HasComment("备注。"); - b.Property("ReservationId") - .HasColumnType("uuid") + b.Property("ReservationId") + .HasColumnType("bigint") .HasComment("预约 ID。"); b.Property("Status") .HasColumnType("integer") .HasComment("当前状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店。"); b.Property("TableNo") @@ -3183,16 +3263,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(32)") .HasComment("就餐桌号。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3210,11 +3290,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttributesJson") .HasColumnType("text") .HasComment("自定义属性 JSON。"); @@ -3223,16 +3305,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DiscountAmount") @@ -3240,12 +3322,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("折扣金额。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("订单 ID。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("商品 ID。"); b.Property("ProductName") @@ -3268,8 +3350,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("小计。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Unit") @@ -3286,8 +3368,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3304,25 +3386,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Notes") @@ -3334,28 +3418,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("发生时间。"); - b.Property("OperatorId") - .HasColumnType("uuid") + b.Property("OperatorId") + .HasColumnType("bigint") .HasComment("操作人标识(可为空表示系统)。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("订单标识。"); b.Property("Status") .HasColumnType("integer") .HasComment("变更后的状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3370,11 +3454,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Amount") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -3384,20 +3470,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单标识。"); b.Property("ProcessedAt") @@ -3429,16 +3515,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("退款状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3454,11 +3540,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Amount") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -3473,24 +3561,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Method") .HasColumnType("integer") .HasComment("支付方式。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单。"); b.Property("PaidAt") @@ -3510,8 +3598,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("支付状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TradeNo") @@ -3523,8 +3611,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3539,11 +3627,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Amount") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -3562,28 +3652,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单标识。"); b.Property("Payload") .HasColumnType("text") .HasComment("渠道返回的原始数据 JSON。"); - b.Property("PaymentRecordId") - .HasColumnType("uuid") + b.Property("PaymentRecordId") + .HasColumnType("bigint") .HasComment("原支付记录标识。"); b.Property("RequestedAt") @@ -3594,16 +3684,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("退款状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3618,13 +3708,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("CategoryId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint") .HasComment("所属分类。"); b.Property("CoverImage") @@ -3636,16 +3728,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -3707,8 +3799,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("库存数量(可选)。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("所属门店。"); b.Property("Subtitle") @@ -3716,8 +3808,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("副标题/卖点。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Unit") @@ -3729,8 +3821,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3748,25 +3840,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsRequired") @@ -3787,8 +3881,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("分组名称。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("所属商品。"); b.Property("SelectionType") @@ -3799,16 +3893,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序值。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3823,29 +3917,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AddonGroupId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddonGroupId") + .HasColumnType("bigint") .HasComment("所属加料分组。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExtraPrice") @@ -3867,16 +3963,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3889,25 +3985,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsRequired") @@ -3920,6 +4018,10 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("分组名称,例如“辣度”“份量”。"); + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + b.Property("SelectionType") .HasColumnType("integer") .HasComment("选择类型(单选/多选)。"); @@ -3928,20 +4030,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("显示排序。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("关联门店,可为空表示所有门店共享。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3956,29 +4058,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AttributeGroupId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributeGroupId") + .HasColumnType("bigint") .HasComment("所属规格组。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExtraPrice") @@ -4000,16 +4104,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4025,25 +4129,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -4065,20 +4171,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序值。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("所属门店。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4093,11 +4199,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Caption") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -4107,40 +4215,40 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("MediaType") .HasColumnType("integer") .HasComment("媒体类型。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("商品标识。"); b.Property("SortOrder") .HasColumnType("integer") .HasComment("排序。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Url") @@ -4159,11 +4267,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ConditionsJson") .IsRequired() .HasColumnType("text") @@ -4173,16 +4283,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndTime") @@ -4194,28 +4304,32 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("特殊价格。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("所属商品。"); b.Property("RuleType") .HasColumnType("integer") .HasComment("策略类型。"); + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + b.Property("StartTime") .HasColumnType("timestamp with time zone") .HasComment("生效开始时间。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("WeekdaysJson") @@ -4234,11 +4348,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttributesJson") .IsRequired() .HasColumnType("text") @@ -4253,16 +4369,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("OriginalPrice") @@ -4275,8 +4391,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("售价。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("所属商品标识。"); b.Property("SkuCode") @@ -4285,20 +4401,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(32)") .HasComment("SKU 编码。"); + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + b.Property("StockQuantity") .HasColumnType("integer") .HasComment("可售库存。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Weight") @@ -4319,11 +4439,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CalledAt") .HasColumnType("timestamp with time zone") .HasComment("叫号时间。"); @@ -4336,16 +4458,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EstimatedWaitMinutes") @@ -4369,11 +4491,11 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("StoreId") - .HasColumnType("uuid"); + b.Property("StoreId") + .HasColumnType("bigint"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TicketNumber") @@ -4386,8 +4508,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4405,11 +4527,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CancelledAt") .HasColumnType("timestamp with time zone") .HasComment("取消时间。"); @@ -4427,8 +4551,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CustomerName") @@ -4447,8 +4571,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("PeopleCount") @@ -4474,8 +4598,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店。"); b.Property("TablePreference") @@ -4483,16 +4607,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("桌型/标签。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4510,11 +4634,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -4553,16 +4679,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryRadiusKm") @@ -4592,8 +4718,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("门店负责人姓名。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("Name") @@ -4640,16 +4766,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("门店标签(逗号分隔)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4667,11 +4793,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CapacityLimit") .HasColumnType("integer") .HasComment("最大接待容量或单量限制。"); @@ -4680,8 +4808,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DayOfWeek") @@ -4692,8 +4820,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndTime") @@ -4713,20 +4841,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("interval") .HasComment("开始时间(本地时间)。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4741,25 +4869,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryFee") @@ -4781,20 +4911,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("GeoJSON 表示的多边形范围。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("ZoneName") @@ -4815,25 +4949,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndTime") @@ -4853,28 +4989,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("班次日期。"); - b.Property("StaffId") - .HasColumnType("uuid") + b.Property("StaffId") + .HasColumnType("bigint") .HasComment("员工标识。"); b.Property("StartTime") .HasColumnType("interval") .HasComment("开始时间。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4890,17 +5026,19 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("Date") @@ -4911,8 +5049,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsClosed") @@ -4924,20 +5062,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("说明内容。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4953,13 +5091,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AreaId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AreaId") + .HasColumnType("bigint") .HasComment("所在区域 ID。"); b.Property("Capacity") @@ -4970,16 +5110,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("QrCodeUrl") @@ -4991,8 +5131,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前桌台状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); b.Property("TableCode") @@ -5006,16 +5146,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("桌台标签(堂食、快餐等)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5031,25 +5171,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -5063,20 +5205,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("区域名称。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5092,11 +5238,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .HasColumnType("text") .HasComment("详细地址信息。"); @@ -5138,16 +5286,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EffectiveFrom") @@ -5178,8 +5326,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("租户全称或品牌名称。"); - b.Property("PrimaryOwnerUserId") - .HasColumnType("uuid") + b.Property("PrimaryOwnerUserId") + .HasColumnType("bigint") .HasComment("系统内对应的租户所有者账号 ID。"); b.Property("Province") @@ -5216,8 +5364,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Website") @@ -5237,11 +5385,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AmountDue") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -5256,16 +5406,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DueDate") @@ -5294,16 +5444,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前付款状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5319,11 +5469,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Channel") .HasColumnType("integer") .HasComment("发布通道(站内、邮件、短信等)。"); @@ -5332,16 +5484,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Message") @@ -5366,8 +5518,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("通知重要级别。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Title") @@ -5380,8 +5532,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5396,25 +5548,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -5468,8 +5622,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("YearlyPrice") @@ -5486,25 +5640,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("LastResetAt") @@ -5523,16 +5679,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("配额刷新周期描述(如月、年)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("UsedValue") @@ -5552,11 +5708,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AutoRenew") .HasColumnType("boolean") .HasComment("是否开启自动续费。"); @@ -5565,16 +5723,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EffectiveFrom") @@ -5593,28 +5751,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("运营备注信息。"); - b.Property("ScheduledPackageId") - .HasColumnType("uuid") + b.Property("ScheduledPackageId") + .HasColumnType("bigint") .HasComment("若已排期升降配,对应的新套餐 ID。"); b.Property("Status") .HasColumnType("integer") .HasComment("订阅当前状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); - b.Property("TenantPackageId") - .HasColumnType("uuid") + b.Property("TenantPackageId") + .HasColumnType("bigint") .HasComment("当前订阅关联的套餐标识。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.cs new file mode 100644 index 0000000..4b31527 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251202005208_InitSnowflake_App.cs @@ -0,0 +1,2550 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations +{ + /// + public partial class InitSnowflake_App : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "affiliate_orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AffiliatePartnerId = table.Column(type: "bigint", nullable: false, comment: "推广人标识。"), + OrderId = table.Column(type: "bigint", nullable: false, comment: "关联订单。"), + BuyerUserId = table.Column(type: "bigint", nullable: false, comment: "用户 ID。"), + OrderAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "订单金额。"), + EstimatedCommission = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "预计佣金。"), + Status = table.Column(type: "integer", nullable: false, comment: "当前状态。"), + SettledAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "结算完成时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_affiliate_orders", x => x.Id); + }, + comment: "分销订单记录。"); + + migrationBuilder.CreateTable( + name: "affiliate_partners", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "bigint", nullable: true, comment: "用户 ID(如绑定平台账号)。"), + DisplayName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "昵称或渠道名称。"), + Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "联系电话。"), + ChannelType = table.Column(type: "integer", nullable: false, comment: "渠道类型。"), + CommissionRate = table.Column(type: "numeric", nullable: false, comment: "分成比例(0-1)。"), + Status = table.Column(type: "integer", nullable: false, comment: "当前状态。"), + Remarks = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "审核备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_affiliate_partners", x => x.Id); + }, + comment: "分销/推广合作伙伴。"); + + migrationBuilder.CreateTable( + name: "affiliate_payouts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AffiliatePartnerId = table.Column(type: "bigint", nullable: false, comment: "合作伙伴标识。"), + Period = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "结算周期描述。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "结算金额。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "打款时间。"), + Remarks = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_affiliate_payouts", x => x.Id); + }, + comment: "佣金结算记录。"); + + migrationBuilder.CreateTable( + name: "cart_item_addons", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CartItemId = table.Column(type: "bigint", nullable: false, comment: "所属购物车条目。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "选项名称。"), + ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "附加价格。"), + OptionId = table.Column(type: "bigint", nullable: true, comment: "选项 ID(可对应 ProductAddonOption)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_cart_item_addons", x => x.Id); + }, + comment: "购物车条目的加料/附加项。"); + + migrationBuilder.CreateTable( + name: "cart_items", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ShoppingCartId = table.Column(type: "bigint", nullable: false, comment: "所属购物车标识。"), + ProductId = table.Column(type: "bigint", nullable: false, comment: "商品或 SKU 标识。"), + ProductSkuId = table.Column(type: "bigint", nullable: true, comment: "SKU 标识。"), + ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "商品名称快照。"), + UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "单价快照。"), + Quantity = table.Column(type: "integer", nullable: false, comment: "数量。"), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "自定义备注(口味要求)。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + AttributesJson = table.Column(type: "text", nullable: true, comment: "扩展 JSON(规格、加料选项等)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_cart_items", x => x.Id); + }, + comment: "购物车条目。"); + + migrationBuilder.CreateTable( + name: "chat_messages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ChatSessionId = table.Column(type: "bigint", nullable: false, comment: "会话标识。"), + SenderType = table.Column(type: "integer", nullable: false, comment: "发送方类型。"), + SenderUserId = table.Column(type: "bigint", nullable: true, comment: "发送方用户 ID。"), + Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false, comment: "消息内容。"), + ContentType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "消息类型(文字/图片/语音等)。"), + IsRead = table.Column(type: "boolean", nullable: false, comment: "是否已读。"), + ReadAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "读取时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_chat_messages", x => x.Id); + }, + comment: "会话消息。"); + + migrationBuilder.CreateTable( + name: "chat_sessions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SessionCode = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会话编号。"), + CustomerUserId = table.Column(type: "bigint", nullable: false, comment: "顾客用户 ID。"), + AgentUserId = table.Column(type: "bigint", nullable: true, comment: "当前客服员工 ID。"), + StoreId = table.Column(type: "bigint", nullable: true, comment: "所属门店(可空为平台)。"), + Status = table.Column(type: "integer", nullable: false, comment: "会话状态。"), + IsBotActive = table.Column(type: "boolean", nullable: false, comment: "是否机器人接待中。"), + StartedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "开始时间。"), + EndedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "结束时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_chat_sessions", x => x.Id); + }, + comment: "客服会话。"); + + migrationBuilder.CreateTable( + name: "checkin_campaigns", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "活动名称。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "活动描述。"), + StartDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "开始日期。"), + EndDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "结束日期。"), + AllowMakeupCount = table.Column(type: "integer", nullable: false, comment: "支持补签次数。"), + RewardsJson = table.Column(type: "text", nullable: false, comment: "连签奖励 JSON。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_checkin_campaigns", x => x.Id); + }, + comment: "签到活动配置。"); + + migrationBuilder.CreateTable( + name: "checkin_records", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CheckInCampaignId = table.Column(type: "bigint", nullable: false, comment: "活动标识。"), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户标识。"), + CheckInDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "签到日期(本地)。"), + IsMakeup = table.Column(type: "boolean", nullable: false, comment: "是否补签。"), + RewardJson = table.Column(type: "text", nullable: false, comment: "获得奖励 JSON。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_checkin_records", x => x.Id); + }, + comment: "用户签到记录。"); + + migrationBuilder.CreateTable( + name: "checkout_sessions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户标识。"), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + SessionToken = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会话 Token。"), + Status = table.Column(type: "integer", nullable: false, comment: "会话状态。"), + ValidationResultJson = table.Column(type: "text", nullable: false, comment: "校验结果明细 JSON。"), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "过期时间(UTC)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_checkout_sessions", x => x.Id); + }, + comment: "结账会话,记录校验上下文。"); + + migrationBuilder.CreateTable( + name: "community_comments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PostId = table.Column(type: "bigint", nullable: false, comment: "动态标识。"), + AuthorUserId = table.Column(type: "bigint", nullable: false, comment: "评论人。"), + Content = table.Column(type: "character varying(512)", maxLength: 512, nullable: false, comment: "评论内容。"), + ParentId = table.Column(type: "bigint", nullable: true, comment: "父级评论 ID。"), + IsDeleted = table.Column(type: "boolean", nullable: false, comment: "状态。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_community_comments", x => x.Id); + }, + comment: "社区评论。"); + + migrationBuilder.CreateTable( + name: "community_posts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AuthorUserId = table.Column(type: "bigint", nullable: false, comment: "作者用户 ID。"), + Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "标题。"), + Content = table.Column(type: "text", nullable: false, comment: "内容。"), + MediaJson = table.Column(type: "text", nullable: true, comment: "媒体资源 JSON。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + LikeCount = table.Column(type: "integer", nullable: false, comment: "点赞数。"), + CommentCount = table.Column(type: "integer", nullable: false, comment: "评论数。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_community_posts", x => x.Id); + }, + comment: "社区动态。"); + + migrationBuilder.CreateTable( + name: "community_reactions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PostId = table.Column(type: "bigint", nullable: false, comment: "动态 ID。"), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户 ID。"), + ReactionType = table.Column(type: "integer", nullable: false, comment: "反应类型。"), + ReactedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "时间戳。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_community_reactions", x => x.Id); + }, + comment: "社区互动反馈。"); + + migrationBuilder.CreateTable( + name: "coupon_templates", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "模板名称。"), + CouponType = table.Column(type: "integer", nullable: false, comment: "券类型。"), + Value = table.Column(type: "numeric", nullable: false, comment: "面值或折扣额度。"), + DiscountCap = table.Column(type: "numeric", nullable: true, comment: "折扣上限(针对折扣券)。"), + MinimumSpend = table.Column(type: "numeric", nullable: true, comment: "最低消费门槛。"), + ValidFrom = table.Column(type: "timestamp with time zone", nullable: true, comment: "可用开始时间。"), + ValidTo = table.Column(type: "timestamp with time zone", nullable: true, comment: "可用结束时间。"), + RelativeValidDays = table.Column(type: "integer", nullable: true, comment: "有效天数(相对发放时间)。"), + TotalQuantity = table.Column(type: "integer", nullable: true, comment: "总发放数量上限。"), + ClaimedQuantity = table.Column(type: "integer", nullable: false, comment: "已领取数量。"), + StoreScopeJson = table.Column(type: "text", nullable: true, comment: "适用门店 ID 集合(JSON)。"), + ProductScopeJson = table.Column(type: "text", nullable: true, comment: "适用品类或商品范围(JSON)。"), + ChannelsJson = table.Column(type: "text", nullable: true, comment: "发放渠道(JSON)。"), + AllowStack = table.Column(type: "boolean", nullable: false, comment: "是否允许叠加其他优惠。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_coupon_templates", x => x.Id); + }, + comment: "优惠券模板。"); + + migrationBuilder.CreateTable( + name: "coupons", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CouponTemplateId = table.Column(type: "bigint", nullable: false, comment: "模板标识。"), + Code = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "券码或序列号。"), + UserId = table.Column(type: "bigint", nullable: false, comment: "归属用户。"), + OrderId = table.Column(type: "bigint", nullable: true, comment: "订单 ID(已使用时记录)。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "发放时间。"), + UsedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "使用时间。"), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "到期时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_coupons", x => x.Id); + }, + comment: "用户领取的券。"); + + migrationBuilder.CreateTable( + name: "delivery_events", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DeliveryOrderId = table.Column(type: "bigint", nullable: false, comment: "配送单标识。"), + EventType = table.Column(type: "integer", nullable: false, comment: "事件类型。"), + Message = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "事件描述。"), + Payload = table.Column(type: "text", nullable: true, comment: "原始数据 JSON。"), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "发生时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_delivery_events", x => x.Id); + }, + comment: "配送状态事件流水。"); + + migrationBuilder.CreateTable( + name: "delivery_orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderId = table.Column(type: "bigint", nullable: false), + Provider = table.Column(type: "integer", nullable: false, comment: "配送服务商。"), + ProviderOrderId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "第三方配送单号。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + DeliveryFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "配送费。"), + CourierName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "骑手姓名。"), + CourierPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "骑手电话。"), + DispatchedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "下发时间。"), + PickedUpAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "取餐时间。"), + DeliveredAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "完成时间。"), + FailureReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "异常原因。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_delivery_orders", x => x.Id); + }, + comment: "配送单。"); + + migrationBuilder.CreateTable( + name: "group_orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + ProductId = table.Column(type: "bigint", nullable: false, comment: "关联商品或套餐。"), + GroupOrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "拼单编号。"), + LeaderUserId = table.Column(type: "bigint", nullable: false, comment: "团长用户 ID。"), + TargetCount = table.Column(type: "integer", nullable: false, comment: "成团需要的人数。"), + CurrentCount = table.Column(type: "integer", nullable: false, comment: "当前已参与人数。"), + GroupPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "拼团价格。"), + StartAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "开始时间。"), + EndAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "结束时间。"), + Status = table.Column(type: "integer", nullable: false, comment: "拼团状态。"), + SucceededAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "成团时间。"), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "取消时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_group_orders", x => x.Id); + }, + comment: "拼单活动。"); + + migrationBuilder.CreateTable( + name: "group_participants", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + GroupOrderId = table.Column(type: "bigint", nullable: false, comment: "拼单活动标识。"), + OrderId = table.Column(type: "bigint", nullable: false, comment: "对应订单标识。"), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户标识。"), + Status = table.Column(type: "integer", nullable: false, comment: "参与状态。"), + JoinedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "参与时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_group_participants", x => x.Id); + }, + comment: "拼单参与者。"); + + migrationBuilder.CreateTable( + name: "inventory_adjustments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + InventoryItemId = table.Column(type: "bigint", nullable: false, comment: "对应的库存记录标识。"), + AdjustmentType = table.Column(type: "integer", nullable: false, comment: "调整类型。"), + Quantity = table.Column(type: "integer", nullable: false, comment: "调整数量,正数增加,负数减少。"), + Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "原因说明。"), + OperatorId = table.Column(type: "bigint", nullable: true, comment: "操作人标识。"), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "发生时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_inventory_adjustments", x => x.Id); + }, + comment: "库存调整记录。"); + + migrationBuilder.CreateTable( + name: "inventory_batches", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + ProductSkuId = table.Column(type: "bigint", nullable: false, comment: "SKU 标识。"), + BatchNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "批次编号。"), + ProductionDate = table.Column(type: "timestamp with time zone", nullable: true, comment: "生产日期。"), + ExpireDate = table.Column(type: "timestamp with time zone", nullable: true, comment: "过期日期。"), + Quantity = table.Column(type: "integer", nullable: false, comment: "入库数量。"), + RemainingQuantity = table.Column(type: "integer", nullable: false, comment: "剩余数量。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_inventory_batches", x => x.Id); + }, + comment: "SKU 批次信息。"); + + migrationBuilder.CreateTable( + name: "inventory_items", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + ProductSkuId = table.Column(type: "bigint", nullable: false, comment: "SKU 标识。"), + BatchNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "批次编号,可为空表示混批。"), + QuantityOnHand = table.Column(type: "integer", nullable: false, comment: "可用库存。"), + QuantityReserved = table.Column(type: "integer", nullable: false, comment: "已锁定库存(订单占用)。"), + SafetyStock = table.Column(type: "integer", nullable: true, comment: "安全库存阈值。"), + Location = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "储位或仓位信息。"), + ExpireDate = table.Column(type: "timestamp with time zone", nullable: true, comment: "过期日期。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_inventory_items", x => x.Id); + }, + comment: "SKU 在门店的库存信息。"); + + migrationBuilder.CreateTable( + name: "map_locations", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: true, comment: "关联门店 ID,可空表示独立 POI。"), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "名称。"), + Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "地址。"), + Longitude = table.Column(type: "double precision", nullable: false, comment: "经度。"), + Latitude = table.Column(type: "double precision", nullable: false, comment: "纬度。"), + Landmark = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "打车/导航落点描述。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_map_locations", x => x.Id); + }, + comment: "地图 POI 信息,用于门店定位和推荐。"); + + migrationBuilder.CreateTable( + name: "member_growth_logs", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MemberId = table.Column(type: "bigint", nullable: false, comment: "会员标识。"), + ChangeValue = table.Column(type: "integer", nullable: false, comment: "变动数量。"), + CurrentValue = table.Column(type: "integer", nullable: false, comment: "当前成长值。"), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "发生时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_member_growth_logs", x => x.Id); + }, + comment: "成长值变动日志。"); + + migrationBuilder.CreateTable( + name: "member_point_ledgers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MemberId = table.Column(type: "bigint", nullable: false, comment: "会员标识。"), + ChangeAmount = table.Column(type: "integer", nullable: false, comment: "变动数量,可为负值。"), + BalanceAfterChange = table.Column(type: "integer", nullable: false, comment: "变动后余额。"), + Reason = table.Column(type: "integer", nullable: false, comment: "变动原因。"), + SourceId = table.Column(type: "bigint", nullable: true, comment: "来源 ID(订单、活动等)。"), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "发生时间。"), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "过期时间(如适用)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_member_point_ledgers", x => x.Id); + }, + comment: "积分变动流水。"); + + migrationBuilder.CreateTable( + name: "member_profiles", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户标识。"), + Mobile = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "手机号。"), + Nickname = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "昵称。"), + AvatarUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "头像。"), + MemberTierId = table.Column(type: "bigint", nullable: true, comment: "当前会员等级 ID。"), + Status = table.Column(type: "integer", nullable: false, comment: "会员状态。"), + PointsBalance = table.Column(type: "integer", nullable: false, comment: "会员积分余额。"), + GrowthValue = table.Column(type: "integer", nullable: false, comment: "成长值/经验值。"), + BirthDate = table.Column(type: "timestamp with time zone", nullable: true, comment: "生日。"), + JoinedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "注册时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_member_profiles", x => x.Id); + }, + comment: "会员档案。"); + + migrationBuilder.CreateTable( + name: "member_tiers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "等级名称。"), + RequiredGrowth = table.Column(type: "integer", nullable: false, comment: "所需成长值。"), + BenefitsJson = table.Column(type: "text", nullable: false, comment: "等级权益(JSON)。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_member_tiers", x => x.Id); + }, + comment: "会员等级定义。"); + + migrationBuilder.CreateTable( + name: "merchant_contracts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MerchantId = table.Column(type: "bigint", nullable: false, comment: "所属商户标识。"), + ContractNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "合同编号。"), + Status = table.Column(type: "integer", nullable: false, comment: "合同状态。"), + StartDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "合同开始时间。"), + EndDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "合同结束时间。"), + FileUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false, comment: "合同文件存储地址。"), + SignedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "签署时间。"), + TerminatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "终止时间。"), + TerminationReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "终止原因。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_merchant_contracts", x => x.Id); + }, + comment: "商户合同记录。"); + + migrationBuilder.CreateTable( + name: "merchant_documents", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MerchantId = table.Column(type: "bigint", nullable: false, comment: "所属商户标识。"), + DocumentType = table.Column(type: "integer", nullable: false, comment: "证照类型。"), + Status = table.Column(type: "integer", nullable: false, comment: "审核状态。"), + FileUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: false, comment: "证照文件链接。"), + DocumentNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "证照编号。"), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "签发日期。"), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "到期日期。"), + Remarks = table.Column(type: "text", nullable: true, comment: "审核备注或驳回原因。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_merchant_documents", x => x.Id); + }, + comment: "商户提交的资质或证照材料。"); + + migrationBuilder.CreateTable( + name: "merchant_staff", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MerchantId = table.Column(type: "bigint", nullable: false, comment: "所属商户标识。"), + StoreId = table.Column(type: "bigint", nullable: true, comment: "可选的关联门店 ID。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "员工姓名。"), + Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "手机号。"), + Email = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "邮箱地址。"), + IdentityUserId = table.Column(type: "bigint", nullable: true, comment: "登录账号 ID(指向统一身份体系)。"), + RoleType = table.Column(type: "integer", nullable: false, comment: "员工角色类型。"), + Status = table.Column(type: "integer", nullable: false, comment: "员工状态。"), + PermissionsJson = table.Column(type: "text", nullable: true, comment: "自定义权限(JSON)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_merchant_staff", x => x.Id); + }, + comment: "商户员工账号,支持门店维度分配。"); + + migrationBuilder.CreateTable( + name: "merchants", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + BrandName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "品牌名称(对外展示)。"), + BrandAlias = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "品牌简称或别名。"), + LogoUrl = table.Column(type: "text", nullable: true, comment: "品牌 Logo。"), + Category = table.Column(type: "text", nullable: true, comment: "品牌所属品类,如火锅、咖啡等。"), + BusinessLicenseNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "营业执照号。"), + BusinessLicenseImageUrl = table.Column(type: "text", nullable: true, comment: "营业执照扫描件地址。"), + TaxNumber = table.Column(type: "text", nullable: true, comment: "税号/统一社会信用代码。"), + LegalPerson = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "法人或负责人姓名。"), + ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "联系电话。"), + ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "联系邮箱。"), + ServicePhone = table.Column(type: "text", nullable: true, comment: "客服电话。"), + SupportEmail = table.Column(type: "text", nullable: true, comment: "客服邮箱。"), + Province = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "所在省份。"), + City = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "所在城市。"), + District = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "所在区县。"), + Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "详细地址。"), + Longitude = table.Column(type: "double precision", nullable: true, comment: "经度信息。"), + Latitude = table.Column(type: "double precision", nullable: true, comment: "纬度信息。"), + Status = table.Column(type: "integer", nullable: false, comment: "入驻状态。"), + ReviewRemarks = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "审核备注或驳回原因。"), + JoinedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "入驻时间。"), + LastReviewedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次审核时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_merchants", x => x.Id); + }, + comment: "商户主体信息,承载入驻和资质审核结果。"); + + migrationBuilder.CreateTable( + name: "metric_alert_rules", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MetricDefinitionId = table.Column(type: "bigint", nullable: false, comment: "关联指标。"), + ConditionJson = table.Column(type: "text", nullable: false, comment: "触发条件 JSON。"), + Severity = table.Column(type: "integer", nullable: false, comment: "告警级别。"), + NotificationChannels = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "通知渠道。"), + Enabled = table.Column(type: "boolean", nullable: false, comment: "是否启用。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_metric_alert_rules", x => x.Id); + }, + comment: "指标告警规则。"); + + migrationBuilder.CreateTable( + name: "metric_definitions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "指标编码。"), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "指标名称。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "说明。"), + DimensionsJson = table.Column(type: "text", nullable: true, comment: "维度描述 JSON。"), + DefaultAggregation = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "默认聚合方式。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_metric_definitions", x => x.Id); + }, + comment: "指标定义,描述可观测的数据点。"); + + migrationBuilder.CreateTable( + name: "metric_snapshots", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MetricDefinitionId = table.Column(type: "bigint", nullable: false, comment: "指标定义 ID。"), + DimensionKey = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "维度键(JSON)。"), + WindowStart = table.Column(type: "timestamp with time zone", nullable: false, comment: "统计时间窗口开始。"), + WindowEnd = table.Column(type: "timestamp with time zone", nullable: false, comment: "统计时间窗口结束。"), + Value = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false, comment: "数值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_metric_snapshots", x => x.Id); + }, + comment: "指标快照,用于大盘展示。"); + + migrationBuilder.CreateTable( + name: "navigation_requests", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户 ID。"), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + Channel = table.Column(type: "integer", nullable: false, comment: "来源通道(小程序、H5 等)。"), + TargetApp = table.Column(type: "integer", nullable: false, comment: "跳转的地图应用。"), + RequestedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "请求时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_navigation_requests", x => x.Id); + }, + comment: "用户发起的导航请求日志。"); + + migrationBuilder.CreateTable( + name: "order_status_histories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderId = table.Column(type: "bigint", nullable: false, comment: "订单标识。"), + Status = table.Column(type: "integer", nullable: false, comment: "变更后的状态。"), + OperatorId = table.Column(type: "bigint", nullable: true, comment: "操作人标识(可为空表示系统)。"), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注信息。"), + OccurredAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "发生时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_order_status_histories", x => x.Id); + }, + comment: "订单状态流转记录。"); + + migrationBuilder.CreateTable( + name: "orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "订单号。"), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店。"), + Channel = table.Column(type: "integer", nullable: false, comment: "下单渠道。"), + DeliveryType = table.Column(type: "integer", nullable: false, comment: "履约类型。"), + Status = table.Column(type: "integer", nullable: false, comment: "当前状态。"), + PaymentStatus = table.Column(type: "integer", nullable: false, comment: "支付状态。"), + CustomerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "顾客姓名。"), + CustomerPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "顾客手机号。"), + TableNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "就餐桌号。"), + QueueNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "排队号(如有)。"), + ReservationId = table.Column(type: "bigint", nullable: true, comment: "预约 ID。"), + ItemsAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "商品总额。"), + DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "优惠金额。"), + PayableAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "应付金额。"), + PaidAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "实付金额。"), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "支付时间。"), + FinishedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "完成时间。"), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "取消时间。"), + CancelReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "取消原因。"), + Remark = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_orders", x => x.Id); + }, + comment: "交易订单。"); + + migrationBuilder.CreateTable( + name: "payment_records", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderId = table.Column(type: "bigint", nullable: false, comment: "关联订单。"), + Method = table.Column(type: "integer", nullable: false, comment: "支付方式。"), + Status = table.Column(type: "integer", nullable: false, comment: "支付状态。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "支付金额。"), + TradeNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "平台交易号。"), + ChannelTransactionId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "第三方渠道单号。"), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "支付完成时间。"), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "错误/备注。"), + Payload = table.Column(type: "text", nullable: true, comment: "原始回调内容。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_payment_records", x => x.Id); + }, + comment: "支付流水。"); + + migrationBuilder.CreateTable( + name: "payment_refund_records", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PaymentRecordId = table.Column(type: "bigint", nullable: false, comment: "原支付记录标识。"), + OrderId = table.Column(type: "bigint", nullable: false, comment: "关联订单标识。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "退款金额。"), + ChannelRefundId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "渠道退款流水号。"), + Status = table.Column(type: "integer", nullable: false, comment: "退款状态。"), + RequestedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "退款请求时间。"), + CompletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "完成时间。"), + Payload = table.Column(type: "text", nullable: true, comment: "渠道返回的原始数据 JSON。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_payment_refund_records", x => x.Id); + }, + comment: "支付渠道退款流水。"); + + migrationBuilder.CreateTable( + name: "product_addon_groups", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false, comment: "所属商品。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "分组名称。"), + SelectionType = table.Column(type: "integer", nullable: false, comment: "选择类型。"), + MinSelect = table.Column(type: "integer", nullable: true, comment: "最小选择数量。"), + MaxSelect = table.Column(type: "integer", nullable: true, comment: "最大选择数量。"), + IsRequired = table.Column(type: "boolean", nullable: false, comment: "是否必选。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_addon_groups", x => x.Id); + }, + comment: "加料/做法分组。"); + + migrationBuilder.CreateTable( + name: "product_addon_options", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AddonGroupId = table.Column(type: "bigint", nullable: false, comment: "所属加料分组。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "选项名称。"), + ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "附加价格。"), + IsDefault = table.Column(type: "boolean", nullable: false, comment: "是否默认选项。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_addon_options", x => x.Id); + }, + comment: "加料选项。"); + + migrationBuilder.CreateTable( + name: "product_attribute_groups", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: true, comment: "关联门店,可为空表示所有门店共享。"), + ProductId = table.Column(type: "bigint", nullable: false, comment: "所属商品标识。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "分组名称,例如“辣度”“份量”。"), + SelectionType = table.Column(type: "integer", nullable: false, comment: "选择类型(单选/多选)。"), + IsRequired = table.Column(type: "boolean", nullable: false, comment: "是否必选。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "显示排序。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_attribute_groups", x => x.Id); + }, + comment: "商品规格/属性分组。"); + + migrationBuilder.CreateTable( + name: "product_attribute_options", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AttributeGroupId = table.Column(type: "bigint", nullable: false, comment: "所属规格组。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "选项名称。"), + ExtraPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "附加价格。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序。"), + IsDefault = table.Column(type: "boolean", nullable: false, comment: "是否默认选中。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_attribute_options", x => x.Id); + }, + comment: "商品规格选项。"); + + migrationBuilder.CreateTable( + name: "product_categories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "所属门店。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "分类名称。"), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "分类描述。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + IsEnabled = table.Column(type: "boolean", nullable: false, comment: "是否启用。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_categories", x => x.Id); + }, + comment: "商品分类。"); + + migrationBuilder.CreateTable( + name: "product_media_assets", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false, comment: "商品标识。"), + MediaType = table.Column(type: "integer", nullable: false, comment: "媒体类型。"), + Url = table.Column(type: "character varying(512)", maxLength: 512, nullable: false, comment: "媒资链接。"), + Caption = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "描述或标题。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_media_assets", x => x.Id); + }, + comment: "商品媒资素材。"); + + migrationBuilder.CreateTable( + name: "product_pricing_rules", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false, comment: "所属商品。"), + RuleType = table.Column(type: "integer", nullable: false, comment: "策略类型。"), + ConditionsJson = table.Column(type: "text", nullable: false, comment: "条件描述(JSON),如会员等级、渠道等。"), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "特殊价格。"), + StartTime = table.Column(type: "timestamp with time zone", nullable: true, comment: "生效开始时间。"), + EndTime = table.Column(type: "timestamp with time zone", nullable: true, comment: "生效结束时间。"), + WeekdaysJson = table.Column(type: "text", nullable: true, comment: "生效星期(JSON 数组)。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_pricing_rules", x => x.Id); + }, + comment: "商品价格策略,支持会员价/时段价等。"); + + migrationBuilder.CreateTable( + name: "product_skus", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false, comment: "所属商品标识。"), + SkuCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "SKU 编码。"), + Barcode = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "条形码。"), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "售价。"), + OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "原价。"), + StockQuantity = table.Column(type: "integer", nullable: true, comment: "可售库存。"), + Weight = table.Column(type: "numeric(10,3)", precision: 10, scale: 3, nullable: true, comment: "重量(千克)。"), + AttributesJson = table.Column(type: "text", nullable: false, comment: "规格属性 JSON(记录选项 ID)。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_product_skus", x => x.Id); + }, + comment: "商品 SKU,记录具体规格组合价格。"); + + migrationBuilder.CreateTable( + name: "products", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "所属门店。"), + CategoryId = table.Column(type: "bigint", nullable: false, comment: "所属分类。"), + SpuCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "商品编码。"), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "商品名称。"), + Subtitle = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "副标题/卖点。"), + Unit = table.Column(type: "character varying(16)", maxLength: 16, nullable: true, comment: "售卖单位(份/杯等)。"), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "现价。"), + OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "原价。"), + StockQuantity = table.Column(type: "integer", nullable: true, comment: "库存数量(可选)。"), + MaxQuantityPerOrder = table.Column(type: "integer", nullable: true, comment: "最大每单限购。"), + Status = table.Column(type: "integer", nullable: false, comment: "商品状态。"), + CoverImage = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "主图。"), + GalleryImages = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true, comment: "Gallery 图片逗号分隔。"), + Description = table.Column(type: "text", nullable: true, comment: "商品描述。"), + EnableDineIn = table.Column(type: "boolean", nullable: false, comment: "支持堂食。"), + EnablePickup = table.Column(type: "boolean", nullable: false, comment: "支持自提。"), + EnableDelivery = table.Column(type: "boolean", nullable: false, comment: "支持配送。"), + IsFeatured = table.Column(type: "boolean", nullable: false, comment: "是否热门推荐。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_products", x => x.Id); + }, + comment: "商品(SPU)信息。"); + + migrationBuilder.CreateTable( + name: "promotion_campaigns", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "活动名称。"), + PromotionType = table.Column(type: "integer", nullable: false, comment: "活动类型。"), + Status = table.Column(type: "integer", nullable: false, comment: "活动状态。"), + StartAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "开始时间。"), + EndAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "结束时间。"), + Budget = table.Column(type: "numeric", nullable: true, comment: "预算金额。"), + RulesJson = table.Column(type: "text", nullable: false, comment: "活动规则 JSON。"), + AudienceDescription = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "目标人群描述。"), + BannerUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "营销素材(如 banner)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_promotion_campaigns", x => x.Id); + }, + comment: "营销活动配置。"); + + migrationBuilder.CreateTable( + name: "queue_tickets", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false), + TicketNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "排队编号。"), + PartySize = table.Column(type: "integer", nullable: false, comment: "就餐人数。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + EstimatedWaitMinutes = table.Column(type: "integer", nullable: true, comment: "预计等待分钟。"), + CalledAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "叫号时间。"), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "过号时间。"), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "取消时间。"), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_queue_tickets", x => x.Id); + }, + comment: "排队叫号。"); + + migrationBuilder.CreateTable( + name: "refund_requests", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderId = table.Column(type: "bigint", nullable: false, comment: "关联订单标识。"), + RefundNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "退款单号。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "申请金额。"), + Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "申请原因。"), + Status = table.Column(type: "integer", nullable: false, comment: "退款状态。"), + RequestedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "用户提交时间。"), + ProcessedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "审核完成时间。"), + ReviewNotes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "审核备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_refund_requests", x => x.Id); + }, + comment: "售后/退款申请。"); + + migrationBuilder.CreateTable( + name: "reservations", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店。"), + ReservationNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "预约号。"), + CustomerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "客户姓名。"), + CustomerPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "联系电话。"), + PeopleCount = table.Column(type: "integer", nullable: false, comment: "用餐人数。"), + ReservationTime = table.Column(type: "timestamp with time zone", nullable: false, comment: "预约时间(UTC)。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + TablePreference = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "桌型/标签。"), + Remark = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "备注。"), + CheckInCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "核销码/到店码。"), + CheckedInAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "实际签到时间。"), + CancelledAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "取消时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_reservations", x => x.Id); + }, + comment: "预约/预订记录。"); + + migrationBuilder.CreateTable( + name: "shopping_carts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户标识。"), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + Status = table.Column(type: "integer", nullable: false, comment: "购物车状态,包含正常/锁定。"), + TableContext = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "桌码或场景标识(扫码点餐)。"), + DeliveryPreference = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "履约方式(堂食/自提/配送)缓存。"), + LastModifiedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "最近一次修改时间(UTC)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_shopping_carts", x => x.Id); + }, + comment: "用户购物车,按租户/门店隔离。"); + + migrationBuilder.CreateTable( + name: "store_business_hours", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + DayOfWeek = table.Column(type: "integer", nullable: false, comment: "星期几,0 表示周日。"), + HourType = table.Column(type: "integer", nullable: false, comment: "时段类型(正常营业、休息、预约等)。"), + StartTime = table.Column(type: "interval", nullable: false, comment: "开始时间(本地时间)。"), + EndTime = table.Column(type: "interval", nullable: false, comment: "结束时间(本地时间)。"), + CapacityLimit = table.Column(type: "integer", nullable: true, comment: "最大接待容量或单量限制。"), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_store_business_hours", x => x.Id); + }, + comment: "门店营业时段配置。"); + + migrationBuilder.CreateTable( + name: "store_delivery_zones", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + ZoneName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "区域名称。"), + PolygonGeoJson = table.Column(type: "text", nullable: false, comment: "GeoJSON 表示的多边形范围。"), + MinimumOrderAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "起送价。"), + DeliveryFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "配送费。"), + EstimatedMinutes = table.Column(type: "integer", nullable: true, comment: "预计送达分钟。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_store_delivery_zones", x => x.Id); + }, + comment: "门店配送范围配置。"); + + migrationBuilder.CreateTable( + name: "store_employee_shifts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + StaffId = table.Column(type: "bigint", nullable: false, comment: "员工标识。"), + ShiftDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "班次日期。"), + StartTime = table.Column(type: "interval", nullable: false, comment: "开始时间。"), + EndTime = table.Column(type: "interval", nullable: false, comment: "结束时间。"), + RoleType = table.Column(type: "integer", nullable: false, comment: "排班角色。"), + Notes = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_store_employee_shifts", x => x.Id); + }, + comment: "门店员工排班记录。"); + + migrationBuilder.CreateTable( + name: "store_holidays", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + Date = table.Column(type: "timestamp with time zone", nullable: false, comment: "日期。"), + IsClosed = table.Column(type: "boolean", nullable: false, comment: "是否全天闭店。"), + Reason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "说明内容。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_store_holidays", x => x.Id); + }, + comment: "门店休息日或特殊营业日。"); + + migrationBuilder.CreateTable( + name: "store_table_areas", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "区域名称。"), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "区域描述。"), + SortOrder = table.Column(type: "integer", nullable: false, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_store_table_areas", x => x.Id); + }, + comment: "门店桌台区域配置。"); + + migrationBuilder.CreateTable( + name: "store_tables", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店标识。"), + AreaId = table.Column(type: "bigint", nullable: true, comment: "所在区域 ID。"), + TableCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "桌码。"), + Capacity = table.Column(type: "integer", nullable: false, comment: "可容纳人数。"), + Tags = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "桌台标签(堂食、快餐等)。"), + Status = table.Column(type: "integer", nullable: false, comment: "当前桌台状态。"), + QrCodeUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "桌码二维码地址。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_store_tables", x => x.Id); + }, + comment: "桌台信息与二维码绑定。"); + + migrationBuilder.CreateTable( + name: "stores", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MerchantId = table.Column(type: "bigint", nullable: false, comment: "所属商户标识。"), + Code = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "门店编码,便于扫码及外部对接。"), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "门店名称。"), + Phone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "联系电话。"), + ManagerName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "门店负责人姓名。"), + Status = table.Column(type: "integer", nullable: false, comment: "门店当前运营状态。"), + Country = table.Column(type: "text", nullable: true, comment: "所在国家或地区。"), + Province = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "所在省份。"), + City = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "所在城市。"), + District = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "区县信息。"), + Address = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "详细地址。"), + Longitude = table.Column(type: "double precision", nullable: true, comment: "高德/腾讯地图经度。"), + Latitude = table.Column(type: "double precision", nullable: true, comment: "纬度。"), + Description = table.Column(type: "text", nullable: true, comment: "门店描述或公告。"), + BusinessHours = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "门店营业时段描述(备用字符串)。"), + SupportsDineIn = table.Column(type: "boolean", nullable: false, comment: "是否支持堂食。"), + SupportsPickup = table.Column(type: "boolean", nullable: false, comment: "是否支持自提。"), + SupportsDelivery = table.Column(type: "boolean", nullable: false, comment: "是否支持配送。"), + SupportsReservation = table.Column(type: "boolean", nullable: false, comment: "支持预约。"), + SupportsQueueing = table.Column(type: "boolean", nullable: false, comment: "支持排队叫号。"), + DeliveryRadiusKm = table.Column(type: "numeric(6,2)", precision: 6, scale: 2, nullable: false, comment: "默认配送半径(公里)。"), + Announcement = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "门店公告。"), + Tags = table.Column(type: "text", nullable: true, comment: "门店标签(逗号分隔)。"), + CoverImageUrl = table.Column(type: "text", nullable: true, comment: "门店海报。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_stores", x => x.Id); + }, + comment: "门店信息,承载营业配置与能力。"); + + migrationBuilder.CreateTable( + name: "support_tickets", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TicketNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "工单编号。"), + CustomerUserId = table.Column(type: "bigint", nullable: false, comment: "客户用户 ID。"), + OrderId = table.Column(type: "bigint", nullable: true, comment: "关联订单(如有)。"), + Subject = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "工单主题。"), + Description = table.Column(type: "text", nullable: false, comment: "工单详情。"), + Priority = table.Column(type: "integer", nullable: false, comment: "优先级。"), + Status = table.Column(type: "integer", nullable: false, comment: "状态。"), + AssignedAgentId = table.Column(type: "bigint", nullable: true, comment: "指派的客服。"), + ClosedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "关闭时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_support_tickets", x => x.Id); + }, + comment: "客服工单。"); + + migrationBuilder.CreateTable( + name: "tenant_billing_statements", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StatementNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "账单编号,供对账查询。"), + PeriodStart = table.Column(type: "timestamp with time zone", nullable: false, comment: "账单周期开始时间。"), + PeriodEnd = table.Column(type: "timestamp with time zone", nullable: false, comment: "账单周期结束时间。"), + AmountDue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "应付金额。"), + AmountPaid = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "实付金额。"), + Status = table.Column(type: "integer", nullable: false, comment: "当前付款状态。"), + DueDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "到期日。"), + LineItemsJson = table.Column(type: "text", nullable: true, comment: "账单明细 JSON,记录各项费用。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_billing_statements", x => x.Id); + }, + comment: "租户账单,用于呈现周期性收费。"); + + migrationBuilder.CreateTable( + name: "tenant_notifications", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "通知标题。"), + Message = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false, comment: "通知正文。"), + Channel = table.Column(type: "integer", nullable: false, comment: "发布通道(站内、邮件、短信等)。"), + Severity = table.Column(type: "integer", nullable: false, comment: "通知重要级别。"), + SentAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "推送时间。"), + ReadAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "租户是否已阅读。"), + MetadataJson = table.Column(type: "text", nullable: true, comment: "附加元数据 JSON。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_notifications", x => x.Id); + }, + comment: "面向租户的站内通知或消息推送。"); + + migrationBuilder.CreateTable( + name: "tenant_packages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "套餐名称,展示给租户的简称。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "套餐描述,包含适用场景、权益等。"), + PackageType = table.Column(type: "integer", nullable: false, comment: "套餐分类(试用、标准、旗舰等)。"), + MonthlyPrice = table.Column(type: "numeric", nullable: true, comment: "月付价格,单位:人民币元。"), + YearlyPrice = table.Column(type: "numeric", nullable: true, comment: "年付价格,单位:人民币元。"), + MaxStoreCount = table.Column(type: "integer", nullable: true, comment: "允许的最大门店数。"), + MaxAccountCount = table.Column(type: "integer", nullable: true, comment: "允许创建的最大账号数。"), + MaxStorageGb = table.Column(type: "integer", nullable: true, comment: "存储容量上限(GB)。"), + MaxSmsCredits = table.Column(type: "integer", nullable: true, comment: "每月短信额度上限。"), + MaxDeliveryOrders = table.Column(type: "integer", nullable: true, comment: "每月可调用的配送单数量上限。"), + FeaturePoliciesJson = table.Column(type: "text", nullable: true, comment: "权益明细 JSON,记录自定义特性开关。"), + IsActive = table.Column(type: "boolean", nullable: false, comment: "是否仍可售卖。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。") + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_packages", x => x.Id); + }, + comment: "平台提供的租户套餐定义。"); + + migrationBuilder.CreateTable( + name: "tenant_quota_usages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + QuotaType = table.Column(type: "integer", nullable: false, comment: "配额类型,例如门店数、短信条数等。"), + LimitValue = table.Column(type: "numeric", nullable: false, comment: "当前配额上限。"), + UsedValue = table.Column(type: "numeric", nullable: false, comment: "已消耗的数量。"), + ResetCycle = table.Column(type: "text", nullable: true, comment: "配额刷新周期描述(如月、年)。"), + LastResetAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次重置时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_quota_usages", x => x.Id); + }, + comment: "租户配额使用情况快照。"); + + migrationBuilder.CreateTable( + name: "tenant_subscriptions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TenantPackageId = table.Column(type: "bigint", nullable: false, comment: "当前订阅关联的套餐标识。"), + EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: false, comment: "订阅生效时间(UTC)。"), + EffectiveTo = table.Column(type: "timestamp with time zone", nullable: false, comment: "订阅到期时间(UTC)。"), + NextBillingDate = table.Column(type: "timestamp with time zone", nullable: true, comment: "下一个计费时间,配合自动续费使用。"), + Status = table.Column(type: "integer", nullable: false, comment: "订阅当前状态。"), + AutoRenew = table.Column(type: "boolean", nullable: false, comment: "是否开启自动续费。"), + ScheduledPackageId = table.Column(type: "bigint", nullable: true, comment: "若已排期升降配,对应的新套餐 ID。"), + Notes = table.Column(type: "text", nullable: true, comment: "运营备注信息。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_tenant_subscriptions", x => x.Id); + }, + comment: "租户套餐订阅记录。"); + + migrationBuilder.CreateTable( + name: "tenants", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "租户短编码,作为跨系统引用的唯一标识。"), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "租户全称或品牌名称。"), + ShortName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "对外展示的简称。"), + LegalEntityName = table.Column(type: "text", nullable: true, comment: "法人或公司主体名称。"), + Industry = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "所属行业,如餐饮、零售等。"), + LogoUrl = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "LOGO 图片地址。"), + CoverImageUrl = table.Column(type: "text", nullable: true, comment: "品牌海报或封面图。"), + Website = table.Column(type: "text", nullable: true, comment: "官网或主要宣传链接。"), + Country = table.Column(type: "text", nullable: true, comment: "所在国家/地区。"), + Province = table.Column(type: "text", nullable: true, comment: "所在省份或州。"), + City = table.Column(type: "text", nullable: true, comment: "所在城市。"), + Address = table.Column(type: "text", nullable: true, comment: "详细地址信息。"), + ContactName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "主联系人姓名。"), + ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "主联系人电话。"), + ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "主联系人邮箱。"), + PrimaryOwnerUserId = table.Column(type: "bigint", nullable: true, comment: "系统内对应的租户所有者账号 ID。"), + Status = table.Column(type: "integer", nullable: false, comment: "租户当前状态,涵盖审核、启用、停用等场景。"), + EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: true, comment: "服务生效时间(UTC)。"), + EffectiveTo = table.Column(type: "timestamp with time zone", nullable: true, comment: "服务到期时间(UTC)。"), + SuspendedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次暂停服务时间。"), + SuspensionReason = table.Column(type: "text", nullable: true, comment: "暂停或终止的原因说明。"), + Tags = table.Column(type: "text", nullable: true, comment: "业务标签集合(逗号分隔)。"), + Remarks = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "备注信息,用于运营记录特殊说明。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。") + }, + constraints: table => + { + table.PrimaryKey("PK_tenants", x => x.Id); + }, + comment: "平台租户信息,描述租户的生命周期与基础资料。"); + + migrationBuilder.CreateTable( + name: "ticket_comments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SupportTicketId = table.Column(type: "bigint", nullable: false, comment: "工单标识。"), + AuthorUserId = table.Column(type: "bigint", nullable: true, comment: "评论人 ID。"), + Content = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false, comment: "评论内容。"), + IsInternal = table.Column(type: "boolean", nullable: false, comment: "是否内部备注。"), + AttachmentsJson = table.Column(type: "text", nullable: true, comment: "附件 JSON。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_ticket_comments", x => x.Id); + }, + comment: "工单评论/流转记录。"); + + migrationBuilder.CreateTable( + name: "order_items", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderId = table.Column(type: "bigint", nullable: false, comment: "订单 ID。"), + ProductId = table.Column(type: "bigint", nullable: false, comment: "商品 ID。"), + ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "商品名称。"), + SkuName = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "SKU/规格描述。"), + Unit = table.Column(type: "character varying(16)", maxLength: 16, nullable: true, comment: "单位。"), + Quantity = table.Column(type: "integer", nullable: false, comment: "数量。"), + UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "单价。"), + DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "折扣金额。"), + SubTotal = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "小计。"), + AttributesJson = table.Column(type: "text", nullable: true, comment: "自定义属性 JSON。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_order_items", x => x.Id); + table.ForeignKey( + name: "FK_order_items_orders_OrderId", + column: x => x.OrderId, + principalTable: "orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }, + comment: "订单明细。"); + + migrationBuilder.CreateIndex( + name: "IX_affiliate_orders_TenantId_AffiliatePartnerId_OrderId", + table: "affiliate_orders", + columns: new[] { "TenantId", "AffiliatePartnerId", "OrderId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_affiliate_partners_TenantId_DisplayName", + table: "affiliate_partners", + columns: new[] { "TenantId", "DisplayName" }); + + migrationBuilder.CreateIndex( + name: "IX_affiliate_payouts_TenantId_AffiliatePartnerId_Period", + table: "affiliate_payouts", + columns: new[] { "TenantId", "AffiliatePartnerId", "Period" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_cart_items_TenantId_ShoppingCartId", + table: "cart_items", + columns: new[] { "TenantId", "ShoppingCartId" }); + + migrationBuilder.CreateIndex( + name: "IX_chat_messages_TenantId_ChatSessionId_CreatedAt", + table: "chat_messages", + columns: new[] { "TenantId", "ChatSessionId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_chat_sessions_TenantId_SessionCode", + table: "chat_sessions", + columns: new[] { "TenantId", "SessionCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_checkin_campaigns_TenantId_Name", + table: "checkin_campaigns", + columns: new[] { "TenantId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_checkin_records_TenantId_CheckInCampaignId_UserId_CheckInDa~", + table: "checkin_records", + columns: new[] { "TenantId", "CheckInCampaignId", "UserId", "CheckInDate" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_checkout_sessions_TenantId_SessionToken", + table: "checkout_sessions", + columns: new[] { "TenantId", "SessionToken" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_community_comments_TenantId_PostId_CreatedAt", + table: "community_comments", + columns: new[] { "TenantId", "PostId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_community_posts_TenantId_AuthorUserId_CreatedAt", + table: "community_posts", + columns: new[] { "TenantId", "AuthorUserId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_community_reactions_TenantId_PostId_UserId", + table: "community_reactions", + columns: new[] { "TenantId", "PostId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_coupons_TenantId_Code", + table: "coupons", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_delivery_events_TenantId_DeliveryOrderId_EventType", + table: "delivery_events", + columns: new[] { "TenantId", "DeliveryOrderId", "EventType" }); + + migrationBuilder.CreateIndex( + name: "IX_delivery_orders_TenantId_OrderId", + table: "delivery_orders", + columns: new[] { "TenantId", "OrderId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_group_orders_TenantId_GroupOrderNo", + table: "group_orders", + columns: new[] { "TenantId", "GroupOrderNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_group_participants_TenantId_GroupOrderId_UserId", + table: "group_participants", + columns: new[] { "TenantId", "GroupOrderId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_inventory_adjustments_TenantId_InventoryItemId_OccurredAt", + table: "inventory_adjustments", + columns: new[] { "TenantId", "InventoryItemId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_inventory_batches_TenantId_StoreId_ProductSkuId_BatchNumber", + table: "inventory_batches", + columns: new[] { "TenantId", "StoreId", "ProductSkuId", "BatchNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_inventory_items_TenantId_StoreId_ProductSkuId_BatchNumber", + table: "inventory_items", + columns: new[] { "TenantId", "StoreId", "ProductSkuId", "BatchNumber" }); + + migrationBuilder.CreateIndex( + name: "IX_map_locations_TenantId_StoreId", + table: "map_locations", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_member_growth_logs_TenantId_MemberId_OccurredAt", + table: "member_growth_logs", + columns: new[] { "TenantId", "MemberId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_member_point_ledgers_TenantId_MemberId_OccurredAt", + table: "member_point_ledgers", + columns: new[] { "TenantId", "MemberId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_member_profiles_TenantId_Mobile", + table: "member_profiles", + columns: new[] { "TenantId", "Mobile" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_member_tiers_TenantId_Name", + table: "member_tiers", + columns: new[] { "TenantId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_merchant_contracts_TenantId_MerchantId_ContractNumber", + table: "merchant_contracts", + columns: new[] { "TenantId", "MerchantId", "ContractNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_merchant_documents_TenantId_MerchantId_DocumentType", + table: "merchant_documents", + columns: new[] { "TenantId", "MerchantId", "DocumentType" }); + + migrationBuilder.CreateIndex( + name: "IX_merchant_staff_TenantId_MerchantId_Phone", + table: "merchant_staff", + columns: new[] { "TenantId", "MerchantId", "Phone" }); + + migrationBuilder.CreateIndex( + name: "IX_merchants_TenantId", + table: "merchants", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_metric_alert_rules_TenantId_MetricDefinitionId_Severity", + table: "metric_alert_rules", + columns: new[] { "TenantId", "MetricDefinitionId", "Severity" }); + + migrationBuilder.CreateIndex( + name: "IX_metric_definitions_TenantId_Code", + table: "metric_definitions", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_metric_snapshots_TenantId_MetricDefinitionId_DimensionKey_W~", + table: "metric_snapshots", + columns: new[] { "TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_navigation_requests_TenantId_UserId_StoreId_RequestedAt", + table: "navigation_requests", + columns: new[] { "TenantId", "UserId", "StoreId", "RequestedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_order_items_OrderId", + table: "order_items", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_order_items_TenantId_OrderId", + table: "order_items", + columns: new[] { "TenantId", "OrderId" }); + + migrationBuilder.CreateIndex( + name: "IX_order_status_histories_TenantId_OrderId_OccurredAt", + table: "order_status_histories", + columns: new[] { "TenantId", "OrderId", "OccurredAt" }); + + migrationBuilder.CreateIndex( + name: "IX_orders_TenantId_OrderNo", + table: "orders", + columns: new[] { "TenantId", "OrderNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_orders_TenantId_StoreId_Status", + table: "orders", + columns: new[] { "TenantId", "StoreId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_payment_records_TenantId_OrderId", + table: "payment_records", + columns: new[] { "TenantId", "OrderId" }); + + migrationBuilder.CreateIndex( + name: "IX_payment_refund_records_TenantId_PaymentRecordId", + table: "payment_refund_records", + columns: new[] { "TenantId", "PaymentRecordId" }); + + migrationBuilder.CreateIndex( + name: "IX_product_addon_groups_TenantId_ProductId_Name", + table: "product_addon_groups", + columns: new[] { "TenantId", "ProductId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_product_attribute_groups_TenantId_StoreId_Name", + table: "product_attribute_groups", + columns: new[] { "TenantId", "StoreId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_product_attribute_options_TenantId_AttributeGroupId_Name", + table: "product_attribute_options", + columns: new[] { "TenantId", "AttributeGroupId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_product_categories_TenantId_StoreId", + table: "product_categories", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_product_pricing_rules_TenantId_ProductId_RuleType", + table: "product_pricing_rules", + columns: new[] { "TenantId", "ProductId", "RuleType" }); + + migrationBuilder.CreateIndex( + name: "IX_product_skus_TenantId_SkuCode", + table: "product_skus", + columns: new[] { "TenantId", "SkuCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_products_TenantId_SpuCode", + table: "products", + columns: new[] { "TenantId", "SpuCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_products_TenantId_StoreId", + table: "products", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_queue_tickets_TenantId_StoreId", + table: "queue_tickets", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_queue_tickets_TenantId_StoreId_TicketNumber", + table: "queue_tickets", + columns: new[] { "TenantId", "StoreId", "TicketNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_refund_requests_TenantId_RefundNo", + table: "refund_requests", + columns: new[] { "TenantId", "RefundNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_reservations_TenantId_ReservationNo", + table: "reservations", + columns: new[] { "TenantId", "ReservationNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_reservations_TenantId_StoreId", + table: "reservations", + columns: new[] { "TenantId", "StoreId" }); + + migrationBuilder.CreateIndex( + name: "IX_shopping_carts_TenantId_UserId_StoreId", + table: "shopping_carts", + columns: new[] { "TenantId", "UserId", "StoreId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_business_hours_TenantId_StoreId_DayOfWeek", + table: "store_business_hours", + columns: new[] { "TenantId", "StoreId", "DayOfWeek" }); + + migrationBuilder.CreateIndex( + name: "IX_store_delivery_zones_TenantId_StoreId_ZoneName", + table: "store_delivery_zones", + columns: new[] { "TenantId", "StoreId", "ZoneName" }); + + migrationBuilder.CreateIndex( + name: "IX_store_employee_shifts_TenantId_StoreId_ShiftDate_StaffId", + table: "store_employee_shifts", + columns: new[] { "TenantId", "StoreId", "ShiftDate", "StaffId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_holidays_TenantId_StoreId_Date", + table: "store_holidays", + columns: new[] { "TenantId", "StoreId", "Date" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_table_areas_TenantId_StoreId_Name", + table: "store_table_areas", + columns: new[] { "TenantId", "StoreId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_store_tables_TenantId_StoreId_TableCode", + table: "store_tables", + columns: new[] { "TenantId", "StoreId", "TableCode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_stores_TenantId_Code", + table: "stores", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_stores_TenantId_MerchantId", + table: "stores", + columns: new[] { "TenantId", "MerchantId" }); + + migrationBuilder.CreateIndex( + name: "IX_support_tickets_TenantId_TicketNo", + table: "support_tickets", + columns: new[] { "TenantId", "TicketNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_tenant_billing_statements_TenantId_StatementNo", + table: "tenant_billing_statements", + columns: new[] { "TenantId", "StatementNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_tenant_notifications_TenantId_Channel_SentAt", + table: "tenant_notifications", + columns: new[] { "TenantId", "Channel", "SentAt" }); + + migrationBuilder.CreateIndex( + name: "IX_tenant_quota_usages_TenantId_QuotaType", + table: "tenant_quota_usages", + columns: new[] { "TenantId", "QuotaType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_tenant_subscriptions_TenantId_TenantPackageId", + table: "tenant_subscriptions", + columns: new[] { "TenantId", "TenantPackageId" }); + + migrationBuilder.CreateIndex( + name: "IX_tenants_Code", + table: "tenants", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ticket_comments_TenantId_SupportTicketId", + table: "ticket_comments", + columns: new[] { "TenantId", "SupportTicketId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "affiliate_orders"); + + migrationBuilder.DropTable( + name: "affiliate_partners"); + + migrationBuilder.DropTable( + name: "affiliate_payouts"); + + migrationBuilder.DropTable( + name: "cart_item_addons"); + + migrationBuilder.DropTable( + name: "cart_items"); + + migrationBuilder.DropTable( + name: "chat_messages"); + + migrationBuilder.DropTable( + name: "chat_sessions"); + + migrationBuilder.DropTable( + name: "checkin_campaigns"); + + migrationBuilder.DropTable( + name: "checkin_records"); + + migrationBuilder.DropTable( + name: "checkout_sessions"); + + migrationBuilder.DropTable( + name: "community_comments"); + + migrationBuilder.DropTable( + name: "community_posts"); + + migrationBuilder.DropTable( + name: "community_reactions"); + + migrationBuilder.DropTable( + name: "coupon_templates"); + + migrationBuilder.DropTable( + name: "coupons"); + + migrationBuilder.DropTable( + name: "delivery_events"); + + migrationBuilder.DropTable( + name: "delivery_orders"); + + migrationBuilder.DropTable( + name: "group_orders"); + + migrationBuilder.DropTable( + name: "group_participants"); + + migrationBuilder.DropTable( + name: "inventory_adjustments"); + + migrationBuilder.DropTable( + name: "inventory_batches"); + + migrationBuilder.DropTable( + name: "inventory_items"); + + migrationBuilder.DropTable( + name: "map_locations"); + + migrationBuilder.DropTable( + name: "member_growth_logs"); + + migrationBuilder.DropTable( + name: "member_point_ledgers"); + + migrationBuilder.DropTable( + name: "member_profiles"); + + migrationBuilder.DropTable( + name: "member_tiers"); + + migrationBuilder.DropTable( + name: "merchant_contracts"); + + migrationBuilder.DropTable( + name: "merchant_documents"); + + migrationBuilder.DropTable( + name: "merchant_staff"); + + migrationBuilder.DropTable( + name: "merchants"); + + migrationBuilder.DropTable( + name: "metric_alert_rules"); + + migrationBuilder.DropTable( + name: "metric_definitions"); + + migrationBuilder.DropTable( + name: "metric_snapshots"); + + migrationBuilder.DropTable( + name: "navigation_requests"); + + migrationBuilder.DropTable( + name: "order_items"); + + migrationBuilder.DropTable( + name: "order_status_histories"); + + migrationBuilder.DropTable( + name: "payment_records"); + + migrationBuilder.DropTable( + name: "payment_refund_records"); + + migrationBuilder.DropTable( + name: "product_addon_groups"); + + migrationBuilder.DropTable( + name: "product_addon_options"); + + migrationBuilder.DropTable( + name: "product_attribute_groups"); + + migrationBuilder.DropTable( + name: "product_attribute_options"); + + migrationBuilder.DropTable( + name: "product_categories"); + + migrationBuilder.DropTable( + name: "product_media_assets"); + + migrationBuilder.DropTable( + name: "product_pricing_rules"); + + migrationBuilder.DropTable( + name: "product_skus"); + + migrationBuilder.DropTable( + name: "products"); + + migrationBuilder.DropTable( + name: "promotion_campaigns"); + + migrationBuilder.DropTable( + name: "queue_tickets"); + + migrationBuilder.DropTable( + name: "refund_requests"); + + migrationBuilder.DropTable( + name: "reservations"); + + migrationBuilder.DropTable( + name: "shopping_carts"); + + migrationBuilder.DropTable( + name: "store_business_hours"); + + migrationBuilder.DropTable( + name: "store_delivery_zones"); + + migrationBuilder.DropTable( + name: "store_employee_shifts"); + + migrationBuilder.DropTable( + name: "store_holidays"); + + migrationBuilder.DropTable( + name: "store_table_areas"); + + migrationBuilder.DropTable( + name: "store_tables"); + + migrationBuilder.DropTable( + name: "stores"); + + migrationBuilder.DropTable( + name: "support_tickets"); + + migrationBuilder.DropTable( + name: "tenant_billing_statements"); + + migrationBuilder.DropTable( + name: "tenant_notifications"); + + migrationBuilder.DropTable( + name: "tenant_packages"); + + migrationBuilder.DropTable( + name: "tenant_quota_usages"); + + migrationBuilder.DropTable( + name: "tenant_subscriptions"); + + migrationBuilder.DropTable( + name: "tenants"); + + migrationBuilder.DropTable( + name: "ticket_comments"); + + migrationBuilder.DropTable( + name: "orders"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.Designer.cs similarity index 83% rename from src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs rename to src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.Designer.cs index 2f36f70..96bfd4a 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/20251201094456_AddEntityComments.Designer.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.Designer.cs @@ -9,11 +9,11 @@ using TakeoutSaaS.Infrastructure.Dictionary.Persistence; #nullable disable -namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb { [DbContext(typeof(DictionaryDbContext))] - [Migration("20251201094456_AddEntityComments")] - partial class AddEntityComments + [Migration("20251202005247_InitSnowflake_Dictionary")] + partial class InitSnowflake_Dictionary { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -27,11 +27,13 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Code") .IsRequired() .HasMaxLength(64) @@ -42,16 +44,16 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -75,16 +77,16 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasColumnType("integer") .HasComment("分组作用域:系统/业务。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -102,25 +104,27 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -128,8 +132,8 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasColumnType("character varying(512)") .HasComment("描述信息。"); - b.Property("GroupId") - .HasColumnType("uuid") + b.Property("GroupId") + .HasColumnType("bigint") .HasComment("关联分组 ID。"); b.Property("IsDefault") @@ -154,16 +158,16 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasDefaultValue(100) .HasComment("排序值,越小越靠前。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Value") diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.cs new file mode 100644 index 0000000..374be49 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202005247_InitSnowflake_Dictionary.cs @@ -0,0 +1,106 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb +{ + /// + public partial class InitSnowflake_Dictionary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "dictionary_groups", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "分组编码(唯一)。"), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "分组名称。"), + Scope = table.Column(type: "integer", nullable: false, comment: "分组作用域:系统/业务。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "描述信息。"), + IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true, comment: "是否启用。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_dictionary_groups", x => x.Id); + }, + comment: "参数字典分组(系统参数、业务参数)。"); + + migrationBuilder.CreateTable( + name: "dictionary_items", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + GroupId = table.Column(type: "bigint", nullable: false, comment: "关联分组 ID。"), + Key = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "字典项键。"), + Value = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "字典项值。"), + IsDefault = table.Column(type: "boolean", nullable: false, comment: "是否默认项。"), + IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true, comment: "是否启用。"), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100, comment: "排序值,越小越靠前。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "描述信息。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_dictionary_items", x => x.Id); + table.ForeignKey( + name: "FK_dictionary_items_dictionary_groups_GroupId", + column: x => x.GroupId, + principalTable: "dictionary_groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }, + comment: "参数字典项。"); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_groups_TenantId", + table: "dictionary_groups", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_groups_TenantId_Code", + table: "dictionary_groups", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_items_GroupId_Key", + table: "dictionary_items", + columns: new[] { "GroupId", "Key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_dictionary_items_TenantId", + table: "dictionary_items", + column: "TenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "dictionary_items"); + + migrationBuilder.DropTable( + name: "dictionary_groups"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs similarity index 84% rename from src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs rename to src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs index d55b340..1df55bf 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Migrations/DictionaryDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs @@ -8,7 +8,7 @@ using TakeoutSaaS.Infrastructure.Dictionary.Persistence; #nullable disable -namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb { [DbContext(typeof(DictionaryDbContext))] partial class DictionaryDbContextModelSnapshot : ModelSnapshot @@ -24,11 +24,13 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Code") .IsRequired() .HasMaxLength(64) @@ -39,16 +41,16 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -72,16 +74,16 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasColumnType("integer") .HasComment("分组作用域:系统/业务。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -99,25 +101,27 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -125,8 +129,8 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasColumnType("character varying(512)") .HasComment("描述信息。"); - b.Property("GroupId") - .HasColumnType("uuid") + b.Property("GroupId") + .HasColumnType("bigint") .HasComment("关联分组 ID。"); b.Property("IsDefault") @@ -151,16 +155,16 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations .HasDefaultValue(100) .HasComment("排序值,越小越靠前。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Value") diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.Designer.cs similarity index 81% rename from src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs rename to src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.Designer.cs index 9f9c680..87e37a5 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/20251201094410_AddEntityComments.Designer.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.Designer.cs @@ -9,11 +9,11 @@ using TakeoutSaaS.Infrastructure.Identity.Persistence; #nullable disable -namespace TakeoutSaaS.Infrastructure.Identity.Migrations +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb { [DbContext(typeof(IdentityDbContext))] - [Migration("20251201094410_AddEntityComments")] - partial class AddEntityComments + [Migration("20251202005226_InitSnowflake_Identity")] + partial class InitSnowflake_Identity { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -27,11 +27,13 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Account") .IsRequired() .HasMaxLength(64) @@ -47,16 +49,16 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DisplayName") @@ -65,8 +67,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("character varying(64)") .HasComment("展示名称。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户(平台管理员为空)。"); b.Property("PasswordHash") @@ -85,16 +87,16 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("text") .HasComment("角色集合。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -112,11 +114,13 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Avatar") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -126,16 +130,16 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Nickname") @@ -150,8 +154,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("character varying(128)") .HasComment("微信 OpenId。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UnionId") @@ -163,8 +167,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.cs new file mode 100644 index 0000000..772ad91 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202005226_InitSnowflake_Identity.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + /// + public partial class InitSnowflake_Identity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "identity_users", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Account = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "登录账号。"), + DisplayName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "展示名称。"), + PasswordHash = table.Column(type: "character varying(256)", maxLength: 256, nullable: false, comment: "密码哈希。"), + MerchantId = table.Column(type: "bigint", nullable: true, comment: "所属商户(平台管理员为空)。"), + Roles = table.Column(type: "text", nullable: false, comment: "角色集合。"), + Permissions = table.Column(type: "text", nullable: false, comment: "权限集合。"), + Avatar = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "头像地址。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_identity_users", x => x.Id); + }, + comment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + + migrationBuilder.CreateTable( + name: "mini_users", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OpenId = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "微信 OpenId。"), + UnionId = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "微信 UnionId,可能为空。"), + Nickname = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "昵称。"), + Avatar = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "头像地址。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_mini_users", x => x.Id); + }, + comment: "小程序用户实体。"); + + migrationBuilder.CreateIndex( + name: "IX_identity_users_TenantId", + table: "identity_users", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_identity_users_TenantId_Account", + table: "identity_users", + columns: new[] { "TenantId", "Account" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_mini_users_TenantId", + table: "mini_users", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_mini_users_TenantId_OpenId", + table: "mini_users", + columns: new[] { "TenantId", "OpenId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "identity_users"); + + migrationBuilder.DropTable( + name: "mini_users"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs similarity index 82% rename from src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs rename to src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs index 1814980..477e211 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Migrations/IdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs @@ -8,7 +8,7 @@ using TakeoutSaaS.Infrastructure.Identity.Persistence; #nullable disable -namespace TakeoutSaaS.Infrastructure.Identity.Migrations +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb { [DbContext(typeof(IdentityDbContext))] partial class IdentityDbContextModelSnapshot : ModelSnapshot @@ -24,11 +24,13 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Account") .IsRequired() .HasMaxLength(64) @@ -44,16 +46,16 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DisplayName") @@ -62,8 +64,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("character varying(64)") .HasComment("展示名称。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户(平台管理员为空)。"); b.Property("PasswordHash") @@ -82,16 +84,16 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("text") .HasComment("角色集合。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -109,11 +111,13 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Avatar") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -123,16 +127,16 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Nickname") @@ -147,8 +151,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("character varying(128)") .HasComment("微信 OpenId。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UnionId") @@ -160,8 +164,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs similarity index 79% rename from src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs rename to src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs index 50ffaf5..fc6058e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Migrations/TakeoutAppDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs @@ -8,7 +8,7 @@ using TakeoutSaaS.Infrastructure.App.Persistence; #nullable disable -namespace TakeoutSaaS.Infrastructure.App.Migrations +namespace TakeoutSaaS.Infrastructure.Migrations { [DbContext(typeof(TakeoutAppDbContext))] partial class TakeoutAppDbContextModelSnapshot : ModelSnapshot @@ -24,11 +24,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ConditionJson") .IsRequired() .HasColumnType("text") @@ -38,24 +40,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Enabled") .HasColumnType("boolean") .HasComment("是否启用。"); - b.Property("MetricDefinitionId") - .HasColumnType("uuid") + b.Property("MetricDefinitionId") + .HasColumnType("bigint") .HasComment("关联指标。"); b.Property("NotificationChannels") @@ -68,16 +70,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("告警级别。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -92,11 +94,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Code") .IsRequired() .HasMaxLength(64) @@ -107,8 +111,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DefaultAggregation") @@ -121,8 +125,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -140,16 +144,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("指标名称。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -165,25 +169,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DimensionKey") @@ -192,20 +198,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("维度键(JSON)。"); - b.Property("MetricDefinitionId") - .HasColumnType("uuid") + b.Property("MetricDefinitionId") + .HasColumnType("bigint") .HasComment("指标定义 ID。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Value") @@ -234,35 +240,37 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Code") .IsRequired() .HasMaxLength(32) .HasColumnType("character varying(32)") .HasComment("券码或序列号。"); - b.Property("CouponTemplateId") - .HasColumnType("uuid") + b.Property("CouponTemplateId") + .HasColumnType("bigint") .HasComment("模板标识。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireAt") @@ -273,32 +281,32 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("发放时间。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("订单 ID(已使用时记录)。"); b.Property("Status") .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("UsedAt") .HasColumnType("timestamp with time zone") .HasComment("使用时间。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("归属用户。"); b.HasKey("Id"); @@ -314,11 +322,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AllowStack") .HasColumnType("boolean") .HasComment("是否允许叠加其他优惠。"); @@ -339,16 +349,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -386,8 +396,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("适用门店 ID 集合(JSON)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TotalQuantity") @@ -398,8 +408,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("ValidFrom") @@ -424,11 +434,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AudienceDescription") .HasMaxLength(512) .HasColumnType("character varying(512)") @@ -447,16 +459,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndAt") @@ -486,16 +498,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("活动状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -508,13 +520,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("ChatSessionId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSessionId") + .HasColumnType("bigint") .HasComment("会话标识。"); b.Property("Content") @@ -533,16 +547,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsRead") @@ -557,20 +571,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("发送方类型。"); - b.Property("SenderUserId") - .HasColumnType("uuid") + b.Property("SenderUserId") + .HasColumnType("bigint") .HasComment("发送方用户 ID。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -585,33 +599,35 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AgentUserId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentUserId") + .HasColumnType("bigint") .HasComment("当前客服员工 ID。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); - b.Property("CustomerUserId") - .HasColumnType("uuid") + b.Property("CustomerUserId") + .HasColumnType("bigint") .HasComment("顾客用户 ID。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndedAt") @@ -636,20 +652,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("会话状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("所属门店(可空为平台)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -665,13 +681,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AssignedAgentId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAgentId") + .HasColumnType("bigint") .HasComment("指派的客服。"); b.Property("ClosedAt") @@ -682,20 +700,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); - b.Property("CustomerUserId") - .HasColumnType("uuid") + b.Property("CustomerUserId") + .HasColumnType("bigint") .HasComment("客户用户 ID。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -703,8 +721,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("工单详情。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单(如有)。"); b.Property("Priority") @@ -721,8 +739,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("工单主题。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TicketNo") @@ -735,8 +753,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -752,17 +770,19 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttachmentsJson") .HasColumnType("text") .HasComment("附件 JSON。"); - b.Property("AuthorUserId") - .HasColumnType("uuid") + b.Property("AuthorUserId") + .HasColumnType("bigint") .HasComment("评论人 ID。"); b.Property("Content") @@ -775,36 +795,36 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsInternal") .HasColumnType("boolean") .HasComment("是否内部备注。"); - b.Property("SupportTicketId") - .HasColumnType("uuid") + b.Property("SupportTicketId") + .HasColumnType("bigint") .HasComment("工单标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -819,29 +839,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("DeliveryOrderId") - .HasColumnType("uuid") + b.Property("DeliveryOrderId") + .HasColumnType("bigint") .HasComment("配送单标识。"); b.Property("EventType") @@ -862,16 +884,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("原始数据 JSON。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -886,11 +908,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CourierName") .HasMaxLength(64) .HasColumnType("character varying(64)") @@ -905,16 +929,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveredAt") @@ -935,8 +959,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("异常原因。"); - b.Property("OrderId") - .HasColumnType("uuid"); + b.Property("OrderId") + .HasColumnType("bigint"); b.Property("PickedUpAt") .HasColumnType("timestamp with time zone") @@ -955,16 +979,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -980,33 +1004,35 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AffiliatePartnerId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") .HasComment("推广人标识。"); - b.Property("BuyerUserId") - .HasColumnType("uuid") + b.Property("BuyerUserId") + .HasColumnType("bigint") .HasComment("用户 ID。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EstimatedCommission") @@ -1019,8 +1045,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("订单金额。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单。"); b.Property("SettledAt") @@ -1031,16 +1057,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1056,11 +1082,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ChannelType") .HasColumnType("integer") .HasComment("渠道类型。"); @@ -1073,16 +1101,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DisplayName") @@ -1105,20 +1133,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户 ID(如绑定平台账号)。"); b.HasKey("Id"); @@ -1133,13 +1161,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AffiliatePartnerId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") .HasComment("合作伙伴标识。"); b.Property("Amount") @@ -1151,16 +1181,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("PaidAt") @@ -1182,16 +1212,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1207,11 +1237,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AllowMakeupCount") .HasColumnType("integer") .HasComment("支持补签次数。"); @@ -1220,16 +1252,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -1260,16 +1292,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1284,13 +1316,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("CheckInCampaignId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CheckInCampaignId") + .HasColumnType("bigint") .HasComment("活动标识。"); b.Property("CheckInDate") @@ -1301,16 +1335,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsMakeup") @@ -1322,20 +1356,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("获得奖励 JSON。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -1351,13 +1385,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AuthorUserId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") .HasComment("评论人。"); b.Property("Content") @@ -1370,40 +1406,40 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsDeleted") .HasColumnType("boolean") .HasComment("状态。"); - b.Property("ParentId") - .HasColumnType("uuid") + b.Property("ParentId") + .HasColumnType("bigint") .HasComment("父级评论 ID。"); - b.Property("PostId") - .HasColumnType("uuid") + b.Property("PostId") + .HasColumnType("bigint") .HasComment("动态标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1418,13 +1454,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AuthorUserId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") .HasComment("作者用户 ID。"); b.Property("CommentCount") @@ -1440,16 +1478,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("LikeCount") @@ -1464,8 +1502,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Title") @@ -1477,8 +1515,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1493,29 +1531,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("PostId") - .HasColumnType("uuid") + b.Property("PostId") + .HasColumnType("bigint") .HasComment("动态 ID。"); b.Property("ReactedAt") @@ -1526,20 +1566,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("反应类型。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户 ID。"); b.HasKey("Id"); @@ -1555,11 +1595,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CancelledAt") .HasColumnType("timestamp with time zone") .HasComment("取消时间。"); @@ -1568,8 +1610,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CurrentCount") @@ -1580,8 +1622,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndAt") @@ -1599,12 +1641,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("拼团价格。"); - b.Property("LeaderUserId") - .HasColumnType("uuid") + b.Property("LeaderUserId") + .HasColumnType("bigint") .HasComment("团长用户 ID。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("关联商品或套餐。"); b.Property("StartAt") @@ -1615,8 +1657,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("拼团状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); b.Property("SucceededAt") @@ -1627,16 +1669,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("成团需要的人数。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1652,57 +1694,59 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("GroupOrderId") - .HasColumnType("uuid") + b.Property("GroupOrderId") + .HasColumnType("bigint") .HasComment("拼单活动标识。"); b.Property("JoinedAt") .HasColumnType("timestamp with time zone") .HasComment("参与时间。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("对应订单标识。"); b.Property("Status") .HasColumnType("integer") .HasComment("参与状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -1718,11 +1762,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AdjustmentType") .HasColumnType("integer") .HasComment("调整类型。"); @@ -1731,28 +1777,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("InventoryItemId") - .HasColumnType("uuid") + b.Property("InventoryItemId") + .HasColumnType("bigint") .HasComment("对应的库存记录标识。"); b.Property("OccurredAt") .HasColumnType("timestamp with time zone") .HasComment("发生时间。"); - b.Property("OperatorId") - .HasColumnType("uuid") + b.Property("OperatorId") + .HasColumnType("bigint") .HasComment("操作人标识。"); b.Property("Quantity") @@ -1764,16 +1810,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("原因说明。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1788,11 +1834,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BatchNumber") .IsRequired() .HasMaxLength(64) @@ -1803,24 +1851,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireDate") .HasColumnType("timestamp with time zone") .HasComment("过期日期。"); - b.Property("ProductSkuId") - .HasColumnType("uuid") + b.Property("ProductSkuId") + .HasColumnType("bigint") .HasComment("SKU 标识。"); b.Property("ProductionDate") @@ -1835,20 +1883,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("剩余数量。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1864,11 +1912,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BatchNumber") .HasMaxLength(64) .HasColumnType("character varying(64)") @@ -1878,16 +1928,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireDate") @@ -1899,8 +1949,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("储位或仓位信息。"); - b.Property("ProductSkuId") - .HasColumnType("uuid") + b.Property("ProductSkuId") + .HasColumnType("bigint") .HasComment("SKU 标识。"); b.Property("QuantityOnHand") @@ -1915,20 +1965,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("安全库存阈值。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -1943,11 +1993,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ChangeValue") .HasColumnType("integer") .HasComment("变动数量。"); @@ -1956,8 +2008,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CurrentValue") @@ -1968,12 +2020,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("MemberId") - .HasColumnType("uuid") + b.Property("MemberId") + .HasColumnType("bigint") .HasComment("会员标识。"); b.Property("Notes") @@ -1985,16 +2037,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("发生时间。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2009,11 +2061,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BalanceAfterChange") .HasColumnType("integer") .HasComment("变动后余额。"); @@ -2026,24 +2080,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpireAt") .HasColumnType("timestamp with time zone") .HasComment("过期时间(如适用)。"); - b.Property("MemberId") - .HasColumnType("uuid") + b.Property("MemberId") + .HasColumnType("bigint") .HasComment("会员标识。"); b.Property("OccurredAt") @@ -2054,20 +2108,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("变动原因。"); - b.Property("SourceId") - .HasColumnType("uuid") + b.Property("SourceId") + .HasColumnType("bigint") .HasComment("来源 ID(订单、活动等)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2082,11 +2136,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AvatarUrl") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -2100,16 +2156,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("GrowthValue") @@ -2120,8 +2176,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("注册时间。"); - b.Property("MemberTierId") - .HasColumnType("uuid") + b.Property("MemberTierId") + .HasColumnType("bigint") .HasComment("当前会员等级 ID。"); b.Property("Mobile") @@ -2143,20 +2199,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("会员状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -2172,11 +2228,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BenefitsJson") .IsRequired() .HasColumnType("text") @@ -2186,16 +2244,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Name") @@ -2212,16 +2270,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序值。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2237,11 +2295,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -2291,16 +2351,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("District") @@ -2359,16 +2419,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("税号/统一社会信用代码。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2383,11 +2443,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ContractNumber") .IsRequired() .HasMaxLength(64) @@ -2398,16 +2460,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndDate") @@ -2420,8 +2482,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(512)") .HasComment("合同文件存储地址。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("SignedAt") @@ -2436,8 +2498,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("合同状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TerminatedAt") @@ -2453,8 +2515,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2470,25 +2532,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DocumentNumber") @@ -2514,8 +2578,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("签发日期。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("Remarks") @@ -2526,16 +2590,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("审核状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2550,25 +2614,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Email") @@ -2576,12 +2642,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("邮箱地址。"); - b.Property("IdentityUserId") - .HasColumnType("uuid") + b.Property("IdentityUserId") + .HasColumnType("bigint") .HasComment("登录账号 ID(指向统一身份体系)。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("Name") @@ -2608,20 +2674,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("员工状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("可选的关联门店 ID。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2636,11 +2702,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .IsRequired() .HasMaxLength(256) @@ -2651,16 +2719,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Landmark") @@ -2682,20 +2750,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("名称。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("关联门店 ID,可空表示独立 POI。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2710,11 +2778,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Channel") .HasColumnType("integer") .HasComment("来源通道(小程序、H5 等)。"); @@ -2723,44 +2793,44 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("RequestedAt") .HasColumnType("timestamp with time zone") .HasComment("请求时间。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店 ID。"); b.Property("TargetApp") .HasColumnType("integer") .HasComment("跳转的地图应用。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户 ID。"); b.HasKey("Id"); @@ -2775,11 +2845,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttributesJson") .HasColumnType("text") .HasComment("扩展 JSON(规格、加料选项等)。"); @@ -2788,20 +2860,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("商品或 SKU 标识。"); b.Property("ProductName") @@ -2810,8 +2882,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("商品名称快照。"); - b.Property("ProductSkuId") - .HasColumnType("uuid") + b.Property("ProductSkuId") + .HasColumnType("bigint") .HasComment("SKU 标识。"); b.Property("Quantity") @@ -2823,16 +2895,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("自定义备注(口味要求)。"); - b.Property("ShoppingCartId") - .HasColumnType("uuid") + b.Property("ShoppingCartId") + .HasColumnType("bigint") .HasComment("所属购物车标识。"); b.Property("Status") .HasColumnType("integer") .HasComment("状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UnitPrice") @@ -2844,8 +2916,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2860,29 +2932,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("CartItemId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CartItemId") + .HasColumnType("bigint") .HasComment("所属购物车条目。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExtraPrice") @@ -2896,20 +2970,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("选项名称。"); - b.Property("OptionId") - .HasColumnType("uuid") + b.Property("OptionId") + .HasColumnType("bigint") .HasComment("选项 ID(可对应 ProductAddonOption)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -2922,25 +2996,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExpiresAt") @@ -2957,24 +3033,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("会话状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.Property("ValidationResultJson") @@ -2995,25 +3071,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryPreference") @@ -3029,8 +3107,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("购物车状态,包含正常/锁定。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); b.Property("TableContext") @@ -3038,20 +3116,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("桌码或场景标识(扫码点餐)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); - b.Property("UserId") - .HasColumnType("uuid") + b.Property("UserId") + .HasColumnType("bigint") .HasComment("用户标识。"); b.HasKey("Id"); @@ -3067,11 +3145,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CancelReason") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -3089,8 +3169,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CustomerName") @@ -3107,8 +3187,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryType") @@ -3163,16 +3243,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(512)") .HasComment("备注。"); - b.Property("ReservationId") - .HasColumnType("uuid") + b.Property("ReservationId") + .HasColumnType("bigint") .HasComment("预约 ID。"); b.Property("Status") .HasColumnType("integer") .HasComment("当前状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店。"); b.Property("TableNo") @@ -3180,16 +3260,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(32)") .HasComment("就餐桌号。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3207,11 +3287,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttributesJson") .HasColumnType("text") .HasComment("自定义属性 JSON。"); @@ -3220,16 +3302,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DiscountAmount") @@ -3237,12 +3319,12 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("折扣金额。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("订单 ID。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("商品 ID。"); b.Property("ProductName") @@ -3265,8 +3347,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("小计。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Unit") @@ -3283,8 +3365,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3301,25 +3383,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Notes") @@ -3331,28 +3415,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("发生时间。"); - b.Property("OperatorId") - .HasColumnType("uuid") + b.Property("OperatorId") + .HasColumnType("bigint") .HasComment("操作人标识(可为空表示系统)。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("订单标识。"); b.Property("Status") .HasColumnType("integer") .HasComment("变更后的状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3367,11 +3451,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Amount") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -3381,20 +3467,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单标识。"); b.Property("ProcessedAt") @@ -3426,16 +3512,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("退款状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3451,11 +3537,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Amount") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -3470,24 +3558,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Method") .HasColumnType("integer") .HasComment("支付方式。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单。"); b.Property("PaidAt") @@ -3507,8 +3595,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("支付状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TradeNo") @@ -3520,8 +3608,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3536,11 +3624,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Amount") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -3559,28 +3649,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); - b.Property("OrderId") - .HasColumnType("uuid") + b.Property("OrderId") + .HasColumnType("bigint") .HasComment("关联订单标识。"); b.Property("Payload") .HasColumnType("text") .HasComment("渠道返回的原始数据 JSON。"); - b.Property("PaymentRecordId") - .HasColumnType("uuid") + b.Property("PaymentRecordId") + .HasColumnType("bigint") .HasComment("原支付记录标识。"); b.Property("RequestedAt") @@ -3591,16 +3681,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("退款状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3615,13 +3705,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("CategoryId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint") .HasComment("所属分类。"); b.Property("CoverImage") @@ -3633,16 +3725,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -3704,8 +3796,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("库存数量(可选)。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("所属门店。"); b.Property("Subtitle") @@ -3713,8 +3805,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("副标题/卖点。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Unit") @@ -3726,8 +3818,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3745,25 +3837,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsRequired") @@ -3784,8 +3878,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("分组名称。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("所属商品。"); b.Property("SelectionType") @@ -3796,16 +3890,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序值。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3820,29 +3914,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AddonGroupId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddonGroupId") + .HasColumnType("bigint") .HasComment("所属加料分组。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExtraPrice") @@ -3864,16 +3960,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3886,25 +3982,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsRequired") @@ -3917,6 +4015,10 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("分组名称,例如“辣度”“份量”。"); + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + b.Property("SelectionType") .HasColumnType("integer") .HasComment("选择类型(单选/多选)。"); @@ -3925,20 +4027,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("显示排序。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("关联门店,可为空表示所有门店共享。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -3953,29 +4055,31 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AttributeGroupId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributeGroupId") + .HasColumnType("bigint") .HasComment("所属规格组。"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("ExtraPrice") @@ -3997,16 +4101,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4022,25 +4126,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -4062,20 +4168,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("排序值。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("所属门店。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4090,11 +4196,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Caption") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -4104,40 +4212,40 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("MediaType") .HasColumnType("integer") .HasComment("媒体类型。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("商品标识。"); b.Property("SortOrder") .HasColumnType("integer") .HasComment("排序。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Url") @@ -4156,11 +4264,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ConditionsJson") .IsRequired() .HasColumnType("text") @@ -4170,16 +4280,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndTime") @@ -4191,28 +4301,32 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("特殊价格。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("所属商品。"); b.Property("RuleType") .HasColumnType("integer") .HasComment("策略类型。"); + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + b.Property("StartTime") .HasColumnType("timestamp with time zone") .HasComment("生效开始时间。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("WeekdaysJson") @@ -4231,11 +4345,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AttributesJson") .IsRequired() .HasColumnType("text") @@ -4250,16 +4366,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("OriginalPrice") @@ -4272,8 +4388,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("numeric(18,2)") .HasComment("售价。"); - b.Property("ProductId") - .HasColumnType("uuid") + b.Property("ProductId") + .HasColumnType("bigint") .HasComment("所属商品标识。"); b.Property("SkuCode") @@ -4282,20 +4398,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(32)") .HasComment("SKU 编码。"); + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + b.Property("StockQuantity") .HasColumnType("integer") .HasComment("可售库存。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Weight") @@ -4316,11 +4436,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CalledAt") .HasColumnType("timestamp with time zone") .HasComment("叫号时间。"); @@ -4333,16 +4455,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EstimatedWaitMinutes") @@ -4366,11 +4488,11 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("StoreId") - .HasColumnType("uuid"); + b.Property("StoreId") + .HasColumnType("bigint"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("TicketNumber") @@ -4383,8 +4505,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4402,11 +4524,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CancelledAt") .HasColumnType("timestamp with time zone") .HasComment("取消时间。"); @@ -4424,8 +4548,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("CustomerName") @@ -4444,8 +4568,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("PeopleCount") @@ -4471,8 +4595,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店。"); b.Property("TablePreference") @@ -4480,16 +4604,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("桌型/标签。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4507,11 +4631,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -4550,16 +4676,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryRadiusKm") @@ -4589,8 +4715,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("门店负责人姓名。"); - b.Property("MerchantId") - .HasColumnType("uuid") + b.Property("MerchantId") + .HasColumnType("bigint") .HasComment("所属商户标识。"); b.Property("Name") @@ -4637,16 +4763,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("门店标签(逗号分隔)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4664,11 +4790,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CapacityLimit") .HasColumnType("integer") .HasComment("最大接待容量或单量限制。"); @@ -4677,8 +4805,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DayOfWeek") @@ -4689,8 +4817,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndTime") @@ -4710,20 +4838,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("interval") .HasComment("开始时间(本地时间)。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4738,25 +4866,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DeliveryFee") @@ -4778,20 +4908,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("GeoJSON 表示的多边形范围。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("ZoneName") @@ -4812,25 +4946,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EndTime") @@ -4850,28 +4986,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("班次日期。"); - b.Property("StaffId") - .HasColumnType("uuid") + b.Property("StaffId") + .HasColumnType("bigint") .HasComment("员工标识。"); b.Property("StartTime") .HasColumnType("interval") .HasComment("开始时间。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4887,17 +5023,19 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("Date") @@ -4908,8 +5046,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("IsClosed") @@ -4921,20 +5059,20 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(256)") .HasComment("说明内容。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -4950,13 +5088,15 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); - b.Property("AreaId") - .HasColumnType("uuid") + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AreaId") + .HasColumnType("bigint") .HasComment("所在区域 ID。"); b.Property("Capacity") @@ -4967,16 +5107,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("QrCodeUrl") @@ -4988,8 +5128,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前桌台状态。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); b.Property("TableCode") @@ -5003,16 +5143,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("桌台标签(堂食、快餐等)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5028,25 +5168,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -5060,20 +5202,24 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(64)") .HasComment("区域名称。"); - b.Property("StoreId") - .HasColumnType("uuid") + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") .HasComment("门店标识。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5089,11 +5235,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Address") .HasColumnType("text") .HasComment("详细地址信息。"); @@ -5135,16 +5283,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EffectiveFrom") @@ -5175,8 +5323,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("character varying(128)") .HasComment("租户全称或品牌名称。"); - b.Property("PrimaryOwnerUserId") - .HasColumnType("uuid") + b.Property("PrimaryOwnerUserId") + .HasColumnType("bigint") .HasComment("系统内对应的租户所有者账号 ID。"); b.Property("Province") @@ -5213,8 +5361,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("Website") @@ -5234,11 +5382,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AmountDue") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)") @@ -5253,16 +5403,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("DueDate") @@ -5291,16 +5441,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("当前付款状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5316,11 +5466,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Channel") .HasColumnType("integer") .HasComment("发布通道(站内、邮件、短信等)。"); @@ -5329,16 +5481,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Message") @@ -5363,8 +5515,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("integer") .HasComment("通知重要级别。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("Title") @@ -5377,8 +5529,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); @@ -5393,25 +5545,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("Description") @@ -5465,8 +5619,8 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("YearlyPrice") @@ -5483,25 +5637,27 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("LastResetAt") @@ -5520,16 +5676,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("配额刷新周期描述(如月、年)。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.Property("UsedValue") @@ -5549,11 +5705,13 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") + .HasColumnType("bigint") .HasComment("实体唯一标识。"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AutoRenew") .HasColumnType("boolean") .HasComment("是否开启自动续费。"); @@ -5562,16 +5720,16 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); - b.Property("CreatedBy") - .HasColumnType("uuid") + b.Property("CreatedBy") + .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); - b.Property("DeletedBy") - .HasColumnType("uuid") + b.Property("DeletedBy") + .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); b.Property("EffectiveFrom") @@ -5590,28 +5748,28 @@ namespace TakeoutSaaS.Infrastructure.App.Migrations .HasColumnType("text") .HasComment("运营备注信息。"); - b.Property("ScheduledPackageId") - .HasColumnType("uuid") + b.Property("ScheduledPackageId") + .HasColumnType("bigint") .HasComment("若已排期升降配,对应的新套餐 ID。"); b.Property("Status") .HasColumnType("integer") .HasComment("订阅当前状态。"); - b.Property("TenantId") - .HasColumnType("uuid") + b.Property("TenantId") + .HasColumnType("bigint") .HasComment("所属租户 ID。"); - b.Property("TenantPackageId") - .HasColumnType("uuid") + b.Property("TenantPackageId") + .HasColumnType("bigint") .HasComment("当前订阅关联的套餐标识。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - b.Property("UpdatedBy") - .HasColumnType("uuid") + b.Property("UpdatedBy") + .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); b.HasKey("Id"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index 90cd078..4661491 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj @@ -23,5 +23,6 @@ + diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs index 1adf1d1..7faff05 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs @@ -19,6 +19,6 @@ public sealed class TenantProvider : ITenantProvider } /// - public Guid GetCurrentTenantId() - => _tenantContextAccessor.Current?.TenantId ?? Guid.Empty; + public long GetCurrentTenantId() + => _tenantContextAccessor.Current?.TenantId ?? 0; } diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs index e7a2c2f..a1066c1 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs @@ -107,7 +107,7 @@ public sealed class TenantResolutionMiddleware // 1. Header 中的租户 ID if (!string.IsNullOrWhiteSpace(options.TenantIdHeaderName) && request.Headers.TryGetValue(options.TenantIdHeaderName, out var tenantHeader) && - Guid.TryParse(tenantHeader.FirstOrDefault(), out var headerTenantId)) + long.TryParse(tenantHeader.FirstOrDefault(), out var headerTenantId)) { return new TenantContext(headerTenantId, null, $"header:{options.TenantIdHeaderName}"); } @@ -141,7 +141,7 @@ public sealed class TenantResolutionMiddleware // 4. Token Claim var claim = context.User?.FindFirst("tenant_id"); - if (claim != null && Guid.TryParse(claim.Value, out var claimTenant)) + if (claim != null && long.TryParse(claim.Value, out var claimTenant)) { return new TenantContext(claimTenant, null, "claim:tenant_id"); } @@ -149,9 +149,9 @@ public sealed class TenantResolutionMiddleware return TenantContext.Empty; } - private static bool TryResolveByCode(string? code, TenantResolutionOptions options, out Guid tenantId) + private static bool TryResolveByCode(string? code, TenantResolutionOptions options, out long tenantId) { - tenantId = Guid.Empty; + tenantId = 0; if (string.IsNullOrWhiteSpace(code)) { return false; diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs index 8e0c5ff..7cd928d 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs @@ -20,14 +20,14 @@ public sealed class TenantResolutionOptions /// /// 明确指定 host 与租户 ID 对应关系的映射表(精确匹配)。 /// - public IDictionary DomainTenantMap { get; set; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); + public IDictionary DomainTenantMap { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// 租户编码到租户 ID 的映射表,用于 header 或子域名解析。 /// - public IDictionary CodeTenantMap { get; set; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); + public IDictionary CodeTenantMap { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// 根域(不含子域),用于形如 {tenant}.rootDomain 的场景,例如 admin.takeoutsaas.com。 @@ -47,10 +47,10 @@ public sealed class TenantResolutionOptions /// /// 对外只读视图,便于审计日志输出。 /// - public IReadOnlyDictionary DomainMappings => new ReadOnlyDictionary(DomainTenantMap); + public IReadOnlyDictionary DomainMappings => new ReadOnlyDictionary(DomainTenantMap); /// /// 对外只读的编码映射。 /// - public IReadOnlyDictionary CodeMappings => new ReadOnlyDictionary(CodeTenantMap); + public IReadOnlyDictionary CodeMappings => new ReadOnlyDictionary(CodeTenantMap); } From ffc4f0885f628beae56363350df5983790c40e1b Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 09:10:30 +0800 Subject: [PATCH 20/56] chore: split seed config to dedicated file --- Document/13_AppSeed说明.md | 80 +++++++++++-------- src/Api/TakeoutSaaS.AdminApi/Program.cs | 4 + .../appsettings.Development.json | 34 -------- .../appsettings.Seed.Development.json | 36 +++++++++ 4 files changed, 85 insertions(+), 69 deletions(-) create mode 100644 src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json diff --git a/Document/13_AppSeed说明.md b/Document/13_AppSeed说明.md index 8ab02e7..95e2d0e 100644 --- a/Document/13_AppSeed说明.md +++ b/Document/13_AppSeed说明.md @@ -1,52 +1,62 @@ # App 数据种子使用说明(App:Seed) - > 作用:在启动时自动创建默认租户与基础字典,便于本地/测试环境快速落地必备数据。由 `AppDataSeeder` 执行,支持幂等多次运行。 ## 配置入口 -- 文件位置:`appsettings.{Environment}.json`(示例已写入 AdminApi 的 Development 配置)。 -- 配置节:`App:Seed`。 +- 文件位置:`appsettings.Seed.{Environment}.json`(AdminApi 下新增独立种子文件,示例已写入 Development) +- 配置节:`App:Seed` -示例(已写入 `src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json`): +示例(已写入 `src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json`): ```json -"App": { - "Seed": { - "Enabled": true, - "DefaultTenant": { - "TenantId": 1000000000001, - "Code": "demo", - "Name": "Demo租户", - "ShortName": "Demo", - "ContactName": "DemoAdmin", - "ContactPhone": "13800000000" - }, - "DictionaryGroups": [ - { - "Code": "order_status", - "Name": "订单状态", - "Scope": "Business", - "Items": [ - { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, - { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, - { "Key": "finished", "Value": "已完成", "SortOrder": 30 } - ] - } - ] +{ + "App": { + "Seed": { + "Enabled": true, + "DefaultTenant": { + "TenantId": 1000000000001, + "Code": "demo", + "Name": "Demo租户", + "ShortName": "Demo", + "ContactName": "DemoAdmin", + "ContactPhone": "13800000000" + }, + "DictionaryGroups": [ + { + "Code": "order_status", + "Name": "订单状态", + "Scope": "Business", + "Items": [ + { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, + { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, + { "Key": "finished", "Value": "已完成", "SortOrder": 30 } + ] + }, + { + "Code": "store_tags", + "Name": "门店标签", + "Scope": "Business", + "Items": [ + { "Key": "hot", "Value": "热门", "SortOrder": 10 }, + { "Key": "new", "Value": "新店", "SortOrder": 20 } + ] + } + ] + } } } ``` -字段说明: -- `Enabled`: 是否启用种子。 -- `DefaultTenant`: 默认租户(使用雪花 long ID,0 表示让雪花生成)。 -- `DictionaryGroups`: 基础字典,`Scope` 可选 `System`/`Business`,`Items` 支持重复执行更新。 +## 字段说明 +- `Enabled`: 是否启用种子 +- `DefaultTenant`: 默认租户(使用雪花 long ID;0 表示让雪花生成) +- `DictionaryGroups`: 基础字典,`Scope` 可选 `System`/`Business`,`Items` 支持幂等运行更新 ## 运行方式 -1. 确保 Admin API 已调用 `AddAppInfrastructure`(已在 Program.cs 中注册,会启动 `AppDataSeeder`)。 -2. 修改 `appsettings.{Environment}.json` 的 `App:Seed` 后,启动 Admin API,即会自动执行种子逻辑(幂等)。 +1. 确保 Admin API 已调用 `AddAppInfrastructure`(Program.cs 已注册,会启用 `AppDataSeeder`)。 +2. 修改 `appsettings.Seed.{Environment}.json` 的 `App:Seed` 后,启动 Admin API,即会自动执行种子逻辑(幂等)。 3. 查看日志:`AppSeed` 前缀会输出创建/更新结果。 ## 注意事项 -- ID 必须为 long(雪花),不要再使用 Guid/自增。 +- ID 必须用 long(雪花),不要再使用 Guid/自增。 - 系统租户使用 `TenantId = 0`;业务租户请填写实际雪花 ID。 - 字典分组编码需唯一;重复运行会按编码合并更新。 -- 生产环境请按需开启 `Enabled`,避免误写入。 +- 生产环境请按需开启 `Enabled`,避免误写入。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 14eb59d..1cdf937 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -25,6 +25,10 @@ using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); +builder.Configuration + .AddJsonFile("appsettings.Seed.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.Seed.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true); + builder.Host.UseSerilog((context, _, configuration) => { configuration diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index 01c5725..4eb2c0e 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -149,39 +149,5 @@ "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" - }, - "App": { - "Seed": { - "Enabled": true, - "DefaultTenant": { - "TenantId": 1000000000001, - "Code": "demo", - "Name": "Demo租户", - "ShortName": "Demo", - "ContactName": "DemoAdmin", - "ContactPhone": "13800000000" - }, - "DictionaryGroups": [ - { - "Code": "order_status", - "Name": "订单状态", - "Scope": "Business", - "Items": [ - { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, - { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, - { "Key": "finished", "Value": "已完成", "SortOrder": 30 } - ] - }, - { - "Code": "store_tags", - "Name": "门店标签", - "Scope": "Business", - "Items": [ - { "Key": "hot", "Value": "热门", "SortOrder": 10 }, - { "Key": "new", "Value": "新店", "SortOrder": 20 } - ] - } - ] - } } } diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json new file mode 100644 index 0000000..4a20620 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json @@ -0,0 +1,36 @@ +{ + "App": { + "Seed": { + "Enabled": true, + "DefaultTenant": { + "TenantId": 1000000000001, + "Code": "demo", + "Name": "Demo租户", + "ShortName": "Demo", + "ContactName": "DemoAdmin", + "ContactPhone": "13800000000" + }, + "DictionaryGroups": [ + { + "Code": "order_status", + "Name": "订单状态", + "Scope": "Business", + "Items": [ + { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, + { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, + { "Key": "finished", "Value": "已完成", "SortOrder": 30 } + ] + }, + { + "Code": "store_tags", + "Name": "门店标签", + "Scope": "Business", + "Items": [ + { "Key": "hot", "Value": "热门", "SortOrder": 10 }, + { "Key": "new", "Value": "新店", "SortOrder": 20 } + ] + } + ] + } + } +} From 1a014542669202fa87845da9d64af51585547668 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 09:46:44 +0800 Subject: [PATCH 21/56] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E4=BB=93?= =?UTF-8?q?=E5=82=A8CRUD=E4=B8=8E=E7=A7=8D=E5=AD=90=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appsettings.Seed.Development.json | 18 ++ .../Repositories/IDeliveryRepository.cs | 10 + .../Repositories/IMerchantRepository.cs | 10 + .../Orders/Repositories/IOrderRepository.cs | 10 + .../Repositories/IPaymentRepository.cs | 10 + .../Repositories/IProductRepository.cs | 45 +++++ .../Stores/Repositories/IStoreRepository.cs | 10 + .../App/Options/AppSeedOptions.cs | 5 + .../App/Options/SystemParameterSeedOptions.cs | 37 ++++ .../App/Persistence/AppDataSeeder.cs | 180 ++++++++++++------ .../App/Repositories/EfDeliveryRepository.cs | 31 +++ .../App/Repositories/EfMerchantRepository.cs | 22 +++ .../App/Repositories/EfOrderRepository.cs | 45 +++++ .../App/Repositories/EfPaymentRepository.cs | 29 +++ .../App/Repositories/EfProductRepository.cs | 159 ++++++++++++++++ .../App/Repositories/EfStoreRepository.cs | 22 +++ 16 files changed, 587 insertions(+), 56 deletions(-) create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/SystemParameterSeedOptions.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json index 4a20620..3079e81 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json @@ -30,6 +30,24 @@ { "Key": "new", "Value": "新店", "SortOrder": 20 } ] } + ], + "SystemParameters": [ + { "Key": "site_name", "Value": "外卖SaaS Demo", "Description": "演示环境站点名称", "SortOrder": 10, "IsEnabled": true }, + { "Key": "order_auto_cancel_minutes", "Value": "30", "Description": "待支付自动取消时间(分钟)", "SortOrder": 20, "IsEnabled": true } + ] + } + }, + "Identity": { + "AdminSeed": { + "Users": [ + { + "Account": "admin", + "DisplayName": "平台管理员", + "Password": "Admin@123456", + "TenantId": 1000000000001, + "Roles": [ "PlatformAdmin" ], + "Permissions": [ "merchant:*", "store:*", "product:*", "order:*", "payment:*", "delivery:*" ] + } ] } } diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs index 010793d..d0a2132 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs @@ -39,4 +39,14 @@ public interface IDeliveryRepository /// 持久化变更。 /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 更新配送单。 + /// + Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default); + + /// + /// 删除配送单及事件。 + /// + Task DeleteDeliveryOrderAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs index a735ff7..9e8710a 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs @@ -60,4 +60,14 @@ public interface IMerchantRepository /// 持久化变更。 /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 更新商户信息。 + /// + Task UpdateMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default); + + /// + /// 删除商户。 + /// + Task DeleteMerchantAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs index baba30b..d48ecf3 100644 --- a/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Orders/Repositories/IOrderRepository.cs @@ -66,4 +66,14 @@ public interface IOrderRepository /// 持久化变更。 /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 更新订单。 + /// + Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default); + + /// + /// 删除订单。 + /// + Task DeleteOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs index c217ce2..ec286b5 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs @@ -39,4 +39,14 @@ public interface IPaymentRepository /// 持久化变更。 /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 更新支付记录。 + /// + Task UpdatePaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default); + + /// + /// 删除支付记录及关联退款。 + /// + Task DeletePaymentAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs b/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs index 093ee81..6f01804 100644 --- a/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Products/Repositories/IProductRepository.cs @@ -100,4 +100,49 @@ public interface IProductRepository /// 持久化变更。 /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 更新商品。 + /// + Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default); + + /// + /// 删除商品。 + /// + Task DeleteProductAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 更新分类。 + /// + Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default); + + /// + /// 删除分类。 + /// + Task DeleteCategoryAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 删除商品下的 SKU。 + /// + Task RemoveSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 删除商品下的加料组及选项。 + /// + Task RemoveAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 删除商品下的规格组及选项。 + /// + Task RemoveAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 删除商品媒资。 + /// + Task RemoveMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 删除商品定价规则。 + /// + Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs index dccde79..bbbf7e0 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs @@ -90,4 +90,14 @@ public interface IStoreRepository /// 持久化变更。 /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 更新门店。 + /// + Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default); + + /// + /// 删除门店。 + /// + Task DeleteStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs index d5992f7..b3ec66d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/AppSeedOptions.cs @@ -26,4 +26,9 @@ public sealed class AppSeedOptions /// 基础字典分组。 /// public List DictionaryGroups { get; set; } = new(); + + /// + /// 系统参数配置。 + /// + public List SystemParameters { get; set; } = new(); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/SystemParameterSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/SystemParameterSeedOptions.cs new file mode 100644 index 0000000..a9df90b --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Options/SystemParameterSeedOptions.cs @@ -0,0 +1,37 @@ +namespace TakeoutSaaS.Infrastructure.App.Options; + +/// +/// 系统参数种子配置项。 +/// +public sealed class SystemParameterSeedOptions +{ + /// + /// 目标租户,null 时使用默认租户或 0。 + /// + public long? TenantId { get; set; } + + /// + /// 参数键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 参数值。 + /// + public string Value { get; set; } = string.Empty; + + /// + /// 说明。 + /// + public string? Description { get; set; } + + /// + /// 排序。 + /// + public int SortOrder { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs index eede6a4..e9a70d7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs @@ -1,9 +1,11 @@ +using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.Dictionary.Enums; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Infrastructure.App.Options; @@ -142,81 +144,147 @@ public sealed class AppDataSeeder : IHostedService /// private async Task EnsureDictionarySeedsAsync(DictionaryDbContext dbContext, long? defaultTenantId, CancellationToken cancellationToken) { - if (_options.DictionaryGroups == null || _options.DictionaryGroups.Count == 0) + var dictionaryGroups = _options.DictionaryGroups ?? new List(); + var hasDictionaryGroups = dictionaryGroups.Count > 0; + + if (!hasDictionaryGroups) { _logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子"); + } + + if (hasDictionaryGroups) + { + foreach (var groupOptions in dictionaryGroups) + { + if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name)) + { + _logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空"); + continue; + } + + var tenantId = groupOptions.TenantId ?? defaultTenantId ?? 0; + var code = groupOptions.Code.Trim(); + + var group = await dbContext.DictionaryGroups + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Code == code, cancellationToken); + + if (group == null) + { + group = new DictionaryGroup + { + Id = 0, + TenantId = tenantId, + Code = code, + Name = groupOptions.Name.Trim(), + Scope = groupOptions.Scope, + Description = groupOptions.Description?.Trim(), + IsEnabled = groupOptions.IsEnabled + }; + + await dbContext.DictionaryGroups.AddAsync(group, cancellationToken); + _logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId); + } + else + { + var groupUpdated = false; + + if (!string.Equals(group.Name, groupOptions.Name, StringComparison.Ordinal)) + { + group.Name = groupOptions.Name.Trim(); + groupUpdated = true; + } + + if (!string.Equals(group.Description, groupOptions.Description, StringComparison.Ordinal)) + { + group.Description = groupOptions.Description?.Trim(); + groupUpdated = true; + } + + if (group.Scope != groupOptions.Scope) + { + group.Scope = groupOptions.Scope; + groupUpdated = true; + } + + if (group.IsEnabled != groupOptions.IsEnabled) + { + group.IsEnabled = groupOptions.IsEnabled; + groupUpdated = true; + } + + if (groupUpdated) + { + dbContext.DictionaryGroups.Update(group); + } + } + + await UpsertDictionaryItemsAsync(dbContext, group, groupOptions.Items, tenantId, cancellationToken); + } + } + + await EnsureSystemParametersAsync(dbContext, defaultTenantId, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + } + + /// + /// 确保系统参数以字典形式幂等种子。 + /// + private async Task EnsureSystemParametersAsync(DictionaryDbContext dbContext, long? defaultTenantId, CancellationToken cancellationToken) + { + var systemParameters = _options.SystemParameters ?? new List(); + + if (systemParameters.Count == 0) + { + _logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子"); return; } - foreach (var groupOptions in _options.DictionaryGroups) + var grouped = systemParameters + .Where(x => !string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Value)) + .GroupBy(x => x.TenantId ?? defaultTenantId ?? 0); + + if (!grouped.Any()) { - if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name)) - { - _logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空"); - continue; - } + _logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子"); + return; + } - var tenantId = groupOptions.TenantId ?? defaultTenantId ?? 0; - var code = groupOptions.Code.Trim(); - - var group = await dbContext.DictionaryGroups + foreach (var group in grouped) + { + var tenantId = group.Key; + var dictionaryGroup = await dbContext.DictionaryGroups .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Code == code, cancellationToken); + .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Code == "system_parameters", cancellationToken); - if (group == null) + if (dictionaryGroup == null) { - group = new DictionaryGroup + dictionaryGroup = new DictionaryGroup { Id = 0, TenantId = tenantId, - Code = code, - Name = groupOptions.Name.Trim(), - Scope = groupOptions.Scope, - Description = groupOptions.Description?.Trim(), - IsEnabled = groupOptions.IsEnabled + Code = "system_parameters", + Name = "系统参数", + Scope = tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business, + Description = "系统参数配置", + IsEnabled = true }; - await dbContext.DictionaryGroups.AddAsync(group, cancellationToken); - _logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId); + await dbContext.DictionaryGroups.AddAsync(dictionaryGroup, cancellationToken); + _logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId); } - else + + var seedItems = group.Select(x => new DictionarySeedItemOptions { - var groupUpdated = false; + Key = x.Key.Trim(), + Value = x.Value.Trim(), + Description = x.Description?.Trim(), + SortOrder = x.SortOrder, + IsEnabled = x.IsEnabled + }); - if (!string.Equals(group.Name, groupOptions.Name, StringComparison.Ordinal)) - { - group.Name = groupOptions.Name.Trim(); - groupUpdated = true; - } - - if (!string.Equals(group.Description, groupOptions.Description, StringComparison.Ordinal)) - { - group.Description = groupOptions.Description?.Trim(); - groupUpdated = true; - } - - if (group.Scope != groupOptions.Scope) - { - group.Scope = groupOptions.Scope; - groupUpdated = true; - } - - if (group.IsEnabled != groupOptions.IsEnabled) - { - group.IsEnabled = groupOptions.IsEnabled; - groupUpdated = true; - } - - if (groupUpdated) - { - dbContext.DictionaryGroups.Update(group); - } - } - - await UpsertDictionaryItemsAsync(dbContext, group, groupOptions.Items, tenantId, cancellationToken); + await UpsertDictionaryItemsAsync(dbContext, dictionaryGroup, seedItems, tenantId, cancellationToken); } - - await dbContext.SaveChangesAsync(cancellationToken); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs index 45db052..5c408ef 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs @@ -68,4 +68,35 @@ public sealed class EfDeliveryRepository : IDeliveryRepository { return _context.SaveChangesAsync(cancellationToken); } + + /// + public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) + { + _context.DeliveryOrders.Update(deliveryOrder); + return Task.CompletedTask; + } + + /// + public async Task DeleteDeliveryOrderAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) + { + var events = await _context.DeliveryEvents + .Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId) + .ToListAsync(cancellationToken); + + if (events.Count > 0) + { + _context.DeliveryEvents.RemoveRange(events); + } + + var existing = await _context.DeliveryOrders + .Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing == null) + { + return; + } + + _context.DeliveryOrders.Remove(existing); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs index a17b212..3aaa2c5 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs @@ -113,4 +113,26 @@ public sealed class EfMerchantRepository : IMerchantRepository { return _context.SaveChangesAsync(cancellationToken); } + + /// + public Task UpdateMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default) + { + _context.Merchants.Update(merchant); + return Task.CompletedTask; + } + + /// + public async Task DeleteMerchantAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await _context.Merchants + .Where(x => x.TenantId == tenantId && x.Id == merchantId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing == null) + { + return; + } + + _context.Merchants.Remove(existing); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs index 9517ef9..91aa04f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs @@ -130,4 +130,49 @@ public sealed class EfOrderRepository : IOrderRepository { return _context.SaveChangesAsync(cancellationToken); } + + /// + public Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default) + { + _context.Orders.Update(order); + return Task.CompletedTask; + } + + /// + public async Task DeleteOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) + { + var items = await _context.OrderItems + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .ToListAsync(cancellationToken); + if (items.Count > 0) + { + _context.OrderItems.RemoveRange(items); + } + + var histories = await _context.OrderStatusHistories + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .ToListAsync(cancellationToken); + if (histories.Count > 0) + { + _context.OrderStatusHistories.RemoveRange(histories); + } + + var refunds = await _context.RefundRequests + .Where(x => x.TenantId == tenantId && x.OrderId == orderId) + .ToListAsync(cancellationToken); + if (refunds.Count > 0) + { + _context.RefundRequests.RemoveRange(refunds); + } + + var existing = await _context.Orders + .Where(x => x.TenantId == tenantId && x.Id == orderId) + .FirstOrDefaultAsync(cancellationToken); + if (existing == null) + { + return; + } + + _context.Orders.Remove(existing); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs index af5034d..8ea2124 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs @@ -68,4 +68,33 @@ public sealed class EfPaymentRepository : IPaymentRepository { return _context.SaveChangesAsync(cancellationToken); } + + /// + public Task UpdatePaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default) + { + _context.PaymentRecords.Update(payment); + return Task.CompletedTask; + } + + /// + public async Task DeletePaymentAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) + { + var refunds = await _context.PaymentRefundRecords + .Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId) + .ToListAsync(cancellationToken); + if (refunds.Count > 0) + { + _context.PaymentRefundRecords.RemoveRange(refunds); + } + + var existing = await _context.PaymentRecords + .Where(x => x.TenantId == tenantId && x.Id == paymentId) + .FirstOrDefaultAsync(cancellationToken); + if (existing == null) + { + return; + } + + _context.PaymentRecords.Remove(existing); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs index fde29fe..234a367 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs @@ -224,4 +224,163 @@ public sealed class EfProductRepository : IProductRepository { return _context.SaveChangesAsync(cancellationToken); } + + /// + public Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default) + { + _context.Products.Update(product); + return Task.CompletedTask; + } + + /// + public async Task DeleteProductAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + await RemovePricingRulesAsync(productId, tenantId, cancellationToken); + await RemoveMediaAssetsAsync(productId, tenantId, cancellationToken); + await RemoveAttributeGroupsAsync(productId, tenantId, cancellationToken); + await RemoveAddonGroupsAsync(productId, tenantId, cancellationToken); + await RemoveSkusAsync(productId, tenantId, cancellationToken); + + var existing = await _context.Products + .Where(x => x.TenantId == tenantId && x.Id == productId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing == null) + { + return; + } + + _context.Products.Remove(existing); + } + + /// + public Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default) + { + _context.ProductCategories.Update(category); + return Task.CompletedTask; + } + + /// + public async Task DeleteCategoryAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await _context.ProductCategories + .Where(x => x.TenantId == tenantId && x.Id == categoryId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing == null) + { + return; + } + + _context.ProductCategories.Remove(existing); + } + + /// + public async Task RemoveSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var skus = await _context.ProductSkus + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .ToListAsync(cancellationToken); + + if (skus.Count == 0) + { + return; + } + + _context.ProductSkus.RemoveRange(skus); + } + + /// + public async Task RemoveAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var groupIds = await _context.ProductAddonGroups + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .Select(x => x.Id) + .ToListAsync(cancellationToken); + + if (groupIds.Count == 0) + { + return; + } + + var options = await _context.ProductAddonOptions + .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId)) + .ToListAsync(cancellationToken); + + if (options.Count > 0) + { + _context.ProductAddonOptions.RemoveRange(options); + } + + var groups = await _context.ProductAddonGroups + .Where(x => groupIds.Contains(x.Id)) + .ToListAsync(cancellationToken); + + if (groups.Count > 0) + { + _context.ProductAddonGroups.RemoveRange(groups); + } + } + + /// + public async Task RemoveAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var groupIds = await _context.ProductAttributeGroups + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .Select(x => x.Id) + .ToListAsync(cancellationToken); + + if (groupIds.Count == 0) + { + return; + } + + var options = await _context.ProductAttributeOptions + .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId)) + .ToListAsync(cancellationToken); + + if (options.Count > 0) + { + _context.ProductAttributeOptions.RemoveRange(options); + } + + var groups = await _context.ProductAttributeGroups + .Where(x => groupIds.Contains(x.Id)) + .ToListAsync(cancellationToken); + + if (groups.Count > 0) + { + _context.ProductAttributeGroups.RemoveRange(groups); + } + } + + /// + public async Task RemoveMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var assets = await _context.ProductMediaAssets + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .ToListAsync(cancellationToken); + + if (assets.Count == 0) + { + return; + } + + _context.ProductMediaAssets.RemoveRange(assets); + } + + /// + public async Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default) + { + var rules = await _context.ProductPricingRules + .Where(x => x.TenantId == tenantId && x.ProductId == productId) + .ToListAsync(cancellationToken); + + if (rules.Count == 0) + { + return; + } + + _context.ProductPricingRules.RemoveRange(rules); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs index 120f1d0..4ed2412 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -171,4 +171,26 @@ public sealed class EfStoreRepository : IStoreRepository { return _context.SaveChangesAsync(cancellationToken); } + + /// + public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default) + { + _context.Stores.Update(store); + return Task.CompletedTask; + } + + /// + public async Task DeleteStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await _context.Stores + .Where(x => x.TenantId == tenantId && x.Id == storeId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing == null) + { + return; + } + + _context.Stores.Remove(existing); + } } From 93141fbf0cdd0cedc359c5cfe81c256c53818cdb Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 10:19:35 +0800 Subject: [PATCH 22/56] =?UTF-8?q?feat:=20=E7=AE=A1=E7=90=86=E7=AB=AF?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=AE=9E=E4=BD=93CRUD=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DeliveriesController.cs | 108 ++++++++++++ .../Controllers/MerchantsController.cs | 32 ++++ .../Controllers/OrdersController.cs | 116 +++++++++++++ .../Controllers/PaymentsController.cs | 108 ++++++++++++ .../Controllers/ProductsController.cs | 109 ++++++++++++ .../Controllers/StoresController.cs | 108 ++++++++++++ .../Commands/CreateDeliveryOrderCommand.cs | 66 ++++++++ .../Commands/DeleteDeliveryOrderCommand.cs | 14 ++ .../Commands/UpdateDeliveryOrderCommand.cs | 71 ++++++++ .../App/Deliveries/Dto/DeliveryEventDto.cs | 43 +++++ .../App/Deliveries/Dto/DeliveryOrderDto.cs | 84 ++++++++++ .../CreateDeliveryOrderCommandHandler.cs | 69 ++++++++ .../DeleteDeliveryOrderCommandHandler.cs | 38 +++++ .../GetDeliveryOrderByIdQueryHandler.cs | 60 +++++++ .../SearchDeliveryOrdersQueryHandler.cs | 43 +++++ .../UpdateDeliveryOrderCommandHandler.cs | 79 +++++++++ .../Queries/GetDeliveryOrderByIdQuery.cs | 15 ++ .../Queries/SearchDeliveryOrdersQuery.cs | 21 +++ .../Commands/DeleteMerchantCommand.cs | 14 ++ .../Commands/UpdateMerchantCommand.cs | 51 ++++++ .../Handlers/DeleteMerchantCommandHandler.cs | 40 +++++ .../Handlers/UpdateMerchantCommandHandler.cs | 65 ++++++++ .../App/Orders/Commands/CreateOrderCommand.cs | 117 +++++++++++++ .../App/Orders/Commands/DeleteOrderCommand.cs | 14 ++ .../App/Orders/Commands/OrderItemRequest.cs | 52 ++++++ .../App/Orders/Commands/UpdateOrderCommand.cs | 117 +++++++++++++ .../App/Orders/Dto/OrderDto.cs | 141 ++++++++++++++++ .../App/Orders/Dto/OrderItemDto.cs | 68 ++++++++ .../App/Orders/Dto/OrderStatusHistoryDto.cs | 44 +++++ .../App/Orders/Dto/RefundRequestDto.cs | 58 +++++++ .../Handlers/CreateOrderCommandHandler.cs | 156 ++++++++++++++++++ .../Handlers/DeleteOrderCommandHandler.cs | 40 +++++ .../Handlers/GetOrderByIdQueryHandler.cs | 102 ++++++++++++ .../Handlers/SearchOrdersQueryHandler.cs | 65 ++++++++ .../Handlers/UpdateOrderCommandHandler.cs | 134 +++++++++++++++ .../App/Orders/Queries/GetOrderByIdQuery.cs | 15 ++ .../App/Orders/Queries/SearchOrdersQuery.cs | 32 ++++ .../Payments/Commands/CreatePaymentCommand.cs | 56 +++++++ .../Payments/Commands/DeletePaymentCommand.cs | 14 ++ .../Payments/Commands/UpdatePaymentCommand.cs | 61 +++++++ .../App/Payments/Dto/PaymentDto.cs | 74 +++++++++ .../App/Payments/Dto/PaymentRefundDto.cs | 49 ++++++ .../Handlers/CreatePaymentCommandHandler.cs | 66 ++++++++ .../Handlers/DeletePaymentCommandHandler.cs | 38 +++++ .../Handlers/GetPaymentByIdQueryHandler.cs | 59 +++++++ .../Handlers/SearchPaymentsQueryHandler.cs | 46 ++++++ .../Handlers/UpdatePaymentCommandHandler.cs | 76 +++++++++ .../Payments/Queries/GetPaymentByIdQuery.cs | 15 ++ .../Payments/Queries/SearchPaymentsQuery.cs | 21 +++ .../Products/Commands/CreateProductCommand.cs | 101 ++++++++++++ .../Products/Commands/DeleteProductCommand.cs | 14 ++ .../Products/Commands/UpdateProductCommand.cs | 106 ++++++++++++ .../App/Products/Dto/ProductDto.cs | 115 +++++++++++++ .../Handlers/CreateProductCommandHandler.cs | 77 +++++++++ .../Handlers/DeleteProductCommandHandler.cs | 40 +++++ .../Handlers/GetProductByIdQueryHandler.cs | 52 ++++++ .../Handlers/SearchProductsQueryHandler.cs | 57 +++++++ .../Handlers/UpdateProductCommandHandler.cs | 87 ++++++++++ .../Products/Queries/GetProductByIdQuery.cs | 15 ++ .../Products/Queries/SearchProductsQuery.cs | 26 +++ .../App/Stores/Commands/CreateStoreCommand.cs | 101 ++++++++++++ .../App/Stores/Commands/DeleteStoreCommand.cs | 14 ++ .../App/Stores/Commands/UpdateStoreCommand.cs | 106 ++++++++++++ .../App/Stores/Dto/StoreDto.cs | 114 +++++++++++++ .../Handlers/CreateStoreCommandHandler.cs | 77 +++++++++ .../Handlers/DeleteStoreCommandHandler.cs | 40 +++++ .../Handlers/GetStoreByIdQueryHandler.cs | 52 ++++++ .../Handlers/SearchStoresQueryHandler.cs | 59 +++++++ .../Handlers/UpdateStoreCommandHandler.cs | 87 ++++++++++ .../App/Stores/Queries/GetStoreByIdQuery.cs | 15 ++ .../App/Stores/Queries/SearchStoresQuery.cs | 21 +++ .../Repositories/IDeliveryRepository.cs | 6 + .../Repositories/IPaymentRepository.cs | 6 + .../App/Repositories/EfDeliveryRepository.cs | 23 +++ .../App/Repositories/EfPaymentRepository.cs | 18 ++ 75 files changed, 4513 insertions(+) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs new file mode 100644 index 0000000..a36e2e2 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs @@ -0,0 +1,108 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Application.App.Deliveries.Queries; +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 配送单管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/deliveries")] +public sealed class DeliveriesController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public DeliveriesController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建配送单。 + /// + [HttpPost] + [PermissionAuthorize("delivery:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateDeliveryOrderCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询配送单列表。 + /// + [HttpGet] + [PermissionAuthorize("delivery:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? orderId, [FromQuery] DeliveryStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchDeliveryOrdersQuery + { + OrderId = orderId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取配送单详情。 + /// + [HttpGet("{deliveryOrderId:long}")] + [PermissionAuthorize("delivery:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long deliveryOrderId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetDeliveryOrderByIdQuery { DeliveryOrderId = deliveryOrderId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新配送单。 + /// + [HttpPut("{deliveryOrderId:long}")] + [PermissionAuthorize("delivery:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long deliveryOrderId, [FromBody] UpdateDeliveryOrderCommand command, CancellationToken cancellationToken) + { + command.DeliveryOrderId = command.DeliveryOrderId == 0 ? deliveryOrderId : command.DeliveryOrderId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除配送单。 + /// + [HttpDelete("{deliveryOrderId:long}")] + [PermissionAuthorize("delivery:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long deliveryOrderId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteDeliveryOrderCommand { DeliveryOrderId = deliveryOrderId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs index cc11ec1..c24ec8d 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs @@ -55,6 +55,38 @@ public sealed class MerchantsController : BaseApiController return ApiResponse>.Ok(result); } + /// + /// 更新商户。 + /// + [HttpPut("{merchantId:long}")] + [PermissionAuthorize("merchant:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken) + { + command.MerchantId = command.MerchantId == 0 ? merchantId : command.MerchantId; + + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商户不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除商户。 + /// + [HttpDelete("{merchantId:long}")] + [PermissionAuthorize("merchant:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long merchantId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteMerchantCommand { MerchantId = merchantId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "商户不存在"); + } + /// /// 获取商户详情。 /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs new file mode 100644 index 0000000..f66b159 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs @@ -0,0 +1,116 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Application.App.Orders.Queries; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 订单管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/orders")] +public sealed class OrdersController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public OrdersController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建订单。 + /// + [HttpPost] + [PermissionAuthorize("order:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateOrderCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询订单列表。 + /// + [HttpGet] + [PermissionAuthorize("order:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + [FromQuery] long? storeId, + [FromQuery] OrderStatus? status, + [FromQuery] PaymentStatus? paymentStatus, + [FromQuery] string? orderNo, + CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchOrdersQuery + { + StoreId = storeId, + Status = status, + PaymentStatus = paymentStatus, + OrderNo = orderNo + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取订单详情。 + /// + [HttpGet("{orderId:long}")] + [PermissionAuthorize("order:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long orderId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetOrderByIdQuery { OrderId = orderId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "订单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新订单。 + /// + [HttpPut("{orderId:long}")] + [PermissionAuthorize("order:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long orderId, [FromBody] UpdateOrderCommand command, CancellationToken cancellationToken) + { + command.OrderId = command.OrderId == 0 ? orderId : command.OrderId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "订单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除订单。 + /// + [HttpDelete("{orderId:long}")] + [PermissionAuthorize("order:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long orderId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteOrderCommand { OrderId = orderId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "订单不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs new file mode 100644 index 0000000..93ca700 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs @@ -0,0 +1,108 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 支付记录管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/payments")] +public sealed class PaymentsController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public PaymentsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建支付记录。 + /// + [HttpPost] + [PermissionAuthorize("payment:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreatePaymentCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询支付记录列表。 + /// + [HttpGet] + [PermissionAuthorize("payment:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? orderId, [FromQuery] PaymentStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchPaymentsQuery + { + OrderId = orderId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取支付记录详情。 + /// + [HttpGet("{paymentId:long}")] + [PermissionAuthorize("payment:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long paymentId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetPaymentByIdQuery { PaymentId = paymentId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新支付记录。 + /// + [HttpPut("{paymentId:long}")] + [PermissionAuthorize("payment:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long paymentId, [FromBody] UpdatePaymentCommand command, CancellationToken cancellationToken) + { + command.PaymentId = command.PaymentId == 0 ? paymentId : command.PaymentId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除支付记录。 + /// + [HttpDelete("{paymentId:long}")] + [PermissionAuthorize("payment:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long paymentId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeletePaymentCommand { PaymentId = paymentId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs new file mode 100644 index 0000000..4c8d42c --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs @@ -0,0 +1,109 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Application.App.Products.Queries; +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 商品管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/products")] +public sealed class ProductsController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public ProductsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建商品。 + /// + [HttpPost] + [PermissionAuthorize("product:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateProductCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询商品列表。 + /// + [HttpGet] + [PermissionAuthorize("product:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? storeId, [FromQuery] long? categoryId, [FromQuery] ProductStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchProductsQuery + { + StoreId = storeId, + CategoryId = categoryId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取商品详情。 + /// + [HttpGet("{productId:long}")] + [PermissionAuthorize("product:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long productId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetProductByIdQuery { ProductId = productId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商品不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新商品。 + /// + [HttpPut("{productId:long}")] + [PermissionAuthorize("product:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long productId, [FromBody] UpdateProductCommand command, CancellationToken cancellationToken) + { + command.ProductId = command.ProductId == 0 ? productId : command.ProductId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商品不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除商品。 + /// + [HttpDelete("{productId:long}")] + [PermissionAuthorize("product:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long productId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteProductCommand { ProductId = productId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "商品不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs new file mode 100644 index 0000000..e485df6 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs @@ -0,0 +1,108 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 门店管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/stores")] +public sealed class StoresController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public StoresController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建门店。 + /// + [HttpPost] + [PermissionAuthorize("store:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateStoreCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询门店列表。 + /// + [HttpGet] + [PermissionAuthorize("store:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? merchantId, [FromQuery] StoreStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchStoresQuery + { + MerchantId = merchantId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取门店详情。 + /// + [HttpGet("{storeId:long}")] + [PermissionAuthorize("store:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long storeId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "门店不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新门店。 + /// + [HttpPut("{storeId:long}")] + [PermissionAuthorize("store:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long storeId, [FromBody] UpdateStoreCommand command, CancellationToken cancellationToken) + { + command.StoreId = command.StoreId == 0 ? storeId : command.StoreId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "门店不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除门店。 + /// + [HttpDelete("{storeId:long}")] + [PermissionAuthorize("store:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long storeId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteStoreCommand { StoreId = storeId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "门店不存在"); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs new file mode 100644 index 0000000..8f5a2b7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs @@ -0,0 +1,66 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Enums; + +namespace TakeoutSaaS.Application.App.Deliveries.Commands; + +/// +/// 创建配送单命令。 +/// +public sealed class CreateDeliveryOrderCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 服务商。 + /// + public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse; + + /// + /// 第三方单号。 + /// + public string? ProviderOrderId { get; set; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending; + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; set; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; set; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; set; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; set; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; set; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs new file mode 100644 index 0000000..b05162d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Deliveries.Commands; + +/// +/// 删除配送单命令。 +/// +public sealed class DeleteDeliveryOrderCommand : IRequest +{ + /// + /// 配送单 ID。 + /// + public long DeliveryOrderId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs new file mode 100644 index 0000000..cf57bbb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs @@ -0,0 +1,71 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Enums; + +namespace TakeoutSaaS.Application.App.Deliveries.Commands; + +/// +/// 更新配送单命令。 +/// +public sealed class UpdateDeliveryOrderCommand : IRequest +{ + /// + /// 配送单 ID。 + /// + public long DeliveryOrderId { get; set; } + + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 服务商。 + /// + public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse; + + /// + /// 第三方单号。 + /// + public string? ProviderOrderId { get; set; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending; + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; set; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; set; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; set; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; set; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; set; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs new file mode 100644 index 0000000..a8d2a7e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Deliveries.Dto; + +/// +/// 配送事件 DTO。 +/// +public sealed class DeliveryEventDto +{ + /// + /// 事件 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 配送单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long DeliveryOrderId { get; init; } + + /// + /// 事件类型。 + /// + public DeliveryEventType EventType { get; init; } + + /// + /// 描述。 + /// + public string? Message { get; init; } + + /// + /// 事件时间。 + /// + public DateTime OccurredAt { get; init; } + + /// + /// 原始载荷。 + /// + public string? Payload { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs new file mode 100644 index 0000000..21fc36c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Deliveries.Dto; + +/// +/// 配送单 DTO。 +/// +public sealed class DeliveryOrderDto +{ + /// + /// 配送单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 配送服务商。 + /// + public DeliveryProvider Provider { get; init; } + + /// + /// 第三方配送单号。 + /// + public string? ProviderOrderId { get; init; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; init; } + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; init; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; init; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; init; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; init; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; init; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; init; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; init; } + + /// + /// 事件列表。 + /// + public IReadOnlyList Events { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs new file mode 100644 index 0000000..8e775e9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs @@ -0,0 +1,69 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 创建配送单命令处理器。 +/// +public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository deliveryRepository, ILogger logger) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateDeliveryOrderCommand request, CancellationToken cancellationToken) + { + var deliveryOrder = new DeliveryOrder + { + OrderId = request.OrderId, + Provider = request.Provider, + ProviderOrderId = request.ProviderOrderId?.Trim(), + Status = request.Status, + DeliveryFee = request.DeliveryFee, + CourierName = request.CourierName?.Trim(), + CourierPhone = request.CourierPhone?.Trim(), + DispatchedAt = request.DispatchedAt, + PickedUpAt = request.PickedUpAt, + DeliveredAt = request.DeliveredAt, + FailureReason = request.FailureReason?.Trim() + }; + + await _deliveryRepository.AddDeliveryOrderAsync(deliveryOrder, cancellationToken); + await _deliveryRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建配送单 {DeliveryOrderId} 对应订单 {OrderId}", deliveryOrder.Id, deliveryOrder.OrderId); + + return MapToDto(deliveryOrder, []); + } + + private static DeliveryOrderDto MapToDto(DeliveryOrder deliveryOrder, IReadOnlyList events) => new() + { + Id = deliveryOrder.Id, + TenantId = deliveryOrder.TenantId, + OrderId = deliveryOrder.OrderId, + Provider = deliveryOrder.Provider, + ProviderOrderId = deliveryOrder.ProviderOrderId, + Status = deliveryOrder.Status, + DeliveryFee = deliveryOrder.DeliveryFee, + CourierName = deliveryOrder.CourierName, + CourierPhone = deliveryOrder.CourierPhone, + DispatchedAt = deliveryOrder.DispatchedAt, + PickedUpAt = deliveryOrder.PickedUpAt, + DeliveredAt = deliveryOrder.DeliveredAt, + FailureReason = deliveryOrder.FailureReason, + Events = events.Select(x => new DeliveryEventDto + { + Id = x.Id, + DeliveryOrderId = x.DeliveryOrderId, + EventType = x.EventType, + Message = x.Message, + OccurredAt = x.OccurredAt, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs new file mode 100644 index 0000000..40974f1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 删除配送单命令处理器。 +/// +public sealed class DeleteDeliveryOrderCommandHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteDeliveryOrderCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + await _deliveryRepository.DeleteDeliveryOrderAsync(request.DeliveryOrderId, tenantId, cancellationToken); + await _deliveryRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除配送单 {DeliveryOrderId}", request.DeliveryOrderId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs new file mode 100644 index 0000000..8d047de --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Application.App.Deliveries.Queries; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 配送单详情查询处理器。 +/// +public sealed class GetDeliveryOrderByIdQueryHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetDeliveryOrderByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var order = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken); + if (order == null) + { + return null; + } + + var events = await _deliveryRepository.GetEventsAsync(order.Id, tenantId, cancellationToken); + return MapToDto(order, events); + } + + private static DeliveryOrderDto MapToDto(DeliveryOrder deliveryOrder, IReadOnlyList events) => new() + { + Id = deliveryOrder.Id, + TenantId = deliveryOrder.TenantId, + OrderId = deliveryOrder.OrderId, + Provider = deliveryOrder.Provider, + ProviderOrderId = deliveryOrder.ProviderOrderId, + Status = deliveryOrder.Status, + DeliveryFee = deliveryOrder.DeliveryFee, + CourierName = deliveryOrder.CourierName, + CourierPhone = deliveryOrder.CourierPhone, + DispatchedAt = deliveryOrder.DispatchedAt, + PickedUpAt = deliveryOrder.PickedUpAt, + DeliveredAt = deliveryOrder.DeliveredAt, + FailureReason = deliveryOrder.FailureReason, + Events = events.Select(x => new DeliveryEventDto + { + Id = x.Id, + DeliveryOrderId = x.DeliveryOrderId, + EventType = x.EventType, + Message = x.Message, + OccurredAt = x.OccurredAt, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs new file mode 100644 index 0000000..5dea6a9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Application.App.Deliveries.Queries; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 配送单列表查询处理器。 +/// +public sealed class SearchDeliveryOrdersQueryHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchDeliveryOrdersQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var orders = await _deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken); + + return orders.Select(order => new DeliveryOrderDto + { + Id = order.Id, + TenantId = order.TenantId, + OrderId = order.OrderId, + Provider = order.Provider, + ProviderOrderId = order.ProviderOrderId, + Status = order.Status, + DeliveryFee = order.DeliveryFee, + CourierName = order.CourierName, + CourierPhone = order.CourierPhone, + DispatchedAt = order.DispatchedAt, + PickedUpAt = order.PickedUpAt, + DeliveredAt = order.DeliveredAt, + FailureReason = order.FailureReason + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs new file mode 100644 index 0000000..424e2a9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs @@ -0,0 +1,79 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 更新配送单命令处理器。 +/// +public sealed class UpdateDeliveryOrderCommandHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateDeliveryOrderCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + existing.OrderId = request.OrderId; + existing.Provider = request.Provider; + existing.ProviderOrderId = request.ProviderOrderId?.Trim(); + existing.Status = request.Status; + existing.DeliveryFee = request.DeliveryFee; + existing.CourierName = request.CourierName?.Trim(); + existing.CourierPhone = request.CourierPhone?.Trim(); + existing.DispatchedAt = request.DispatchedAt; + existing.PickedUpAt = request.PickedUpAt; + existing.DeliveredAt = request.DeliveredAt; + existing.FailureReason = request.FailureReason?.Trim(); + + await _deliveryRepository.UpdateDeliveryOrderAsync(existing, cancellationToken); + await _deliveryRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新配送单 {DeliveryOrderId}", existing.Id); + + var events = await _deliveryRepository.GetEventsAsync(existing.Id, tenantId, cancellationToken); + return MapToDto(existing, events); + } + + private static DeliveryOrderDto MapToDto(DeliveryOrder deliveryOrder, IReadOnlyList events) => new() + { + Id = deliveryOrder.Id, + TenantId = deliveryOrder.TenantId, + OrderId = deliveryOrder.OrderId, + Provider = deliveryOrder.Provider, + ProviderOrderId = deliveryOrder.ProviderOrderId, + Status = deliveryOrder.Status, + DeliveryFee = deliveryOrder.DeliveryFee, + CourierName = deliveryOrder.CourierName, + CourierPhone = deliveryOrder.CourierPhone, + DispatchedAt = deliveryOrder.DispatchedAt, + PickedUpAt = deliveryOrder.PickedUpAt, + DeliveredAt = deliveryOrder.DeliveredAt, + FailureReason = deliveryOrder.FailureReason, + Events = events.Select(x => new DeliveryEventDto + { + Id = x.Id, + DeliveryOrderId = x.DeliveryOrderId, + EventType = x.EventType, + Message = x.Message, + OccurredAt = x.OccurredAt, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs new file mode 100644 index 0000000..0b89cae --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; + +namespace TakeoutSaaS.Application.App.Deliveries.Queries; + +/// +/// 配送单详情查询。 +/// +public sealed class GetDeliveryOrderByIdQuery : IRequest +{ + /// + /// 配送单 ID。 + /// + public long DeliveryOrderId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs new file mode 100644 index 0000000..53d439c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Enums; + +namespace TakeoutSaaS.Application.App.Deliveries.Queries; + +/// +/// 配送单列表查询。 +/// +public sealed class SearchDeliveryOrdersQuery : IRequest> +{ + /// + /// 订单 ID(可选)。 + /// + public long? OrderId { get; init; } + + /// + /// 配送状态。 + /// + public DeliveryStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs new file mode 100644 index 0000000..415665c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Merchants.Commands; + +/// +/// 删除商户命令。 +/// +public sealed class DeleteMerchantCommand : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs new file mode 100644 index 0000000..2b73adc --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs @@ -0,0 +1,51 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Merchants.Commands; + +/// +/// 更新商户命令。 +/// +public sealed class UpdateMerchantCommand : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; set; } + + /// + /// 品牌名称。 + /// + public string BrandName { get; set; } = string.Empty; + + /// + /// 品牌简称。 + /// + public string? BrandAlias { get; set; } + + /// + /// Logo 地址。 + /// + public string? LogoUrl { get; set; } + + /// + /// 品类。 + /// + public string? Category { get; set; } + + /// + /// 联系电话。 + /// + public string ContactPhone { get; set; } = string.Empty; + + /// + /// 联系邮箱。 + /// + public string? ContactEmail { get; set; } + + /// + /// 入驻状态。 + /// + public MerchantStatus Status { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs new file mode 100644 index 0000000..8f69ff0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Merchants.Commands; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 删除商户命令处理器。 +/// +public sealed class DeleteMerchantCommandHandler( + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteMerchantCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _merchantRepository.DeleteMerchantAsync(request.MerchantId, tenantId, cancellationToken); + await _merchantRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除商户 {MerchantId}", request.MerchantId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs new file mode 100644 index 0000000..d825b72 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs @@ -0,0 +1,65 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Merchants.Commands; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 更新商户命令处理器。 +/// +public sealed class UpdateMerchantCommandHandler( + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateMerchantCommand request, CancellationToken cancellationToken) + { + // 1. 读取现有商户 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.BrandName = request.BrandName.Trim(); + existing.BrandAlias = request.BrandAlias?.Trim(); + existing.LogoUrl = request.LogoUrl?.Trim(); + existing.Category = request.Category?.Trim(); + existing.ContactPhone = request.ContactPhone.Trim(); + existing.ContactEmail = request.ContactEmail?.Trim(); + existing.Status = request.Status; + + // 3. 持久化 + await _merchantRepository.UpdateMerchantAsync(existing, cancellationToken); + await _merchantRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新商户 {MerchantId} - {BrandName}", existing.Id, existing.BrandName); + + // 4. 返回 DTO + return MapToDto(existing); + } + + private static MerchantDto MapToDto(Domain.Merchants.Entities.Merchant merchant) => new() + { + Id = merchant.Id, + TenantId = merchant.TenantId, + BrandName = merchant.BrandName, + BrandAlias = merchant.BrandAlias, + LogoUrl = merchant.LogoUrl, + Category = merchant.Category, + ContactPhone = merchant.ContactPhone, + ContactEmail = merchant.ContactEmail, + Status = merchant.Status, + JoinedAt = merchant.JoinedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs new file mode 100644 index 0000000..824de33 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs @@ -0,0 +1,117 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 创建订单命令。 +/// +public sealed class CreateOrderCommand : IRequest +{ + /// + /// 订单号。 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 渠道。 + /// + public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram; + + /// + /// 履约方式。 + /// + public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn; + + /// + /// 状态。 + /// + public OrderStatus Status { get; set; } = OrderStatus.PendingPayment; + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid; + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; set; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; set; } + + /// + /// 桌号。 + /// + public string? TableNo { get; set; } + + /// + /// 排队号。 + /// + public string? QueueNumber { get; set; } + + /// + /// 预约 ID。 + /// + public long? ReservationId { get; set; } + + /// + /// 商品金额。 + /// + public decimal ItemsAmount { get; set; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; set; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 明细。 + /// + public IReadOnlyList Items { get; set; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs new file mode 100644 index 0000000..f7632c6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 删除订单命令。 +/// +public sealed class DeleteOrderCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs new file mode 100644 index 0000000..544cd85 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs @@ -0,0 +1,52 @@ +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 订单明细请求。 +/// +public sealed class OrderItemRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; set; } + + /// + /// 商品名称。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// SKU 描述。 + /// + public string? SkuName { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 数量。 + /// + public int Quantity { get; set; } + + /// + /// 单价。 + /// + public decimal UnitPrice { get; set; } + + /// + /// 折扣金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 小计。 + /// + public decimal SubTotal { get; set; } + + /// + /// 属性 JSON。 + /// + public string? AttributesJson { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs new file mode 100644 index 0000000..a9c832a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs @@ -0,0 +1,117 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 更新订单命令。 +/// +public sealed class UpdateOrderCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 订单号。 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 渠道。 + /// + public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram; + + /// + /// 履约方式。 + /// + public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn; + + /// + /// 状态。 + /// + public OrderStatus Status { get; set; } = OrderStatus.PendingPayment; + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid; + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; set; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; set; } + + /// + /// 桌号。 + /// + public string? TableNo { get; set; } + + /// + /// 排队号。 + /// + public string? QueueNumber { get; set; } + + /// + /// 预约 ID。 + /// + public long? ReservationId { get; set; } + + /// + /// 商品金额。 + /// + public decimal ItemsAmount { get; set; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; set; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs new file mode 100644 index 0000000..0358f7c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs @@ -0,0 +1,141 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 订单 DTO。 +/// +public sealed class OrderDto +{ + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 订单号。 + /// + public string OrderNo { get; init; } = string.Empty; + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 渠道。 + /// + public OrderChannel Channel { get; init; } + + /// + /// 履约方式。 + /// + public DeliveryType DeliveryType { get; init; } + + /// + /// 状态。 + /// + public OrderStatus Status { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; init; } + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; init; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; init; } + + /// + /// 桌号。 + /// + public string? TableNo { get; init; } + + /// + /// 排队号。 + /// + public string? QueueNumber { get; init; } + + /// + /// 预约 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? ReservationId { get; init; } + + /// + /// 商品金额。 + /// + public decimal ItemsAmount { get; init; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; init; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; init; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; init; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; init; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; init; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; init; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; init; } + + /// + /// 备注。 + /// + public string? Remark { get; init; } + + /// + /// 明细。 + /// + public IReadOnlyList Items { get; init; } = Array.Empty(); + + /// + /// 状态流转。 + /// + public IReadOnlyList StatusHistory { get; init; } = Array.Empty(); + + /// + /// 退款申请。 + /// + public IReadOnlyList Refunds { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs new file mode 100644 index 0000000..6baa720 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs @@ -0,0 +1,68 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 订单明细 DTO。 +/// +public sealed class OrderItemDto +{ + /// + /// 明细 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 商品 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long ProductId { get; init; } + + /// + /// 商品名称。 + /// + public string ProductName { get; init; } = string.Empty; + + /// + /// SKU 描述。 + /// + public string? SkuName { get; init; } + + /// + /// 单位。 + /// + public string? Unit { get; init; } + + /// + /// 数量。 + /// + public int Quantity { get; init; } + + /// + /// 单价。 + /// + public decimal UnitPrice { get; init; } + + /// + /// 折扣金额。 + /// + public decimal DiscountAmount { get; init; } + + /// + /// 小计。 + /// + public decimal SubTotal { get; init; } + + /// + /// 属性 JSON。 + /// + public string? AttributesJson { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs new file mode 100644 index 0000000..e62e45e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 订单状态流转 DTO。 +/// +public sealed class OrderStatusHistoryDto +{ + /// + /// 记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 状态。 + /// + public OrderStatus Status { get; init; } + + /// + /// 操作人。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? OperatorId { get; init; } + + /// + /// 备注。 + /// + public string? Notes { get; init; } + + /// + /// 时间。 + /// + public DateTime OccurredAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs new file mode 100644 index 0000000..d389554 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 退款申请 DTO。 +/// +public sealed class RefundRequestDto +{ + /// + /// 退款 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 退款单号。 + /// + public string RefundNo { get; init; } = string.Empty; + + /// + /// 金额。 + /// + public decimal Amount { get; init; } + + /// + /// 原因。 + /// + public string Reason { get; init; } = string.Empty; + + /// + /// 状态。 + /// + public RefundStatus Status { get; init; } + + /// + /// 申请时间。 + /// + public DateTime RequestedAt { get; init; } + + /// + /// 处理时间。 + /// + public DateTime? ProcessedAt { get; init; } + + /// + /// 审核备注。 + /// + public string? ReviewNotes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs new file mode 100644 index 0000000..057b018 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs @@ -0,0 +1,156 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 创建订单命令处理器。 +/// +public sealed class CreateOrderCommandHandler( + IOrderRepository orderRepository, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly IIdGenerator _idGenerator = idGenerator; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken) + { + // 1. 构建订单 + var order = new Order + { + Id = _idGenerator.NextId(), + OrderNo = request.OrderNo.Trim(), + StoreId = request.StoreId, + Channel = request.Channel, + DeliveryType = request.DeliveryType, + Status = request.Status, + PaymentStatus = request.PaymentStatus, + CustomerName = request.CustomerName?.Trim(), + CustomerPhone = request.CustomerPhone?.Trim(), + TableNo = request.TableNo?.Trim(), + QueueNumber = request.QueueNumber?.Trim(), + ReservationId = request.ReservationId, + ItemsAmount = request.ItemsAmount, + DiscountAmount = request.DiscountAmount, + PayableAmount = request.PayableAmount, + PaidAmount = request.PaidAmount, + PaidAt = request.PaidAt, + FinishedAt = request.FinishedAt, + CancelledAt = request.CancelledAt, + CancelReason = request.CancelReason?.Trim(), + Remark = request.Remark?.Trim() + }; + + // 2. 构建明细 + var items = request.Items.Select(item => new OrderItem + { + OrderId = order.Id, + ProductId = item.ProductId, + ProductName = item.ProductName.Trim(), + SkuName = item.SkuName?.Trim(), + Unit = item.Unit?.Trim(), + Quantity = item.Quantity, + UnitPrice = item.UnitPrice, + DiscountAmount = item.DiscountAmount, + SubTotal = item.SubTotal, + AttributesJson = item.AttributesJson?.Trim() + }).ToList(); + + // 3. 补充金额字段 + if (items.Count > 0) + { + var itemsAmount = items.Sum(x => x.SubTotal); + order.ItemsAmount = itemsAmount; + if (order.PayableAmount <= 0) + { + order.PayableAmount = itemsAmount - order.DiscountAmount; + } + } + + // 4. 持久化 + await _orderRepository.AddOrderAsync(order, cancellationToken); + if (items.Count > 0) + { + await _orderRepository.AddItemsAsync(items, cancellationToken); + } + await _orderRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id); + + // 5. 返回 DTO + return MapToDto(order, items, [], []); + } + + private static OrderDto MapToDto( + Order order, + IReadOnlyList items, + IReadOnlyList histories, + IReadOnlyList refunds) => new() + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark, + Items = items.Select(x => new OrderItemDto + { + Id = x.Id, + OrderId = x.OrderId, + ProductId = x.ProductId, + ProductName = x.ProductName, + SkuName = x.SkuName, + Unit = x.Unit, + Quantity = x.Quantity, + UnitPrice = x.UnitPrice, + DiscountAmount = x.DiscountAmount, + SubTotal = x.SubTotal, + AttributesJson = x.AttributesJson + }).ToList(), + StatusHistory = histories.Select(x => new OrderStatusHistoryDto + { + Id = x.Id, + OrderId = x.OrderId, + Status = x.Status, + OperatorId = x.OperatorId, + Notes = x.Notes, + OccurredAt = x.OccurredAt + }).ToList(), + Refunds = refunds.Select(x => new RefundRequestDto + { + Id = x.Id, + OrderId = x.OrderId, + RefundNo = x.RefundNo, + Amount = x.Amount, + Reason = x.Reason, + Status = x.Status, + RequestedAt = x.RequestedAt, + ProcessedAt = x.ProcessedAt, + ReviewNotes = x.ReviewNotes + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs new file mode 100644 index 0000000..d376e47 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 删除订单命令处理器。 +/// +public sealed class DeleteOrderCommandHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteOrderCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _orderRepository.DeleteOrderAsync(request.OrderId, tenantId, cancellationToken); + await _orderRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除订单 {OrderId}", request.OrderId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs new file mode 100644 index 0000000..dbdd1c2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs @@ -0,0 +1,102 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Application.App.Orders.Queries; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 订单详情查询处理器。 +/// +public sealed class GetOrderByIdQueryHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var order = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken); + if (order == null) + { + return null; + } + + var items = await _orderRepository.GetItemsAsync(order.Id, tenantId, cancellationToken); + var histories = await _orderRepository.GetStatusHistoryAsync(order.Id, tenantId, cancellationToken); + var refunds = await _orderRepository.GetRefundsAsync(order.Id, tenantId, cancellationToken); + + return MapToDto(order, items, histories, refunds); + } + + private static OrderDto MapToDto( + Order order, + IReadOnlyList items, + IReadOnlyList histories, + IReadOnlyList refunds) => new() + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark, + Items = items.Select(x => new OrderItemDto + { + Id = x.Id, + OrderId = x.OrderId, + ProductId = x.ProductId, + ProductName = x.ProductName, + SkuName = x.SkuName, + Unit = x.Unit, + Quantity = x.Quantity, + UnitPrice = x.UnitPrice, + DiscountAmount = x.DiscountAmount, + SubTotal = x.SubTotal, + AttributesJson = x.AttributesJson + }).ToList(), + StatusHistory = histories.Select(x => new OrderStatusHistoryDto + { + Id = x.Id, + OrderId = x.OrderId, + Status = x.Status, + OperatorId = x.OperatorId, + Notes = x.Notes, + OccurredAt = x.OccurredAt + }).ToList(), + Refunds = refunds.Select(x => new RefundRequestDto + { + Id = x.Id, + OrderId = x.OrderId, + RefundNo = x.RefundNo, + Amount = x.Amount, + Reason = x.Reason, + Status = x.Status, + RequestedAt = x.RequestedAt, + ProcessedAt = x.ProcessedAt, + ReviewNotes = x.ReviewNotes + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs new file mode 100644 index 0000000..d1b054a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs @@ -0,0 +1,65 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Application.App.Orders.Queries; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 订单列表查询处理器。 +/// +public sealed class SearchOrdersQueryHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchOrdersQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var orders = await _orderRepository.SearchAsync(tenantId, request.Status, request.PaymentStatus, cancellationToken); + + if (request.StoreId.HasValue) + { + orders = orders.Where(x => x.StoreId == request.StoreId.Value).ToList(); + } + + if (!string.IsNullOrWhiteSpace(request.OrderNo)) + { + var orderNo = request.OrderNo.Trim(); + orders = orders + .Where(x => x.OrderNo.Contains(orderNo, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + return orders.Select(order => new OrderDto + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs new file mode 100644 index 0000000..3901fd9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs @@ -0,0 +1,134 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 更新订单命令处理器。 +/// +public sealed class UpdateOrderCommandHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateOrderCommand request, CancellationToken cancellationToken) + { + // 1. 读取订单 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.OrderNo = request.OrderNo.Trim(); + existing.StoreId = request.StoreId; + existing.Channel = request.Channel; + existing.DeliveryType = request.DeliveryType; + existing.Status = request.Status; + existing.PaymentStatus = request.PaymentStatus; + existing.CustomerName = request.CustomerName?.Trim(); + existing.CustomerPhone = request.CustomerPhone?.Trim(); + existing.TableNo = request.TableNo?.Trim(); + existing.QueueNumber = request.QueueNumber?.Trim(); + existing.ReservationId = request.ReservationId; + existing.ItemsAmount = request.ItemsAmount; + existing.DiscountAmount = request.DiscountAmount; + existing.PayableAmount = request.PayableAmount; + existing.PaidAmount = request.PaidAmount; + existing.PaidAt = request.PaidAt; + existing.FinishedAt = request.FinishedAt; + existing.CancelledAt = request.CancelledAt; + existing.CancelReason = request.CancelReason?.Trim(); + existing.Remark = request.Remark?.Trim(); + + // 3. 持久化 + await _orderRepository.UpdateOrderAsync(existing, cancellationToken); + await _orderRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新订单 {OrderNo} ({OrderId})", existing.OrderNo, existing.Id); + + // 4. 读取关联数据并返回 + var items = await _orderRepository.GetItemsAsync(existing.Id, tenantId, cancellationToken); + var histories = await _orderRepository.GetStatusHistoryAsync(existing.Id, tenantId, cancellationToken); + var refunds = await _orderRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken); + + return MapToDto(existing, items, histories, refunds); + } + + private static OrderDto MapToDto( + Order order, + IReadOnlyList items, + IReadOnlyList histories, + IReadOnlyList refunds) => new() + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark, + Items = items.Select(x => new OrderItemDto + { + Id = x.Id, + OrderId = x.OrderId, + ProductId = x.ProductId, + ProductName = x.ProductName, + SkuName = x.SkuName, + Unit = x.Unit, + Quantity = x.Quantity, + UnitPrice = x.UnitPrice, + DiscountAmount = x.DiscountAmount, + SubTotal = x.SubTotal, + AttributesJson = x.AttributesJson + }).ToList(), + StatusHistory = histories.Select(x => new OrderStatusHistoryDto + { + Id = x.Id, + OrderId = x.OrderId, + Status = x.Status, + OperatorId = x.OperatorId, + Notes = x.Notes, + OccurredAt = x.OccurredAt + }).ToList(), + Refunds = refunds.Select(x => new RefundRequestDto + { + Id = x.Id, + OrderId = x.OrderId, + RefundNo = x.RefundNo, + Amount = x.Amount, + Reason = x.Reason, + Status = x.Status, + RequestedAt = x.RequestedAt, + ProcessedAt = x.ProcessedAt, + ReviewNotes = x.ReviewNotes + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs new file mode 100644 index 0000000..1446c27 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; + +namespace TakeoutSaaS.Application.App.Orders.Queries; + +/// +/// 获取订单详情查询。 +/// +public sealed class GetOrderByIdQuery : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs new file mode 100644 index 0000000..99fa7b3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs @@ -0,0 +1,32 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Orders.Queries; + +/// +/// 订单列表查询。 +/// +public sealed class SearchOrdersQuery : IRequest> +{ + /// + /// 门店 ID(可选)。 + /// + public long? StoreId { get; init; } + + /// + /// 订单状态。 + /// + public OrderStatus? Status { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus? PaymentStatus { get; init; } + + /// + /// 订单号(模糊或精确,由调用方控制)。 + /// + public string? OrderNo { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs new file mode 100644 index 0000000..ed3c66c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs @@ -0,0 +1,56 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Payments.Commands; + +/// +/// 创建支付记录命令。 +/// +public sealed class CreatePaymentCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; set; } = PaymentMethod.Unknown; + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; set; } + + /// + /// 渠道单号。 + /// + public string? ChannelTransactionId { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 原始回调。 + /// + public string? Payload { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs new file mode 100644 index 0000000..5c42b47 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Payments.Commands; + +/// +/// 删除支付记录命令。 +/// +public sealed class DeletePaymentCommand : IRequest +{ + /// + /// 支付记录 ID。 + /// + public long PaymentId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs new file mode 100644 index 0000000..8d8263f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs @@ -0,0 +1,61 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Payments.Commands; + +/// +/// 更新支付记录命令。 +/// +public sealed class UpdatePaymentCommand : IRequest +{ + /// + /// 支付记录 ID。 + /// + public long PaymentId { get; set; } + + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; set; } = PaymentMethod.Unknown; + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; set; } + + /// + /// 渠道单号。 + /// + public string? ChannelTransactionId { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 原始回调。 + /// + public string? Payload { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs new file mode 100644 index 0000000..807d0f1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs @@ -0,0 +1,74 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Payments.Dto; + +/// +/// 支付记录 DTO。 +/// +public sealed class PaymentDto +{ + /// + /// 支付记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; init; } + + /// + /// 金额。 + /// + public decimal Amount { get; init; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; init; } + + /// + /// 渠道单号。 + /// + public string? ChannelTransactionId { get; init; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; init; } + + /// + /// 备注。 + /// + public string? Remark { get; init; } + + /// + /// 原始回调。 + /// + public string? Payload { get; init; } + + /// + /// 退款记录。 + /// + public IReadOnlyList Refunds { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs new file mode 100644 index 0000000..5c9cfa8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Payments.Dto; + +/// +/// 退款记录 DTO。 +/// +public sealed class PaymentRefundDto +{ + /// + /// 退款记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 支付记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long PaymentRecordId { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 金额。 + /// + public decimal Amount { get; init; } + + /// + /// 渠道退款号。 + /// + public string? ChannelRefundId { get; init; } + + /// + /// 状态。 + /// + public PaymentRefundStatus Status { get; init; } + + /// + /// 原始回调。 + /// + public string? Payload { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs new file mode 100644 index 0000000..463d903 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 创建支付记录命令处理器。 +/// +public sealed class CreatePaymentCommandHandler(IPaymentRepository paymentRepository, ILogger logger) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreatePaymentCommand request, CancellationToken cancellationToken) + { + var payment = new PaymentRecord + { + OrderId = request.OrderId, + Method = request.Method, + Status = request.Status, + Amount = request.Amount, + TradeNo = request.TradeNo?.Trim(), + ChannelTransactionId = request.ChannelTransactionId?.Trim(), + PaidAt = request.PaidAt, + Remark = request.Remark?.Trim(), + Payload = request.Payload + }; + + await _paymentRepository.AddPaymentAsync(payment, cancellationToken); + await _paymentRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建支付记录 {PaymentId} 对应订单 {OrderId}", payment.Id, payment.OrderId); + + return MapToDto(payment, []); + } + + private static PaymentDto MapToDto(PaymentRecord payment, IReadOnlyList refunds) => new() + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload, + Refunds = refunds.Select(x => new PaymentRefundDto + { + Id = x.Id, + PaymentRecordId = x.PaymentRecordId, + OrderId = x.OrderId, + Amount = x.Amount, + ChannelRefundId = x.ChannelRefundId, + Status = x.Status, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs new file mode 100644 index 0000000..07b2ca9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 删除支付记录命令处理器。 +/// +public sealed class DeletePaymentCommandHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeletePaymentCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _paymentRepository.FindByIdAsync(request.PaymentId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + await _paymentRepository.DeletePaymentAsync(request.PaymentId, tenantId, cancellationToken); + await _paymentRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除支付记录 {PaymentId}", request.PaymentId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs new file mode 100644 index 0000000..225697c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs @@ -0,0 +1,59 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 支付记录详情查询处理器。 +/// +public sealed class GetPaymentByIdQueryHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetPaymentByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var payment = await _paymentRepository.FindByIdAsync(request.PaymentId, tenantId, cancellationToken); + if (payment == null) + { + return null; + } + + var refunds = await _paymentRepository.GetRefundsAsync(payment.Id, tenantId, cancellationToken); + return MapToDto(payment, refunds); + } + + private static PaymentDto MapToDto(PaymentRecord payment, IReadOnlyList refunds) => new() + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload, + Refunds = refunds.Select(x => new PaymentRefundDto + { + Id = x.Id, + PaymentRecordId = x.PaymentRecordId, + OrderId = x.OrderId, + Amount = x.Amount, + ChannelRefundId = x.ChannelRefundId, + Status = x.Status, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs new file mode 100644 index 0000000..45ff7cb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 支付记录列表查询处理器。 +/// +public sealed class SearchPaymentsQueryHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchPaymentsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var payments = await _paymentRepository.SearchAsync(tenantId, request.Status, cancellationToken); + + if (request.OrderId.HasValue) + { + payments = payments.Where(x => x.OrderId == request.OrderId.Value).ToList(); + } + + return payments.Select(payment => new PaymentDto + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs new file mode 100644 index 0000000..f55a37d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs @@ -0,0 +1,76 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 更新支付记录命令处理器。 +/// +public sealed class UpdatePaymentCommandHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdatePaymentCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _paymentRepository.FindByIdAsync(request.PaymentId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + existing.OrderId = request.OrderId; + existing.Method = request.Method; + existing.Status = request.Status; + existing.Amount = request.Amount; + existing.TradeNo = request.TradeNo?.Trim(); + existing.ChannelTransactionId = request.ChannelTransactionId?.Trim(); + existing.PaidAt = request.PaidAt; + existing.Remark = request.Remark?.Trim(); + existing.Payload = request.Payload; + + await _paymentRepository.UpdatePaymentAsync(existing, cancellationToken); + await _paymentRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新支付记录 {PaymentId}", existing.Id); + + var refunds = await _paymentRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken); + return MapToDto(existing, refunds); + } + + private static PaymentDto MapToDto(PaymentRecord payment, IReadOnlyList refunds) => new() + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload, + Refunds = refunds.Select(x => new PaymentRefundDto + { + Id = x.Id, + PaymentRecordId = x.PaymentRecordId, + OrderId = x.OrderId, + Amount = x.Amount, + ChannelRefundId = x.ChannelRefundId, + Status = x.Status, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs new file mode 100644 index 0000000..3ca6c8c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; + +namespace TakeoutSaaS.Application.App.Payments.Queries; + +/// +/// 获取支付记录详情。 +/// +public sealed class GetPaymentByIdQuery : IRequest +{ + /// + /// 支付记录 ID。 + /// + public long PaymentId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs new file mode 100644 index 0000000..91b5bad --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Payments.Queries; + +/// +/// 支付记录列表查询。 +/// +public sealed class SearchPaymentsQuery : IRequest> +{ + /// + /// 订单 ID(可选)。 + /// + public long? OrderId { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs new file mode 100644 index 0000000..50d08c0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs @@ -0,0 +1,101 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Application.App.Products.Commands; + +/// +/// 创建商品命令。 +/// +public sealed class CreateProductCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 分类 ID。 + /// + public long CategoryId { get; set; } + + /// + /// 商品编码。 + /// + public string SpuCode { get; set; } = string.Empty; + + /// + /// 名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 副标题。 + /// + public string? Subtitle { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 现价。 + /// + public decimal Price { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 库存数量。 + /// + public int? StockQuantity { get; set; } + + /// + /// 每单限购。 + /// + public int? MaxQuantityPerOrder { get; set; } + + /// + /// 状态。 + /// + public ProductStatus Status { get; set; } = ProductStatus.Draft; + + /// + /// 主图。 + /// + public string? CoverImage { get; set; } + + /// + /// 图集。 + /// + public string? GalleryImages { get; set; } + + /// + /// 描述。 + /// + public string? Description { get; set; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; set; } = true; + + /// + /// 是否推荐。 + /// + public bool IsFeatured { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs new file mode 100644 index 0000000..9a17c86 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Products.Commands; + +/// +/// 删除商品命令。 +/// +public sealed class DeleteProductCommand : IRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs new file mode 100644 index 0000000..ba3de2b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs @@ -0,0 +1,106 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Application.App.Products.Commands; + +/// +/// 更新商品命令。 +/// +public sealed class UpdateProductCommand : IRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; set; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 分类 ID。 + /// + public long CategoryId { get; set; } + + /// + /// 商品编码。 + /// + public string SpuCode { get; set; } = string.Empty; + + /// + /// 名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 副标题。 + /// + public string? Subtitle { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 现价。 + /// + public decimal Price { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 库存数量。 + /// + public int? StockQuantity { get; set; } + + /// + /// 每单限购。 + /// + public int? MaxQuantityPerOrder { get; set; } + + /// + /// 状态。 + /// + public ProductStatus Status { get; set; } = ProductStatus.Draft; + + /// + /// 主图。 + /// + public string? CoverImage { get; set; } + + /// + /// 图集。 + /// + public string? GalleryImages { get; set; } + + /// + /// 描述。 + /// + public string? Description { get; set; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; set; } = true; + + /// + /// 是否推荐。 + /// + public bool IsFeatured { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs b/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs new file mode 100644 index 0000000..7661594 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs @@ -0,0 +1,115 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Products.Dto; + +/// +/// 商品 DTO。 +/// +public sealed class ProductDto +{ + /// + /// 商品 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 分类 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long CategoryId { get; init; } + + /// + /// SPU 编码。 + /// + public string SpuCode { get; init; } = string.Empty; + + /// + /// 名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 副标题。 + /// + public string? Subtitle { get; init; } + + /// + /// 单位。 + /// + public string? Unit { get; init; } + + /// + /// 现价。 + /// + public decimal Price { get; init; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; init; } + + /// + /// 库存数量。 + /// + public int? StockQuantity { get; init; } + + /// + /// 每单限购。 + /// + public int? MaxQuantityPerOrder { get; init; } + + /// + /// 状态。 + /// + public ProductStatus Status { get; init; } + + /// + /// 主图。 + /// + public string? CoverImage { get; init; } + + /// + /// 图集。 + /// + public string? GalleryImages { get; init; } + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; init; } + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; init; } + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; init; } + + /// + /// 是否推荐。 + /// + public bool IsFeatured { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs new file mode 100644 index 0000000..46201fa --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs @@ -0,0 +1,77 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Repositories; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 创建商品命令处理器。 +/// +public sealed class CreateProductCommandHandler(IProductRepository productRepository, ILogger logger) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken) + { + // 1. 构建实体 + var product = new Product + { + StoreId = request.StoreId, + CategoryId = request.CategoryId, + SpuCode = request.SpuCode.Trim(), + Name = request.Name.Trim(), + Subtitle = request.Subtitle?.Trim(), + Unit = request.Unit?.Trim(), + Price = request.Price, + OriginalPrice = request.OriginalPrice, + StockQuantity = request.StockQuantity, + MaxQuantityPerOrder = request.MaxQuantityPerOrder, + Status = request.Status, + CoverImage = request.CoverImage?.Trim(), + GalleryImages = request.GalleryImages?.Trim(), + Description = request.Description?.Trim(), + EnableDineIn = request.EnableDineIn, + EnablePickup = request.EnablePickup, + EnableDelivery = request.EnableDelivery, + IsFeatured = request.IsFeatured + }; + + // 2. 持久化 + await _productRepository.AddProductAsync(product, cancellationToken); + await _productRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建商品 {ProductId} - {ProductName}", product.Id, product.Name); + + // 3. 返回 DTO + return MapToDto(product); + } + + private static ProductDto MapToDto(Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs new file mode 100644 index 0000000..f06cfa1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 删除商品命令处理器。 +/// +public sealed class DeleteProductCommandHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteProductCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _productRepository.DeleteProductAsync(request.ProductId, tenantId, cancellationToken); + await _productRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除商品 {ProductId}", request.ProductId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs new file mode 100644 index 0000000..018c325 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Application.App.Products.Queries; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 商品详情查询处理器。 +/// +public sealed class GetProductByIdQueryHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetProductByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var product = await _productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken); + return product == null ? null : MapToDto(product); + } + + private static ProductDto MapToDto(Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs new file mode 100644 index 0000000..d24784e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs @@ -0,0 +1,57 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Application.App.Products.Queries; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 商品列表查询处理器。 +/// +public sealed class SearchProductsQueryHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchProductsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var products = await _productRepository.SearchAsync(tenantId, request.CategoryId, request.Status, cancellationToken); + + if (request.StoreId.HasValue) + { + products = products.Where(x => x.StoreId == request.StoreId.Value).ToList(); + } + + return products.Select(MapToDto).ToList(); + } + + private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs new file mode 100644 index 0000000..df33a23 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs @@ -0,0 +1,87 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 更新商品命令处理器。 +/// +public sealed class UpdateProductCommandHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateProductCommand request, CancellationToken cancellationToken) + { + // 1. 读取商品 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.StoreId = request.StoreId; + existing.CategoryId = request.CategoryId; + existing.SpuCode = request.SpuCode.Trim(); + existing.Name = request.Name.Trim(); + existing.Subtitle = request.Subtitle?.Trim(); + existing.Unit = request.Unit?.Trim(); + existing.Price = request.Price; + existing.OriginalPrice = request.OriginalPrice; + existing.StockQuantity = request.StockQuantity; + existing.MaxQuantityPerOrder = request.MaxQuantityPerOrder; + existing.Status = request.Status; + existing.CoverImage = request.CoverImage?.Trim(); + existing.GalleryImages = request.GalleryImages?.Trim(); + existing.Description = request.Description?.Trim(); + existing.EnableDineIn = request.EnableDineIn; + existing.EnablePickup = request.EnablePickup; + existing.EnableDelivery = request.EnableDelivery; + existing.IsFeatured = request.IsFeatured; + + // 3. 持久化 + await _productRepository.UpdateProductAsync(existing, cancellationToken); + await _productRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新商品 {ProductId} - {ProductName}", existing.Id, existing.Name); + + // 4. 返回 DTO + return MapToDto(existing); + } + + private static ProductDto MapToDto(Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs new file mode 100644 index 0000000..08830cb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; + +namespace TakeoutSaaS.Application.App.Products.Queries; + +/// +/// 获取商品详情查询。 +/// +public sealed class GetProductByIdQuery : IRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs new file mode 100644 index 0000000..07541c8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Application.App.Products.Queries; + +/// +/// 商品列表查询。 +/// +public sealed class SearchProductsQuery : IRequest> +{ + /// + /// 门店 ID(可选)。 + /// + public long? StoreId { get; init; } + + /// + /// 分类 ID(可选)。 + /// + public long? CategoryId { get; init; } + + /// + /// 状态过滤。 + /// + public ProductStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs new file mode 100644 index 0000000..21eb9c5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs @@ -0,0 +1,101 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 创建门店命令。 +/// +public sealed class CreateStoreCommand : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; set; } + + /// + /// 门店编码。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 电话。 + /// + public string? Phone { get; set; } + + /// + /// 负责人。 + /// + public string? ManagerName { get; set; } + + /// + /// 状态。 + /// + public StoreStatus Status { get; set; } = StoreStatus.Closed; + + /// + /// 省份。 + /// + public string? Province { get; set; } + + /// + /// 城市。 + /// + public string? City { get; set; } + + /// + /// 区县。 + /// + public string? District { get; set; } + + /// + /// 详细地址。 + /// + public string? Address { get; set; } + + /// + /// 经度。 + /// + public double? Longitude { get; set; } + + /// + /// 纬度。 + /// + public double? Latitude { get; set; } + + /// + /// 公告。 + /// + public string? Announcement { get; set; } + + /// + /// 标签。 + /// + public string? Tags { get; set; } + + /// + /// 配送半径。 + /// + public decimal DeliveryRadiusKm { get; set; } + + /// + /// 支持堂食。 + /// + public bool SupportsDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool SupportsPickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool SupportsDelivery { get; set; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs new file mode 100644 index 0000000..9a66dbb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除门店命令。 +/// +public sealed class DeleteStoreCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs new file mode 100644 index 0000000..0cecf53 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs @@ -0,0 +1,106 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新门店命令。 +/// +public sealed class UpdateStoreCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 商户 ID。 + /// + public long MerchantId { get; set; } + + /// + /// 门店编码。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 电话。 + /// + public string? Phone { get; set; } + + /// + /// 负责人。 + /// + public string? ManagerName { get; set; } + + /// + /// 状态。 + /// + public StoreStatus Status { get; set; } = StoreStatus.Closed; + + /// + /// 省份。 + /// + public string? Province { get; set; } + + /// + /// 城市。 + /// + public string? City { get; set; } + + /// + /// 区县。 + /// + public string? District { get; set; } + + /// + /// 详细地址。 + /// + public string? Address { get; set; } + + /// + /// 经度。 + /// + public double? Longitude { get; set; } + + /// + /// 纬度。 + /// + public double? Latitude { get; set; } + + /// + /// 公告。 + /// + public string? Announcement { get; set; } + + /// + /// 标签。 + /// + public string? Tags { get; set; } + + /// + /// 配送半径。 + /// + public decimal DeliveryRadiusKm { get; set; } + + /// + /// 支持堂食。 + /// + public bool SupportsDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool SupportsPickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool SupportsDelivery { get; set; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs new file mode 100644 index 0000000..e924b43 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs @@ -0,0 +1,114 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 门店 DTO。 +/// +public sealed class StoreDto +{ + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 商户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long MerchantId { get; init; } + + /// + /// 门店编码。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 电话。 + /// + public string? Phone { get; init; } + + /// + /// 负责人。 + /// + public string? ManagerName { get; init; } + + /// + /// 状态。 + /// + public StoreStatus Status { get; init; } + + /// + /// 省份。 + /// + public string? Province { get; init; } + + /// + /// 城市。 + /// + public string? City { get; init; } + + /// + /// 区县。 + /// + public string? District { get; init; } + + /// + /// 详细地址。 + /// + public string? Address { get; init; } + + /// + /// 经度。 + /// + public double? Longitude { get; init; } + + /// + /// 纬度。 + /// + public double? Latitude { get; init; } + + /// + /// 公告。 + /// + public string? Announcement { get; init; } + + /// + /// 标签。 + /// + public string? Tags { get; init; } + + /// + /// 默认配送半径。 + /// + public decimal DeliveryRadiusKm { get; init; } + + /// + /// 支持堂食。 + /// + public bool SupportsDineIn { get; init; } + + /// + /// 支持自提。 + /// + public bool SupportsPickup { get; init; } + + /// + /// 支持配送。 + /// + public bool SupportsDelivery { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs new file mode 100644 index 0000000..56f9d9c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs @@ -0,0 +1,77 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 创建门店命令处理器。 +/// +public sealed class CreateStoreCommandHandler(IStoreRepository storeRepository, ILogger logger) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateStoreCommand request, CancellationToken cancellationToken) + { + // 1. 构建实体 + var store = new Store + { + MerchantId = request.MerchantId, + Code = request.Code.Trim(), + Name = request.Name.Trim(), + Phone = request.Phone?.Trim(), + ManagerName = request.ManagerName?.Trim(), + Status = request.Status, + Province = request.Province?.Trim(), + City = request.City?.Trim(), + District = request.District?.Trim(), + Address = request.Address?.Trim(), + Longitude = request.Longitude, + Latitude = request.Latitude, + Announcement = request.Announcement?.Trim(), + Tags = request.Tags?.Trim(), + DeliveryRadiusKm = request.DeliveryRadiusKm, + SupportsDineIn = request.SupportsDineIn, + SupportsPickup = request.SupportsPickup, + SupportsDelivery = request.SupportsDelivery + }; + + // 2. 持久化 + await _storeRepository.AddStoreAsync(store, cancellationToken); + await _storeRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建门店 {StoreId} - {StoreName}", store.Id, store.Name); + + // 3. 返回 DTO + return MapToDto(store); + } + + private static StoreDto MapToDto(Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs new file mode 100644 index 0000000..7e30eaf --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 删除门店命令处理器。 +/// +public sealed class DeleteStoreCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteStoreCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _storeRepository.DeleteStoreAsync(request.StoreId, tenantId, cancellationToken); + await _storeRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除门店 {StoreId}", request.StoreId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs new file mode 100644 index 0000000..4cb8cb0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 门店详情查询处理器。 +/// +public sealed class GetStoreByIdQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetStoreByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var store = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + return store == null ? null : MapToDto(store); + } + + private static StoreDto MapToDto(Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs new file mode 100644 index 0000000..4efe538 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs @@ -0,0 +1,59 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 门店列表查询处理器。 +/// +public sealed class SearchStoresQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchStoresQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var stores = await _storeRepository.SearchAsync(tenantId, request.Status, cancellationToken); + + if (request.MerchantId.HasValue) + { + stores = stores.Where(x => x.MerchantId == request.MerchantId.Value).ToList(); + } + + return stores + .Select(MapToDto) + .ToList(); + } + + private static StoreDto MapToDto(Domain.Stores.Entities.Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs new file mode 100644 index 0000000..1680932 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs @@ -0,0 +1,87 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 更新门店命令处理器。 +/// +public sealed class UpdateStoreCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateStoreCommand request, CancellationToken cancellationToken) + { + // 1. 读取门店 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.MerchantId = request.MerchantId; + existing.Code = request.Code.Trim(); + existing.Name = request.Name.Trim(); + existing.Phone = request.Phone?.Trim(); + existing.ManagerName = request.ManagerName?.Trim(); + existing.Status = request.Status; + existing.Province = request.Province?.Trim(); + existing.City = request.City?.Trim(); + existing.District = request.District?.Trim(); + existing.Address = request.Address?.Trim(); + existing.Longitude = request.Longitude; + existing.Latitude = request.Latitude; + existing.Announcement = request.Announcement?.Trim(); + existing.Tags = request.Tags?.Trim(); + existing.DeliveryRadiusKm = request.DeliveryRadiusKm; + existing.SupportsDineIn = request.SupportsDineIn; + existing.SupportsPickup = request.SupportsPickup; + existing.SupportsDelivery = request.SupportsDelivery; + + // 3. 持久化 + await _storeRepository.UpdateStoreAsync(existing, cancellationToken); + await _storeRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新门店 {StoreId} - {StoreName}", existing.Id, existing.Name); + + // 4. 返回 DTO + return MapToDto(existing); + } + + private static StoreDto MapToDto(Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs new file mode 100644 index 0000000..7f0699e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 获取门店详情查询。 +/// +public sealed class GetStoreByIdQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs new file mode 100644 index 0000000..085a557 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 门店列表查询。 +/// +public sealed class SearchStoresQuery : IRequest> +{ + /// + /// 商户 ID(可选)。 + /// + public long? MerchantId { get; init; } + + /// + /// 状态过滤。 + /// + public StoreStatus? Status { get; init; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs index d0a2132..27d5c95 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Enums; namespace TakeoutSaaS.Domain.Deliveries.Repositories; @@ -40,6 +41,11 @@ public interface IDeliveryRepository /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + /// + /// 按状态查询配送单。 + /// + Task> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default); + /// /// 更新配送单。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs index ec286b5..f12c937 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Enums; namespace TakeoutSaaS.Domain.Payments.Repositories; @@ -40,6 +41,11 @@ public interface IPaymentRepository /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + /// + /// 按状态筛选支付记录。 + /// + Task> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default); + /// /// 更新支付记录。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs index 5c408ef..32b482f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs @@ -1,6 +1,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Enums; using TakeoutSaaS.Domain.Deliveries.Repositories; using TakeoutSaaS.Infrastructure.App.Persistence; @@ -69,6 +70,28 @@ public sealed class EfDeliveryRepository : IDeliveryRepository return _context.SaveChangesAsync(cancellationToken); } + /// + public async Task> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default) + { + var query = _context.DeliveryOrders + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + if (orderId.HasValue) + { + query = query.Where(x => x.OrderId == orderId.Value); + } + + return await query + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + } + /// public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs index 8ea2124..f4dfb6c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs @@ -1,6 +1,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Enums; using TakeoutSaaS.Domain.Payments.Repositories; using TakeoutSaaS.Infrastructure.App.Persistence; @@ -63,6 +64,23 @@ public sealed class EfPaymentRepository : IPaymentRepository return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask(); } + /// + public async Task> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default) + { + var query = _context.PaymentRecords + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + return await query + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + } + /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { From 97bf6cacb0ded8047ed3609e42149365f98c5b67 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 10:50:43 +0800 Subject: [PATCH 23/56] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E6=8E=92=E5=BA=8F=E4=B8=8EFluentValidation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DeliveriesController.cs | 15 +++++- .../Controllers/MerchantsController.cs | 17 +++++- .../Controllers/OrdersController.cs | 12 ++++- .../Controllers/PaymentsController.cs | 15 +++++- .../Controllers/ProductsController.cs | 16 +++++- .../Controllers/StoresController.cs | 15 +++++- .../Common/Behaviors/ValidationBehavior.cs | 35 +++++++++++++ .../App/Deliveries/Dto/DeliveryOrderDto.cs | 5 ++ .../CreateDeliveryOrderCommandHandler.cs | 1 + .../GetDeliveryOrderByIdQueryHandler.cs | 1 + .../SearchDeliveryOrdersQueryHandler.cs | 24 ++++++++- .../UpdateDeliveryOrderCommandHandler.cs | 1 + .../Queries/SearchDeliveryOrdersQuery.cs | 20 +++++++ .../CreateDeliveryOrderCommandValidator.cs | 23 ++++++++ .../SearchDeliveryOrdersQueryValidator.cs | 20 +++++++ .../UpdateDeliveryOrderCommandValidator.cs | 24 +++++++++ ...pApplicationServiceCollectionExtensions.cs | 4 ++ .../App/Merchants/Dto/MerchantDto.cs | 5 ++ .../Handlers/CreateMerchantCommandHandler.cs | 3 +- .../Handlers/GetMerchantByIdQueryHandler.cs | 3 +- .../Handlers/SearchMerchantsQueryHandler.cs | 52 ++++++++++++++----- .../Handlers/UpdateMerchantCommandHandler.cs | 3 +- .../Merchants/Queries/SearchMerchantsQuery.cs | 20 +++++++ .../CreateMerchantCommandValidator.cs | 23 ++++++++ .../SearchMerchantsQueryValidator.cs | 20 +++++++ .../UpdateMerchantCommandValidator.cs | 24 +++++++++ .../App/Orders/Dto/OrderDto.cs | 5 ++ .../Handlers/CreateOrderCommandHandler.cs | 3 +- .../Handlers/GetOrderByIdQueryHandler.cs | 3 +- .../Handlers/SearchOrdersQueryHandler.cs | 25 ++++++++- .../Handlers/UpdateOrderCommandHandler.cs | 3 +- .../App/Orders/Queries/SearchOrdersQuery.cs | 20 +++++++ .../Validators/CreateOrderCommandValidator.cs | 46 ++++++++++++++++ .../Validators/SearchOrdersQueryValidator.cs | 21 ++++++++ .../Validators/UpdateOrderCommandValidator.cs | 30 +++++++++++ .../App/Payments/Dto/PaymentDto.cs | 5 ++ .../Handlers/CreatePaymentCommandHandler.cs | 1 + .../Handlers/GetPaymentByIdQueryHandler.cs | 1 + .../Handlers/SearchPaymentsQueryHandler.cs | 26 +++++++++- .../Handlers/UpdatePaymentCommandHandler.cs | 1 + .../Payments/Queries/SearchPaymentsQuery.cs | 20 +++++++ .../CreatePaymentCommandValidator.cs | 22 ++++++++ .../SearchPaymentsQueryValidator.cs | 20 +++++++ .../UpdatePaymentCommandValidator.cs | 23 ++++++++ .../App/Products/Dto/ProductDto.cs | 5 ++ .../Handlers/CreateProductCommandHandler.cs | 3 +- .../Handlers/GetProductByIdQueryHandler.cs | 3 +- .../Handlers/SearchProductsQueryHandler.cs | 25 ++++++++- .../Handlers/UpdateProductCommandHandler.cs | 3 +- .../Products/Queries/SearchProductsQuery.cs | 20 +++++++ .../CreateProductCommandValidator.cs | 29 +++++++++++ .../SearchProductsQueryValidator.cs | 20 +++++++ .../UpdateProductCommandValidator.cs | 30 +++++++++++ .../App/Stores/Dto/StoreDto.cs | 5 ++ .../Handlers/CreateStoreCommandHandler.cs | 3 +- .../Handlers/GetStoreByIdQueryHandler.cs | 3 +- .../Handlers/SearchStoresQueryHandler.cs | 25 +++++++-- .../Handlers/UpdateStoreCommandHandler.cs | 3 +- .../App/Stores/Queries/SearchStoresQuery.cs | 20 +++++++ .../Validators/CreateStoreCommandValidator.cs | 29 +++++++++++ .../Validators/SearchStoresQueryValidator.cs | 20 +++++++ .../Validators/UpdateStoreCommandValidator.cs | 30 +++++++++++ .../TakeoutSaaS.Application.csproj | 1 + 63 files changed, 904 insertions(+), 49 deletions(-) create mode 100644 src/Application/TakeoutSaaS.Application/App/Common/Behaviors/ValidationBehavior.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/CreateDeliveryOrderCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/SearchDeliveryOrdersQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/UpdateDeliveryOrderCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Validators/CreateMerchantCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Validators/SearchMerchantsQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrdersQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Validators/SearchPaymentsQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Validators/CreateProductCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Validators/SearchProductsQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Products/Validators/UpdateProductCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Validators/SearchStoresQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs index a36e2e2..fbc2277 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs @@ -49,12 +49,23 @@ public sealed class DeliveriesController : BaseApiController [HttpGet] [PermissionAuthorize("delivery:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List([FromQuery] long? orderId, [FromQuery] DeliveryStatus? status, CancellationToken cancellationToken) + public async Task>> List( + [FromQuery] long? orderId, + [FromQuery] DeliveryStatus? status, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) { var result = await _mediator.Send(new SearchDeliveryOrdersQuery { OrderId = orderId, - Status = status + Status = status, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc }, cancellationToken); return ApiResponse>.Ok(result); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs index c24ec8d..545af0d 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs @@ -49,9 +49,22 @@ public sealed class MerchantsController : BaseApiController [HttpGet] [PermissionAuthorize("merchant:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List([FromQuery] MerchantStatus? status, CancellationToken cancellationToken) + public async Task>> List( + [FromQuery] MerchantStatus? status, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchMerchantsQuery { Status = status }, cancellationToken); + var result = await _mediator.Send(new SearchMerchantsQuery + { + Status = status, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc + }, cancellationToken); return ApiResponse>.Ok(result); } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs index f66b159..b14a915 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs @@ -55,14 +55,22 @@ public sealed class OrdersController : BaseApiController [FromQuery] OrderStatus? status, [FromQuery] PaymentStatus? paymentStatus, [FromQuery] string? orderNo, - CancellationToken cancellationToken) + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) { var result = await _mediator.Send(new SearchOrdersQuery { StoreId = storeId, Status = status, PaymentStatus = paymentStatus, - OrderNo = orderNo + OrderNo = orderNo, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc }, cancellationToken); return ApiResponse>.Ok(result); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs index 93ca700..5f91315 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs @@ -49,12 +49,23 @@ public sealed class PaymentsController : BaseApiController [HttpGet] [PermissionAuthorize("payment:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List([FromQuery] long? orderId, [FromQuery] PaymentStatus? status, CancellationToken cancellationToken) + public async Task>> List( + [FromQuery] long? orderId, + [FromQuery] PaymentStatus? status, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) { var result = await _mediator.Send(new SearchPaymentsQuery { OrderId = orderId, - Status = status + Status = status, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc }, cancellationToken); return ApiResponse>.Ok(result); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs index 4c8d42c..949cf0b 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs @@ -49,13 +49,25 @@ public sealed class ProductsController : BaseApiController [HttpGet] [PermissionAuthorize("product:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List([FromQuery] long? storeId, [FromQuery] long? categoryId, [FromQuery] ProductStatus? status, CancellationToken cancellationToken) + public async Task>> List( + [FromQuery] long? storeId, + [FromQuery] long? categoryId, + [FromQuery] ProductStatus? status, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) { var result = await _mediator.Send(new SearchProductsQuery { StoreId = storeId, CategoryId = categoryId, - Status = status + Status = status, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc }, cancellationToken); return ApiResponse>.Ok(result); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs index e485df6..5680396 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs @@ -49,12 +49,23 @@ public sealed class StoresController : BaseApiController [HttpGet] [PermissionAuthorize("store:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List([FromQuery] long? merchantId, [FromQuery] StoreStatus? status, CancellationToken cancellationToken) + public async Task>> List( + [FromQuery] long? merchantId, + [FromQuery] StoreStatus? status, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) { var result = await _mediator.Send(new SearchStoresQuery { MerchantId = merchantId, - Status = status + Status = status, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc }, cancellationToken); return ApiResponse>.Ok(result); diff --git a/src/Application/TakeoutSaaS.Application/App/Common/Behaviors/ValidationBehavior.cs b/src/Application/TakeoutSaaS.Application/App/Common/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..177ebd9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Common/Behaviors/ValidationBehavior.cs @@ -0,0 +1,35 @@ +using FluentValidation; +using MediatR; + +namespace TakeoutSaaS.Application.App.Common.Behaviors; + +/// +/// MediatR 请求验证行为,统一触发 FluentValidation。 +/// +/// 请求类型。 +/// 响应类型。 +public sealed class ValidationBehavior(IEnumerable> validators) : IPipelineBehavior + where TRequest : notnull, IRequest +{ + private readonly IEnumerable> _validators = validators; + + /// + /// 执行验证并在通过时继续后续处理。 + /// + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (_validators.Any()) + { + var context = new ValidationContext(request); + var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); + var failures = validationResults.SelectMany(r => r.Errors).Where(f => f is not null).ToList(); + + if (failures.Count > 0) + { + throw new ValidationException(failures); + } + } + + return await next(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs index 21fc36c..641d67a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs @@ -81,4 +81,9 @@ public sealed class DeliveryOrderDto /// 事件列表。 /// public IReadOnlyList Events { get; init; } = Array.Empty(); + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs index 8e775e9..1a67240 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs @@ -56,6 +56,7 @@ public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository delive PickedUpAt = deliveryOrder.PickedUpAt, DeliveredAt = deliveryOrder.DeliveredAt, FailureReason = deliveryOrder.FailureReason, + CreatedAt = deliveryOrder.CreatedAt, Events = events.Select(x => new DeliveryEventDto { Id = x.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs index 8d047de..24e425f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs @@ -47,6 +47,7 @@ public sealed class GetDeliveryOrderByIdQueryHandler( PickedUpAt = deliveryOrder.PickedUpAt, DeliveredAt = deliveryOrder.DeliveredAt, FailureReason = deliveryOrder.FailureReason, + CreatedAt = deliveryOrder.CreatedAt, Events = events.Select(x => new DeliveryEventDto { Id = x.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs index 5dea6a9..f0b3750 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs @@ -23,7 +23,13 @@ public sealed class SearchDeliveryOrdersQueryHandler( var tenantId = _tenantProvider.GetCurrentTenantId(); var orders = await _deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken); - return orders.Select(order => new DeliveryOrderDto + var sorted = ApplySorting(orders, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + return paged.Select(order => new DeliveryOrderDto { Id = order.Id, TenantId = order.TenantId, @@ -37,7 +43,21 @@ public sealed class SearchDeliveryOrdersQueryHandler( DispatchedAt = order.DispatchedAt, PickedUpAt = order.PickedUpAt, DeliveredAt = order.DeliveredAt, - FailureReason = order.FailureReason + FailureReason = order.FailureReason, + CreatedAt = order.CreatedAt }).ToList(); } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection orders, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "status" => sortDescending ? orders.OrderByDescending(x => x.Status) : orders.OrderBy(x => x.Status), + "provider" => sortDescending ? orders.OrderByDescending(x => x.Provider) : orders.OrderBy(x => x.Provider), + _ => sortDescending ? orders.OrderByDescending(x => x.CreatedAt) : orders.OrderBy(x => x.CreatedAt) + }; + } } diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs index 424e2a9..c8df929 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs @@ -66,6 +66,7 @@ public sealed class UpdateDeliveryOrderCommandHandler( PickedUpAt = deliveryOrder.PickedUpAt, DeliveredAt = deliveryOrder.DeliveredAt, FailureReason = deliveryOrder.FailureReason, + CreatedAt = deliveryOrder.CreatedAt, Events = events.Select(x => new DeliveryEventDto { Id = x.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs index 53d439c..7175ac3 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs @@ -18,4 +18,24 @@ public sealed class SearchDeliveryOrdersQuery : IRequest public DeliveryStatus? Status { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(createdAt/status/provider)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/CreateDeliveryOrderCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/CreateDeliveryOrderCommandValidator.cs new file mode 100644 index 0000000..765e47f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/CreateDeliveryOrderCommandValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Deliveries.Commands; + +namespace TakeoutSaaS.Application.App.Deliveries.Validators; + +/// +/// 创建配送单命令验证器。 +/// +public sealed class CreateDeliveryOrderCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateDeliveryOrderCommandValidator() + { + RuleFor(x => x.OrderId).GreaterThan(0); + RuleFor(x => x.ProviderOrderId).MaximumLength(64); + RuleFor(x => x.CourierName).MaximumLength(64); + RuleFor(x => x.CourierPhone).MaximumLength(32); + RuleFor(x => x.FailureReason).MaximumLength(256); + RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/SearchDeliveryOrdersQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/SearchDeliveryOrdersQueryValidator.cs new file mode 100644 index 0000000..2119152 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/SearchDeliveryOrdersQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Deliveries.Queries; + +namespace TakeoutSaaS.Application.App.Deliveries.Validators; + +/// +/// 配送单列表查询验证器。 +/// +public sealed class SearchDeliveryOrdersQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchDeliveryOrdersQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.SortBy).MaximumLength(64); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/UpdateDeliveryOrderCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/UpdateDeliveryOrderCommandValidator.cs new file mode 100644 index 0000000..e2cbc19 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Validators/UpdateDeliveryOrderCommandValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Deliveries.Commands; + +namespace TakeoutSaaS.Application.App.Deliveries.Validators; + +/// +/// 更新配送单命令验证器。 +/// +public sealed class UpdateDeliveryOrderCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateDeliveryOrderCommandValidator() + { + RuleFor(x => x.DeliveryOrderId).GreaterThan(0); + RuleFor(x => x.OrderId).GreaterThan(0); + RuleFor(x => x.ProviderOrderId).MaximumLength(64); + RuleFor(x => x.CourierName).MaximumLength(64); + RuleFor(x => x.CourierPhone).MaximumLength(32); + RuleFor(x => x.FailureReason).MaximumLength(256); + RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs index 61d259f..62284b1 100644 --- a/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs +++ b/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using System.Reflection; +using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Application.App.Common.Behaviors; namespace TakeoutSaaS.Application.App.Extensions; @@ -17,6 +19,8 @@ public static class AppApplicationServiceCollectionExtensions public static IServiceCollection AddAppApplication(this IServiceCollection services) { services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); return services; } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs index d1552c8..6124ec7 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantDto.cs @@ -60,4 +60,9 @@ public sealed class MerchantDto /// 入驻时间。 /// public DateTime? JoinedAt { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs index 4b32e70..70c0982 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/CreateMerchantCommandHandler.cs @@ -49,6 +49,7 @@ public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRep ContactPhone = merchant.ContactPhone, ContactEmail = merchant.ContactEmail, Status = merchant.Status, - JoinedAt = merchant.JoinedAt + JoinedAt = merchant.JoinedAt, + CreatedAt = merchant.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs index f689743..3c2313f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantByIdQueryHandler.cs @@ -36,7 +36,8 @@ public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepo ContactPhone = merchant.ContactPhone, ContactEmail = merchant.ContactEmail, Status = merchant.Status, - JoinedAt = merchant.JoinedAt + JoinedAt = merchant.JoinedAt, + CreatedAt = merchant.CreatedAt }; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs index bfd2f9b..71d18e3 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs @@ -23,20 +23,44 @@ public sealed class SearchMerchantsQueryHandler( var tenantId = _tenantProvider.GetCurrentTenantId(); var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken); - return merchants - .Select(merchant => new MerchantDto - { - Id = merchant.Id, - TenantId = merchant.TenantId, - BrandName = merchant.BrandName, - BrandAlias = merchant.BrandAlias, - LogoUrl = merchant.LogoUrl, - Category = merchant.Category, - ContactPhone = merchant.ContactPhone, - ContactEmail = merchant.ContactEmail, - Status = merchant.Status, - JoinedAt = merchant.JoinedAt - }) + var sorted = ApplySorting(merchants, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) .ToList(); + + return paged.Select(merchant => new MerchantDto + { + Id = merchant.Id, + TenantId = merchant.TenantId, + BrandName = merchant.BrandName, + BrandAlias = merchant.BrandAlias, + LogoUrl = merchant.LogoUrl, + Category = merchant.Category, + ContactPhone = merchant.ContactPhone, + ContactEmail = merchant.ContactEmail, + Status = merchant.Status, + JoinedAt = merchant.JoinedAt, + CreatedAt = merchant.CreatedAt + }).ToList(); + } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection merchants, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "brandname" => sortDescending + ? merchants.OrderByDescending(x => x.BrandName) + : merchants.OrderBy(x => x.BrandName), + "status" => sortDescending + ? merchants.OrderByDescending(x => x.Status) + : merchants.OrderBy(x => x.Status), + _ => sortDescending + ? merchants.OrderByDescending(x => x.CreatedAt) + : merchants.OrderBy(x => x.CreatedAt) + }; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs index d825b72..38753c9 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs @@ -60,6 +60,7 @@ public sealed class UpdateMerchantCommandHandler( ContactPhone = merchant.ContactPhone, ContactEmail = merchant.ContactEmail, Status = merchant.Status, - JoinedAt = merchant.JoinedAt + JoinedAt = merchant.JoinedAt, + CreatedAt = merchant.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs index 3ec7561..b8a8b1a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs @@ -13,4 +13,24 @@ public sealed class SearchMerchantsQuery : IRequest> /// 按状态过滤。 /// public MerchantStatus? Status { get; init; } + + /// + /// 页码(从 1 开始)。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(brandName/status/createdAt)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/CreateMerchantCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/CreateMerchantCommandValidator.cs new file mode 100644 index 0000000..bd930d7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/CreateMerchantCommandValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Merchants.Commands; + +namespace TakeoutSaaS.Application.App.Merchants.Validators; + +/// +/// 创建商户命令验证器。 +/// +public sealed class CreateMerchantCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateMerchantCommandValidator() + { + RuleFor(x => x.BrandName).NotEmpty().MaximumLength(128); + RuleFor(x => x.BrandAlias).MaximumLength(64); + RuleFor(x => x.LogoUrl).MaximumLength(256); + RuleFor(x => x.Category).MaximumLength(64); + RuleFor(x => x.ContactPhone).NotEmpty().MaximumLength(32); + RuleFor(x => x.ContactEmail).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ContactEmail)); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/SearchMerchantsQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/SearchMerchantsQueryValidator.cs new file mode 100644 index 0000000..e14707f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/SearchMerchantsQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Merchants.Queries; + +namespace TakeoutSaaS.Application.App.Merchants.Validators; + +/// +/// 商户列表查询验证器。 +/// +public sealed class SearchMerchantsQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchMerchantsQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.SortBy).MaximumLength(64); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs new file mode 100644 index 0000000..6781a0b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Merchants.Commands; + +namespace TakeoutSaaS.Application.App.Merchants.Validators; + +/// +/// 更新商户命令验证器。 +/// +public sealed class UpdateMerchantCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateMerchantCommandValidator() + { + RuleFor(x => x.MerchantId).GreaterThan(0); + RuleFor(x => x.BrandName).NotEmpty().MaximumLength(128); + RuleFor(x => x.BrandAlias).MaximumLength(64); + RuleFor(x => x.LogoUrl).MaximumLength(256); + RuleFor(x => x.Category).MaximumLength(64); + RuleFor(x => x.ContactPhone).NotEmpty().MaximumLength(32); + RuleFor(x => x.ContactEmail).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ContactEmail)); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs index 0358f7c..6add72b 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs @@ -138,4 +138,9 @@ public sealed class OrderDto /// 退款申请。 /// public IReadOnlyList Refunds { get; init; } = Array.Empty(); + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs index 057b018..a7b1289 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs @@ -151,6 +151,7 @@ public sealed class CreateOrderCommandHandler( RequestedAt = x.RequestedAt, ProcessedAt = x.ProcessedAt, ReviewNotes = x.ReviewNotes - }).ToList() + }).ToList(), + CreatedAt = order.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs index dbdd1c2..5d6b791 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs @@ -97,6 +97,7 @@ public sealed class GetOrderByIdQueryHandler( RequestedAt = x.RequestedAt, ProcessedAt = x.ProcessedAt, ReviewNotes = x.ReviewNotes - }).ToList() + }).ToList(), + CreatedAt = order.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs index d1b054a..caf2a50 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs @@ -36,7 +36,13 @@ public sealed class SearchOrdersQueryHandler( .ToList(); } - return orders.Select(order => new OrderDto + var sorted = ApplySorting(orders, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + return paged.Select(order => new OrderDto { Id = order.Id, TenantId = order.TenantId, @@ -59,7 +65,22 @@ public sealed class SearchOrdersQueryHandler( FinishedAt = order.FinishedAt, CancelledAt = order.CancelledAt, CancelReason = order.CancelReason, - Remark = order.Remark + Remark = order.Remark, + CreatedAt = order.CreatedAt }).ToList(); } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection orders, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "paidat" => sortDescending ? orders.OrderByDescending(x => x.PaidAt) : orders.OrderBy(x => x.PaidAt), + "status" => sortDescending ? orders.OrderByDescending(x => x.Status) : orders.OrderBy(x => x.Status), + "payableamount" => sortDescending ? orders.OrderByDescending(x => x.PayableAmount) : orders.OrderBy(x => x.PayableAmount), + _ => sortDescending ? orders.OrderByDescending(x => x.CreatedAt) : orders.OrderBy(x => x.CreatedAt) + }; + } } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs index 3901fd9..46df4ed 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs @@ -129,6 +129,7 @@ public sealed class UpdateOrderCommandHandler( RequestedAt = x.RequestedAt, ProcessedAt = x.ProcessedAt, ReviewNotes = x.ReviewNotes - }).ToList() + }).ToList(), + CreatedAt = order.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs index 99fa7b3..6d33583 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs @@ -29,4 +29,24 @@ public sealed class SearchOrdersQuery : IRequest> /// 订单号(模糊或精确,由调用方控制)。 /// public string? OrderNo { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(createdAt/paidAt/status/payableAmount)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs new file mode 100644 index 0000000..729d166 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs @@ -0,0 +1,46 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Orders.Commands; + +namespace TakeoutSaaS.Application.App.Orders.Validators; + +/// +/// 创建订单命令验证器。 +/// +public sealed class CreateOrderCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateOrderCommandValidator() + { + RuleFor(x => x.OrderNo).NotEmpty().MaximumLength(32); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.CustomerPhone).MaximumLength(32); + RuleFor(x => x.CustomerName).MaximumLength(64); + RuleFor(x => x.TableNo).MaximumLength(32); + RuleFor(x => x.QueueNumber).MaximumLength(32); + RuleFor(x => x.CancelReason).MaximumLength(256); + RuleFor(x => x.Remark).MaximumLength(512); + RuleFor(x => x.ItemsAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.DiscountAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.PayableAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0); + + RuleFor(x => x.Items) + .NotEmpty() + .WithMessage("订单明细不能为空"); + + RuleForEach(x => x.Items).ChildRules(item => + { + item.RuleFor(i => i.ProductId).GreaterThan(0); + item.RuleFor(i => i.ProductName).NotEmpty().MaximumLength(128); + item.RuleFor(i => i.SkuName).MaximumLength(128); + item.RuleFor(i => i.Unit).MaximumLength(16); + item.RuleFor(i => i.Quantity).GreaterThan(0); + item.RuleFor(i => i.UnitPrice).GreaterThanOrEqualTo(0); + item.RuleFor(i => i.DiscountAmount).GreaterThanOrEqualTo(0); + item.RuleFor(i => i.SubTotal).GreaterThanOrEqualTo(0); + item.RuleFor(i => i.AttributesJson).MaximumLength(4000); + }); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrdersQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrdersQueryValidator.cs new file mode 100644 index 0000000..b5dac41 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/SearchOrdersQueryValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Orders.Queries; + +namespace TakeoutSaaS.Application.App.Orders.Validators; + +/// +/// 订单列表查询验证器。 +/// +public sealed class SearchOrdersQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchOrdersQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.SortBy).MaximumLength(64); + RuleFor(x => x.OrderNo).MaximumLength(32); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs new file mode 100644 index 0000000..2dba2f7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Orders.Commands; + +namespace TakeoutSaaS.Application.App.Orders.Validators; + +/// +/// 更新订单命令验证器。 +/// +public sealed class UpdateOrderCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateOrderCommandValidator() + { + RuleFor(x => x.OrderId).GreaterThan(0); + RuleFor(x => x.OrderNo).NotEmpty().MaximumLength(32); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.CustomerPhone).MaximumLength(32); + RuleFor(x => x.CustomerName).MaximumLength(64); + RuleFor(x => x.TableNo).MaximumLength(32); + RuleFor(x => x.QueueNumber).MaximumLength(32); + RuleFor(x => x.CancelReason).MaximumLength(256); + RuleFor(x => x.Remark).MaximumLength(512); + RuleFor(x => x.ItemsAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.DiscountAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.PayableAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs index 807d0f1..d127427 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs @@ -71,4 +71,9 @@ public sealed class PaymentDto /// 退款记录。 /// public IReadOnlyList Refunds { get; init; } = Array.Empty(); + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs index 463d903..793a2a9 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs @@ -52,6 +52,7 @@ public sealed class CreatePaymentCommandHandler(IPaymentRepository paymentReposi PaidAt = payment.PaidAt, Remark = payment.Remark, Payload = payment.Payload, + CreatedAt = payment.CreatedAt, Refunds = refunds.Select(x => new PaymentRefundDto { Id = x.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs index 225697c..8e5db7a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs @@ -45,6 +45,7 @@ public sealed class GetPaymentByIdQueryHandler( PaidAt = payment.PaidAt, Remark = payment.Remark, Payload = payment.Payload, + CreatedAt = payment.CreatedAt, Refunds = refunds.Select(x => new PaymentRefundDto { Id = x.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs index 45ff7cb..be6c10a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs @@ -1,6 +1,7 @@ using MediatR; using TakeoutSaaS.Application.App.Payments.Dto; using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.Entities; using TakeoutSaaS.Domain.Payments.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -28,7 +29,13 @@ public sealed class SearchPaymentsQueryHandler( payments = payments.Where(x => x.OrderId == request.OrderId.Value).ToList(); } - return payments.Select(payment => new PaymentDto + var sorted = ApplySorting(payments, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + return paged.Select(payment => new PaymentDto { Id = payment.Id, TenantId = payment.TenantId, @@ -40,7 +47,22 @@ public sealed class SearchPaymentsQueryHandler( ChannelTransactionId = payment.ChannelTransactionId, PaidAt = payment.PaidAt, Remark = payment.Remark, - Payload = payment.Payload + Payload = payment.Payload, + CreatedAt = payment.CreatedAt }).ToList(); } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection payments, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "paidat" => sortDescending ? payments.OrderByDescending(x => x.PaidAt) : payments.OrderBy(x => x.PaidAt), + "status" => sortDescending ? payments.OrderByDescending(x => x.Status) : payments.OrderBy(x => x.Status), + "amount" => sortDescending ? payments.OrderByDescending(x => x.Amount) : payments.OrderBy(x => x.Amount), + _ => sortDescending ? payments.OrderByDescending(x => x.CreatedAt) : payments.OrderBy(x => x.CreatedAt) + }; + } } diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs index f55a37d..e0e9fec 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs @@ -62,6 +62,7 @@ public sealed class UpdatePaymentCommandHandler( PaidAt = payment.PaidAt, Remark = payment.Remark, Payload = payment.Payload, + CreatedAt = payment.CreatedAt, Refunds = refunds.Select(x => new PaymentRefundDto { Id = x.Id, diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs index 91b5bad..efe8861 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs @@ -18,4 +18,24 @@ public sealed class SearchPaymentsQuery : IRequest> /// 支付状态。 /// public PaymentStatus? Status { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(createdAt/paidAt/status/amount)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs new file mode 100644 index 0000000..f7e7abb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Payments.Commands; + +namespace TakeoutSaaS.Application.App.Payments.Validators; + +/// +/// 创建支付记录命令验证器。 +/// +public sealed class CreatePaymentCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreatePaymentCommandValidator() + { + RuleFor(x => x.OrderId).GreaterThan(0); + RuleFor(x => x.Amount).GreaterThan(0); + RuleFor(x => x.TradeNo).MaximumLength(64); + RuleFor(x => x.ChannelTransactionId).MaximumLength(64); + RuleFor(x => x.Remark).MaximumLength(256); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/SearchPaymentsQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/SearchPaymentsQueryValidator.cs new file mode 100644 index 0000000..fe7580e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/SearchPaymentsQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Payments.Queries; + +namespace TakeoutSaaS.Application.App.Payments.Validators; + +/// +/// 支付记录查询验证器。 +/// +public sealed class SearchPaymentsQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchPaymentsQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.SortBy).MaximumLength(64); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs new file mode 100644 index 0000000..997e07b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Payments.Commands; + +namespace TakeoutSaaS.Application.App.Payments.Validators; + +/// +/// 更新支付记录命令验证器。 +/// +public sealed class UpdatePaymentCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdatePaymentCommandValidator() + { + RuleFor(x => x.PaymentId).GreaterThan(0); + RuleFor(x => x.OrderId).GreaterThan(0); + RuleFor(x => x.Amount).GreaterThan(0); + RuleFor(x => x.TradeNo).MaximumLength(64); + RuleFor(x => x.ChannelTransactionId).MaximumLength(64); + RuleFor(x => x.Remark).MaximumLength(256); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs b/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs index 7661594..bfcd321 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs @@ -112,4 +112,9 @@ public sealed class ProductDto /// 是否推荐。 /// public bool IsFeatured { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs index 46201fa..2bf1e33 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs @@ -72,6 +72,7 @@ public sealed class CreateProductCommandHandler(IProductRepository productReposi EnableDineIn = product.EnableDineIn, EnablePickup = product.EnablePickup, EnableDelivery = product.EnableDelivery, - IsFeatured = product.IsFeatured + IsFeatured = product.IsFeatured, + CreatedAt = product.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs index 018c325..c3b6a60 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs @@ -47,6 +47,7 @@ public sealed class GetProductByIdQueryHandler( EnableDineIn = product.EnableDineIn, EnablePickup = product.EnablePickup, EnableDelivery = product.EnableDelivery, - IsFeatured = product.IsFeatured + IsFeatured = product.IsFeatured, + CreatedAt = product.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs index d24784e..5527166 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs @@ -28,7 +28,27 @@ public sealed class SearchProductsQueryHandler( products = products.Where(x => x.StoreId == request.StoreId.Value).ToList(); } - return products.Select(MapToDto).ToList(); + var sorted = ApplySorting(products, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + return paged.Select(MapToDto).ToList(); + } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection products, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "name" => sortDescending ? products.OrderByDescending(x => x.Name) : products.OrderBy(x => x.Name), + "price" => sortDescending ? products.OrderByDescending(x => x.Price) : products.OrderBy(x => x.Price), + "status" => sortDescending ? products.OrderByDescending(x => x.Status) : products.OrderBy(x => x.Status), + _ => sortDescending ? products.OrderByDescending(x => x.CreatedAt) : products.OrderBy(x => x.CreatedAt) + }; } private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new() @@ -52,6 +72,7 @@ public sealed class SearchProductsQueryHandler( EnableDineIn = product.EnableDineIn, EnablePickup = product.EnablePickup, EnableDelivery = product.EnableDelivery, - IsFeatured = product.IsFeatured + IsFeatured = product.IsFeatured, + CreatedAt = product.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs index df33a23..e616788 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs @@ -82,6 +82,7 @@ public sealed class UpdateProductCommandHandler( EnableDineIn = product.EnableDineIn, EnablePickup = product.EnablePickup, EnableDelivery = product.EnableDelivery, - IsFeatured = product.IsFeatured + IsFeatured = product.IsFeatured, + CreatedAt = product.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs index 07541c8..e7d3c55 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs @@ -23,4 +23,24 @@ public sealed class SearchProductsQuery : IRequest> /// 状态过滤。 /// public ProductStatus? Status { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(name/price/status/createdAt)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Validators/CreateProductCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Products/Validators/CreateProductCommandValidator.cs new file mode 100644 index 0000000..91bd6ac --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Validators/CreateProductCommandValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Products.Commands; + +namespace TakeoutSaaS.Application.App.Products.Validators; + +/// +/// 创建商品命令验证器。 +/// +public sealed class CreateProductCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateProductCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.CategoryId).GreaterThan(0); + RuleFor(x => x.SpuCode).NotEmpty().MaximumLength(32); + RuleFor(x => x.Name).NotEmpty().MaximumLength(128); + RuleFor(x => x.Subtitle).MaximumLength(256); + RuleFor(x => x.Unit).MaximumLength(16); + RuleFor(x => x.Price).GreaterThanOrEqualTo(0); + RuleFor(x => x.OriginalPrice).GreaterThanOrEqualTo(0).When(x => x.OriginalPrice.HasValue); + RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue); + RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue); + RuleFor(x => x.CoverImage).MaximumLength(256); + RuleFor(x => x.GalleryImages).MaximumLength(1024); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Validators/SearchProductsQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Products/Validators/SearchProductsQueryValidator.cs new file mode 100644 index 0000000..23e3962 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Validators/SearchProductsQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Products.Queries; + +namespace TakeoutSaaS.Application.App.Products.Validators; + +/// +/// 商品列表查询验证器。 +/// +public sealed class SearchProductsQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchProductsQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.SortBy).MaximumLength(64); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Validators/UpdateProductCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Products/Validators/UpdateProductCommandValidator.cs new file mode 100644 index 0000000..20200f2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Validators/UpdateProductCommandValidator.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Products.Commands; + +namespace TakeoutSaaS.Application.App.Products.Validators; + +/// +/// 更新商品命令验证器。 +/// +public sealed class UpdateProductCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateProductCommandValidator() + { + RuleFor(x => x.ProductId).GreaterThan(0); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.CategoryId).GreaterThan(0); + RuleFor(x => x.SpuCode).NotEmpty().MaximumLength(32); + RuleFor(x => x.Name).NotEmpty().MaximumLength(128); + RuleFor(x => x.Subtitle).MaximumLength(256); + RuleFor(x => x.Unit).MaximumLength(16); + RuleFor(x => x.Price).GreaterThanOrEqualTo(0); + RuleFor(x => x.OriginalPrice).GreaterThanOrEqualTo(0).When(x => x.OriginalPrice.HasValue); + RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue); + RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue); + RuleFor(x => x.CoverImage).MaximumLength(256); + RuleFor(x => x.GalleryImages).MaximumLength(1024); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs index e924b43..5412717 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs @@ -111,4 +111,9 @@ public sealed class StoreDto /// 支持配送。 /// public bool SupportsDelivery { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs index 56f9d9c..f254d6f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs @@ -72,6 +72,7 @@ public sealed class CreateStoreCommandHandler(IStoreRepository storeRepository, DeliveryRadiusKm = store.DeliveryRadiusKm, SupportsDineIn = store.SupportsDineIn, SupportsPickup = store.SupportsPickup, - SupportsDelivery = store.SupportsDelivery + SupportsDelivery = store.SupportsDelivery, + CreatedAt = store.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs index 4cb8cb0..995ddde 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs @@ -47,6 +47,7 @@ public sealed class GetStoreByIdQueryHandler( DeliveryRadiusKm = store.DeliveryRadiusKm, SupportsDineIn = store.SupportsDineIn, SupportsPickup = store.SupportsPickup, - SupportsDelivery = store.SupportsDelivery + SupportsDelivery = store.SupportsDelivery, + CreatedAt = store.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs index 4efe538..0141ae4 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs @@ -28,9 +28,27 @@ public sealed class SearchStoresQueryHandler( stores = stores.Where(x => x.MerchantId == request.MerchantId.Value).ToList(); } - return stores - .Select(MapToDto) + var sorted = ApplySorting(stores, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) .ToList(); + + return paged.Select(MapToDto).ToList(); + } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection stores, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "name" => sortDescending ? stores.OrderByDescending(x => x.Name) : stores.OrderBy(x => x.Name), + "code" => sortDescending ? stores.OrderByDescending(x => x.Code) : stores.OrderBy(x => x.Code), + "status" => sortDescending ? stores.OrderByDescending(x => x.Status) : stores.OrderBy(x => x.Status), + _ => sortDescending ? stores.OrderByDescending(x => x.CreatedAt) : stores.OrderBy(x => x.CreatedAt) + }; } private static StoreDto MapToDto(Domain.Stores.Entities.Store store) => new() @@ -54,6 +72,7 @@ public sealed class SearchStoresQueryHandler( DeliveryRadiusKm = store.DeliveryRadiusKm, SupportsDineIn = store.SupportsDineIn, SupportsPickup = store.SupportsPickup, - SupportsDelivery = store.SupportsDelivery + SupportsDelivery = store.SupportsDelivery, + CreatedAt = store.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs index 1680932..934bb54 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs @@ -82,6 +82,7 @@ public sealed class UpdateStoreCommandHandler( DeliveryRadiusKm = store.DeliveryRadiusKm, SupportsDineIn = store.SupportsDineIn, SupportsPickup = store.SupportsPickup, - SupportsDelivery = store.SupportsDelivery + SupportsDelivery = store.SupportsDelivery, + CreatedAt = store.CreatedAt }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs index 085a557..8a0b75b 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs @@ -18,4 +18,24 @@ public sealed class SearchStoresQuery : IRequest> /// 状态过滤。 /// public StoreStatus? Status { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(name/code/status/createdAt)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs new file mode 100644 index 0000000..a947ae2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 创建门店命令验证器。 +/// +public sealed class CreateStoreCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateStoreCommandValidator() + { + RuleFor(x => x.MerchantId).GreaterThan(0); + RuleFor(x => x.Code).NotEmpty().MaximumLength(32); + RuleFor(x => x.Name).NotEmpty().MaximumLength(128); + RuleFor(x => x.Phone).MaximumLength(32); + RuleFor(x => x.ManagerName).MaximumLength(64); + RuleFor(x => x.Province).MaximumLength(64); + RuleFor(x => x.City).MaximumLength(64); + RuleFor(x => x.District).MaximumLength(64); + RuleFor(x => x.Address).MaximumLength(256); + RuleFor(x => x.Announcement).MaximumLength(512); + RuleFor(x => x.Tags).MaximumLength(256); + RuleFor(x => x.DeliveryRadiusKm).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/SearchStoresQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/SearchStoresQueryValidator.cs new file mode 100644 index 0000000..67ea047 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/SearchStoresQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Queries; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 门店列表查询验证器。 +/// +public sealed class SearchStoresQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchStoresQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.SortBy).MaximumLength(64); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs new file mode 100644 index 0000000..d021aae --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 更新门店命令验证器。 +/// +public sealed class UpdateStoreCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateStoreCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.MerchantId).GreaterThan(0); + RuleFor(x => x.Code).NotEmpty().MaximumLength(32); + RuleFor(x => x.Name).NotEmpty().MaximumLength(128); + RuleFor(x => x.Phone).MaximumLength(32); + RuleFor(x => x.ManagerName).MaximumLength(64); + RuleFor(x => x.Province).MaximumLength(64); + RuleFor(x => x.City).MaximumLength(64); + RuleFor(x => x.District).MaximumLength(64); + RuleFor(x => x.Address).MaximumLength(256); + RuleFor(x => x.Announcement).MaximumLength(512); + RuleFor(x => x.Tags).MaximumLength(256); + RuleFor(x => x.DeliveryRadiusKm).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index e320f29..7e49e3f 100644 --- a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj +++ b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj @@ -9,6 +9,7 @@ + From 92e4f8caa4dc06c3c21fe8026fa80ef9c9a84f63 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 10:52:06 +0800 Subject: [PATCH 24/56] =?UTF-8?q?docs:=E5=88=A0=E9=99=A4=E8=BF=87=E6=97=B6?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/04A_管理后台API.md | 93 ------------------------------- Document/04B_小程序API.md | 108 ------------------------------------ 2 files changed, 201 deletions(-) delete mode 100644 Document/04A_管理后台API.md delete mode 100644 Document/04B_小程序API.md diff --git a/Document/04A_管理后台API.md b/Document/04A_管理后台API.md deleted file mode 100644 index 9a2d7cf..0000000 --- a/Document/04A_管理后台API.md +++ /dev/null @@ -1,93 +0,0 @@ -# 管理后台 API 设计(Admin API) - -- 项目:TakeoutSaaS.AdminApi -- 版本前缀:/api/admin/v1 -- 认证:JWT + RBAC(平台、租户、商家角色) -- 租户识别:X-Tenant-Id 头或 Token Claim - -## 1. 通用规范 -- Content-Type: application/json -- 成功响应 -{ - "success": true, - "code": 200, - "message": "OK", - "data": {} -} -- 失败响应 -{ - "success": false, - "code": 422, - "message": "业务异常" -} - -## 2. 认证与权限 -- POST /api/admin/v1/auth/login -- POST /api/admin/v1/auth/refresh -- GET /api/admin/v1/auth/profile -- 角色:PlatformAdmin、TenantAdmin、MerchantAdmin、Staff - -## 3. 租户与商家管理 -- 租户 - - GET /api/admin/v1/tenants - - POST /api/admin/v1/tenants - - PUT /api/admin/v1/tenants/{id} - - PATCH/api/admin/v1/tenants/{id}/status -- 商家 - - GET /api/admin/v1/merchants - - POST /api/admin/v1/merchants - - GET /api/admin/v1/merchants/{id} - - PUT /api/admin/v1/merchants/{id} - - DELETE /api/admin/v1/merchants/{id} -- 门店 - - GET /api/admin/v1/stores - - POST /api/admin/v1/stores - -## 4. 菜品管理 -- 分类 - - GET /api/admin/v1/categories - - POST /api/admin/v1/categories - - PUT /api/admin/v1/categories/{id} - - DELETE /api/admin/v1/categories/{id} -- 菜品 - - GET /api/admin/v1/dishes - - POST /api/admin/v1/dishes - - GET /api/admin/v1/dishes/{id} - - PUT /api/admin/v1/dishes/{id} - - PATCH/api/admin/v1/dishes/batch-status - -## 5. 订单与售后 -- 订单 - - GET /api/admin/v1/orders - - GET /api/admin/v1/orders/{id} - - POST /api/admin/v1/orders/{id}/accept - - POST /api/admin/v1/orders/{id}/cook - - POST /api/admin/v1/orders/{id}/deliver - - POST /api/admin/v1/orders/{id}/complete - - POST /api/admin/v1/orders/{id}/cancel -- 售后 - - GET /api/admin/v1/refunds - - POST /api/admin/v1/refunds/{id}/approve - - POST /api/admin/v1/refunds/{id}/reject - -## 6. 营销与用户运营 -- 优惠券 - - GET /api/admin/v1/coupons - - POST /api/admin/v1/coupons - - PUT /api/admin/v1/coupons/{id} - - PATCH/api/admin/v1/coupons/{id}/status -- 评价 - - GET /api/admin/v1/reviews - - POST /api/admin/v1/reviews/{id}/reply - -## 7. 统计报表 -- GET /api/admin/v1/statistics/merchant/overview?merchantId= -- GET /api/admin/v1/statistics/platform/overview - -## 8. 文件上传 -- POST /api/admin/v1/files/upload (multipart/form-data) - -## 9. WebSocket(可选) -- ws://{host}/ws/admin?token=xxx -- 主题:order.new、order.status、refund.updated - diff --git a/Document/04B_小程序API.md b/Document/04B_小程序API.md deleted file mode 100644 index 6a1e340..0000000 --- a/Document/04B_小程序API.md +++ /dev/null @@ -1,108 +0,0 @@ -# 小程序/用户端 API 设计(Mini API) - -- 项目:TakeoutSaaS.MiniApi -- 版本前缀:/api/mini/v1 -- 认证:JWT(小程序登录态)/ 第三方登录(微信/支付宝) -- 租户识别:X-Tenant-Id 头或域名/小程序场景参数 - -## 1. 通用规范 -- Content-Type: application/json -- 成功响应 -{ - "success": true, - "code": 200, - "message": "OK", - "data": {} -} - -## 2. 认证登录 -- 微信登录 - - POST /api/mini/v1/auth/wechat/login - - { code, encryptedData?, iv? } -- 刷新Token - - POST /api/mini/v1/auth/refresh -- 获取用户信息 - - GET /api/mini/v1/me - -## 3. 商家与门店 -- 获取推荐商家 - - GET /api/mini/v1/merchants/recommend?lat=&lng=&pageIndex=&pageSize= -- 商家详情(含门店与公告) - - GET /api/mini/v1/merchants/{id} -- 门店列表(按距离) - - GET /api/mini/v1/merchants/{id}/stores?lat=&lng= - -## 4. 菜品与分类 -- 分类列表 - - GET /api/mini/v1/categories?merchantId= -- 菜品列表 - - GET /api/mini/v1/dishes?merchantId=&categoryId=&keyword=&sort= -- 菜品详情 - - GET /api/mini/v1/dishes/{id} - -## 5. 购物车 -- 获取购物车 - - GET /api/mini/v1/cart?merchantId= -- 同步购物车(幂等) - - PUT /api/mini/v1/cart - - { merchantId, items:[{dishId,specId?,quantity}] } -- 清空购物车 - - DELETE /api/mini/v1/cart?merchantId= - -## 6. 地址簿 -- 地址列表 - - GET /api/mini/v1/addresses -- 新增地址 - - POST /api/mini/v1/addresses -- 更新地址 - - PUT /api/mini/v1/addresses/{id} -- 删除地址 - - DELETE /api/mini/v1/addresses/{id} -- 设为默认地址 - - POST /api/mini/v1/addresses/{id}/default - -## 7. 订单 -- 创建订单(下单) - - POST /api/mini/v1/orders - - { merchantId, storeId, items:[{dishId,specId?,quantity}], addressId, remark?, couponId? } -- 订单列表 - - GET /api/mini/v1/orders?status=&pageIndex=&pageSize= -- 订单详情 - - GET /api/mini/v1/orders/{id} -- 取消订单 - - POST /api/mini/v1/orders/{id}/cancel { reason } -- 再来一单 - - POST /api/mini/v1/orders/{id}/reorder - -## 8. 支付 -- 预下单(获取支付参数) - - POST /api/mini/v1/payments - - { orderId, method: wechat|alipay } -- 查询支付状态 - - GET /api/mini/v1/payments/{paymentNo} -- 第三方回调(回调专用) - - POST /api/mini/v1/payments/callback/wechat - - POST /api/mini/v1/payments/callback/alipay - -## 9. 优惠券 -- 可领取优惠券列表 - - GET /api/mini/v1/coupons/available?merchantId= -- 领取优惠券 - - POST /api/mini/v1/coupons/{id}/receive -- 我的优惠券 - - GET /api/mini/v1/user-coupons?status= - -## 10. 评价 -- 发表评价 - - POST /api/mini/v1/reviews { orderId, rating, content?, images?[] } -- 商家评价列表 - - GET /api/mini/v1/reviews?merchantId=&rating=&page= - -## 11. 文件上传 -- 上传评价图片/头像 - - POST /api/mini/v1/files/upload (multipart/form-data) - -## 12. WebSocket(可选) -- ws://{host}/ws/mini?token=xxx -- 主题:order.status, payment.success - From e8777faf71f789dddb9588ef2c1b3c608ba97119 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 11:13:14 +0800 Subject: [PATCH 25/56] =?UTF-8?q?feat:=20=E5=88=97=E8=A1=A8=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=88=86=E9=A1=B5=E6=8E=92=E5=BA=8F=E4=B8=8E=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DeliveriesController.cs | 6 +-- .../Controllers/MerchantsController.cs | 4 +- .../Controllers/OrdersController.cs | 6 +-- .../Controllers/PaymentsController.cs | 6 +-- .../Controllers/ProductsController.cs | 6 +-- .../Controllers/StoresController.cs | 6 +-- .../SearchDeliveryOrdersQueryHandler.cs | 9 ++-- .../Queries/SearchDeliveryOrdersQuery.cs | 3 +- .../Handlers/SearchMerchantsQueryHandler.cs | 9 ++-- .../Merchants/Queries/SearchMerchantsQuery.cs | 3 +- .../Handlers/SearchOrdersQueryHandler.cs | 9 ++-- .../App/Orders/Queries/SearchOrdersQuery.cs | 3 +- .../Handlers/SearchPaymentsQueryHandler.cs | 9 ++-- .../Payments/Queries/SearchPaymentsQuery.cs | 3 +- .../Handlers/SearchProductsQueryHandler.cs | 8 ++-- .../Products/Queries/SearchProductsQuery.cs | 3 +- .../Handlers/SearchStoresQueryHandler.cs | 8 ++-- .../App/Stores/Queries/SearchStoresQuery.cs | 3 +- .../Results/PagedResult.cs | 47 +++++++++++++++++++ 19 files changed, 110 insertions(+), 41 deletions(-) create mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs index fbc2277..45e9aa0 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs @@ -48,8 +48,8 @@ public sealed class DeliveriesController : BaseApiController /// [HttpGet] [PermissionAuthorize("delivery:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( [FromQuery] long? orderId, [FromQuery] DeliveryStatus? status, [FromQuery] int page = 1, @@ -68,7 +68,7 @@ public sealed class DeliveriesController : BaseApiController SortDescending = sortDesc }, cancellationToken); - return ApiResponse>.Ok(result); + return ApiResponse>.Ok(result); } /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs index 545af0d..5bbe4bf 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs @@ -49,7 +49,7 @@ public sealed class MerchantsController : BaseApiController [HttpGet] [PermissionAuthorize("merchant:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( + public async Task>> List( [FromQuery] MerchantStatus? status, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, @@ -65,7 +65,7 @@ public sealed class MerchantsController : BaseApiController SortBy = sortBy, SortDescending = sortDesc }, cancellationToken); - return ApiResponse>.Ok(result); + return ApiResponse>.Ok(result); } /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs index b14a915..bfcf6b3 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs @@ -49,8 +49,8 @@ public sealed class OrdersController : BaseApiController /// [HttpGet] [PermissionAuthorize("order:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( [FromQuery] long? storeId, [FromQuery] OrderStatus? status, [FromQuery] PaymentStatus? paymentStatus, @@ -73,7 +73,7 @@ public sealed class OrdersController : BaseApiController SortDescending = sortDesc }, cancellationToken); - return ApiResponse>.Ok(result); + return ApiResponse>.Ok(result); } /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs index 5f91315..30d87a8 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs @@ -48,8 +48,8 @@ public sealed class PaymentsController : BaseApiController /// [HttpGet] [PermissionAuthorize("payment:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( [FromQuery] long? orderId, [FromQuery] PaymentStatus? status, [FromQuery] int page = 1, @@ -68,7 +68,7 @@ public sealed class PaymentsController : BaseApiController SortDescending = sortDesc }, cancellationToken); - return ApiResponse>.Ok(result); + return ApiResponse>.Ok(result); } /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs index 949cf0b..2064156 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs @@ -48,8 +48,8 @@ public sealed class ProductsController : BaseApiController /// [HttpGet] [PermissionAuthorize("product:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( [FromQuery] long? storeId, [FromQuery] long? categoryId, [FromQuery] ProductStatus? status, @@ -70,7 +70,7 @@ public sealed class ProductsController : BaseApiController SortDescending = sortDesc }, cancellationToken); - return ApiResponse>.Ok(result); + return ApiResponse>.Ok(result); } /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs index 5680396..8d35bcf 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs @@ -48,8 +48,8 @@ public sealed class StoresController : BaseApiController /// [HttpGet] [PermissionAuthorize("store:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( [FromQuery] long? merchantId, [FromQuery] StoreStatus? status, [FromQuery] int page = 1, @@ -68,7 +68,7 @@ public sealed class StoresController : BaseApiController SortDescending = sortDesc }, cancellationToken); - return ApiResponse>.Ok(result); + return ApiResponse>.Ok(result); } /// diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs index f0b3750..cc748db 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.App.Deliveries.Dto; using TakeoutSaaS.Application.App.Deliveries.Queries; using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Deliveries.Handlers; @@ -12,13 +13,13 @@ namespace TakeoutSaaS.Application.App.Deliveries.Handlers; public sealed class SearchDeliveryOrdersQueryHandler( IDeliveryRepository deliveryRepository, ITenantProvider tenantProvider) - : IRequestHandler> + : IRequestHandler> { private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// - public async Task> Handle(SearchDeliveryOrdersQuery request, CancellationToken cancellationToken) + public async Task> Handle(SearchDeliveryOrdersQuery request, CancellationToken cancellationToken) { var tenantId = _tenantProvider.GetCurrentTenantId(); var orders = await _deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken); @@ -29,7 +30,7 @@ public sealed class SearchDeliveryOrdersQueryHandler( .Take(request.PageSize) .ToList(); - return paged.Select(order => new DeliveryOrderDto + var items = paged.Select(order => new DeliveryOrderDto { Id = order.Id, TenantId = order.TenantId, @@ -46,6 +47,8 @@ public sealed class SearchDeliveryOrdersQueryHandler( FailureReason = order.FailureReason, CreatedAt = order.CreatedAt }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, orders.Count); } private static IOrderedEnumerable ApplySorting( diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs index 7175ac3..751d90c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs @@ -1,13 +1,14 @@ using MediatR; using TakeoutSaaS.Application.App.Deliveries.Dto; using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.App.Deliveries.Queries; /// /// 配送单列表查询。 /// -public sealed class SearchDeliveryOrdersQuery : IRequest> +public sealed class SearchDeliveryOrdersQuery : IRequest> { /// /// 订单 ID(可选)。 diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs index 71d18e3..8d46242 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/SearchMerchantsQueryHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.App.Merchants.Dto; using TakeoutSaaS.Application.App.Merchants.Queries; using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Merchants.Handlers; @@ -12,13 +13,13 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers; public sealed class SearchMerchantsQueryHandler( IMerchantRepository merchantRepository, ITenantProvider tenantProvider) - : IRequestHandler> + : IRequestHandler> { private readonly IMerchantRepository _merchantRepository = merchantRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// - public async Task> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken) + public async Task> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken) { var tenantId = _tenantProvider.GetCurrentTenantId(); var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken); @@ -29,7 +30,7 @@ public sealed class SearchMerchantsQueryHandler( .Take(request.PageSize) .ToList(); - return paged.Select(merchant => new MerchantDto + var items = paged.Select(merchant => new MerchantDto { Id = merchant.Id, TenantId = merchant.TenantId, @@ -43,6 +44,8 @@ public sealed class SearchMerchantsQueryHandler( JoinedAt = merchant.JoinedAt, CreatedAt = merchant.CreatedAt }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, merchants.Count); } private static IOrderedEnumerable ApplySorting( diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs index b8a8b1a..b3ca969 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/SearchMerchantsQuery.cs @@ -1,13 +1,14 @@ using MediatR; using TakeoutSaaS.Application.App.Merchants.Dto; using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.App.Merchants.Queries; /// /// 搜索商户列表。 /// -public sealed class SearchMerchantsQuery : IRequest> +public sealed class SearchMerchantsQuery : IRequest> { /// /// 按状态过滤。 diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs index caf2a50..867332b 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.App.Orders.Dto; using TakeoutSaaS.Application.App.Orders.Queries; using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Orders.Handlers; @@ -12,13 +13,13 @@ namespace TakeoutSaaS.Application.App.Orders.Handlers; public sealed class SearchOrdersQueryHandler( IOrderRepository orderRepository, ITenantProvider tenantProvider) - : IRequestHandler> + : IRequestHandler> { private readonly IOrderRepository _orderRepository = orderRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// - public async Task> Handle(SearchOrdersQuery request, CancellationToken cancellationToken) + public async Task> Handle(SearchOrdersQuery request, CancellationToken cancellationToken) { var tenantId = _tenantProvider.GetCurrentTenantId(); var orders = await _orderRepository.SearchAsync(tenantId, request.Status, request.PaymentStatus, cancellationToken); @@ -42,7 +43,7 @@ public sealed class SearchOrdersQueryHandler( .Take(request.PageSize) .ToList(); - return paged.Select(order => new OrderDto + var items = paged.Select(order => new OrderDto { Id = order.Id, TenantId = order.TenantId, @@ -68,6 +69,8 @@ public sealed class SearchOrdersQueryHandler( Remark = order.Remark, CreatedAt = order.CreatedAt }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, orders.Count); } private static IOrderedEnumerable ApplySorting( diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs index 6d33583..74c6753 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs @@ -2,13 +2,14 @@ using MediatR; using TakeoutSaaS.Application.App.Orders.Dto; using TakeoutSaaS.Domain.Orders.Enums; using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.App.Orders.Queries; /// /// 订单列表查询。 /// -public sealed class SearchOrdersQuery : IRequest> +public sealed class SearchOrdersQuery : IRequest> { /// /// 门店 ID(可选)。 diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs index be6c10a..9d6c4ae 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs @@ -3,6 +3,7 @@ using TakeoutSaaS.Application.App.Payments.Dto; using TakeoutSaaS.Application.App.Payments.Queries; using TakeoutSaaS.Domain.Payments.Entities; using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Payments.Handlers; @@ -13,13 +14,13 @@ namespace TakeoutSaaS.Application.App.Payments.Handlers; public sealed class SearchPaymentsQueryHandler( IPaymentRepository paymentRepository, ITenantProvider tenantProvider) - : IRequestHandler> + : IRequestHandler> { private readonly IPaymentRepository _paymentRepository = paymentRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// - public async Task> Handle(SearchPaymentsQuery request, CancellationToken cancellationToken) + public async Task> Handle(SearchPaymentsQuery request, CancellationToken cancellationToken) { var tenantId = _tenantProvider.GetCurrentTenantId(); var payments = await _paymentRepository.SearchAsync(tenantId, request.Status, cancellationToken); @@ -35,7 +36,7 @@ public sealed class SearchPaymentsQueryHandler( .Take(request.PageSize) .ToList(); - return paged.Select(payment => new PaymentDto + var items = paged.Select(payment => new PaymentDto { Id = payment.Id, TenantId = payment.TenantId, @@ -50,6 +51,8 @@ public sealed class SearchPaymentsQueryHandler( Payload = payment.Payload, CreatedAt = payment.CreatedAt }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, payments.Count); } private static IOrderedEnumerable ApplySorting( diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs index efe8861..2fbf13f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs @@ -1,13 +1,14 @@ using MediatR; using TakeoutSaaS.Application.App.Payments.Dto; using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.App.Payments.Queries; /// /// 支付记录列表查询。 /// -public sealed class SearchPaymentsQuery : IRequest> +public sealed class SearchPaymentsQuery : IRequest> { /// /// 订单 ID(可选)。 diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs index 5527166..70e91f2 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.App.Products.Dto; using TakeoutSaaS.Application.App.Products.Queries; using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Products.Handlers; @@ -12,13 +13,13 @@ namespace TakeoutSaaS.Application.App.Products.Handlers; public sealed class SearchProductsQueryHandler( IProductRepository productRepository, ITenantProvider tenantProvider) - : IRequestHandler> + : IRequestHandler> { private readonly IProductRepository _productRepository = productRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// - public async Task> Handle(SearchProductsQuery request, CancellationToken cancellationToken) + public async Task> Handle(SearchProductsQuery request, CancellationToken cancellationToken) { var tenantId = _tenantProvider.GetCurrentTenantId(); var products = await _productRepository.SearchAsync(tenantId, request.CategoryId, request.Status, cancellationToken); @@ -34,7 +35,8 @@ public sealed class SearchProductsQueryHandler( .Take(request.PageSize) .ToList(); - return paged.Select(MapToDto).ToList(); + var items = paged.Select(MapToDto).ToList(); + return new PagedResult(items, request.Page, request.PageSize, products.Count); } private static IOrderedEnumerable ApplySorting( diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs index e7d3c55..b1d4b31 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs @@ -1,13 +1,14 @@ using MediatR; using TakeoutSaaS.Application.App.Products.Dto; using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.App.Products.Queries; /// /// 商品列表查询。 /// -public sealed class SearchProductsQuery : IRequest> +public sealed class SearchProductsQuery : IRequest> { /// /// 门店 ID(可选)。 diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs index 0141ae4..1bb609d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -12,13 +13,13 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; public sealed class SearchStoresQueryHandler( IStoreRepository storeRepository, ITenantProvider tenantProvider) - : IRequestHandler> + : IRequestHandler> { private readonly IStoreRepository _storeRepository = storeRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// - public async Task> Handle(SearchStoresQuery request, CancellationToken cancellationToken) + public async Task> Handle(SearchStoresQuery request, CancellationToken cancellationToken) { var tenantId = _tenantProvider.GetCurrentTenantId(); var stores = await _storeRepository.SearchAsync(tenantId, request.Status, cancellationToken); @@ -34,7 +35,8 @@ public sealed class SearchStoresQueryHandler( .Take(request.PageSize) .ToList(); - return paged.Select(MapToDto).ToList(); + var items = paged.Select(MapToDto).ToList(); + return new PagedResult(items, request.Page, request.PageSize, stores.Count); } private static IOrderedEnumerable ApplySorting( diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs index 8a0b75b..f429439 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs @@ -1,13 +1,14 @@ using MediatR; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.App.Stores.Queries; /// /// 门店列表查询。 /// -public sealed class SearchStoresQuery : IRequest> +public sealed class SearchStoresQuery : IRequest> { /// /// 商户 ID(可选)。 diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs new file mode 100644 index 0000000..aee0aee --- /dev/null +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace TakeoutSaaS.Shared.Abstractions.Results; + +/// +/// 分页结果包装,携带列表与总条数等元数据。 +/// +/// 数据类型。 +public sealed class PagedResult +{ + /// + /// 数据列表。 + /// + public IReadOnlyList Items { get; } + + /// + /// 当前页码,从 1 开始。 + /// + public int Page { get; } + + /// + /// 每页条数。 + /// + public int PageSize { get; } + + /// + /// 总条数。 + /// + public int TotalCount { get; } + + /// + /// 总页数。 + /// + public int TotalPages { get; } + + /// + /// 初始化分页结果。 + /// + public PagedResult(IReadOnlyList items, int page, int pageSize, int totalCount) + { + Items = items; + Page = page; + PageSize = pageSize; + TotalCount = totalCount; + TotalPages = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); + } +} From 3e01943727cd1877f2f8547bad0ba0f9d506bb80 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 11:18:38 +0800 Subject: [PATCH 26/56] =?UTF-8?q?chore:=20=E6=B6=88=E9=99=A4=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E8=AD=A6=E5=91=8A=E5=B9=B6=E5=8D=87=E7=BA=A7=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/RabbitMqMessagePublisher.cs | 7 ++++--- .../Services/RabbitMqMessageSubscriber.cs | 20 ++++++++++--------- .../SchedulerServiceCollectionExtensions.cs | 5 ++++- .../TakeoutSaaS.Module.Scheduler.csproj | 1 + 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs index 113ee3c..701a992 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs @@ -25,14 +25,15 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio EnsureChannel(); var options = optionsMonitor.CurrentValue; - _channel!.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); + var channel = _channel ?? throw new InvalidOperationException("RabbitMQ channel is not available."); + channel.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); var body = serializer.Serialize(message); - var props = _channel.CreateBasicProperties(); + var props = channel.CreateBasicProperties(); props.ContentType = "application/json"; props.DeliveryMode = 2; props.MessageId = Guid.NewGuid().ToString("N"); - _channel.BasicPublish(options.Exchange, routingKey, props, body); + channel.BasicPublish(options.Exchange, routingKey, props, body); logger.LogDebug("发布消息到交换机 {Exchange} RoutingKey {RoutingKey}", options.Exchange, routingKey); return Task.CompletedTask; } diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs index 88f19a9..1acba98 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs @@ -24,18 +24,20 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti EnsureChannel(); var options = optionsMonitor.CurrentValue; - _channel!.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); - _channel.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false); - _channel.QueueBind(queue, options.Exchange, routingKey); - _channel.BasicQos(0, options.PrefetchCount, global: false); + var channel = _channel ?? throw new InvalidOperationException("RabbitMQ channel is not available."); - var consumer = new AsyncEventingBasicConsumer(_channel); + channel.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); + channel.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false); + channel.QueueBind(queue, options.Exchange, routingKey); + channel.BasicQos(0, options.PrefetchCount, global: false); + + var consumer = new AsyncEventingBasicConsumer(channel); consumer.Received += async (_, ea) => { var message = serializer.Deserialize(ea.Body.ToArray()); if (message == null) { - _channel.BasicAck(ea.DeliveryTag, multiple: false); + channel.BasicAck(ea.DeliveryTag, multiple: false); return; } @@ -51,15 +53,15 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti if (success) { - _channel.BasicAck(ea.DeliveryTag, multiple: false); + channel.BasicAck(ea.DeliveryTag, multiple: false); } else { - _channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); + channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); } }; - _channel.BasicConsume(queue, autoAck: false, consumer); + channel.BasicConsume(queue, autoAck: false, consumer); await Task.CompletedTask.ConfigureAwait(false); } diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs index f80b65d..db86c46 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs @@ -33,7 +33,10 @@ public static class SchedulerServiceCollectionExtensions config .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() - .UsePostgreSqlStorage(options.ConnectionString); + .UsePostgreSqlStorage(storage => + { + storage.UseNpgsqlConnection(options.ConnectionString); + }); }); services.AddHangfireServer((serviceProvider, options) => diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj index 8e4c663..6c7f697 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj @@ -7,6 +7,7 @@ + From 541b75ecd852cdb2a9f6325c01e5b2b78f446eaf Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:09:46 +0800 Subject: [PATCH 27/56] =?UTF-8?q?style:=20=E5=91=BD=E4=BB=A4=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E5=8F=98=E5=8C=96=E4=B8=8E=E8=A7=84=E8=8C=83=E8=A1=A5?= =?UTF-8?q?=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 45 ++++++++++++++++++- .../Controllers/DeliveriesController.cs | 28 ++++++------ .../Controllers/MerchantsController.cs | 28 ++++++------ .../Controllers/OrdersController.cs | 28 ++++++------ .../Controllers/PaymentsController.cs | 28 ++++++------ .../Controllers/ProductsController.cs | 28 ++++++------ .../Controllers/StoresController.cs | 28 ++++++------ .../Commands/UpdateDeliveryOrderCommand.cs | 26 +++++------ .../Commands/UpdateMerchantCommand.cs | 18 ++++---- .../App/Orders/Commands/UpdateOrderCommand.cs | 44 +++++++++--------- .../Payments/Commands/UpdatePaymentCommand.cs | 22 ++++----- .../Products/Commands/UpdateProductCommand.cs | 40 ++++++++--------- .../App/Stores/Commands/UpdateStoreCommand.cs | 40 ++++++++--------- 13 files changed, 217 insertions(+), 186 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6ff2f4f..1ed8c30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,8 +143,51 @@ 6. [ ] **精度丢失**:Long 类型的 ID 是否转为了 String? 7. [ ] **配置硬编码**:是否直接写死了连接串或密钥? +## 17. .NET 10 / C# 14 现代语法最佳实践(增量) +> 2025 年推荐的 20 条语法规范,新增特性优先,保持极简。 +1. **field 关键字**:属性内直接使用 `field` 处理后备字段,`set => field = value.Trim();`。 +2. **空值条件赋值 `?.=`**:仅对象非空时赋值,减少 `if`。 +3. **未绑定泛型 nameof**:`nameof(List<>)` 获取泛型类型名,无需占位类型参数。 +4. **Lambda 参数修饰符**:在 Lambda 中可用 `ref/out/in` 与默认参数,例如 `(ref int x, int bonus = 10) => x += bonus;`。 +5. **主构造函数 (Primary Constructor)**:服务/数据类优先 `class Foo(IDep dep, ILogger logger) { }`。 +6. **record/required/init**:DTO 默认用 record;关键属性用 `required`;不可变属性用 `init`。 +7. **集合表达式与展开**:使用 `[]` 创建集合,`[..other]` 拼接,`str[1..^1]` 进行切片。 +8. **模式匹配**:列表模式 `[1, 2, .. var rest]`、属性模式 `{ IsActive: true }`、switch 表达式简化分支。 +9. **文件范围命名空间/全局 using**:减少缩进与重复引用;复杂泛型用别名。 +10. **顶级语句**:Program.cs 保持顶级语句风格。 +11. **原始/UTF-8 字面量**:多行文本用 `"""`,性能场景用 `"text"u8`。 +12. **不可变命令优先**:命令/DTO 优先用 record 和 `with` 非破坏性拷贝,例如 `command = command with { MerchantId = merchantId };`,避免直接 `command.Property = ...` 带来的副作用。 + +(其余规则继续遵循上文约束:分层、命名、异步、日志、验证、租户/ID 策略等。) + +## 18. .NET 10 极致性能优化最佳实践(增量) +> 侧重零分配、并发与底层优化,遵循 2025 推荐方案。 +1. **Span/ReadOnlySpan 优先**:API 参数尽量用 `ReadOnlySpan` 处理字符串/切片,避免 Substring/复制。 +2. **栈分配与数组池**:小缓冲用 `stackalloc`,大缓冲统一用 `ArrayPool.Shared`,禁止直接 `new` 大数组。 +3. **UTF-8 字面量**:常量字节使用 `"text"u8`,避免运行时编码。 +4. **避免装箱**:热点路径规避隐式装箱,必要时用 `ref struct` 约束栈分配。 +5. **Frozen 集合**:只读查找表用 `FrozenDictionary/FrozenSet`,初始化后不再修改。 +6. **SearchValues SIMD 查找**:Span 内多字符搜索用 `SearchValues.Create(...)` + `ContainsAny`。 +7. **预设集合容量**:`List/Dictionary` 预知规模必须指定 `Capacity`。 +8. **ValueTask 热点返回**:可能同步完成的异步返回 `ValueTask`,减少 Task 分配。 +9. **Parallel.ForEachAsync 控并发**:I/O 并发用 Parallel.ForEachAsync 控制并行度,替代粗暴 Task.WhenAll。 +10. **避免 Task.Run**:在 ASP.NET Core 请求中不使用 Task.Run 做后台工作,改用 IHostedService 或 Channel 模式。 +11. **Channel 代替锁**:多线程数据传递优先使用 Channels,实现无锁生产者-消费者。 +12. **NativeAOT/PGO/向量化**:微服务/工具开启 NativeAOT;保留动态 PGO;计算密集场景考虑 System.Runtime.Intrinsics。 +13. **System.Text.Json + 源生成器**:全面替换 Newtonsoft.Json;使用 `[JsonSerializable]` + 生成的 `JsonSerializerContext`,兼容 NativeAOT,零反射。 +14. **Pipelines 处理流**:TCP/文件流解析使用 `PipeReader/PipeWriter`,获得零拷贝与缓冲管理。 +15. **HybridCache**:内存+分布式缓存统一用 HybridCache,利用防击穿合并并发请求。 + +## 19. 架构优化(增量) +> 架构优化方案 +1. **Chiseled 容器优先**:生产镜像基于 `mcr.microsoft.com/dotnet/runtime-deps:10.0-jammy-chiseled`,无 Shell、非 root,缩小攻击面,符合零信任要求。 +2. **默认集成 OpenTelemetry**:架构内置 OTel,统一通过 OTLP 导出 Metrics/Traces/Logs,避免依赖专有 APM 探针。 +3. **内部同步调用首选 gRPC**:微服务间禁止 JSON over HTTP,同步调用统一使用 gRPC,配合 Protobuf 源生成器获取强类型契约与更小载荷。 +4. **Outbox 模式强制**:处理领域事件时,事件记录必须与业务数据同事务写入 Outbox 表;后台 Worker 轮询 Outbox 再推送 MQ(RabbitMQ/Kafka),禁止事务提交后直接发消息以避免不一致。 +5. **共享资源必加分布式锁**:涉及库存扣减、定时任务抢占等共享资源时,必须引入分布式锁(如 Redis RedLock),防止并发竞争与脏写。 + --- # Working agreements -- 严格遵循上述技术栈和命名规范。 \ No newline at end of file +- 严格遵循上述技术栈和命名规范。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs index 45e9aa0..fe5d2cf 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs @@ -16,20 +16,15 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 配送单管理。 /// +/// +/// 初始化控制器。 +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/deliveries")] -public sealed class DeliveriesController : BaseApiController +public sealed class DeliveriesController(IMediator mediator) : BaseApiController { - private readonly IMediator _mediator; - /// - /// 初始化控制器。 - /// - public DeliveriesController(IMediator mediator) - { - _mediator = mediator; - } /// /// 创建配送单。 @@ -39,7 +34,7 @@ public sealed class DeliveriesController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody] CreateDeliveryOrderCommand command, CancellationToken cancellationToken) { - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } @@ -58,7 +53,7 @@ public sealed class DeliveriesController : BaseApiController [FromQuery] bool sortDesc = true, CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchDeliveryOrdersQuery + var result = await mediator.Send(new SearchDeliveryOrdersQuery { OrderId = orderId, Status = status, @@ -80,7 +75,7 @@ public sealed class DeliveriesController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long deliveryOrderId, CancellationToken cancellationToken) { - var result = await _mediator.Send(new GetDeliveryOrderByIdQuery { DeliveryOrderId = deliveryOrderId }, cancellationToken); + var result = await mediator.Send(new GetDeliveryOrderByIdQuery { DeliveryOrderId = deliveryOrderId }, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在") : ApiResponse.Ok(result); @@ -95,8 +90,11 @@ public sealed class DeliveriesController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Update(long deliveryOrderId, [FromBody] UpdateDeliveryOrderCommand command, CancellationToken cancellationToken) { - command.DeliveryOrderId = command.DeliveryOrderId == 0 ? deliveryOrderId : command.DeliveryOrderId; - var result = await _mediator.Send(command, cancellationToken); + if (command.DeliveryOrderId == 0) + { + command = command with { DeliveryOrderId = deliveryOrderId }; + } + var result = await mediator.Send(command, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在") : ApiResponse.Ok(result); @@ -111,7 +109,7 @@ public sealed class DeliveriesController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Delete(long deliveryOrderId, CancellationToken cancellationToken) { - var success = await _mediator.Send(new DeleteDeliveryOrderCommand { DeliveryOrderId = deliveryOrderId }, cancellationToken); + var success = await mediator.Send(new DeleteDeliveryOrderCommand { DeliveryOrderId = deliveryOrderId }, cancellationToken); return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在"); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs index 5bbe4bf..dd99bb6 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs @@ -16,20 +16,15 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 商户管理。 /// +/// +/// 初始化控制器。 +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/merchants")] -public sealed class MerchantsController : BaseApiController +public sealed class MerchantsController(IMediator mediator) : BaseApiController { - private readonly IMediator _mediator; - /// - /// 初始化控制器。 - /// - public MerchantsController(IMediator mediator) - { - _mediator = mediator; - } /// /// 创建商户。 @@ -39,7 +34,7 @@ public sealed class MerchantsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody] CreateMerchantCommand command, CancellationToken cancellationToken) { - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } @@ -57,7 +52,7 @@ public sealed class MerchantsController : BaseApiController [FromQuery] bool sortDesc = true, CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchMerchantsQuery + var result = await mediator.Send(new SearchMerchantsQuery { Status = status, Page = page, @@ -77,9 +72,12 @@ public sealed class MerchantsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken) { - command.MerchantId = command.MerchantId == 0 ? merchantId : command.MerchantId; + if (command.MerchantId == 0) + { + command = command with { MerchantId = merchantId }; + } - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "商户不存在") : ApiResponse.Ok(result); @@ -94,7 +92,7 @@ public sealed class MerchantsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Delete(long merchantId, CancellationToken cancellationToken) { - var success = await _mediator.Send(new DeleteMerchantCommand { MerchantId = merchantId }, cancellationToken); + var success = await mediator.Send(new DeleteMerchantCommand { MerchantId = merchantId }, cancellationToken); return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "商户不存在"); @@ -109,7 +107,7 @@ public sealed class MerchantsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long merchantId, CancellationToken cancellationToken) { - var result = await _mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken); + var result = await mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "商户不存在") : ApiResponse.Ok(result); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs index bfcf6b3..4d04390 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs @@ -17,20 +17,15 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 订单管理。 /// +/// +/// 初始化控制器。 +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/orders")] -public sealed class OrdersController : BaseApiController +public sealed class OrdersController(IMediator mediator) : BaseApiController { - private readonly IMediator _mediator; - /// - /// 初始化控制器。 - /// - public OrdersController(IMediator mediator) - { - _mediator = mediator; - } /// /// 创建订单。 @@ -40,7 +35,7 @@ public sealed class OrdersController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody] CreateOrderCommand command, CancellationToken cancellationToken) { - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } @@ -61,7 +56,7 @@ public sealed class OrdersController : BaseApiController [FromQuery] bool sortDesc = true, CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchOrdersQuery + var result = await mediator.Send(new SearchOrdersQuery { StoreId = storeId, Status = status, @@ -85,7 +80,7 @@ public sealed class OrdersController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long orderId, CancellationToken cancellationToken) { - var result = await _mediator.Send(new GetOrderByIdQuery { OrderId = orderId }, cancellationToken); + var result = await mediator.Send(new GetOrderByIdQuery { OrderId = orderId }, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "订单不存在") : ApiResponse.Ok(result); @@ -100,8 +95,11 @@ public sealed class OrdersController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Update(long orderId, [FromBody] UpdateOrderCommand command, CancellationToken cancellationToken) { - command.OrderId = command.OrderId == 0 ? orderId : command.OrderId; - var result = await _mediator.Send(command, cancellationToken); + if (command.OrderId == 0) + { + command = command with { OrderId = orderId }; + } + var result = await mediator.Send(command, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "订单不存在") : ApiResponse.Ok(result); @@ -116,7 +114,7 @@ public sealed class OrdersController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Delete(long orderId, CancellationToken cancellationToken) { - var success = await _mediator.Send(new DeleteOrderCommand { OrderId = orderId }, cancellationToken); + var success = await mediator.Send(new DeleteOrderCommand { OrderId = orderId }, cancellationToken); return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "订单不存在"); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs index 30d87a8..de8f322 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs @@ -16,20 +16,15 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 支付记录管理。 /// +/// +/// 初始化控制器。 +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/payments")] -public sealed class PaymentsController : BaseApiController +public sealed class PaymentsController(IMediator mediator) : BaseApiController { - private readonly IMediator _mediator; - /// - /// 初始化控制器。 - /// - public PaymentsController(IMediator mediator) - { - _mediator = mediator; - } /// /// 创建支付记录。 @@ -39,7 +34,7 @@ public sealed class PaymentsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody] CreatePaymentCommand command, CancellationToken cancellationToken) { - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } @@ -58,7 +53,7 @@ public sealed class PaymentsController : BaseApiController [FromQuery] bool sortDesc = true, CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchPaymentsQuery + var result = await mediator.Send(new SearchPaymentsQuery { OrderId = orderId, Status = status, @@ -80,7 +75,7 @@ public sealed class PaymentsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long paymentId, CancellationToken cancellationToken) { - var result = await _mediator.Send(new GetPaymentByIdQuery { PaymentId = paymentId }, cancellationToken); + var result = await mediator.Send(new GetPaymentByIdQuery { PaymentId = paymentId }, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在") : ApiResponse.Ok(result); @@ -95,8 +90,11 @@ public sealed class PaymentsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Update(long paymentId, [FromBody] UpdatePaymentCommand command, CancellationToken cancellationToken) { - command.PaymentId = command.PaymentId == 0 ? paymentId : command.PaymentId; - var result = await _mediator.Send(command, cancellationToken); + if (command.PaymentId == 0) + { + command = command with { PaymentId = paymentId }; + } + var result = await mediator.Send(command, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在") : ApiResponse.Ok(result); @@ -111,7 +109,7 @@ public sealed class PaymentsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Delete(long paymentId, CancellationToken cancellationToken) { - var success = await _mediator.Send(new DeletePaymentCommand { PaymentId = paymentId }, cancellationToken); + var success = await mediator.Send(new DeletePaymentCommand { PaymentId = paymentId }, cancellationToken); return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在"); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs index 2064156..24e334c 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs @@ -16,20 +16,15 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 商品管理。 /// +/// +/// 初始化控制器。 +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/products")] -public sealed class ProductsController : BaseApiController +public sealed class ProductsController(IMediator mediator) : BaseApiController { - private readonly IMediator _mediator; - /// - /// 初始化控制器。 - /// - public ProductsController(IMediator mediator) - { - _mediator = mediator; - } /// /// 创建商品。 @@ -39,7 +34,7 @@ public sealed class ProductsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody] CreateProductCommand command, CancellationToken cancellationToken) { - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } @@ -59,7 +54,7 @@ public sealed class ProductsController : BaseApiController [FromQuery] bool sortDesc = true, CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchProductsQuery + var result = await mediator.Send(new SearchProductsQuery { StoreId = storeId, CategoryId = categoryId, @@ -82,7 +77,7 @@ public sealed class ProductsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long productId, CancellationToken cancellationToken) { - var result = await _mediator.Send(new GetProductByIdQuery { ProductId = productId }, cancellationToken); + var result = await mediator.Send(new GetProductByIdQuery { ProductId = productId }, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "商品不存在") : ApiResponse.Ok(result); @@ -97,8 +92,11 @@ public sealed class ProductsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Update(long productId, [FromBody] UpdateProductCommand command, CancellationToken cancellationToken) { - command.ProductId = command.ProductId == 0 ? productId : command.ProductId; - var result = await _mediator.Send(command, cancellationToken); + if (command.ProductId == 0) + { + command = command with { ProductId = productId }; + } + var result = await mediator.Send(command, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "商品不存在") : ApiResponse.Ok(result); @@ -113,7 +111,7 @@ public sealed class ProductsController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Delete(long productId, CancellationToken cancellationToken) { - var success = await _mediator.Send(new DeleteProductCommand { ProductId = productId }, cancellationToken); + var success = await mediator.Send(new DeleteProductCommand { ProductId = productId }, cancellationToken); return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "商品不存在"); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs index 8d35bcf..6abdac2 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs @@ -16,20 +16,15 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 门店管理。 /// +/// +/// 初始化控制器。 +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/stores")] -public sealed class StoresController : BaseApiController +public sealed class StoresController(IMediator mediator) : BaseApiController { - private readonly IMediator _mediator; - /// - /// 初始化控制器。 - /// - public StoresController(IMediator mediator) - { - _mediator = mediator; - } /// /// 创建门店。 @@ -39,7 +34,7 @@ public sealed class StoresController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody] CreateStoreCommand command, CancellationToken cancellationToken) { - var result = await _mediator.Send(command, cancellationToken); + var result = await mediator.Send(command, cancellationToken); return ApiResponse.Ok(result); } @@ -58,7 +53,7 @@ public sealed class StoresController : BaseApiController [FromQuery] bool sortDesc = true, CancellationToken cancellationToken = default) { - var result = await _mediator.Send(new SearchStoresQuery + var result = await mediator.Send(new SearchStoresQuery { MerchantId = merchantId, Status = status, @@ -80,7 +75,7 @@ public sealed class StoresController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail(long storeId, CancellationToken cancellationToken) { - var result = await _mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken); + var result = await mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "门店不存在") : ApiResponse.Ok(result); @@ -95,8 +90,11 @@ public sealed class StoresController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Update(long storeId, [FromBody] UpdateStoreCommand command, CancellationToken cancellationToken) { - command.StoreId = command.StoreId == 0 ? storeId : command.StoreId; - var result = await _mediator.Send(command, cancellationToken); + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + var result = await mediator.Send(command, cancellationToken); return result == null ? ApiResponse.Error(ErrorCodes.NotFound, "门店不存在") : ApiResponse.Ok(result); @@ -111,7 +109,7 @@ public sealed class StoresController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Delete(long storeId, CancellationToken cancellationToken) { - var success = await _mediator.Send(new DeleteStoreCommand { StoreId = storeId }, cancellationToken); + var success = await mediator.Send(new DeleteStoreCommand { StoreId = storeId }, cancellationToken); return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "门店不存在"); diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs index cf57bbb..cb94be2 100644 --- a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs @@ -7,65 +7,65 @@ namespace TakeoutSaaS.Application.App.Deliveries.Commands; /// /// 更新配送单命令。 /// -public sealed class UpdateDeliveryOrderCommand : IRequest +public sealed record UpdateDeliveryOrderCommand : IRequest { /// /// 配送单 ID。 /// - public long DeliveryOrderId { get; set; } + public long DeliveryOrderId { get; init; } /// /// 订单 ID。 /// - public long OrderId { get; set; } + public long OrderId { get; init; } /// /// 服务商。 /// - public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse; + public DeliveryProvider Provider { get; init; } = DeliveryProvider.InHouse; /// /// 第三方单号。 /// - public string? ProviderOrderId { get; set; } + public string? ProviderOrderId { get; init; } /// /// 状态。 /// - public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending; + public DeliveryStatus Status { get; init; } = DeliveryStatus.Pending; /// /// 配送费。 /// - public decimal? DeliveryFee { get; set; } + public decimal? DeliveryFee { get; init; } /// /// 骑手姓名。 /// - public string? CourierName { get; set; } + public string? CourierName { get; init; } /// /// 骑手电话。 /// - public string? CourierPhone { get; set; } + public string? CourierPhone { get; init; } /// /// 下发时间。 /// - public DateTime? DispatchedAt { get; set; } + public DateTime? DispatchedAt { get; init; } /// /// 取餐时间。 /// - public DateTime? PickedUpAt { get; set; } + public DateTime? PickedUpAt { get; init; } /// /// 完成时间。 /// - public DateTime? DeliveredAt { get; set; } + public DateTime? DeliveredAt { get; init; } /// /// 异常原因。 /// - public string? FailureReason { get; set; } + public string? FailureReason { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs index 2b73adc..feb73f9 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs @@ -7,45 +7,45 @@ namespace TakeoutSaaS.Application.App.Merchants.Commands; /// /// 更新商户命令。 /// -public sealed class UpdateMerchantCommand : IRequest +public sealed record UpdateMerchantCommand : IRequest { /// /// 商户 ID。 /// - public long MerchantId { get; set; } + public long MerchantId { get; init; } /// /// 品牌名称。 /// - public string BrandName { get; set; } = string.Empty; + public string BrandName { get; init; } = string.Empty; /// /// 品牌简称。 /// - public string? BrandAlias { get; set; } + public string? BrandAlias { get; init; } /// /// Logo 地址。 /// - public string? LogoUrl { get; set; } + public string? LogoUrl { get; init; } /// /// 品类。 /// - public string? Category { get; set; } + public string? Category { get; init; } /// /// 联系电话。 /// - public string ContactPhone { get; set; } = string.Empty; + public string ContactPhone { get; init; } = string.Empty; /// /// 联系邮箱。 /// - public string? ContactEmail { get; set; } + public string? ContactEmail { get; init; } /// /// 入驻状态。 /// - public MerchantStatus Status { get; set; } + public MerchantStatus Status { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs index a9c832a..12de4b7 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs @@ -8,110 +8,110 @@ namespace TakeoutSaaS.Application.App.Orders.Commands; /// /// 更新订单命令。 /// -public sealed class UpdateOrderCommand : IRequest +public sealed record UpdateOrderCommand : IRequest { /// /// 订单 ID。 /// - public long OrderId { get; set; } + public long OrderId { get; init; } /// /// 订单号。 /// - public string OrderNo { get; set; } = string.Empty; + public string OrderNo { get; init; } = string.Empty; /// /// 门店 ID。 /// - public long StoreId { get; set; } + public long StoreId { get; init; } /// /// 渠道。 /// - public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram; + public OrderChannel Channel { get; init; } = OrderChannel.MiniProgram; /// /// 履约方式。 /// - public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn; + public DeliveryType DeliveryType { get; init; } = DeliveryType.DineIn; /// /// 状态。 /// - public OrderStatus Status { get; set; } = OrderStatus.PendingPayment; + public OrderStatus Status { get; init; } = OrderStatus.PendingPayment; /// /// 支付状态。 /// - public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid; + public PaymentStatus PaymentStatus { get; init; } = PaymentStatus.Unpaid; /// /// 顾客姓名。 /// - public string? CustomerName { get; set; } + public string? CustomerName { get; init; } /// /// 顾客手机号。 /// - public string? CustomerPhone { get; set; } + public string? CustomerPhone { get; init; } /// /// 桌号。 /// - public string? TableNo { get; set; } + public string? TableNo { get; init; } /// /// 排队号。 /// - public string? QueueNumber { get; set; } + public string? QueueNumber { get; init; } /// /// 预约 ID。 /// - public long? ReservationId { get; set; } + public long? ReservationId { get; init; } /// /// 商品金额。 /// - public decimal ItemsAmount { get; set; } + public decimal ItemsAmount { get; init; } /// /// 优惠金额。 /// - public decimal DiscountAmount { get; set; } + public decimal DiscountAmount { get; init; } /// /// 应付金额。 /// - public decimal PayableAmount { get; set; } + public decimal PayableAmount { get; init; } /// /// 实付金额。 /// - public decimal PaidAmount { get; set; } + public decimal PaidAmount { get; init; } /// /// 支付时间。 /// - public DateTime? PaidAt { get; set; } + public DateTime? PaidAt { get; init; } /// /// 完成时间。 /// - public DateTime? FinishedAt { get; set; } + public DateTime? FinishedAt { get; init; } /// /// 取消时间。 /// - public DateTime? CancelledAt { get; set; } + public DateTime? CancelledAt { get; init; } /// /// 取消原因。 /// - public string? CancelReason { get; set; } + public string? CancelReason { get; init; } /// /// 备注。 /// - public string? Remark { get; set; } + public string? Remark { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs index 8d8263f..6b8e9ed 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs @@ -7,55 +7,55 @@ namespace TakeoutSaaS.Application.App.Payments.Commands; /// /// 更新支付记录命令。 /// -public sealed class UpdatePaymentCommand : IRequest +public sealed record UpdatePaymentCommand : IRequest { /// /// 支付记录 ID。 /// - public long PaymentId { get; set; } + public long PaymentId { get; init; } /// /// 订单 ID。 /// - public long OrderId { get; set; } + public long OrderId { get; init; } /// /// 支付方式。 /// - public PaymentMethod Method { get; set; } = PaymentMethod.Unknown; + public PaymentMethod Method { get; init; } = PaymentMethod.Unknown; /// /// 支付状态。 /// - public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid; + public PaymentStatus Status { get; init; } = PaymentStatus.Unpaid; /// /// 金额。 /// - public decimal Amount { get; set; } + public decimal Amount { get; init; } /// /// 平台交易号。 /// - public string? TradeNo { get; set; } + public string? TradeNo { get; init; } /// /// 渠道单号。 /// - public string? ChannelTransactionId { get; set; } + public string? ChannelTransactionId { get; init; } /// /// 支付时间。 /// - public DateTime? PaidAt { get; set; } + public DateTime? PaidAt { get; init; } /// /// 备注。 /// - public string? Remark { get; set; } + public string? Remark { get; init; } /// /// 原始回调。 /// - public string? Payload { get; set; } + public string? Payload { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs index ba3de2b..09cd056 100644 --- a/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs @@ -7,100 +7,100 @@ namespace TakeoutSaaS.Application.App.Products.Commands; /// /// 更新商品命令。 /// -public sealed class UpdateProductCommand : IRequest +public sealed record UpdateProductCommand : IRequest { /// /// 商品 ID。 /// - public long ProductId { get; set; } + public long ProductId { get; init; } /// /// 门店 ID。 /// - public long StoreId { get; set; } + public long StoreId { get; init; } /// /// 分类 ID。 /// - public long CategoryId { get; set; } + public long CategoryId { get; init; } /// /// 商品编码。 /// - public string SpuCode { get; set; } = string.Empty; + public string SpuCode { get; init; } = string.Empty; /// /// 名称。 /// - public string Name { get; set; } = string.Empty; + public string Name { get; init; } = string.Empty; /// /// 副标题。 /// - public string? Subtitle { get; set; } + public string? Subtitle { get; init; } /// /// 单位。 /// - public string? Unit { get; set; } + public string? Unit { get; init; } /// /// 现价。 /// - public decimal Price { get; set; } + public decimal Price { get; init; } /// /// 原价。 /// - public decimal? OriginalPrice { get; set; } + public decimal? OriginalPrice { get; init; } /// /// 库存数量。 /// - public int? StockQuantity { get; set; } + public int? StockQuantity { get; init; } /// /// 每单限购。 /// - public int? MaxQuantityPerOrder { get; set; } + public int? MaxQuantityPerOrder { get; init; } /// /// 状态。 /// - public ProductStatus Status { get; set; } = ProductStatus.Draft; + public ProductStatus Status { get; init; } = ProductStatus.Draft; /// /// 主图。 /// - public string? CoverImage { get; set; } + public string? CoverImage { get; init; } /// /// 图集。 /// - public string? GalleryImages { get; set; } + public string? GalleryImages { get; init; } /// /// 描述。 /// - public string? Description { get; set; } + public string? Description { get; init; } /// /// 支持堂食。 /// - public bool EnableDineIn { get; set; } = true; + public bool EnableDineIn { get; init; } = true; /// /// 支持自提。 /// - public bool EnablePickup { get; set; } = true; + public bool EnablePickup { get; init; } = true; /// /// 支持配送。 /// - public bool EnableDelivery { get; set; } = true; + public bool EnableDelivery { get; init; } = true; /// /// 是否推荐。 /// - public bool IsFeatured { get; set; } + public bool IsFeatured { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs index 0cecf53..63a178d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs @@ -7,100 +7,100 @@ namespace TakeoutSaaS.Application.App.Stores.Commands; /// /// 更新门店命令。 /// -public sealed class UpdateStoreCommand : IRequest +public sealed record UpdateStoreCommand : IRequest { /// /// 门店 ID。 /// - public long StoreId { get; set; } + public long StoreId { get; init; } /// /// 商户 ID。 /// - public long MerchantId { get; set; } + public long MerchantId { get; init; } /// /// 门店编码。 /// - public string Code { get; set; } = string.Empty; + public string Code { get; init; } = string.Empty; /// /// 门店名称。 /// - public string Name { get; set; } = string.Empty; + public string Name { get; init; } = string.Empty; /// /// 电话。 /// - public string? Phone { get; set; } + public string? Phone { get; init; } /// /// 负责人。 /// - public string? ManagerName { get; set; } + public string? ManagerName { get; init; } /// /// 状态。 /// - public StoreStatus Status { get; set; } = StoreStatus.Closed; + public StoreStatus Status { get; init; } = StoreStatus.Closed; /// /// 省份。 /// - public string? Province { get; set; } + public string? Province { get; init; } /// /// 城市。 /// - public string? City { get; set; } + public string? City { get; init; } /// /// 区县。 /// - public string? District { get; set; } + public string? District { get; init; } /// /// 详细地址。 /// - public string? Address { get; set; } + public string? Address { get; init; } /// /// 经度。 /// - public double? Longitude { get; set; } + public double? Longitude { get; init; } /// /// 纬度。 /// - public double? Latitude { get; set; } + public double? Latitude { get; init; } /// /// 公告。 /// - public string? Announcement { get; set; } + public string? Announcement { get; init; } /// /// 标签。 /// - public string? Tags { get; set; } + public string? Tags { get; init; } /// /// 配送半径。 /// - public decimal DeliveryRadiusKm { get; set; } + public decimal DeliveryRadiusKm { get; init; } /// /// 支持堂食。 /// - public bool SupportsDineIn { get; set; } = true; + public bool SupportsDineIn { get; init; } = true; /// /// 支持自提。 /// - public bool SupportsPickup { get; set; } = true; + public bool SupportsPickup { get; init; } = true; /// /// 支持配送。 /// - public bool SupportsDelivery { get; set; } = true; + public bool SupportsDelivery { get; init; } = true; } From 5332c87d9da94aac6101837ffb82b2324979f049 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:11:25 +0800 Subject: [PATCH 28/56] =?UTF-8?q?chore:=20=E6=8F=90=E4=BA=A4=E7=8E=B0?= =?UTF-8?q?=E6=9C=89=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AuthController.cs | 21 ++-- .../Controllers/DictionaryController.cs | 31 +++--- .../Controllers/AuthController.cs | 19 ++-- .../Controllers/MeController.cs | 17 ++- .../Services/DictionaryAppService.cs | 80 ++++++-------- .../Contracts/SendVerificationCodeRequest.cs | 21 ++-- .../VerifyVerificationCodeRequest.cs | 21 ++-- .../Storage/Contracts/DirectUploadRequest.cs | 27 ++--- .../Storage/Contracts/UploadFileRequest.cs | 42 +++----- .../Exceptions/BusinessException.cs | 9 +- .../Exceptions/ValidationException.cs | 10 +- .../Results/PagedResult.cs | 27 ++--- .../Tenancy/TenantContext.cs | 27 ++--- .../Ids/SnowflakeIdGenerator.cs | 24 ++--- .../Middleware/CorrelationIdMiddleware.cs | 19 +--- .../Middleware/ExceptionHandlingMiddleware.cs | 19 +--- .../Middleware/RequestLoggingMiddleware.cs | 14 +-- .../HttpContextCurrentUserAccessor.cs | 15 +-- .../Swagger/ConfigureSwaggerOptions.cs | 17 +-- .../App/Persistence/AppDataSeeder.cs | 50 ++++----- .../App/Repositories/EfDeliveryRepository.cs | 37 +++---- .../App/Repositories/EfMerchantRepository.cs | 39 +++---- .../App/Repositories/EfOrderRepository.cs | 53 +++++---- .../App/Repositories/EfPaymentRepository.cs | 37 +++---- .../App/Repositories/EfProductRepository.cs | 101 +++++++++--------- .../App/Repositories/EfStoreRepository.cs | 51 ++++----- .../Repositories/EfDictionaryRepository.cs | 30 +++--- .../Services/DistributedDictionaryCache.cs | 17 +-- .../Persistence/EfIdentityUserRepository.cs | 12 +-- .../Persistence/EfMiniUserRepository.cs | 18 ++-- .../Services/RedisLoginRateLimiter.cs | 17 +-- .../Services/RedisRefreshTokenStore.cs | 17 +-- .../Identity/Services/WeChatAuthService.cs | 13 +-- .../Models/StorageDirectUploadRequest.cs | 32 +++--- .../Models/StorageUploadRequest.cs | 64 +++++------ .../TenantProvider.cs | 17 ++- .../TenantResolutionMiddleware.cs | 41 +++---- 37 files changed, 429 insertions(+), 677 deletions(-) diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index 962ee57..b406c49 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -17,21 +17,16 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 管理后台认证接口 /// +/// +/// +/// +/// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/auth")] -public sealed class AuthController : BaseApiController +public sealed class AuthController(IAdminAuthService authService) : BaseApiController { - private readonly IAdminAuthService _authService; - /// - /// - /// - /// - public AuthController(IAdminAuthService authService) - { - _authService = authService; - } /// /// 登录获取 Token @@ -41,7 +36,7 @@ public sealed class AuthController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) { - var response = await _authService.LoginAsync(request, cancellationToken); + var response = await authService.LoginAsync(request, cancellationToken); return ApiResponse.Ok(response); } @@ -53,7 +48,7 @@ public sealed class AuthController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) { - var response = await _authService.RefreshTokenAsync(request, cancellationToken); + var response = await authService.RefreshTokenAsync(request, cancellationToken); return ApiResponse.Ok(response); } @@ -72,7 +67,7 @@ public sealed class AuthController : BaseApiController return ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); } - var profile = await _authService.GetProfileAsync(userId, cancellationToken); + var profile = await authService.GetProfileAsync(userId, cancellationToken); return ApiResponse.Ok(profile); } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs index f63c273..d98e420 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DictionaryController.cs @@ -12,21 +12,16 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// /// 参数字典管理。 /// +/// +/// 初始化字典控制器。 +/// +/// 字典服务 [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/dictionaries")] -public sealed class DictionaryController : BaseApiController +public sealed class DictionaryController(IDictionaryAppService dictionaryAppService) : BaseApiController { - private readonly IDictionaryAppService _dictionaryAppService; - /// - /// 初始化字典控制器。 - /// - /// 字典服务 - public DictionaryController(IDictionaryAppService dictionaryAppService) - { - _dictionaryAppService = dictionaryAppService; - } /// /// 查询字典分组。 @@ -36,7 +31,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] public async Task>> GetGroups([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken) { - var groups = await _dictionaryAppService.SearchGroupsAsync(query, cancellationToken); + var groups = await dictionaryAppService.SearchGroupsAsync(query, cancellationToken); return ApiResponse>.Ok(groups); } @@ -48,7 +43,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> CreateGroup([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken) { - var group = await _dictionaryAppService.CreateGroupAsync(request, cancellationToken); + var group = await dictionaryAppService.CreateGroupAsync(request, cancellationToken); return ApiResponse.Ok(group); } @@ -60,7 +55,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken) { - var group = await _dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken); + var group = await dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken); return ApiResponse.Ok(group); } @@ -72,7 +67,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> DeleteGroup(long groupId, CancellationToken cancellationToken) { - await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken); + await dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken); return ApiResponse.Success(); } @@ -85,7 +80,7 @@ public sealed class DictionaryController : BaseApiController public async Task> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken) { request.GroupId = groupId; - var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken); + var item = await dictionaryAppService.CreateItemAsync(request, cancellationToken); return ApiResponse.Ok(item); } @@ -97,7 +92,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken) { - var item = await _dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken); + var item = await dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken); return ApiResponse.Ok(item); } @@ -109,7 +104,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> DeleteItem(long itemId, CancellationToken cancellationToken) { - await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken); + await dictionaryAppService.DeleteItemAsync(itemId, cancellationToken); return ApiResponse.Success(); } @@ -120,7 +115,7 @@ public sealed class DictionaryController : BaseApiController [ProducesResponseType(typeof(ApiResponse>>), StatusCodes.Status200OK)] public async Task>>> BatchGet([FromBody] DictionaryBatchQueryRequest request, CancellationToken cancellationToken) { - var dictionaries = await _dictionaryAppService.GetCachedItemsAsync(request, cancellationToken); + var dictionaries = await dictionaryAppService.GetCachedItemsAsync(request, cancellationToken); return ApiResponse>>.Ok(dictionaries); } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs index 07961a3..332dec2 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs @@ -10,21 +10,16 @@ namespace TakeoutSaaS.MiniApi.Controllers; /// /// 小程序登录认证 /// +/// +/// 小程序登录认证 +/// +/// [ApiVersion("1.0")] [Authorize] [Route("api/mini/v{version:apiVersion}/auth")] -public sealed class AuthController : BaseApiController +public sealed class AuthController(IMiniAuthService authService) : BaseApiController { - private readonly IMiniAuthService _authService; - /// - /// 小程序登录认证 - /// - /// - public AuthController(IMiniAuthService authService) - { - _authService = authService; - } /// /// 微信登录 @@ -34,7 +29,7 @@ public sealed class AuthController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken) { - var response = await _authService.LoginWithWeChatAsync(request, cancellationToken); + var response = await authService.LoginWithWeChatAsync(request, cancellationToken); return ApiResponse.Ok(response); } @@ -46,7 +41,7 @@ public sealed class AuthController : BaseApiController [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) { - var response = await _authService.RefreshTokenAsync(request, cancellationToken); + var response = await authService.RefreshTokenAsync(request, cancellationToken); return ApiResponse.Ok(response); } } diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs index 4dd1fa8..4bd29e4 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs @@ -16,21 +16,16 @@ namespace TakeoutSaaS.MiniApi.Controllers; /// /// 当前用户信息 /// +/// +/// +/// +/// [ApiVersion("1.0")] [Authorize] [Route("api/mini/v{version:apiVersion}/me")] -public sealed class MeController : BaseApiController +public sealed class MeController(IMiniAuthService authService) : BaseApiController { - private readonly IMiniAuthService _authService; - /// - /// - /// - /// - public MeController(IMiniAuthService authService) - { - _authService = authService; - } /// /// 获取用户档案 @@ -46,7 +41,7 @@ public sealed class MeController : BaseApiController return ApiResponse.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); } - var profile = await _authService.GetProfileAsync(userId, cancellationToken); + var profile = await authService.GetProfileAsync(userId, cancellationToken); return ApiResponse.Ok(profile); } } diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs index 8ecfc79..5ff8e9a 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs @@ -15,31 +15,19 @@ namespace TakeoutSaaS.Application.Dictionary.Services; /// /// 参数字典应用服务实现。 /// -public sealed class DictionaryAppService : IDictionaryAppService +public sealed class DictionaryAppService( + IDictionaryRepository repository, + IDictionaryCache cache, + ITenantProvider tenantProvider, + ILogger logger) : IDictionaryAppService { - private readonly IDictionaryRepository _repository; - private readonly IDictionaryCache _cache; - private readonly ITenantProvider _tenantProvider; - private readonly ILogger _logger; - - public DictionaryAppService( - IDictionaryRepository repository, - IDictionaryCache cache, - ITenantProvider tenantProvider, - ILogger logger) - { - _repository = repository; - _cache = cache; - _tenantProvider = tenantProvider; - _logger = logger; - } public async Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { var normalizedCode = NormalizeCode(request.Code); var targetTenant = ResolveTargetTenant(request.Scope); - var existing = await _repository.FindGroupByCodeAsync(normalizedCode, cancellationToken); + var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken); if (existing != null) { throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在"); @@ -56,9 +44,9 @@ public sealed class DictionaryAppService : IDictionaryAppService IsEnabled = true }; - await _repository.AddGroupAsync(group, cancellationToken); - await _repository.SaveChangesAsync(cancellationToken); - _logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope); + await repository.AddGroupAsync(group, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope); return MapGroup(group, includeItems: false); } @@ -71,9 +59,9 @@ public sealed class DictionaryAppService : IDictionaryAppService group.Description = request.Description?.Trim(); group.IsEnabled = request.IsEnabled; - await _repository.SaveChangesAsync(cancellationToken); + await repository.SaveChangesAsync(cancellationToken); await InvalidateCacheAsync(group, cancellationToken); - _logger.LogInformation("更新字典分组:{GroupId}", group.Id); + logger.LogInformation("更新字典分组:{GroupId}", group.Id); return MapGroup(group, includeItems: false); } @@ -82,19 +70,19 @@ public sealed class DictionaryAppService : IDictionaryAppService var group = await RequireGroupAsync(groupId, cancellationToken); EnsureScopePermission(group.Scope); - await _repository.RemoveGroupAsync(group, cancellationToken); - await _repository.SaveChangesAsync(cancellationToken); + await repository.RemoveGroupAsync(group, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); await InvalidateCacheAsync(group, cancellationToken); - _logger.LogInformation("删除字典分组:{GroupId}", group.Id); + logger.LogInformation("删除字典分组:{GroupId}", group.Id); } public async Task> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default) { - var tenantId = _tenantProvider.GetCurrentTenantId(); + var tenantId = tenantProvider.GetCurrentTenantId(); var scope = ResolveScopeForQuery(request.Scope, tenantId); EnsureScopePermission(scope); - var groups = await _repository.SearchGroupsAsync(scope, cancellationToken); + var groups = await repository.SearchGroupsAsync(scope, cancellationToken); var includeItems = request.IncludeItems; var result = new List(groups.Count); @@ -103,7 +91,7 @@ public sealed class DictionaryAppService : IDictionaryAppService IReadOnlyList items = Array.Empty(); if (includeItems) { - var itemEntities = await _repository.GetItemsByGroupIdAsync(group.Id, cancellationToken); + var itemEntities = await repository.GetItemsByGroupIdAsync(group.Id, cancellationToken); items = itemEntities.Select(MapItem).ToList(); } @@ -131,10 +119,10 @@ public sealed class DictionaryAppService : IDictionaryAppService IsEnabled = request.IsEnabled }; - await _repository.AddItemAsync(item, cancellationToken); - await _repository.SaveChangesAsync(cancellationToken); + await repository.AddItemAsync(item, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); await InvalidateCacheAsync(group, cancellationToken); - _logger.LogInformation("新增字典项:{ItemId}", item.Id); + logger.LogInformation("新增字典项:{ItemId}", item.Id); return MapItem(item); } @@ -150,9 +138,9 @@ public sealed class DictionaryAppService : IDictionaryAppService item.IsDefault = request.IsDefault; item.IsEnabled = request.IsEnabled; - await _repository.SaveChangesAsync(cancellationToken); + await repository.SaveChangesAsync(cancellationToken); await InvalidateCacheAsync(group, cancellationToken); - _logger.LogInformation("更新字典项:{ItemId}", item.Id); + logger.LogInformation("更新字典项:{ItemId}", item.Id); return MapItem(item); } @@ -162,10 +150,10 @@ public sealed class DictionaryAppService : IDictionaryAppService var group = await RequireGroupAsync(item.GroupId, cancellationToken); EnsureScopePermission(group.Scope); - await _repository.RemoveItemAsync(item, cancellationToken); - await _repository.SaveChangesAsync(cancellationToken); + await repository.RemoveItemAsync(item, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); await InvalidateCacheAsync(group, cancellationToken); - _logger.LogInformation("删除字典项:{ItemId}", item.Id); + logger.LogInformation("删除字典项:{ItemId}", item.Id); } public async Task>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default) @@ -181,7 +169,7 @@ public sealed class DictionaryAppService : IDictionaryAppService return new Dictionary>(StringComparer.OrdinalIgnoreCase); } - var tenantId = _tenantProvider.GetCurrentTenantId(); + var tenantId = tenantProvider.GetCurrentTenantId(); var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var code in normalizedCodes) @@ -202,7 +190,7 @@ public sealed class DictionaryAppService : IDictionaryAppService private async Task RequireGroupAsync(long groupId, CancellationToken cancellationToken) { - var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken); + var group = await repository.FindGroupByIdAsync(groupId, cancellationToken); if (group == null) { throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在"); @@ -213,7 +201,7 @@ public sealed class DictionaryAppService : IDictionaryAppService private async Task RequireItemAsync(long itemId, CancellationToken cancellationToken) { - var item = await _repository.FindItemByIdAsync(itemId, cancellationToken); + var item = await repository.FindItemByIdAsync(itemId, cancellationToken); if (item == null) { throw new BusinessException(ErrorCodes.NotFound, "字典项不存在"); @@ -224,7 +212,7 @@ public sealed class DictionaryAppService : IDictionaryAppService private long ResolveTargetTenant(DictionaryScope scope) { - var tenantId = _tenantProvider.GetCurrentTenantId(); + var tenantId = tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System) { EnsurePlatformTenant(tenantId); @@ -253,7 +241,7 @@ public sealed class DictionaryAppService : IDictionaryAppService private void EnsureScopePermission(DictionaryScope scope) { - var tenantId = _tenantProvider.GetCurrentTenantId(); + var tenantId = tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System && tenantId != 0) { throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); @@ -270,7 +258,7 @@ public sealed class DictionaryAppService : IDictionaryAppService private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken) { - await _cache.RemoveAsync(group.TenantId, group.Code, cancellationToken); + await cache.RemoveAsync(group.TenantId, group.Code, cancellationToken); if (group.Scope == DictionaryScope.Business) { return; @@ -281,20 +269,20 @@ public sealed class DictionaryAppService : IDictionaryAppService private async Task> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken) { - var cached = await _cache.GetAsync(tenantId, code, cancellationToken); + var cached = await cache.GetAsync(tenantId, code, cancellationToken); if (cached != null) { return cached; } - var entities = await _repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken); + var entities = await repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken); var items = entities .Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true)) .Select(MapItem) .OrderBy(item => item.SortOrder) .ToList(); - await _cache.SetAsync(tenantId, code, items, cancellationToken); + await cache.SetAsync(tenantId, code, items, cancellationToken); return items; } diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs index 16e659d..402385f 100644 --- a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs @@ -5,30 +5,25 @@ namespace TakeoutSaaS.Application.Sms.Contracts; /// /// 发送验证码请求。 /// -public sealed class SendVerificationCodeRequest +/// +/// 创建发送请求。 +/// +public sealed class SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null) { - /// - /// 创建发送请求。 - /// - public SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null) - { - PhoneNumber = phoneNumber; - Scene = scene; - Provider = provider; - } + /// /// 手机号(支持 +86 前缀或纯 11 位)。 /// - public string PhoneNumber { get; } + public string PhoneNumber { get; } = phoneNumber; /// /// 业务场景(如 login/register/reset)。 /// - public string Scene { get; } + public string Scene { get; } = scene; /// /// 指定服务商,未指定则使用默认配置。 /// - public SmsProviderKind? Provider { get; } + public SmsProviderKind? Provider { get; } = provider; } diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs index 57df45e..034230c 100644 --- a/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs @@ -3,30 +3,25 @@ namespace TakeoutSaaS.Application.Sms.Contracts; /// /// 校验验证码请求。 /// -public sealed class VerifyVerificationCodeRequest +/// +/// 创建校验请求。 +/// +public sealed class VerifyVerificationCodeRequest(string phoneNumber, string scene, string code) { - /// - /// 创建校验请求。 - /// - public VerifyVerificationCodeRequest(string phoneNumber, string scene, string code) - { - PhoneNumber = phoneNumber; - Scene = scene; - Code = code; - } + /// /// 手机号。 /// - public string PhoneNumber { get; } + public string PhoneNumber { get; } = phoneNumber; /// /// 业务场景。 /// - public string Scene { get; } + public string Scene { get; } = scene; /// /// 填写的验证码。 /// - public string Code { get; } + public string Code { get; } = code; } diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs index de9a69f..b757e36 100644 --- a/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs @@ -5,42 +5,35 @@ namespace TakeoutSaaS.Application.Storage.Contracts; /// /// 直传凭证请求模型。 /// -public sealed class DirectUploadRequest +/// +/// 创建直传请求。 +/// +public sealed class DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin) { - /// - /// 创建直传请求。 - /// - public DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin) - { - FileType = fileType; - FileName = fileName; - ContentType = contentType; - ContentLength = contentLength; - RequestOrigin = requestOrigin; - } + /// /// 文件类型。 /// - public UploadFileType FileType { get; } + public UploadFileType FileType { get; } = fileType; /// /// 文件名。 /// - public string FileName { get; } + public string FileName { get; } = fileName; /// /// 内容类型。 /// - public string ContentType { get; } + public string ContentType { get; } = contentType; /// /// 文件长度。 /// - public long ContentLength { get; } + public long ContentLength { get; } = contentLength; /// /// 请求来源(Origin/Referer)。 /// - public string? RequestOrigin { get; } + public string? RequestOrigin { get; } = requestOrigin; } diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs index d60bb7b..ea19f91 100644 --- a/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs @@ -6,54 +6,46 @@ namespace TakeoutSaaS.Application.Storage.Contracts; /// /// 上传文件请求模型。 /// -public sealed class UploadFileRequest +/// +/// 创建上传文件请求。 +/// +public sealed class UploadFileRequest( + UploadFileType fileType, + Stream content, + string fileName, + string contentType, + long contentLength, + string? requestOrigin) { - /// - /// 创建上传文件请求。 - /// - public UploadFileRequest( - UploadFileType fileType, - Stream content, - string fileName, - string contentType, - long contentLength, - string? requestOrigin) - { - FileType = fileType; - Content = content; - FileName = fileName; - ContentType = contentType; - ContentLength = contentLength; - RequestOrigin = requestOrigin; - } + /// /// 文件分类。 /// - public UploadFileType FileType { get; } + public UploadFileType FileType { get; } = fileType; /// /// 文件流。 /// - public Stream Content { get; } + public Stream Content { get; } = content; /// /// 原始文件名。 /// - public string FileName { get; } + public string FileName { get; } = fileName; /// /// 内容类型。 /// - public string ContentType { get; } + public string ContentType { get; } = contentType; /// /// 文件大小。 /// - public long ContentLength { get; } + public long ContentLength { get; } = contentLength; /// /// 请求来源(Origin/Referer)。 /// - public string? RequestOrigin { get; } + public string? RequestOrigin { get; } = requestOrigin; } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs index c270130..b14dc4a 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs @@ -3,16 +3,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions; /// /// 业务异常(用于可预期的业务校验错误)。 /// -public class BusinessException : Exception +public class BusinessException(int errorCode, string message) : Exception(message) { /// /// 业务错误码。 /// - public int ErrorCode { get; } - - public BusinessException(int errorCode, string message) : base(message) - { - ErrorCode = errorCode; - } + public int ErrorCode { get; } = errorCode; } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs index a4c48d0..a87def6 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs @@ -6,17 +6,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions; /// /// 验证异常(用于聚合验证错误信息)。 /// -public class ValidationException : Exception +public class ValidationException(IDictionary errors) : Exception("一个或多个验证错误") { /// /// 字段/属性的错误集合。 /// - public IDictionary Errors { get; } - - public ValidationException(IDictionary errors) - : base("一个或多个验证错误") - { - Errors = errors; - } + public IDictionary Errors { get; } = errors; } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs index aee0aee..69c5ba4 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs @@ -6,42 +6,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Results; /// 分页结果包装,携带列表与总条数等元数据。 /// /// 数据类型。 -public sealed class PagedResult +/// +/// 初始化分页结果。 +/// +public sealed class PagedResult(IReadOnlyList items, int page, int pageSize, int totalCount) { /// /// 数据列表。 /// - public IReadOnlyList Items { get; } + public IReadOnlyList Items { get; } = items; /// /// 当前页码,从 1 开始。 /// - public int Page { get; } + public int Page { get; } = page; /// /// 每页条数。 /// - public int PageSize { get; } + public int PageSize { get; } = pageSize; /// /// 总条数。 /// - public int TotalCount { get; } + public int TotalCount { get; } = totalCount; /// /// 总页数。 /// - public int TotalPages { get; } - - /// - /// 初始化分页结果。 - /// - public PagedResult(IReadOnlyList items, int page, int pageSize, int totalCount) - { - Items = items; - Page = page; - PageSize = pageSize; - TotalCount = totalCount; - TotalPages = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); - } + public int TotalPages { get; } = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); } diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs index a53b38f..8ba5da6 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs @@ -3,40 +3,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Tenancy; /// /// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。 /// -public sealed class TenantContext +/// +/// 初始化租户上下文。 +/// +/// 租户 ID +/// 租户编码(可选) +/// 解析来源 +public sealed class TenantContext(long tenantId, string? tenantCode, string source) { /// /// 未解析到租户时的默认上下文。 /// public static TenantContext Empty { get; } = new(0, null, "unresolved"); - /// - /// 初始化租户上下文。 - /// - /// 租户 ID - /// 租户编码(可选) - /// 解析来源 - public TenantContext(long tenantId, string? tenantCode, string source) - { - TenantId = tenantId; - TenantCode = tenantCode; - Source = source; - } - /// /// 当前租户 ID,未解析时为 Guid.Empty。 /// - public long TenantId { get; } + public long TenantId { get; } = tenantId; /// /// 当前租户编码(例如子域名或业务编码),可为空。 /// - public string? TenantCode { get; } + public string? TenantCode { get; } = tenantCode; /// /// 租户解析来源(Header、Host、Token 等)。 /// - public string Source { get; } + public string Source { get; } = source; /// /// 是否已成功解析到租户。 diff --git a/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs b/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs index 533789d..06d22fb 100644 --- a/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs +++ b/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs @@ -8,7 +8,12 @@ namespace TakeoutSaaS.Shared.Kernel.Ids; /// /// 基于雪花算法的长整型 ID 生成器。 /// -public sealed class SnowflakeIdGenerator : IIdGenerator +/// +/// 初始化生成器。 +/// +/// 工作节点 ID。 +/// 机房 ID。 +public sealed class SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0) : IIdGenerator { private const long Twepoch = 1577836800000L; // 2020-01-01 UTC private const int WorkerIdBits = 5; @@ -23,23 +28,12 @@ public sealed class SnowflakeIdGenerator : IIdGenerator private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; private const long SequenceMask = -1L ^ (-1L << SequenceBits); - private readonly long _workerId; - private readonly long _datacenterId; + private readonly long _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId)); + private readonly long _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId)); private long _lastTimestamp = -1L; - private long _sequence; + private long _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask); private readonly object _syncRoot = new(); - /// - /// 初始化生成器。 - /// - /// 工作节点 ID。 - /// 机房 ID。 - public SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0) - { - _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId)); - _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId)); - _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask); - } /// public long NextId() diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs index ddbed3c..07740f7 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs @@ -11,22 +11,11 @@ namespace TakeoutSaaS.Shared.Web.Middleware; /// /// 统一 TraceId/CorrelationId,贯穿日志与响应。 /// -public sealed class CorrelationIdMiddleware +public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger logger, IIdGenerator idGenerator) { private const string TraceHeader = "X-Trace-Id"; private const string RequestHeader = "X-Request-Id"; - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly IIdGenerator _idGenerator; - - public CorrelationIdMiddleware(RequestDelegate next, ILogger logger, IIdGenerator idGenerator) - { - _next = next; - _logger = logger; - _idGenerator = idGenerator; - } - public async Task InvokeAsync(HttpContext context) { var traceId = ResolveTraceId(context); @@ -39,14 +28,14 @@ public sealed class CorrelationIdMiddleware return Task.CompletedTask; }); - using (_logger.BeginScope(new Dictionary + using (logger.BeginScope(new Dictionary { ["TraceId"] = traceId })) { try { - await _next(context); + await next(context); } finally { @@ -67,7 +56,7 @@ public sealed class CorrelationIdMiddleware return requestId; } - return _idGenerator.NextId().ToString(); + return idGenerator.NextId().ToString(); } private static bool TryGetHeader(HttpContext context, string headerName, out string value) diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs index da85777..2babea2 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs @@ -14,34 +14,23 @@ namespace TakeoutSaaS.Shared.Web.Middleware; /// /// 全局异常处理中间件,将异常统一映射为 ApiResponse。 /// -public sealed class ExceptionHandlingMiddleware +public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger, IHostEnvironment environment) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly IHostEnvironment _environment; - private static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger, IHostEnvironment environment) - { - _next = next; - _logger = logger; - _environment = environment; - } - public async Task InvokeAsync(HttpContext context) { try { - await _next(context); + await next(context); } catch (Exception ex) { - _logger.LogError(ex, "未处理异常:{Message}", ex.Message); + logger.LogError(ex, "未处理异常:{Message}", ex.Message); await HandleExceptionAsync(context, ex); } } @@ -50,7 +39,7 @@ public sealed class ExceptionHandlingMiddleware { var (statusCode, response) = BuildErrorResponse(exception); - if (_environment.IsDevelopment()) + if (environment.IsDevelopment()) { response = response with { diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs index 8121499..d2c12db 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs @@ -9,29 +9,21 @@ namespace TakeoutSaaS.Shared.Web.Middleware; /// /// 基础请求日志(方法、路径、耗时、状态码、TraceId)。 /// -public sealed class RequestLoggingMiddleware +public sealed class RequestLoggingMiddleware(RequestDelegate next, ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public RequestLoggingMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } public async Task InvokeAsync(HttpContext context) { var stopwatch = Stopwatch.StartNew(); try { - await _next(context); + await next(context); } finally { stopwatch.Stop(); var traceId = TraceContext.TraceId ?? context.TraceIdentifier; - _logger.LogInformation( + logger.LogInformation( "HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}", context.Request.Method, context.Request.Path, diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs index 6256f05..e7f5209 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs @@ -7,24 +7,19 @@ namespace TakeoutSaaS.Shared.Web.Security; /// /// 基于 HttpContext 的当前用户访问器。 /// -public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor +/// +/// 初始化访问器。 +/// +public sealed class HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) : ICurrentUserAccessor { - private readonly IHttpContextAccessor _httpContextAccessor; - /// - /// 初始化访问器。 - /// - public HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } /// public long UserId { get { - var principal = _httpContextAccessor.HttpContext?.User; + var principal = httpContextAccessor.HttpContext?.User; if (principal == null || !principal.Identity?.IsAuthenticated == true) { return 0; diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs index e620d67..17f0c2a 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs @@ -9,22 +9,15 @@ namespace TakeoutSaaS.Shared.Web.Swagger; /// /// 根据 API 版本动态注册 Swagger 文档。 /// -internal sealed class ConfigureSwaggerOptions : IConfigureOptions +internal sealed class ConfigureSwaggerOptions( + IApiVersionDescriptionProvider provider, + IOptions settings) : IConfigureOptions { - private readonly IApiVersionDescriptionProvider _provider; - private readonly SwaggerDocumentSettings _settings; - - public ConfigureSwaggerOptions( - IApiVersionDescriptionProvider provider, - IOptions settings) - { - _provider = provider; - _settings = settings.Value; - } + private readonly SwaggerDocumentSettings _settings = settings.Value; public void Configure(SwaggerGenOptions options) { - foreach (var description in _provider.ApiVersionDescriptions) + foreach (var description in provider.ApiVersionDescriptions) { var info = new OpenApiInfo { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs index e9a70d7..c6949ce 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs @@ -16,42 +16,34 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence; /// /// 业务数据种子,确保默认租户与基础字典可重复执行。 /// -public sealed class AppDataSeeder : IHostedService +/// +/// 初始化种子服务。 +/// +public sealed class AppDataSeeder( + IServiceProvider serviceProvider, + ILogger logger, + IOptions options) : IHostedService { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly AppSeedOptions _options; + private readonly AppSeedOptions _options = options.Value; - /// - /// 初始化种子服务。 - /// - public AppDataSeeder( - IServiceProvider serviceProvider, - ILogger logger, - IOptions options) - { - _serviceProvider = serviceProvider; - _logger = logger; - _options = options.Value; - } /// public async Task StartAsync(CancellationToken cancellationToken) { if (!_options.Enabled) { - _logger.LogInformation("AppSeed 未启用,跳过业务数据初始化"); + logger.LogInformation("AppSeed 未启用,跳过业务数据初始化"); return; } - using var scope = _serviceProvider.CreateScope(); + using var scope = serviceProvider.CreateScope(); var appDbContext = scope.ServiceProvider.GetRequiredService(); var dictionaryDbContext = scope.ServiceProvider.GetRequiredService(); var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken); await EnsureDictionarySeedsAsync(dictionaryDbContext, defaultTenantId, cancellationToken); - _logger.LogInformation("AppSeed 完成业务数据初始化"); + logger.LogInformation("AppSeed 完成业务数据初始化"); } /// @@ -65,7 +57,7 @@ public sealed class AppDataSeeder : IHostedService var tenantOptions = _options.DefaultTenant; if (tenantOptions == null || string.IsNullOrWhiteSpace(tenantOptions.Code) || string.IsNullOrWhiteSpace(tenantOptions.Name)) { - _logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子"); + logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子"); return null; } @@ -89,7 +81,7 @@ public sealed class AppDataSeeder : IHostedService await dbContext.Tenants.AddAsync(tenant, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); - _logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code); + logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code); return tenant.Id; } @@ -129,11 +121,11 @@ public sealed class AppDataSeeder : IHostedService { dbContext.Tenants.Update(existingTenant); await dbContext.SaveChangesAsync(cancellationToken); - _logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code); + logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code); } else { - _logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code); + logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code); } return existingTenant.Id; @@ -149,7 +141,7 @@ public sealed class AppDataSeeder : IHostedService if (!hasDictionaryGroups) { - _logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子"); + logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子"); } if (hasDictionaryGroups) @@ -158,7 +150,7 @@ public sealed class AppDataSeeder : IHostedService { if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name)) { - _logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空"); + logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空"); continue; } @@ -183,7 +175,7 @@ public sealed class AppDataSeeder : IHostedService }; await dbContext.DictionaryGroups.AddAsync(group, cancellationToken); - _logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId); + logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId); } else { @@ -236,7 +228,7 @@ public sealed class AppDataSeeder : IHostedService if (systemParameters.Count == 0) { - _logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子"); + logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子"); return; } @@ -246,7 +238,7 @@ public sealed class AppDataSeeder : IHostedService if (!grouped.Any()) { - _logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子"); + logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子"); return; } @@ -271,7 +263,7 @@ public sealed class AppDataSeeder : IHostedService }; await dbContext.DictionaryGroups.AddAsync(dictionaryGroup, cancellationToken); - _logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId); + logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId); } var seedItems = group.Select(x => new DictionarySeedItemOptions diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs index 32b482f..34917c7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs @@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 配送聚合的 EF Core 仓储实现。 /// -public sealed class EfDeliveryRepository : IDeliveryRepository +/// +/// 初始化仓储。 +/// +public sealed class EfDeliveryRepository(TakeoutAppDbContext context) : IDeliveryRepository { - private readonly TakeoutAppDbContext _context; - /// - /// 初始化仓储。 - /// - public EfDeliveryRepository(TakeoutAppDbContext context) - { - _context = context; - } /// public Task FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) { - return _context.DeliveryOrders + return context.DeliveryOrders .AsNoTracking() .Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId) .FirstOrDefaultAsync(cancellationToken); @@ -34,7 +29,7 @@ public sealed class EfDeliveryRepository : IDeliveryRepository /// public Task FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - return _context.DeliveryOrders + return context.DeliveryOrders .AsNoTracking() .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .FirstOrDefaultAsync(cancellationToken); @@ -43,7 +38,7 @@ public sealed class EfDeliveryRepository : IDeliveryRepository /// public async Task> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) { - var events = await _context.DeliveryEvents + var events = await context.DeliveryEvents .AsNoTracking() .Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId) .OrderBy(x => x.CreatedAt) @@ -55,25 +50,25 @@ public sealed class EfDeliveryRepository : IDeliveryRepository /// public Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) { - return _context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask(); + return context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask(); } /// public Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default) { - return _context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask(); + return context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask(); } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { - return _context.SaveChangesAsync(cancellationToken); + return context.SaveChangesAsync(cancellationToken); } /// public async Task> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default) { - var query = _context.DeliveryOrders + var query = context.DeliveryOrders .AsNoTracking() .Where(x => x.TenantId == tenantId); @@ -95,23 +90,23 @@ public sealed class EfDeliveryRepository : IDeliveryRepository /// public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) { - _context.DeliveryOrders.Update(deliveryOrder); + context.DeliveryOrders.Update(deliveryOrder); return Task.CompletedTask; } /// public async Task DeleteDeliveryOrderAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) { - var events = await _context.DeliveryEvents + var events = await context.DeliveryEvents .Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId) .ToListAsync(cancellationToken); if (events.Count > 0) { - _context.DeliveryEvents.RemoveRange(events); + context.DeliveryEvents.RemoveRange(events); } - var existing = await _context.DeliveryOrders + var existing = await context.DeliveryOrders .Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId) .FirstOrDefaultAsync(cancellationToken); @@ -120,6 +115,6 @@ public sealed class EfDeliveryRepository : IDeliveryRepository return; } - _context.DeliveryOrders.Remove(existing); + context.DeliveryOrders.Remove(existing); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs index 3aaa2c5..6158f92 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs @@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 商户聚合的 EF Core 仓储实现。 /// -public sealed class EfMerchantRepository : IMerchantRepository +/// +/// 初始化仓储。 +/// +public sealed class EfMerchantRepository(TakeoutAppDbContext context) : IMerchantRepository { - private readonly TakeoutAppDbContext _context; - /// - /// 初始化仓储。 - /// - public EfMerchantRepository(TakeoutAppDbContext context) - { - _context = context; - } /// public Task FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { - return _context.Merchants + return context.Merchants .AsNoTracking() .Where(x => x.TenantId == tenantId && x.Id == merchantId) .FirstOrDefaultAsync(cancellationToken); @@ -34,7 +29,7 @@ public sealed class EfMerchantRepository : IMerchantRepository /// public async Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default) { - var query = _context.Merchants + var query = context.Merchants .AsNoTracking() .Where(x => x.TenantId == tenantId); @@ -51,7 +46,7 @@ public sealed class EfMerchantRepository : IMerchantRepository /// public async Task> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { - var staffs = await _context.MerchantStaff + var staffs = await context.MerchantStaff .AsNoTracking() .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .OrderBy(x => x.Name) @@ -63,7 +58,7 @@ public sealed class EfMerchantRepository : IMerchantRepository /// public async Task> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { - var contracts = await _context.MerchantContracts + var contracts = await context.MerchantContracts .AsNoTracking() .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .OrderByDescending(x => x.CreatedAt) @@ -75,7 +70,7 @@ public sealed class EfMerchantRepository : IMerchantRepository /// public async Task> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { - var documents = await _context.MerchantDocuments + var documents = await context.MerchantDocuments .AsNoTracking() .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .OrderBy(x => x.CreatedAt) @@ -87,44 +82,44 @@ public sealed class EfMerchantRepository : IMerchantRepository /// public Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default) { - return _context.Merchants.AddAsync(merchant, cancellationToken).AsTask(); + return context.Merchants.AddAsync(merchant, cancellationToken).AsTask(); } /// public Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default) { - return _context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask(); + return context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask(); } /// public Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default) { - return _context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask(); + return context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask(); } /// public Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default) { - return _context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask(); + return context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask(); } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { - return _context.SaveChangesAsync(cancellationToken); + return context.SaveChangesAsync(cancellationToken); } /// public Task UpdateMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default) { - _context.Merchants.Update(merchant); + context.Merchants.Update(merchant); return Task.CompletedTask; } /// public async Task DeleteMerchantAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { - var existing = await _context.Merchants + var existing = await context.Merchants .Where(x => x.TenantId == tenantId && x.Id == merchantId) .FirstOrDefaultAsync(cancellationToken); @@ -133,6 +128,6 @@ public sealed class EfMerchantRepository : IMerchantRepository return; } - _context.Merchants.Remove(existing); + context.Merchants.Remove(existing); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs index 91aa04f..c73a185 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfOrderRepository.cs @@ -11,22 +11,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 订单聚合的 EF Core 仓储实现。 /// -public sealed class EfOrderRepository : IOrderRepository +/// +/// 初始化仓储。 +/// +public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepository { - private readonly TakeoutAppDbContext _context; - /// - /// 初始化仓储。 - /// - public EfOrderRepository(TakeoutAppDbContext context) - { - _context = context; - } /// public Task FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - return _context.Orders + return context.Orders .AsNoTracking() .Where(x => x.TenantId == tenantId && x.Id == orderId) .FirstOrDefaultAsync(cancellationToken); @@ -35,7 +30,7 @@ public sealed class EfOrderRepository : IOrderRepository /// public Task FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default) { - return _context.Orders + return context.Orders .AsNoTracking() .Where(x => x.TenantId == tenantId && x.OrderNo == orderNo) .FirstOrDefaultAsync(cancellationToken); @@ -44,7 +39,7 @@ public sealed class EfOrderRepository : IOrderRepository /// public async Task> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default) { - var query = _context.Orders + var query = context.Orders .AsNoTracking() .Where(x => x.TenantId == tenantId); @@ -68,7 +63,7 @@ public sealed class EfOrderRepository : IOrderRepository /// public async Task> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - var items = await _context.OrderItems + var items = await context.OrderItems .AsNoTracking() .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .OrderBy(x => x.Id) @@ -80,7 +75,7 @@ public sealed class EfOrderRepository : IOrderRepository /// public async Task> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - var histories = await _context.OrderStatusHistories + var histories = await context.OrderStatusHistories .AsNoTracking() .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .OrderBy(x => x.CreatedAt) @@ -92,7 +87,7 @@ public sealed class EfOrderRepository : IOrderRepository /// public async Task> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - var refunds = await _context.RefundRequests + var refunds = await context.RefundRequests .AsNoTracking() .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .OrderByDescending(x => x.CreatedAt) @@ -104,68 +99,68 @@ public sealed class EfOrderRepository : IOrderRepository /// public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default) { - return _context.Orders.AddAsync(order, cancellationToken).AsTask(); + return context.Orders.AddAsync(order, cancellationToken).AsTask(); } /// public Task AddItemsAsync(IEnumerable items, CancellationToken cancellationToken = default) { - return _context.OrderItems.AddRangeAsync(items, cancellationToken); + return context.OrderItems.AddRangeAsync(items, cancellationToken); } /// public Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default) { - return _context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask(); + return context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask(); } /// public Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default) { - return _context.RefundRequests.AddAsync(refund, cancellationToken).AsTask(); + return context.RefundRequests.AddAsync(refund, cancellationToken).AsTask(); } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { - return _context.SaveChangesAsync(cancellationToken); + return context.SaveChangesAsync(cancellationToken); } /// public Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default) { - _context.Orders.Update(order); + context.Orders.Update(order); return Task.CompletedTask; } /// public async Task DeleteOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - var items = await _context.OrderItems + var items = await context.OrderItems .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .ToListAsync(cancellationToken); if (items.Count > 0) { - _context.OrderItems.RemoveRange(items); + context.OrderItems.RemoveRange(items); } - var histories = await _context.OrderStatusHistories + var histories = await context.OrderStatusHistories .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .ToListAsync(cancellationToken); if (histories.Count > 0) { - _context.OrderStatusHistories.RemoveRange(histories); + context.OrderStatusHistories.RemoveRange(histories); } - var refunds = await _context.RefundRequests + var refunds = await context.RefundRequests .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .ToListAsync(cancellationToken); if (refunds.Count > 0) { - _context.RefundRequests.RemoveRange(refunds); + context.RefundRequests.RemoveRange(refunds); } - var existing = await _context.Orders + var existing = await context.Orders .Where(x => x.TenantId == tenantId && x.Id == orderId) .FirstOrDefaultAsync(cancellationToken); if (existing == null) @@ -173,6 +168,6 @@ public sealed class EfOrderRepository : IOrderRepository return; } - _context.Orders.Remove(existing); + context.Orders.Remove(existing); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs index f4dfb6c..90be2a6 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs @@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 支付记录的 EF Core 仓储实现。 /// -public sealed class EfPaymentRepository : IPaymentRepository +/// +/// 初始化仓储。 +/// +public sealed class EfPaymentRepository(TakeoutAppDbContext context) : IPaymentRepository { - private readonly TakeoutAppDbContext _context; - /// - /// 初始化仓储。 - /// - public EfPaymentRepository(TakeoutAppDbContext context) - { - _context = context; - } /// public Task FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) { - return _context.PaymentRecords + return context.PaymentRecords .AsNoTracking() .Where(x => x.TenantId == tenantId && x.Id == paymentId) .FirstOrDefaultAsync(cancellationToken); @@ -34,7 +29,7 @@ public sealed class EfPaymentRepository : IPaymentRepository /// public Task FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) { - return _context.PaymentRecords + return context.PaymentRecords .AsNoTracking() .Where(x => x.TenantId == tenantId && x.OrderId == orderId) .FirstOrDefaultAsync(cancellationToken); @@ -43,7 +38,7 @@ public sealed class EfPaymentRepository : IPaymentRepository /// public async Task> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) { - var refunds = await _context.PaymentRefundRecords + var refunds = await context.PaymentRefundRecords .AsNoTracking() .Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId) .OrderByDescending(x => x.CreatedAt) @@ -55,19 +50,19 @@ public sealed class EfPaymentRepository : IPaymentRepository /// public Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default) { - return _context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask(); + return context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask(); } /// public Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default) { - return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask(); + return context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask(); } /// public async Task> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default) { - var query = _context.PaymentRecords + var query = context.PaymentRecords .AsNoTracking() .Where(x => x.TenantId == tenantId); @@ -84,28 +79,28 @@ public sealed class EfPaymentRepository : IPaymentRepository /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { - return _context.SaveChangesAsync(cancellationToken); + return context.SaveChangesAsync(cancellationToken); } /// public Task UpdatePaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default) { - _context.PaymentRecords.Update(payment); + context.PaymentRecords.Update(payment); return Task.CompletedTask; } /// public async Task DeletePaymentAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) { - var refunds = await _context.PaymentRefundRecords + var refunds = await context.PaymentRefundRecords .Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId) .ToListAsync(cancellationToken); if (refunds.Count > 0) { - _context.PaymentRefundRecords.RemoveRange(refunds); + context.PaymentRefundRecords.RemoveRange(refunds); } - var existing = await _context.PaymentRecords + var existing = await context.PaymentRecords .Where(x => x.TenantId == tenantId && x.Id == paymentId) .FirstOrDefaultAsync(cancellationToken); if (existing == null) @@ -113,6 +108,6 @@ public sealed class EfPaymentRepository : IPaymentRepository return; } - _context.PaymentRecords.Remove(existing); + context.PaymentRecords.Remove(existing); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs index 234a367..65666bb 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfProductRepository.cs @@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 商品聚合的 EF Core 仓储实现。 /// -public sealed class EfProductRepository : IProductRepository +/// +/// 初始化仓储。 +/// +public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductRepository { - private readonly TakeoutAppDbContext _context; - /// - /// 初始化仓储。 - /// - public EfProductRepository(TakeoutAppDbContext context) - { - _context = context; - } /// public Task FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - return _context.Products + return context.Products .AsNoTracking() .Where(x => x.TenantId == tenantId && x.Id == productId) .FirstOrDefaultAsync(cancellationToken); @@ -34,7 +29,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> SearchAsync(long tenantId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default) { - var query = _context.Products + var query = context.Products .AsNoTracking() .Where(x => x.TenantId == tenantId); @@ -58,7 +53,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default) { - var categories = await _context.ProductCategories + var categories = await context.ProductCategories .AsNoTracking() .Where(x => x.TenantId == tenantId) .OrderBy(x => x.SortOrder) @@ -70,7 +65,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var skus = await _context.ProductSkus + var skus = await context.ProductSkus .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .OrderBy(x => x.SortOrder) @@ -82,7 +77,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var groups = await _context.ProductAddonGroups + var groups = await context.ProductAddonGroups .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .OrderBy(x => x.SortOrder) @@ -94,7 +89,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var groupIds = await _context.ProductAddonGroups + var groupIds = await context.ProductAddonGroups .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .Select(x => x.Id) @@ -105,7 +100,7 @@ public sealed class EfProductRepository : IProductRepository return Array.Empty(); } - var options = await _context.ProductAddonOptions + var options = await context.ProductAddonOptions .AsNoTracking() .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId)) .OrderBy(x => x.SortOrder) @@ -117,7 +112,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var groups = await _context.ProductAttributeGroups + var groups = await context.ProductAttributeGroups .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .OrderBy(x => x.SortOrder) @@ -129,7 +124,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var groupIds = await _context.ProductAttributeGroups + var groupIds = await context.ProductAttributeGroups .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .Select(x => x.Id) @@ -140,7 +135,7 @@ public sealed class EfProductRepository : IProductRepository return Array.Empty(); } - var options = await _context.ProductAttributeOptions + var options = await context.ProductAttributeOptions .AsNoTracking() .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId)) .OrderBy(x => x.SortOrder) @@ -152,7 +147,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var assets = await _context.ProductMediaAssets + var assets = await context.ProductMediaAssets .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .OrderBy(x => x.SortOrder) @@ -164,7 +159,7 @@ public sealed class EfProductRepository : IProductRepository /// public async Task> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var rules = await _context.ProductPricingRules + var rules = await context.ProductPricingRules .AsNoTracking() .Where(x => x.TenantId == tenantId && x.ProductId == productId) .OrderBy(x => x.SortOrder) @@ -176,59 +171,59 @@ public sealed class EfProductRepository : IProductRepository /// public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default) { - return _context.ProductCategories.AddAsync(category, cancellationToken).AsTask(); + return context.ProductCategories.AddAsync(category, cancellationToken).AsTask(); } /// public Task AddProductAsync(Product product, CancellationToken cancellationToken = default) { - return _context.Products.AddAsync(product, cancellationToken).AsTask(); + return context.Products.AddAsync(product, cancellationToken).AsTask(); } /// public Task AddSkusAsync(IEnumerable skus, CancellationToken cancellationToken = default) { - return _context.ProductSkus.AddRangeAsync(skus, cancellationToken); + return context.ProductSkus.AddRangeAsync(skus, cancellationToken); } /// public Task AddAddonGroupsAsync(IEnumerable groups, IEnumerable options, CancellationToken cancellationToken = default) { - var addGroupsTask = _context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken); - var addOptionsTask = _context.ProductAddonOptions.AddRangeAsync(options, cancellationToken); + var addGroupsTask = context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken); + var addOptionsTask = context.ProductAddonOptions.AddRangeAsync(options, cancellationToken); return Task.WhenAll(addGroupsTask, addOptionsTask); } /// public Task AddAttributeGroupsAsync(IEnumerable groups, IEnumerable options, CancellationToken cancellationToken = default) { - var addGroupsTask = _context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken); - var addOptionsTask = _context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken); + var addGroupsTask = context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken); + var addOptionsTask = context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken); return Task.WhenAll(addGroupsTask, addOptionsTask); } /// public Task AddMediaAssetsAsync(IEnumerable assets, CancellationToken cancellationToken = default) { - return _context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken); + return context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken); } /// public Task AddPricingRulesAsync(IEnumerable rules, CancellationToken cancellationToken = default) { - return _context.ProductPricingRules.AddRangeAsync(rules, cancellationToken); + return context.ProductPricingRules.AddRangeAsync(rules, cancellationToken); } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { - return _context.SaveChangesAsync(cancellationToken); + return context.SaveChangesAsync(cancellationToken); } /// public Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default) { - _context.Products.Update(product); + context.Products.Update(product); return Task.CompletedTask; } @@ -241,7 +236,7 @@ public sealed class EfProductRepository : IProductRepository await RemoveAddonGroupsAsync(productId, tenantId, cancellationToken); await RemoveSkusAsync(productId, tenantId, cancellationToken); - var existing = await _context.Products + var existing = await context.Products .Where(x => x.TenantId == tenantId && x.Id == productId) .FirstOrDefaultAsync(cancellationToken); @@ -250,20 +245,20 @@ public sealed class EfProductRepository : IProductRepository return; } - _context.Products.Remove(existing); + context.Products.Remove(existing); } /// public Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default) { - _context.ProductCategories.Update(category); + context.ProductCategories.Update(category); return Task.CompletedTask; } /// public async Task DeleteCategoryAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default) { - var existing = await _context.ProductCategories + var existing = await context.ProductCategories .Where(x => x.TenantId == tenantId && x.Id == categoryId) .FirstOrDefaultAsync(cancellationToken); @@ -272,13 +267,13 @@ public sealed class EfProductRepository : IProductRepository return; } - _context.ProductCategories.Remove(existing); + context.ProductCategories.Remove(existing); } /// public async Task RemoveSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var skus = await _context.ProductSkus + var skus = await context.ProductSkus .Where(x => x.TenantId == tenantId && x.ProductId == productId) .ToListAsync(cancellationToken); @@ -287,13 +282,13 @@ public sealed class EfProductRepository : IProductRepository return; } - _context.ProductSkus.RemoveRange(skus); + context.ProductSkus.RemoveRange(skus); } /// public async Task RemoveAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var groupIds = await _context.ProductAddonGroups + var groupIds = await context.ProductAddonGroups .Where(x => x.TenantId == tenantId && x.ProductId == productId) .Select(x => x.Id) .ToListAsync(cancellationToken); @@ -303,29 +298,29 @@ public sealed class EfProductRepository : IProductRepository return; } - var options = await _context.ProductAddonOptions + var options = await context.ProductAddonOptions .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId)) .ToListAsync(cancellationToken); if (options.Count > 0) { - _context.ProductAddonOptions.RemoveRange(options); + context.ProductAddonOptions.RemoveRange(options); } - var groups = await _context.ProductAddonGroups + var groups = await context.ProductAddonGroups .Where(x => groupIds.Contains(x.Id)) .ToListAsync(cancellationToken); if (groups.Count > 0) { - _context.ProductAddonGroups.RemoveRange(groups); + context.ProductAddonGroups.RemoveRange(groups); } } /// public async Task RemoveAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var groupIds = await _context.ProductAttributeGroups + var groupIds = await context.ProductAttributeGroups .Where(x => x.TenantId == tenantId && x.ProductId == productId) .Select(x => x.Id) .ToListAsync(cancellationToken); @@ -335,29 +330,29 @@ public sealed class EfProductRepository : IProductRepository return; } - var options = await _context.ProductAttributeOptions + var options = await context.ProductAttributeOptions .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId)) .ToListAsync(cancellationToken); if (options.Count > 0) { - _context.ProductAttributeOptions.RemoveRange(options); + context.ProductAttributeOptions.RemoveRange(options); } - var groups = await _context.ProductAttributeGroups + var groups = await context.ProductAttributeGroups .Where(x => groupIds.Contains(x.Id)) .ToListAsync(cancellationToken); if (groups.Count > 0) { - _context.ProductAttributeGroups.RemoveRange(groups); + context.ProductAttributeGroups.RemoveRange(groups); } } /// public async Task RemoveMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var assets = await _context.ProductMediaAssets + var assets = await context.ProductMediaAssets .Where(x => x.TenantId == tenantId && x.ProductId == productId) .ToListAsync(cancellationToken); @@ -366,13 +361,13 @@ public sealed class EfProductRepository : IProductRepository return; } - _context.ProductMediaAssets.RemoveRange(assets); + context.ProductMediaAssets.RemoveRange(assets); } /// public async Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default) { - var rules = await _context.ProductPricingRules + var rules = await context.ProductPricingRules .Where(x => x.TenantId == tenantId && x.ProductId == productId) .ToListAsync(cancellationToken); @@ -381,6 +376,6 @@ public sealed class EfProductRepository : IProductRepository return; } - _context.ProductPricingRules.RemoveRange(rules); + context.ProductPricingRules.RemoveRange(rules); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs index 4ed2412..3a0934a 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 门店聚合的 EF Core 仓储实现。 /// -public sealed class EfStoreRepository : IStoreRepository +/// +/// 初始化仓储。 +/// +public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepository { - private readonly TakeoutAppDbContext _context; - /// - /// 初始化仓储。 - /// - public EfStoreRepository(TakeoutAppDbContext context) - { - _context = context; - } /// public Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - return _context.Stores + return context.Stores .AsNoTracking() .Where(x => x.TenantId == tenantId && x.Id == storeId) .FirstOrDefaultAsync(cancellationToken); @@ -34,7 +29,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default) { - var query = _context.Stores + var query = context.Stores .AsNoTracking() .Where(x => x.TenantId == tenantId); @@ -53,7 +48,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var hours = await _context.StoreBusinessHours + var hours = await context.StoreBusinessHours .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.DayOfWeek) @@ -66,7 +61,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var zones = await _context.StoreDeliveryZones + var zones = await context.StoreDeliveryZones .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.SortOrder) @@ -78,7 +73,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var holidays = await _context.StoreHolidays + var holidays = await context.StoreHolidays .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.Date) @@ -90,7 +85,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var areas = await _context.StoreTableAreas + var areas = await context.StoreTableAreas .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.SortOrder) @@ -102,7 +97,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var tables = await _context.StoreTables + var tables = await context.StoreTables .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.TableCode) @@ -114,7 +109,7 @@ public sealed class EfStoreRepository : IStoreRepository /// public async Task> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var shifts = await _context.StoreEmployeeShifts + var shifts = await context.StoreEmployeeShifts .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.ShiftDate) @@ -127,62 +122,62 @@ public sealed class EfStoreRepository : IStoreRepository /// public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default) { - return _context.Stores.AddAsync(store, cancellationToken).AsTask(); + return context.Stores.AddAsync(store, cancellationToken).AsTask(); } /// public Task AddBusinessHoursAsync(IEnumerable hours, CancellationToken cancellationToken = default) { - return _context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken); + return context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken); } /// public Task AddDeliveryZonesAsync(IEnumerable zones, CancellationToken cancellationToken = default) { - return _context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken); + return context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken); } /// public Task AddHolidaysAsync(IEnumerable holidays, CancellationToken cancellationToken = default) { - return _context.StoreHolidays.AddRangeAsync(holidays, cancellationToken); + return context.StoreHolidays.AddRangeAsync(holidays, cancellationToken); } /// public Task AddTableAreasAsync(IEnumerable areas, CancellationToken cancellationToken = default) { - return _context.StoreTableAreas.AddRangeAsync(areas, cancellationToken); + return context.StoreTableAreas.AddRangeAsync(areas, cancellationToken); } /// public Task AddTablesAsync(IEnumerable tables, CancellationToken cancellationToken = default) { - return _context.StoreTables.AddRangeAsync(tables, cancellationToken); + return context.StoreTables.AddRangeAsync(tables, cancellationToken); } /// public Task AddShiftsAsync(IEnumerable shifts, CancellationToken cancellationToken = default) { - return _context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken); + return context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken); } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { - return _context.SaveChangesAsync(cancellationToken); + return context.SaveChangesAsync(cancellationToken); } /// public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default) { - _context.Stores.Update(store); + context.Stores.Update(store); return Task.CompletedTask; } /// public async Task DeleteStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { - var existing = await _context.Stores + var existing = await context.Stores .Where(x => x.TenantId == tenantId && x.Id == storeId) .FirstOrDefaultAsync(cancellationToken); @@ -191,6 +186,6 @@ public sealed class EfStoreRepository : IStoreRepository return; } - _context.Stores.Remove(existing); + context.Stores.Remove(existing); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs index 358a7c7..232ad67 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs @@ -10,24 +10,18 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories; /// /// EF Core 字典仓储实现。 /// -public sealed class EfDictionaryRepository : IDictionaryRepository +public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDictionaryRepository { - private readonly DictionaryDbContext _context; - - public EfDictionaryRepository(DictionaryDbContext context) - { - _context = context; - } public Task FindGroupByIdAsync(long id, CancellationToken cancellationToken = default) - => _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); + => context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); public Task FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default) - => _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken); + => context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken); public async Task> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default) { - var query = _context.DictionaryGroups.AsNoTracking(); + var query = context.DictionaryGroups.AsNoTracking(); if (scope.HasValue) { query = query.Where(group => group.Scope == scope.Value); @@ -40,22 +34,22 @@ public sealed class EfDictionaryRepository : IDictionaryRepository public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) { - _context.DictionaryGroups.Add(group); + context.DictionaryGroups.Add(group); return Task.CompletedTask; } public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) { - _context.DictionaryGroups.Remove(group); + context.DictionaryGroups.Remove(group); return Task.CompletedTask; } public Task FindItemByIdAsync(long id, CancellationToken cancellationToken = default) - => _context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); + => context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); public async Task> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default) { - return await _context.DictionaryItems + return await context.DictionaryItems .AsNoTracking() .Where(item => item.GroupId == groupId) .OrderBy(item => item.SortOrder) @@ -64,18 +58,18 @@ public sealed class EfDictionaryRepository : IDictionaryRepository public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) { - _context.DictionaryItems.Add(item); + context.DictionaryItems.Add(item); return Task.CompletedTask; } public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) { - _context.DictionaryItems.Remove(item); + context.DictionaryItems.Remove(item); return Task.CompletedTask; } public Task SaveChangesAsync(CancellationToken cancellationToken = default) - => _context.SaveChangesAsync(cancellationToken); + => context.SaveChangesAsync(cancellationToken); public async Task> GetItemsByCodesAsync(IEnumerable codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default) { @@ -90,7 +84,7 @@ public sealed class EfDictionaryRepository : IDictionaryRepository return Array.Empty(); } - var query = _context.DictionaryItems + var query = context.DictionaryItems .AsNoTracking() .IgnoreQueryFilters() .Include(item => item.Group) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs index d2708ba..372c467 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs @@ -10,22 +10,15 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Services; /// /// 基于 IDistributedCache 的字典缓存实现。 /// -public sealed class DistributedDictionaryCache : IDictionaryCache +public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions options) : IDictionaryCache { - private readonly IDistributedCache _cache; - private readonly DictionaryCacheOptions _options; + private readonly DictionaryCacheOptions _options = options.Value; private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); - public DistributedDictionaryCache(IDistributedCache cache, IOptions options) - { - _cache = cache; - _options = options.Value; - } - public async Task?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default) { var cacheKey = BuildKey(tenantId, code); - var payload = await _cache.GetAsync(cacheKey, cancellationToken); + var payload = await cache.GetAsync(cacheKey, cancellationToken); if (payload == null || payload.Length == 0) { return null; @@ -42,13 +35,13 @@ public sealed class DistributedDictionaryCache : IDictionaryCache { SlidingExpiration = _options.SlidingExpiration }; - return _cache.SetAsync(cacheKey, payload, options, cancellationToken); + return cache.SetAsync(cacheKey, payload, options, cancellationToken); } public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default) { var cacheKey = BuildKey(tenantId, code); - return _cache.RemoveAsync(cacheKey, cancellationToken); + return cache.RemoveAsync(cacheKey, cancellationToken); } private static string BuildKey(long tenantId, string code) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index f11a02d..a355874 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -10,18 +10,12 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// /// EF Core 后台用户仓储实现。 /// -public sealed class EfIdentityUserRepository : IIdentityUserRepository +public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIdentityUserRepository { - private readonly IdentityDbContext _dbContext; - - public EfIdentityUserRepository(IdentityDbContext dbContext) - { - _dbContext = dbContext; - } public Task FindByAccountAsync(string account, CancellationToken cancellationToken = default) - => _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); + => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); public Task FindByIdAsync(long userId, CancellationToken cancellationToken = default) - => _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); + => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs index 9f843ca..3276793 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs @@ -10,24 +10,18 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// /// EF Core 小程序用户仓储实现。 /// -public sealed class EfMiniUserRepository : IMiniUserRepository +public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUserRepository { - private readonly IdentityDbContext _dbContext; - - public EfMiniUserRepository(IdentityDbContext dbContext) - { - _dbContext = dbContext; - } public Task FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default) - => _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); + => dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) - => _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + => dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); public async Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default) { - var user = await _dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); + var user = await dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); if (user == null) { user = new MiniUser @@ -39,7 +33,7 @@ public sealed class EfMiniUserRepository : IMiniUserRepository Avatar = avatar, TenantId = tenantId }; - _dbContext.MiniUsers.Add(user); + dbContext.MiniUsers.Add(user); } else { @@ -48,7 +42,7 @@ public sealed class EfMiniUserRepository : IMiniUserRepository user.Avatar = avatar ?? user.Avatar; } - await _dbContext.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); return user; } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs index 9c7e339..e997c2e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs @@ -13,21 +13,14 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services; /// /// Redis 登录限流实现。 /// -public sealed class RedisLoginRateLimiter : ILoginRateLimiter +public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions options) : ILoginRateLimiter { - private readonly IDistributedCache _cache; - private readonly LoginRateLimitOptions _options; - - public RedisLoginRateLimiter(IDistributedCache cache, IOptions options) - { - _cache = cache; - _options = options.Value; - } + private readonly LoginRateLimitOptions _options = options.Value; public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default) { var cacheKey = BuildKey(key); - var current = await _cache.GetStringAsync(cacheKey, cancellationToken); + var current = await cache.GetStringAsync(cacheKey, cancellationToken); var count = string.IsNullOrWhiteSpace(current) ? 0 : int.Parse(current); if (count >= _options.MaxAttempts) { @@ -35,7 +28,7 @@ public sealed class RedisLoginRateLimiter : ILoginRateLimiter } count++; - await _cache.SetStringAsync( + await cache.SetStringAsync( cacheKey, count.ToString(), new DistributedCacheEntryOptions @@ -46,7 +39,7 @@ public sealed class RedisLoginRateLimiter : ILoginRateLimiter } public Task ResetAsync(string key, CancellationToken cancellationToken = default) - => _cache.RemoveAsync(BuildKey(key), cancellationToken); + => cache.RemoveAsync(BuildKey(key), cancellationToken); private static string BuildKey(string key) => $"identity:login:{key}"; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs index 3a670a9..36105e9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs @@ -14,17 +14,10 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services; /// /// Redis 刷新令牌存储。 /// -public sealed class RedisRefreshTokenStore : IRefreshTokenStore +public sealed class RedisRefreshTokenStore(IDistributedCache cache, IOptions options) : IRefreshTokenStore { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); - private readonly IDistributedCache _cache; - private readonly RefreshTokenStoreOptions _options; - - public RedisRefreshTokenStore(IDistributedCache cache, IOptions options) - { - _cache = cache; - _options = options.Value; - } + private readonly RefreshTokenStoreOptions _options = options.Value; public async Task IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default) { @@ -33,14 +26,14 @@ public sealed class RedisRefreshTokenStore : IRefreshTokenStore var key = BuildKey(token); var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt }; - await _cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken); + await cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken); return descriptor; } public async Task GetAsync(string refreshToken, CancellationToken cancellationToken = default) { - var json = await _cache.GetStringAsync(BuildKey(refreshToken), cancellationToken); + var json = await cache.GetStringAsync(BuildKey(refreshToken), cancellationToken); return string.IsNullOrWhiteSpace(json) ? null : JsonSerializer.Deserialize(json, JsonOptions); @@ -56,7 +49,7 @@ public sealed class RedisRefreshTokenStore : IRefreshTokenStore var updated = descriptor with { Revoked = true }; var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt }; - await _cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken); + await cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken); } private string BuildKey(string token) => $"{_options.Prefix}{token}"; diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs index 6fa0efb..1bd8b1b 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs @@ -15,21 +15,14 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services; /// /// 微信 code2Session 实现 /// -public sealed class WeChatAuthService : IWeChatAuthService +public sealed class WeChatAuthService(HttpClient httpClient, IOptions options) : IWeChatAuthService { - private readonly HttpClient _httpClient; - private readonly WeChatMiniOptions _options; - - public WeChatAuthService(HttpClient httpClient, IOptions options) - { - _httpClient = httpClient; - _options = options.Value; - } + private readonly WeChatMiniOptions _options = options.Value; public async Task Code2SessionAsync(string code, CancellationToken cancellationToken = default) { var requestUri = $"sns/jscode2session?appid={Uri.EscapeDataString(_options.AppId)}&secret={Uri.EscapeDataString(_options.Secret)}&js_code={Uri.EscapeDataString(code)}&grant_type=authorization_code"; - using var response = await _httpClient.GetAsync(requestUri, cancellationToken); + using var response = await httpClient.GetAsync(requestUri, cancellationToken); response.EnsureSuccessStatusCode(); var payload = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs index 2d8df5b..80d6a81 100644 --- a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs +++ b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageDirectUploadRequest.cs @@ -3,40 +3,34 @@ namespace TakeoutSaaS.Module.Storage.Models; /// /// 直传(预签名上传)请求参数。 /// -public sealed class StorageDirectUploadRequest +/// +/// 初始化请求。 +/// +/// 对象键。 +/// 内容类型。 +/// 内容长度。 +/// 签名有效期。 +public sealed class StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires) { - /// - /// 初始化请求。 - /// - /// 对象键。 - /// 内容类型。 - /// 内容长度。 - /// 签名有效期。 - public StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires) - { - ObjectKey = objectKey; - ContentType = contentType; - ContentLength = contentLength; - Expires = expires; - } + /// /// 目标对象键。 /// - public string ObjectKey { get; } + public string ObjectKey { get; } = objectKey; /// /// 内容类型。 /// - public string ContentType { get; } + public string ContentType { get; } = contentType; /// /// 内容长度。 /// - public long ContentLength { get; } + public long ContentLength { get; } = contentLength; /// /// 签名有效期。 /// - public TimeSpan Expires { get; } + public TimeSpan Expires { get; } = expires; } diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs index 80a2644..3d055ab 100644 --- a/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs +++ b/src/Modules/TakeoutSaaS.Module.Storage/Models/StorageUploadRequest.cs @@ -6,70 +6,60 @@ namespace TakeoutSaaS.Module.Storage.Models; /// /// 对象存储上传请求参数。 /// -public sealed class StorageUploadRequest +/// +/// 初始化上传请求。 +/// +/// 对象键(含路径)。 +/// 文件流。 +/// 内容类型。 +/// 内容长度。 +/// 是否返回签名访问链接。 +/// 签名有效期。 +/// 附加元数据。 +public sealed class StorageUploadRequest( + string objectKey, + Stream content, + string contentType, + long contentLength, + bool generateSignedUrl, + TimeSpan signedUrlExpires, + IDictionary? metadata = null) { - /// - /// 初始化上传请求。 - /// - /// 对象键(含路径)。 - /// 文件流。 - /// 内容类型。 - /// 内容长度。 - /// 是否返回签名访问链接。 - /// 签名有效期。 - /// 附加元数据。 - public StorageUploadRequest( - string objectKey, - Stream content, - string contentType, - long contentLength, - bool generateSignedUrl, - TimeSpan signedUrlExpires, - IDictionary? metadata = null) - { - ObjectKey = objectKey; - Content = content; - ContentType = contentType; - ContentLength = contentLength; - GenerateSignedUrl = generateSignedUrl; - SignedUrlExpires = signedUrlExpires; - Metadata = metadata == null - ? new Dictionary() - : new Dictionary(metadata); - } /// /// 对象键。 /// - public string ObjectKey { get; } + public string ObjectKey { get; } = objectKey; /// /// 文件流。 /// - public Stream Content { get; } + public Stream Content { get; } = content; /// /// 内容类型。 /// - public string ContentType { get; } + public string ContentType { get; } = contentType; /// /// 内容长度。 /// - public long ContentLength { get; } + public long ContentLength { get; } = contentLength; /// /// 是否需要签名访问链接。 /// - public bool GenerateSignedUrl { get; } + public bool GenerateSignedUrl { get; } = generateSignedUrl; /// /// 签名有效期。 /// - public TimeSpan SignedUrlExpires { get; } + public TimeSpan SignedUrlExpires { get; } = signedUrlExpires; /// /// 元数据集合。 /// - public IReadOnlyDictionary Metadata { get; } + public IReadOnlyDictionary Metadata { get; } = metadata == null + ? new Dictionary() + : new Dictionary(metadata); } diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs index 7faff05..069202d 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs @@ -5,20 +5,15 @@ namespace TakeoutSaaS.Module.Tenancy; /// /// 默认租户提供者:基于租户上下文访问器暴露当前租户 ID。 /// -public sealed class TenantProvider : ITenantProvider +/// +/// 初始化租户提供者。 +/// +/// 租户上下文访问器 +public sealed class TenantProvider(ITenantContextAccessor tenantContextAccessor) : ITenantProvider { - private readonly ITenantContextAccessor _tenantContextAccessor; - /// - /// 初始化租户提供者。 - /// - /// 租户上下文访问器 - public TenantProvider(ITenantContextAccessor tenantContextAccessor) - { - _tenantContextAccessor = tenantContextAccessor; - } /// public long GetCurrentTenantId() - => _tenantContextAccessor.Current?.TenantId ?? 0; + => tenantContextAccessor.Current?.TenantId ?? 0; } diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs index a1066c1..59247c8 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs @@ -11,54 +11,43 @@ namespace TakeoutSaaS.Module.Tenancy; /// /// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。 /// -public sealed class TenantResolutionMiddleware +/// +/// 初始化中间件。 +/// +public sealed class TenantResolutionMiddleware( + RequestDelegate next, + ILogger logger, + ITenantContextAccessor tenantContextAccessor, + IOptionsMonitor optionsMonitor) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly ITenantContextAccessor _tenantContextAccessor; - private readonly IOptionsMonitor _optionsMonitor; - /// - /// 初始化中间件。 - /// - public TenantResolutionMiddleware( - RequestDelegate next, - ILogger logger, - ITenantContextAccessor tenantContextAccessor, - IOptionsMonitor optionsMonitor) - { - _next = next; - _logger = logger; - _tenantContextAccessor = tenantContextAccessor; - _optionsMonitor = optionsMonitor; - } /// /// 解析租户并将上下文注入请求。 /// public async Task InvokeAsync(HttpContext context) { - var options = _optionsMonitor.CurrentValue ?? new TenantResolutionOptions(); + var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions(); if (ShouldSkip(context.Request.Path, options)) { - await _next(context); + await next(context); return; } var tenantContext = ResolveTenant(context, options); - _tenantContextAccessor.Current = tenantContext; + tenantContextAccessor.Current = tenantContext; context.Items[TenantConstants.HttpContextItemKey] = tenantContext; if (!tenantContext.IsResolved) { - _logger.LogDebug("未能解析租户:{Path}", context.Request.Path); + logger.LogDebug("未能解析租户:{Path}", context.Request.Path); if (options.ThrowIfUnresolved) { var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识"); context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted); - _tenantContextAccessor.Current = null; + tenantContextAccessor.Current = null; context.Items.Remove(TenantConstants.HttpContextItemKey); return; } @@ -66,11 +55,11 @@ public sealed class TenantResolutionMiddleware try { - await _next(context); + await next(context); } finally { - _tenantContextAccessor.Current = null; + tenantContextAccessor.Current = null; context.Items.Remove(TenantConstants.HttpContextItemKey); } } From 5d1dd54c804951b3b555b59864e51cdcc249b61d Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:17:02 +0800 Subject: [PATCH 29/56] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E6=94=AF=E4=BB=98=E6=A0=A1=E9=AA=8C=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Validators/CreateOrderCommandValidator.cs | 33 +++++++++++++++++++ .../Validators/UpdateOrderCommandValidator.cs | 33 +++++++++++++++++++ .../CreatePaymentCommandValidator.cs | 23 +++++++++++++ .../UpdatePaymentCommandValidator.cs | 23 +++++++++++++ 4 files changed, 112 insertions(+) diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs index 729d166..60f3d0c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/CreateOrderCommandValidator.cs @@ -1,5 +1,7 @@ using FluentValidation; using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; namespace TakeoutSaaS.Application.App.Orders.Validators; @@ -25,6 +27,37 @@ public sealed class CreateOrderCommandValidator : AbstractValidator x.DiscountAmount).GreaterThanOrEqualTo(0); RuleFor(x => x.PayableAmount).GreaterThanOrEqualTo(0); RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.PayableAmount) + .Must((cmd, payable) => payable == cmd.ItemsAmount - cmd.DiscountAmount) + .WithMessage("应付金额必须等于商品金额减去优惠金额"); + RuleFor(x => x.PaidAmount) + .LessThanOrEqualTo(x => x.PayableAmount) + .WithMessage("实付金额不得超过应付金额"); + + RuleFor(x => x.PaymentStatus) + .Must(status => status is PaymentStatus.Unpaid or PaymentStatus.Paying or PaymentStatus.Paid or PaymentStatus.Failed or PaymentStatus.Refunded) + .WithMessage("支付状态不合法"); + When(x => x.PaymentStatus == PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).NotNull().WithMessage("支付成功必须包含支付时间"); + RuleFor(x => x.PaidAmount).GreaterThan(0).WithMessage("支付成功时实付金额必须大于 0"); + }); + When(x => x.PaymentStatus != PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).Must(paidAt => paidAt == null).WithMessage("非支付成功状态不应包含支付时间"); + }); + When(x => x.PaymentStatus == PaymentStatus.Refunded, () => + { + RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0).WithMessage("退款状态下实付金额需合法"); + }); + When(x => x.Status == OrderStatus.Cancelled, () => + { + RuleFor(x => x.CancelReason).NotEmpty().WithMessage("取消订单必须提供取消原因"); + }); + When(x => x.Status != OrderStatus.Cancelled, () => + { + RuleFor(x => x.CancelReason).Must(reason => string.IsNullOrWhiteSpace(reason)).WithMessage("非取消状态不应包含取消原因"); + }); RuleFor(x => x.Items) .NotEmpty() diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs index 2dba2f7..745f033 100644 --- a/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Validators/UpdateOrderCommandValidator.cs @@ -1,5 +1,7 @@ using FluentValidation; using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; namespace TakeoutSaaS.Application.App.Orders.Validators; @@ -26,5 +28,36 @@ public sealed class UpdateOrderCommandValidator : AbstractValidator x.DiscountAmount).GreaterThanOrEqualTo(0); RuleFor(x => x.PayableAmount).GreaterThanOrEqualTo(0); RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0); + RuleFor(x => x.PayableAmount) + .Must((cmd, payable) => payable == cmd.ItemsAmount - cmd.DiscountAmount) + .WithMessage("应付金额必须等于商品金额减去优惠金额"); + RuleFor(x => x.PaidAmount) + .LessThanOrEqualTo(x => x.PayableAmount) + .WithMessage("实付金额不得超过应付金额"); + + RuleFor(x => x.PaymentStatus) + .Must(status => status is PaymentStatus.Unpaid or PaymentStatus.Paying or PaymentStatus.Paid or PaymentStatus.Failed or PaymentStatus.Refunded) + .WithMessage("支付状态不合法"); + When(x => x.PaymentStatus == PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).NotNull().WithMessage("支付成功必须包含支付时间"); + RuleFor(x => x.PaidAmount).GreaterThan(0).WithMessage("支付成功时实付金额必须大于 0"); + }); + When(x => x.PaymentStatus != PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).Must(paidAt => paidAt == null).WithMessage("非支付成功状态不应包含支付时间"); + }); + When(x => x.PaymentStatus == PaymentStatus.Refunded, () => + { + RuleFor(x => x.PaidAmount).GreaterThanOrEqualTo(0).WithMessage("退款状态下实付金额需合法"); + }); + When(x => x.Status == OrderStatus.Cancelled, () => + { + RuleFor(x => x.CancelReason).NotEmpty().WithMessage("取消订单必须提供取消原因"); + }); + When(x => x.Status != OrderStatus.Cancelled, () => + { + RuleFor(x => x.CancelReason).Must(reason => string.IsNullOrWhiteSpace(reason)).WithMessage("非取消状态不应包含取消原因"); + }); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs index f7e7abb..5041e1d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/CreatePaymentCommandValidator.cs @@ -1,5 +1,6 @@ using FluentValidation; using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Domain.Payments.Enums; namespace TakeoutSaaS.Application.App.Payments.Validators; @@ -15,8 +16,30 @@ public sealed class CreatePaymentCommandValidator : AbstractValidator x.OrderId).GreaterThan(0); RuleFor(x => x.Amount).GreaterThan(0); + RuleFor(x => x.Method) + .Must(method => method != PaymentMethod.Unknown) + .WithMessage("支付方式不可为空"); RuleFor(x => x.TradeNo).MaximumLength(64); RuleFor(x => x.ChannelTransactionId).MaximumLength(64); RuleFor(x => x.Remark).MaximumLength(256); + + RuleFor(x => x.Status) + .Must(status => status is PaymentStatus.Unpaid or PaymentStatus.Paying or PaymentStatus.Paid or PaymentStatus.Failed or PaymentStatus.Refunded) + .WithMessage("支付状态不合法"); + When(x => x.Status == PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).NotNull().WithMessage("支付成功必须包含支付时间"); + }); + When(x => x.Status != PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).Must(paidAt => paidAt == null).WithMessage("非支付成功状态不应包含支付时间"); + }); + + When(x => x.Method is PaymentMethod.Cash or PaymentMethod.Card or PaymentMethod.Balance, () => + { + RuleFor(x => x.Status) + .Must(status => status is not PaymentStatus.Paying) + .WithMessage("线下/余额支付不允许处于 Paying 状态"); + }); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs index 997e07b..65dfd1c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Validators/UpdatePaymentCommandValidator.cs @@ -1,5 +1,6 @@ using FluentValidation; using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Domain.Payments.Enums; namespace TakeoutSaaS.Application.App.Payments.Validators; @@ -16,8 +17,30 @@ public sealed class UpdatePaymentCommandValidator : AbstractValidator x.PaymentId).GreaterThan(0); RuleFor(x => x.OrderId).GreaterThan(0); RuleFor(x => x.Amount).GreaterThan(0); + RuleFor(x => x.Method) + .Must(method => method != PaymentMethod.Unknown) + .WithMessage("支付方式不可为空"); RuleFor(x => x.TradeNo).MaximumLength(64); RuleFor(x => x.ChannelTransactionId).MaximumLength(64); RuleFor(x => x.Remark).MaximumLength(256); + + RuleFor(x => x.Status) + .Must(status => status is PaymentStatus.Unpaid or PaymentStatus.Paying or PaymentStatus.Paid or PaymentStatus.Failed or PaymentStatus.Refunded) + .WithMessage("支付状态不合法"); + When(x => x.Status == PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).NotNull().WithMessage("支付成功必须包含支付时间"); + }); + When(x => x.Status != PaymentStatus.Paid, () => + { + RuleFor(x => x.PaidAt).Must(paidAt => paidAt == null).WithMessage("非支付成功状态不应包含支付时间"); + }); + + When(x => x.Method is PaymentMethod.Cash or PaymentMethod.Card or PaymentMethod.Balance, () => + { + RuleFor(x => x.Status) + .Must(status => status is not PaymentStatus.Paying) + .WithMessage("线下/余额支付不允许处于 Paying 状态"); + }); } } From dd15a88ff4a68551b69c5d0f038fde4661fc6f91 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:35:33 +0800 Subject: [PATCH 30/56] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=8B=AC=E7=AB=8B=E8=A1=A8=E4=B8=8E=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entities/SystemParameter.cs | 34 +++ .../App/Persistence/AppDataSeeder.cs | 81 +++-- .../Persistence/DictionaryDbContext.cs | 34 ++- ...43204_AddSystemParametersTable.Designer.cs | 288 ++++++++++++++++++ ...20251202043204_AddSystemParametersTable.cs | 59 ++++ .../DictionaryDbContextModelSnapshot.cs | 78 +++++ 6 files changed, 545 insertions(+), 29 deletions(-) create mode 100644 src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs diff --git a/src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs b/src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs new file mode 100644 index 0000000..a6c7826 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/SystemParameters/Entities/SystemParameter.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.SystemParameters.Entities; + +/// +/// 系统参数实体:支持按租户维护的键值型配置。 +/// +public sealed class SystemParameter : MultiTenantEntityBase +{ + /// + /// 参数键,租户内唯一。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 参数值,支持文本或 JSON。 + /// + public string Value { get; set; } = string.Empty; + + /// + /// 描述信息。 + /// + public string? Description { get; set; } + + /// + /// 排序值,越小越靠前。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs index c6949ce..d2926e1 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Domain.Dictionary.Entities; using TakeoutSaaS.Domain.Dictionary.Enums; +using TakeoutSaaS.Domain.SystemParameters.Entities; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Infrastructure.App.Options; @@ -219,8 +220,9 @@ public sealed class AppDataSeeder( await dbContext.SaveChangesAsync(cancellationToken); } + /// - /// 确保系统参数以字典形式幂等种子。 + /// 确保系统参数以独立表形式可重复种子。 /// private async Task EnsureSystemParametersAsync(DictionaryDbContext dbContext, long? defaultTenantId, CancellationToken cancellationToken) { @@ -245,37 +247,64 @@ public sealed class AppDataSeeder( foreach (var group in grouped) { var tenantId = group.Key; - var dictionaryGroup = await dbContext.DictionaryGroups + var existingParameters = await dbContext.SystemParameters .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Code == "system_parameters", cancellationToken); + .Where(x => x.TenantId == tenantId) + .ToListAsync(cancellationToken); - if (dictionaryGroup == null) + foreach (var seed in group) { - dictionaryGroup = new DictionaryGroup + var key = seed.Key.Trim(); + var existing = existingParameters.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + + if (existing == null) { - Id = 0, - TenantId = tenantId, - Code = "system_parameters", - Name = "系统参数", - Scope = tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business, - Description = "系统参数配置", - IsEnabled = true - }; + var parameter = new SystemParameter + { + Id = 0, + TenantId = tenantId, + Key = key, + Value = seed.Value.Trim(), + Description = seed.Description?.Trim(), + SortOrder = seed.SortOrder, + IsEnabled = seed.IsEnabled + }; - await dbContext.DictionaryGroups.AddAsync(dictionaryGroup, cancellationToken); - logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId); + await dbContext.SystemParameters.AddAsync(parameter, cancellationToken); + continue; + } + + var updated = false; + + if (!string.Equals(existing.Value, seed.Value, StringComparison.Ordinal)) + { + existing.Value = seed.Value.Trim(); + updated = true; + } + + if (!string.Equals(existing.Description, seed.Description, StringComparison.Ordinal)) + { + existing.Description = seed.Description?.Trim(); + updated = true; + } + + if (existing.SortOrder != seed.SortOrder) + { + existing.SortOrder = seed.SortOrder; + updated = true; + } + + if (existing.IsEnabled != seed.IsEnabled) + { + existing.IsEnabled = seed.IsEnabled; + updated = true; + } + + if (updated) + { + dbContext.SystemParameters.Update(existing); + } } - - var seedItems = group.Select(x => new DictionarySeedItemOptions - { - Key = x.Key.Trim(), - Value = x.Value.Trim(), - Description = x.Description?.Trim(), - SortOrder = x.SortOrder, - IsEnabled = x.IsEnabled - }); - - await UpsertDictionaryItemsAsync(dbContext, dictionaryGroup, seedItems, tenantId, cancellationToken); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index 6025e35..53e7e24 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Domain.Dictionary.Entities; +using TakeoutSaaS.Domain.SystemParameters.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; @@ -9,7 +10,7 @@ using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; /// -/// 参数字典 DbContext。 +/// 参数字典 DbContext:承载字典与系统参数。 /// public sealed class DictionaryDbContext( DbContextOptions options, @@ -19,15 +20,20 @@ public sealed class DictionaryDbContext( : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// - /// 字典分组集。 + /// 字典分组集合。 /// public DbSet DictionaryGroups => Set(); /// - /// 字典项集。 + /// 字典项集合。 /// public DbSet DictionaryItems => Set(); + /// + /// 系统参数集合。 + /// + public DbSet SystemParameters => Set(); + /// /// 配置实体模型。 /// @@ -37,6 +43,7 @@ public sealed class DictionaryDbContext( base.OnModelCreating(modelBuilder); ConfigureGroup(modelBuilder.Entity()); ConfigureItem(modelBuilder.Entity()); + ConfigureSystemParameter(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } @@ -87,4 +94,25 @@ public sealed class DictionaryDbContext( builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.GroupId, x.Key }).IsUnique(); } + + /// + /// 配置系统参数。 + /// + /// 实体构建器。 + private static void ConfigureSystemParameter(EntityTypeBuilder builder) + { + builder.ToTable("system_parameters"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Key).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Value).HasColumnType("text").IsRequired(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.SortOrder).HasDefaultValue(100); + builder.Property(x => x.IsEnabled).HasDefaultValue(true); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Key }).IsUnique(); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs new file mode 100644 index 0000000..8d3c8c2 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.Designer.cs @@ -0,0 +1,288 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb +{ + [DbContext(typeof(DictionaryDbContext))] + [Migration("20251202043204_AddSystemParametersTable")] + partial class AddSystemParametersTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组编码(唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("分组名称。"); + + b.Property("Scope") + .HasColumnType("integer") + .HasComment("分组作用域:系统/业务。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("dictionary_groups", null, t => + { + t.HasComment("参数字典分组(系统参数、业务参数)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("GroupId") + .HasColumnType("bigint") + .HasComment("关联分组 ID。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认项。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("字典项键。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("字典项值。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("GroupId", "Key") + .IsUnique(); + + b.ToTable("dictionary_items", null, t => + { + t.HasComment("参数字典项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.SystemParameters.Entities.SystemParameter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("参数键,租户内唯一。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasComment("参数值,支持文本或 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Key") + .IsUnique(); + + b.ToTable("system_parameters", null, t => + { + t.HasComment("系统参数实体:支持按租户维护的键值型配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") + .WithMany("Items") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs new file mode 100644 index 0000000..816fe03 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/20251202043204_AddSystemParametersTable.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb +{ + /// + public partial class AddSystemParametersTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "system_parameters", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Key = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "参数键,租户内唯一。"), + Value = table.Column(type: "text", nullable: false, comment: "参数值,支持文本或 JSON。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "描述信息。"), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100, comment: "排序值,越小越靠前。"), + IsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true, comment: "是否启用。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_system_parameters", x => x.Id); + }, + comment: "系统参数实体:支持按租户维护的键值型配置。"); + + migrationBuilder.CreateIndex( + name: "IX_system_parameters_TenantId", + table: "system_parameters", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_system_parameters_TenantId_Key", + table: "system_parameters", + columns: new[] { "TenantId", "Key" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "system_parameters"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs index 1df55bf..35585f3 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/DictionaryDb/DictionaryDbContextModelSnapshot.cs @@ -186,6 +186,84 @@ namespace TakeoutSaaS.Infrastructure.Migrations.DictionaryDb }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.SystemParameters.Entities.SystemParameter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述信息。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("参数键,租户内唯一。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值,越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasComment("参数值,支持文本或 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Key") + .IsUnique(); + + b.ToTable("system_parameters", null, t => + { + t.HasComment("系统参数实体:支持按租户维护的键值型配置。"); + }); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b => { b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group") From 3b2b376787131b515d1f8900a41ee95a9078d668 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:53:06 +0800 Subject: [PATCH 31/56] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=BA=94=E7=94=A8=E5=B1=82=E4=B8=8E=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/SystemParametersController.cs | 115 ++++++++++++++++++ .../Commands/CreateSystemParameterCommand.cs | 35 ++++++ .../Commands/DeleteSystemParameterCommand.cs | 14 +++ .../Commands/UpdateSystemParameterCommand.cs | 40 ++++++ .../Dto/SystemParameterDto.cs | 57 +++++++++ .../CreateSystemParameterCommandHandler.cs | 64 ++++++++++ .../DeleteSystemParameterCommandHandler.cs | 33 +++++ .../GetSystemParameterByIdQueryHandler.cs | 35 ++++++ .../SearchSystemParametersQueryHandler.cs | 69 +++++++++++ .../UpdateSystemParameterCommandHandler.cs | 66 ++++++++++ .../Queries/GetSystemParameterByIdQuery.cs | 9 ++ .../Queries/SearchSystemParametersQuery.cs | 41 +++++++ .../CreateSystemParameterCommandValidator.cs | 21 ++++ .../SearchSystemParametersQueryValidator.cs | 21 ++++ .../UpdateSystemParameterCommandValidator.cs | 22 ++++ .../ISystemParameterRepository.cs | 47 +++++++ .../DictionaryServiceCollectionExtensions.cs | 2 + .../EfSystemParameterRepository.cs | 80 ++++++++++++ 18 files changed, 771 insertions(+) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/SystemParametersController.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/CreateSystemParameterCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/DeleteSystemParameterCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/UpdateSystemParameterCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Dto/SystemParameterDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/CreateSystemParameterCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/DeleteSystemParameterCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/GetSystemParameterByIdQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/SearchSystemParametersQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/UpdateSystemParameterCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/GetSystemParameterByIdQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/SearchSystemParametersQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/CreateSystemParameterCommandValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/SearchSystemParametersQueryValidator.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/UpdateSystemParameterCommandValidator.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/SystemParameters/Repositories/ISystemParameterRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfSystemParameterRepository.cs diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/SystemParametersController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/SystemParametersController.cs new file mode 100644 index 0000000..045d649 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/SystemParametersController.cs @@ -0,0 +1,115 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.SystemParameters.Commands; +using TakeoutSaaS.Application.App.SystemParameters.Dto; +using TakeoutSaaS.Application.App.SystemParameters.Queries; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 系统参数管理。 +/// +/// +/// 提供参数的新增、修改、查询与删除。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/system-parameters")] +public sealed class SystemParametersController(IMediator mediator) : BaseApiController +{ + /// + /// 创建系统参数。 + /// + [HttpPost] + [PermissionAuthorize("system-parameter:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateSystemParameterCommand command, CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询系统参数列表。 + /// + [HttpGet] + [PermissionAuthorize("system-parameter:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + [FromQuery] string? keyword, + [FromQuery] bool? isEnabled, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? sortBy = null, + [FromQuery] bool sortDesc = true, + CancellationToken cancellationToken = default) + { + var result = await mediator.Send(new SearchSystemParametersQuery + { + Keyword = keyword, + IsEnabled = isEnabled, + Page = page, + PageSize = pageSize, + SortBy = sortBy, + SortDescending = sortDesc + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取系统参数详情。 + /// + [HttpGet("{parameterId:long}")] + [PermissionAuthorize("system-parameter:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long parameterId, CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetSystemParameterByIdQuery(parameterId), cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "系统参数不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新系统参数。 + /// + [HttpPut("{parameterId:long}")] + [PermissionAuthorize("system-parameter:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long parameterId, [FromBody] UpdateSystemParameterCommand command, CancellationToken cancellationToken) + { + if (command.ParameterId == 0) + { + command = command with { ParameterId = parameterId }; + } + + var result = await mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "系统参数不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除系统参数。 + /// + [HttpDelete("{parameterId:long}")] + [PermissionAuthorize("system-parameter:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long parameterId, CancellationToken cancellationToken) + { + var success = await mediator.Send(new DeleteSystemParameterCommand { ParameterId = parameterId }, cancellationToken); + return success + ? ApiResponse.Success() + : ApiResponse.Error(ErrorCodes.NotFound, "系统参数不存在"); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/CreateSystemParameterCommand.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/CreateSystemParameterCommand.cs new file mode 100644 index 0000000..c5f332c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/CreateSystemParameterCommand.cs @@ -0,0 +1,35 @@ +using MediatR; +using TakeoutSaaS.Application.App.SystemParameters.Dto; + +namespace TakeoutSaaS.Application.App.SystemParameters.Commands; + +/// +/// 创建系统参数命令。 +/// +public sealed class CreateSystemParameterCommand : IRequest +{ + /// + /// 参数键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 参数值。 + /// + public string Value { get; set; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; set; } + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/DeleteSystemParameterCommand.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/DeleteSystemParameterCommand.cs new file mode 100644 index 0000000..6f49ad8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/DeleteSystemParameterCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.SystemParameters.Commands; + +/// +/// 删除系统参数命令。 +/// +public sealed record DeleteSystemParameterCommand : IRequest +{ + /// + /// 参数 ID。 + /// + public long ParameterId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/UpdateSystemParameterCommand.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/UpdateSystemParameterCommand.cs new file mode 100644 index 0000000..86bbfa7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Commands/UpdateSystemParameterCommand.cs @@ -0,0 +1,40 @@ +using MediatR; +using TakeoutSaaS.Application.App.SystemParameters.Dto; + +namespace TakeoutSaaS.Application.App.SystemParameters.Commands; + +/// +/// 更新系统参数命令。 +/// +public sealed record UpdateSystemParameterCommand : IRequest +{ + /// + /// 参数 ID。 + /// + public long ParameterId { get; init; } + + /// + /// 参数键。 + /// + public string Key { get; init; } = string.Empty; + + /// + /// 参数值。 + /// + public string Value { get; init; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 排序值。 + /// + public int SortOrder { get; init; } = 100; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; init; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Dto/SystemParameterDto.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Dto/SystemParameterDto.cs new file mode 100644 index 0000000..8305ae4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Dto/SystemParameterDto.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.SystemParameters.Dto; + +/// +/// 系统参数 DTO。 +/// +public sealed class SystemParameterDto +{ + /// + /// 参数 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 参数键。 + /// + public string Key { get; init; } = string.Empty; + + /// + /// 参数值。 + /// + public string Value { get; init; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 排序值。 + /// + public int SortOrder { get; init; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } + + /// + /// 最近更新时间。 + /// + public DateTime? UpdatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/CreateSystemParameterCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/CreateSystemParameterCommandHandler.cs new file mode 100644 index 0000000..64ab3c1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/CreateSystemParameterCommandHandler.cs @@ -0,0 +1,64 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.SystemParameters.Commands; +using TakeoutSaaS.Application.App.SystemParameters.Dto; +using TakeoutSaaS.Domain.SystemParameters.Entities; +using TakeoutSaaS.Domain.SystemParameters.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Application.App.SystemParameters.Handlers; + +/// +/// 创建系统参数命令处理器。 +/// +public sealed class CreateSystemParameterCommandHandler( + ISystemParameterRepository repository, + ILogger logger) + : IRequestHandler +{ + private readonly ISystemParameterRepository _repository = repository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateSystemParameterCommand request, CancellationToken cancellationToken) + { + // 1. 唯一性校验 + var existing = await _repository.FindByKeyAsync(request.Key, cancellationToken); + if (existing != null) + { + throw new BusinessException(ErrorCodes.Conflict, "系统参数键已存在"); + } + + // 2. 构建实体 + var parameter = new SystemParameter + { + Key = request.Key.Trim(), + Value = request.Value.Trim(), + Description = request.Description?.Trim(), + SortOrder = request.SortOrder, + IsEnabled = request.IsEnabled + }; + + // 3. 持久化 + await _repository.AddAsync(parameter, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建系统参数 {Key}", parameter.Key); + + // 4. 映射返回 + return MapToDto(parameter); + } + + private static SystemParameterDto MapToDto(SystemParameter parameter) => new() + { + Id = parameter.Id, + TenantId = parameter.TenantId, + Key = parameter.Key, + Value = parameter.Value, + Description = parameter.Description, + SortOrder = parameter.SortOrder, + IsEnabled = parameter.IsEnabled, + CreatedAt = parameter.CreatedAt, + UpdatedAt = parameter.UpdatedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/DeleteSystemParameterCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/DeleteSystemParameterCommandHandler.cs new file mode 100644 index 0000000..cda2c42 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/DeleteSystemParameterCommandHandler.cs @@ -0,0 +1,33 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.SystemParameters.Commands; +using TakeoutSaaS.Domain.SystemParameters.Repositories; + +namespace TakeoutSaaS.Application.App.SystemParameters.Handlers; + +/// +/// 删除系统参数命令处理器。 +/// +public sealed class DeleteSystemParameterCommandHandler( + ISystemParameterRepository repository, + ILogger logger) + : IRequestHandler +{ + private readonly ISystemParameterRepository _repository = repository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteSystemParameterCommand request, CancellationToken cancellationToken) + { + var existing = await _repository.FindByIdAsync(request.ParameterId, cancellationToken); + if (existing == null) + { + return false; + } + + await _repository.RemoveAsync(existing, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除系统参数 {Key}", existing.Key); + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/GetSystemParameterByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/GetSystemParameterByIdQueryHandler.cs new file mode 100644 index 0000000..c2bc53a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/GetSystemParameterByIdQueryHandler.cs @@ -0,0 +1,35 @@ +using MediatR; +using TakeoutSaaS.Application.App.SystemParameters.Dto; +using TakeoutSaaS.Application.App.SystemParameters.Queries; +using TakeoutSaaS.Domain.SystemParameters.Repositories; + +namespace TakeoutSaaS.Application.App.SystemParameters.Handlers; + +/// +/// 获取系统参数详情查询处理器。 +/// +public sealed class GetSystemParameterByIdQueryHandler(ISystemParameterRepository repository) + : IRequestHandler +{ + private readonly ISystemParameterRepository _repository = repository; + + /// + public async Task Handle(GetSystemParameterByIdQuery request, CancellationToken cancellationToken) + { + var parameter = await _repository.FindByIdAsync(request.ParameterId, cancellationToken); + return parameter == null ? null : MapToDto(parameter); + } + + private static SystemParameterDto MapToDto(Domain.SystemParameters.Entities.SystemParameter parameter) => new() + { + Id = parameter.Id, + TenantId = parameter.TenantId, + Key = parameter.Key, + Value = parameter.Value, + Description = parameter.Description, + SortOrder = parameter.SortOrder, + IsEnabled = parameter.IsEnabled, + CreatedAt = parameter.CreatedAt, + UpdatedAt = parameter.UpdatedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/SearchSystemParametersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/SearchSystemParametersQueryHandler.cs new file mode 100644 index 0000000..678b55c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/SearchSystemParametersQueryHandler.cs @@ -0,0 +1,69 @@ +using MediatR; +using TakeoutSaaS.Application.App.SystemParameters.Dto; +using TakeoutSaaS.Application.App.SystemParameters.Queries; +using TakeoutSaaS.Domain.SystemParameters.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.SystemParameters.Handlers; + +/// +/// 系统参数列表查询处理器。 +/// +public sealed class SearchSystemParametersQueryHandler(ISystemParameterRepository repository) + : IRequestHandler> +{ + private readonly ISystemParameterRepository _repository = repository; + + /// + public async Task> Handle(SearchSystemParametersQuery request, CancellationToken cancellationToken) + { + var parameters = await _repository.SearchAsync(request.Keyword, request.IsEnabled, cancellationToken); + + var sorted = ApplySorting(parameters, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + var items = paged.Select(MapToDto).ToList(); + return new PagedResult(items, request.Page, request.PageSize, parameters.Count); + } + + private static IOrderedEnumerable ApplySorting( + IReadOnlyCollection parameters, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "key" => sortDescending + ? parameters.OrderByDescending(x => x.Key) + : parameters.OrderBy(x => x.Key), + "sortorder" => sortDescending + ? parameters.OrderByDescending(x => x.SortOrder) + : parameters.OrderBy(x => x.SortOrder), + "updatedat" => sortDescending + ? parameters.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt) + : parameters.OrderBy(x => x.UpdatedAt ?? x.CreatedAt), + "isenabled" => sortDescending + ? parameters.OrderByDescending(x => x.IsEnabled) + : parameters.OrderBy(x => x.IsEnabled), + _ => sortDescending + ? parameters.OrderByDescending(x => x.CreatedAt) + : parameters.OrderBy(x => x.CreatedAt) + }; + } + + private static SystemParameterDto MapToDto(Domain.SystemParameters.Entities.SystemParameter parameter) => new() + { + Id = parameter.Id, + TenantId = parameter.TenantId, + Key = parameter.Key, + Value = parameter.Value, + Description = parameter.Description, + SortOrder = parameter.SortOrder, + IsEnabled = parameter.IsEnabled, + CreatedAt = parameter.CreatedAt, + UpdatedAt = parameter.UpdatedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/UpdateSystemParameterCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/UpdateSystemParameterCommandHandler.cs new file mode 100644 index 0000000..6ba3104 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Handlers/UpdateSystemParameterCommandHandler.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.SystemParameters.Commands; +using TakeoutSaaS.Application.App.SystemParameters.Dto; +using TakeoutSaaS.Domain.SystemParameters.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Application.App.SystemParameters.Handlers; + +/// +/// 更新系统参数命令处理器。 +/// +public sealed class UpdateSystemParameterCommandHandler( + ISystemParameterRepository repository, + ILogger logger) + : IRequestHandler +{ + private readonly ISystemParameterRepository _repository = repository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateSystemParameterCommand request, CancellationToken cancellationToken) + { + // 1. 读取已有参数 + var existing = await _repository.FindByIdAsync(request.ParameterId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 唯一性校验 + var duplicate = await _repository.FindByKeyAsync(request.Key, cancellationToken); + if (duplicate != null && duplicate.Id != existing.Id) + { + throw new BusinessException(ErrorCodes.Conflict, "系统参数键已存在"); + } + + // 3. 更新字段 + existing.Key = request.Key.Trim(); + existing.Value = request.Value.Trim(); + existing.Description = request.Description?.Trim(); + existing.SortOrder = request.SortOrder; + existing.IsEnabled = request.IsEnabled; + + // 4. 持久化 + await _repository.UpdateAsync(existing, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新系统参数 {Key}", existing.Key); + + return MapToDto(existing); + } + + private static SystemParameterDto MapToDto(Domain.SystemParameters.Entities.SystemParameter parameter) => new() + { + Id = parameter.Id, + TenantId = parameter.TenantId, + Key = parameter.Key, + Value = parameter.Value, + Description = parameter.Description, + SortOrder = parameter.SortOrder, + IsEnabled = parameter.IsEnabled, + CreatedAt = parameter.CreatedAt, + UpdatedAt = parameter.UpdatedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/GetSystemParameterByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/GetSystemParameterByIdQuery.cs new file mode 100644 index 0000000..09c6b70 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/GetSystemParameterByIdQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using TakeoutSaaS.Application.App.SystemParameters.Dto; + +namespace TakeoutSaaS.Application.App.SystemParameters.Queries; + +/// +/// 获取系统参数详情查询。 +/// +public sealed record GetSystemParameterByIdQuery(long ParameterId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/SearchSystemParametersQuery.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/SearchSystemParametersQuery.cs new file mode 100644 index 0000000..d6e0238 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Queries/SearchSystemParametersQuery.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.App.SystemParameters.Dto; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.SystemParameters.Queries; + +/// +/// 系统参数列表查询。 +/// +public sealed class SearchSystemParametersQuery : IRequest> +{ + /// + /// 关键字(匹配 Key/Description)。 + /// + public string? Keyword { get; init; } + + /// + /// 启用状态过滤。 + /// + public bool? IsEnabled { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(key/sortOrder/createdAt/updatedAt/isEnabled)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/CreateSystemParameterCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/CreateSystemParameterCommandValidator.cs new file mode 100644 index 0000000..a624833 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/CreateSystemParameterCommandValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.SystemParameters.Commands; + +namespace TakeoutSaaS.Application.App.SystemParameters.Validators; + +/// +/// 创建系统参数命令验证器。 +/// +public sealed class CreateSystemParameterCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateSystemParameterCommandValidator() + { + RuleFor(x => x.Key).NotEmpty().MaximumLength(128); + RuleFor(x => x.Value).NotEmpty(); + RuleFor(x => x.Description).MaximumLength(512); + RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/SearchSystemParametersQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/SearchSystemParametersQueryValidator.cs new file mode 100644 index 0000000..83586ab --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/SearchSystemParametersQueryValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.SystemParameters.Queries; + +namespace TakeoutSaaS.Application.App.SystemParameters.Validators; + +/// +/// 系统参数列表查询验证器。 +/// +public sealed class SearchSystemParametersQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchSystemParametersQueryValidator() + { + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + RuleFor(x => x.Keyword).MaximumLength(256); + RuleFor(x => x.SortBy).MaximumLength(64); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/UpdateSystemParameterCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/UpdateSystemParameterCommandValidator.cs new file mode 100644 index 0000000..831ad11 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/SystemParameters/Validators/UpdateSystemParameterCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.SystemParameters.Commands; + +namespace TakeoutSaaS.Application.App.SystemParameters.Validators; + +/// +/// 更新系统参数命令验证器。 +/// +public sealed class UpdateSystemParameterCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateSystemParameterCommandValidator() + { + RuleFor(x => x.ParameterId).GreaterThan(0); + RuleFor(x => x.Key).NotEmpty().MaximumLength(128); + RuleFor(x => x.Value).NotEmpty(); + RuleFor(x => x.Description).MaximumLength(512); + RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Domain/TakeoutSaaS.Domain/SystemParameters/Repositories/ISystemParameterRepository.cs b/src/Domain/TakeoutSaaS.Domain/SystemParameters/Repositories/ISystemParameterRepository.cs new file mode 100644 index 0000000..c222342 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/SystemParameters/Repositories/ISystemParameterRepository.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.SystemParameters.Entities; + +namespace TakeoutSaaS.Domain.SystemParameters.Repositories; + +/// +/// 系统参数仓储接口:提供基础 CRUD 与查询能力。 +/// +public interface ISystemParameterRepository +{ + /// + /// 根据标识获取系统参数。 + /// + Task FindByIdAsync(long id, CancellationToken cancellationToken = default); + + /// + /// 根据键获取系统参数(当前租户)。 + /// + Task FindByKeyAsync(string key, CancellationToken cancellationToken = default); + + /// + /// 查询系统参数列表。 + /// + Task> SearchAsync(string? keyword, bool? isEnabled, CancellationToken cancellationToken = default); + + /// + /// 新增系统参数。 + /// + Task AddAsync(SystemParameter parameter, CancellationToken cancellationToken = default); + + /// + /// 删除系统参数。 + /// + Task RemoveAsync(SystemParameter parameter, CancellationToken cancellationToken = default); + + /// + /// 更新系统参数。 + /// + Task UpdateAsync(SystemParameter parameter, CancellationToken cancellationToken = default); + + /// + /// 持久化更改。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs index c680f1e..41cc530 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Extensions/DictionaryServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using TakeoutSaaS.Infrastructure.Dictionary.Persistence; using TakeoutSaaS.Infrastructure.Dictionary.Repositories; using TakeoutSaaS.Infrastructure.Dictionary.Services; using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Domain.SystemParameters.Repositories; namespace TakeoutSaaS.Infrastructure.Dictionary.Extensions; @@ -31,6 +32,7 @@ public static class DictionaryServiceCollectionExtensions services.AddPostgresDbContext(DatabaseConstants.DictionaryDataSource); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddOptions() diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfSystemParameterRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfSystemParameterRepository.cs new file mode 100644 index 0000000..1ec7868 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfSystemParameterRepository.cs @@ -0,0 +1,80 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.SystemParameters.Entities; +using TakeoutSaaS.Domain.SystemParameters.Repositories; +using TakeoutSaaS.Infrastructure.Dictionary.Persistence; + +namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories; + +/// +/// 系统参数 EF Core 仓储实现。 +/// +public sealed class EfSystemParameterRepository(DictionaryDbContext context) : ISystemParameterRepository +{ + /// + public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) + { + return context.SystemParameters + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + } + + /// + public Task FindByKeyAsync(string key, CancellationToken cancellationToken = default) + { + var normalizedKey = key.Trim(); + return context.SystemParameters + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Key == normalizedKey, cancellationToken); + } + + /// + public async Task> SearchAsync(string? keyword, bool? isEnabled, CancellationToken cancellationToken = default) + { + var query = context.SystemParameters.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalized = keyword.Trim(); + query = query.Where(x => x.Key.Contains(normalized) || (x.Description != null && x.Description.Contains(normalized))); + } + + if (isEnabled.HasValue) + { + query = query.Where(x => x.IsEnabled == isEnabled.Value); + } + + var parameters = await query + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.Key) + .ToListAsync(cancellationToken); + + return parameters; + } + + /// + public Task AddAsync(SystemParameter parameter, CancellationToken cancellationToken = default) + { + return context.SystemParameters.AddAsync(parameter, cancellationToken).AsTask(); + } + + /// + public Task RemoveAsync(SystemParameter parameter, CancellationToken cancellationToken = default) + { + context.SystemParameters.Remove(parameter); + return Task.CompletedTask; + } + + /// + public Task UpdateAsync(SystemParameter parameter, CancellationToken cancellationToken = default) + { + context.SystemParameters.Update(parameter); + return Task.CompletedTask; + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return context.SaveChangesAsync(cancellationToken); + } +} From 331edbb44a78d69da3551f30c609cd4ee4ce7168 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:55:06 +0800 Subject: [PATCH 32/56] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0SystemTodo?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index f715bbc..b657be7 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -23,7 +23,7 @@ ## 3. 稳定性与质量 - [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 -- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。 +- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ 的集成测试模板。 - [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 ## 4. 安全与合规 From 2043e227d7319f9aadf502904164d8d009433f73 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:56:23 +0800 Subject: [PATCH 33/56] =?UTF-8?q?fix:=20=E7=A8=B3=E5=AE=9A=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E7=A7=8D=E5=AD=90=E5=A4=96=E9=94=AE=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Persistence/AppDataSeeder.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs index d2926e1..4072f0c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs @@ -318,6 +318,12 @@ public sealed class AppDataSeeder( long tenantId, CancellationToken cancellationToken) { + // 确保分组已持久化以获取正确的主键,避免 FK 约束报错。 + if (!dbContext.Entry(group).IsKeySet || group.Id == 0) + { + await dbContext.SaveChangesAsync(cancellationToken); + } + var materializedItems = seedItems .Where(item => !string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value)) .ToList(); From fcce44b767dc9aa80d6506d9497b6222d1916ba1 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 12:58:27 +0800 Subject: [PATCH 34/56] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B011=E5=8F=B7?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index b657be7..d37014e 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -18,8 +18,8 @@ ## 2. 数据与迁移(高优) - [x] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 -- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 -- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 +- [x] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 +- [x] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 ## 3. 稳定性与质量 - [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 From 753c10c492b0f51c6391c71e865a4d1131f40944 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 13:27:43 +0800 Subject: [PATCH 35/56] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90OpenTelemetry?= =?UTF-8?q?=E5=9F=8B=E7=82=B9=E4=B8=8E=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Api/TakeoutSaaS.AdminApi/Program.cs | 63 +++++++++++++++++- .../TakeoutSaaS.AdminApi.csproj | 7 ++ .../appsettings.Development.json | 33 ++++++++-- .../appsettings.Production.json | 33 ++++++++-- src/Api/TakeoutSaaS.MiniApi/Program.cs | 66 ++++++++++++++++++- .../TakeoutSaaS.MiniApi.csproj | 7 ++ .../appsettings.Development.json | 33 ++++++++-- .../appsettings.Production.json | 33 ++++++++-- src/Api/TakeoutSaaS.UserApi/Program.cs | 66 ++++++++++++++++++- .../TakeoutSaaS.UserApi.csproj | 7 ++ .../appsettings.Development.json | 11 +++- .../appsettings.Production.json | 11 +++- 12 files changed, 343 insertions(+), 27 deletions(-) diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 1cdf937..e9619a0 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Serilog; using TakeoutSaaS.Application.App.Extensions; using TakeoutSaaS.Application.Identity.Extensions; @@ -34,7 +37,12 @@ builder.Host.UseSerilog((context, _, configuration) => configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "AdminApi") - .WriteTo.Console(); + .WriteTo.Console() + .WriteTo.File( + "logs/admin-api-.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 7, + shared: true); }); builder.Services.AddSharedWebCore(); @@ -60,6 +68,59 @@ builder.Services.AddSmsApplication(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddMessagingApplication(); builder.Services.AddSchedulerModule(builder.Configuration); +var otelSection = builder.Configuration.GetSection("Otel"); +var otelEndpoint = otelSection.GetValue("Endpoint"); +var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService( + serviceName: "TakeoutSaaS.AdminApi", + serviceVersion: "1.0.0", + serviceInstanceId: Environment.MachineName)) + .WithTracing(tracing => + { + tracing + .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation(options => + { + options.SetDbStatementForText = false; + options.SetDbStatementForStoredProcedure = false; + }); + + if (!string.IsNullOrWhiteSpace(otelEndpoint)) + { + tracing.AddOtlpExporter(exporter => + { + exporter.Endpoint = new Uri(otelEndpoint); + }); + } + + if (useConsoleExporter) + { + tracing.AddConsoleExporter(); + } + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + + if (!string.IsNullOrWhiteSpace(otelEndpoint)) + { + metrics.AddOtlpExporter(exporter => + { + exporter.Endpoint = new Uri(otelEndpoint); + }); + } + + if (useConsoleExporter) + { + metrics.AddConsoleExporter(); + } + }); var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin"); builder.Services.AddCors(options => diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index c8258af..810c4b7 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -8,6 +8,13 @@ + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index 4eb2c0e..1bc5044 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -60,7 +60,9 @@ "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ "/health" ], + "IgnoredPaths": [ + "/health" + ], "RootDomain": "" }, "Storage": { @@ -95,11 +97,27 @@ }, "Security": { "MaxFileSizeBytes": 10485760, - "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], - "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "AllowedImageExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif" + ], + "AllowedFileExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif", + ".pdf" + ], "DefaultUrlExpirationMinutes": 30, "EnableRefererValidation": true, - "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AllowedReferers": [ + "https://admin.example.com", + "https://miniapp.example.com" + ], "AntiLeechTokenSecret": "ReplaceWithARandomToken" } }, @@ -149,5 +167,10 @@ "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" + }, + "Otel": { + "Endpoint": "", + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true } -} +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json index 4eb2c0e..1bc5044 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json @@ -60,7 +60,9 @@ "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ "/health" ], + "IgnoredPaths": [ + "/health" + ], "RootDomain": "" }, "Storage": { @@ -95,11 +97,27 @@ }, "Security": { "MaxFileSizeBytes": 10485760, - "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], - "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "AllowedImageExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif" + ], + "AllowedFileExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif", + ".pdf" + ], "DefaultUrlExpirationMinutes": 30, "EnableRefererValidation": true, - "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AllowedReferers": [ + "https://admin.example.com", + "https://miniapp.example.com" + ], "AntiLeechTokenSecret": "ReplaceWithARandomToken" } }, @@ -149,5 +167,10 @@ "WorkerCount": 5, "DashboardEnabled": false, "DashboardPath": "/hangfire" + }, + "Otel": { + "Endpoint": "", + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true } -} +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index 5dd9962..2c44874 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -1,4 +1,10 @@ +using System; +using System.Linq; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Serilog; using TakeoutSaaS.Application.Messaging.Extensions; using TakeoutSaaS.Application.Sms.Extensions; @@ -17,7 +23,12 @@ builder.Host.UseSerilog((_, _, configuration) => configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "MiniApi") - .WriteTo.Console(); + .WriteTo.Console() + .WriteTo.File( + "logs/mini-api-.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 7, + shared: true); }); builder.Services.AddSharedWebCore(); @@ -34,6 +45,59 @@ builder.Services.AddSmsModule(builder.Configuration); builder.Services.AddSmsApplication(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddMessagingApplication(); +var otelSection = builder.Configuration.GetSection("Otel"); +var otelEndpoint = otelSection.GetValue("Endpoint"); +var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService( + serviceName: "TakeoutSaaS.MiniApi", + serviceVersion: "1.0.0", + serviceInstanceId: Environment.MachineName)) + .WithTracing(tracing => + { + tracing + .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation(options => + { + options.SetDbStatementForText = false; + options.SetDbStatementForStoredProcedure = false; + }); + + if (!string.IsNullOrWhiteSpace(otelEndpoint)) + { + tracing.AddOtlpExporter(exporter => + { + exporter.Endpoint = new Uri(otelEndpoint); + }); + } + + if (useConsoleExporter) + { + tracing.AddConsoleExporter(); + } + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + + if (!string.IsNullOrWhiteSpace(otelEndpoint)) + { + metrics.AddOtlpExporter(exporter => + { + exporter.Endpoint = new Uri(otelEndpoint); + }); + } + + if (useConsoleExporter) + { + metrics.AddConsoleExporter(); + } + }); var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini"); builder.Services.AddCors(options => diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index 2269e2e..a5ca222 100644 --- a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj +++ b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj @@ -9,6 +9,13 @@ + + + + + + + diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json index 3b288cf..8eb84b7 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json @@ -55,7 +55,9 @@ "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ "/health" ], + "IgnoredPaths": [ + "/health" + ], "RootDomain": "" }, "Storage": { @@ -90,11 +92,27 @@ }, "Security": { "MaxFileSizeBytes": 10485760, - "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], - "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "AllowedImageExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif" + ], + "AllowedFileExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif", + ".pdf" + ], "DefaultUrlExpirationMinutes": 30, "EnableRefererValidation": true, - "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AllowedReferers": [ + "https://admin.example.com", + "https://miniapp.example.com" + ], "AntiLeechTokenSecret": "ReplaceWithARandomToken" } }, @@ -138,5 +156,10 @@ "Exchange": "takeout.events", "ExchangeType": "topic", "PrefetchCount": 20 + }, + "Otel": { + "Endpoint": "", + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true } -} +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json index 3b288cf..8eb84b7 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json @@ -55,7 +55,9 @@ "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ "/health" ], + "IgnoredPaths": [ + "/health" + ], "RootDomain": "" }, "Storage": { @@ -90,11 +92,27 @@ }, "Security": { "MaxFileSizeBytes": 10485760, - "AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ], - "AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ], + "AllowedImageExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif" + ], + "AllowedFileExtensions": [ + ".jpg", + ".jpeg", + ".png", + ".webp", + ".gif", + ".pdf" + ], "DefaultUrlExpirationMinutes": 30, "EnableRefererValidation": true, - "AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ], + "AllowedReferers": [ + "https://admin.example.com", + "https://miniapp.example.com" + ], "AntiLeechTokenSecret": "ReplaceWithARandomToken" } }, @@ -138,5 +156,10 @@ "Exchange": "takeout.events", "ExchangeType": "topic", "PrefetchCount": 20 + }, + "Otel": { + "Endpoint": "", + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true } -} +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index ddfa3af..e8d458b 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -1,4 +1,10 @@ +using System; +using System.Linq; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Serilog; using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; @@ -11,7 +17,12 @@ builder.Host.UseSerilog((_, _, configuration) => configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "UserApi") - .WriteTo.Console(); + .WriteTo.Console() + .WriteTo.File( + "logs/user-api-.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 7, + shared: true); }); builder.Services.AddSharedWebCore(); @@ -22,6 +33,59 @@ builder.Services.AddSharedSwagger(options => options.EnableAuthorization = true; }); builder.Services.AddTenantResolution(builder.Configuration); +var otelSection = builder.Configuration.GetSection("Otel"); +var otelEndpoint = otelSection.GetValue("Endpoint"); +var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService( + serviceName: "TakeoutSaaS.UserApi", + serviceVersion: "1.0.0", + serviceInstanceId: Environment.MachineName)) + .WithTracing(tracing => + { + tracing + .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation(options => + { + options.SetDbStatementForText = false; + options.SetDbStatementForStoredProcedure = false; + }); + + if (!string.IsNullOrWhiteSpace(otelEndpoint)) + { + tracing.AddOtlpExporter(exporter => + { + exporter.Endpoint = new Uri(otelEndpoint); + }); + } + + if (useConsoleExporter) + { + tracing.AddConsoleExporter(); + } + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + + if (!string.IsNullOrWhiteSpace(otelEndpoint)) + { + metrics.AddOtlpExporter(exporter => + { + exporter.Endpoint = new Uri(otelEndpoint); + }); + } + + if (useConsoleExporter) + { + metrics.AddConsoleExporter(); + } + }); var userOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:User"); builder.Services.AddCors(options => diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index ca338f6..247af3e 100644 --- a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj +++ b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj @@ -8,6 +8,13 @@ + + + + + + + diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json index 7a3af9b..585bef6 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json @@ -55,7 +55,14 @@ "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ "/health" ], + "IgnoredPaths": [ + "/health" + ], "RootDomain": "" + }, + "Otel": { + "Endpoint": "", + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true } -} +} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json index 7a3af9b..585bef6 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json @@ -55,7 +55,14 @@ "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ "/health" ], + "IgnoredPaths": [ + "/health" + ], "RootDomain": "" + }, + "Otel": { + "Endpoint": "", + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true } -} +} \ No newline at end of file From bd9a67d776b2587705729d06374d3443f0a43113 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 13:31:10 +0800 Subject: [PATCH 36/56] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9EOpenTelemetry?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 2 +- Document/14_OpenTelemetry接入指引.md | 40 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Document/14_OpenTelemetry接入指引.md diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index d37014e..076085d 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -21,7 +21,7 @@ - [x] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 - [x] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 -## 3. 稳定性与质量 +## 3. 稳定性与质量(低优先级) - [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 - [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ 的集成测试模板。 - [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 diff --git a/Document/14_OpenTelemetry接入指引.md b/Document/14_OpenTelemetry接入指引.md new file mode 100644 index 0000000..bdfcd50 --- /dev/null +++ b/Document/14_OpenTelemetry接入指引.md @@ -0,0 +1,40 @@ +# 14_OpenTelemetry 接入指引 + +> 现状:Admin/Mini/User API 已集成 OTel 埋点,可导出到 Collector/控制台/文件日志,默认关闭 OTLP 导出。 + +## 1. 依赖与版本 +- NuGet:`OpenTelemetry.Extensions.Hosting`、`OpenTelemetry.Instrumentation.AspNetCore`、`OpenTelemetry.Instrumentation.Http`、`OpenTelemetry.Instrumentation.EntityFrameworkCore`、`OpenTelemetry.Instrumentation.Runtime`、`OpenTelemetry.Exporter.OpenTelemetryProtocol`、`OpenTelemetry.Exporter.Console`。 +- 当前 EF Core instrumentation 由 NuGet 回退到 `1.10.0-beta.1`(会提示 NU1603/NU1902),待可用时统一升级到稳定版以消除告警。 + +## 2. 程序内配置(Admin/Mini/User API) +- Resource:`ServiceName` 分别为 `TakeoutSaaS.AdminApi|MiniApi|UserApi`,`ServiceInstanceId = Environment.MachineName`。 +- Tracing:开启 ASP.NET Core、HttpClient、EF Core(禁用 SQL 文本)、Runtime;采样器默认 `ParentBased + AlwaysOn`。 +- Metrics:开启 ASP.NET Core、HttpClient、Runtime。 +- Exporter: + - OTLP(可选):读取 `Otel:Endpoint`,非空时启用。 + - Console:`Otel:UseConsoleExporter`(默认 Dev 开启,Prod 关闭)。 +- 日志:Serilog 输出 Console + 文件(按天滚动,保留 7 天),模板已包含 TraceId/SpanId(通过 Enrich FromLogContext)。 + +## 3. appsettings 配置键 +```json +"Otel": { + "Endpoint": "", // 为空则不推 OTLP,例如 http://otel-collector:4317 + "Sampling": "ParentBasedAlwaysOn", + "UseConsoleExporter": true // Dev 默认 true,Prod 建议 false +} +``` +- 环境变量可覆盖:`OTEL_SERVICE_NAME`、`OTEL_EXPORTER_OTLP_ENDPOINT` 等。 + +## 4. Collector/后端接入建议 +- Collector 监听 4317/4318(gRPC/HTTP OTLP),做采样/脱敏/分流,再转发 Jaeger/Tempo/ELK/Datadog 等。 +- 生产注意:限制导出 SQL 文本(已关闭)、对敏感字段脱敏,必要时在 Collector 做 TraceIdRatioBased 采样以控量。 + +## 5. 验证步骤 +1) 开启 `Otel:UseConsoleExporter=true`,本地运行 API,观察控制台是否输出 Span/Metric。 +2) 配置 `Otel:Endpoint=http://localhost:4317` 并启动 Collector,使用 Jaeger/Tempo UI 或 `curl http://localhost:4318/v1/traces` 验证链路。 +3) 文件日志:查看 `logs/admin-api-*.log` 等,确认包含 TraceId/SpanId。 + +## 6. 后续工作 +- 待 NuGet 源更新后,升级到稳定版 OTel 包并消除 NU1603/NU1902 告警。 +- 如需采集日志到 ELK,可直接用 Filebeat/Vector 读取 `logs/*.log` 推送,无需改代码。 +- 如需控制采样率或关闭某些 instrumentation,调整 appsettings 中的 Sampling/开关后重启即可。 From 4935e5390b27153071e0d8fac7e9f9eb6c52c49d Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 14:07:03 +0800 Subject: [PATCH 37/56] =?UTF-8?q?chore:=20=E9=80=82=E9=85=8D=20OTEL=20EF?= =?UTF-8?q?=20Core=201.14.0-beta.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Api/TakeoutSaaS.AdminApi/Program.cs | 6 +----- .../TakeoutSaaS.AdminApi.csproj | 14 +++++++------- src/Api/TakeoutSaaS.MiniApi/Program.cs | 6 +----- .../TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj | 14 +++++++------- src/Api/TakeoutSaaS.UserApi/Program.cs | 6 +----- .../TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj | 14 +++++++------- 6 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index e9619a0..245188d 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -82,11 +82,7 @@ builder.Services.AddOpenTelemetry() .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation(options => - { - options.SetDbStatementForText = false; - options.SetDbStatementForStoredProcedure = false; - }); + .AddEntityFrameworkCoreInstrumentation(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index 810c4b7..e773c1f 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -8,13 +8,13 @@ - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index 2c44874..e99e957 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -59,11 +59,7 @@ builder.Services.AddOpenTelemetry() .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation(options => - { - options.SetDbStatementForText = false; - options.SetDbStatementForStoredProcedure = false; - }); + .AddEntityFrameworkCoreInstrumentation(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index a5ca222..fd1fba4 100644 --- a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj +++ b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj @@ -9,13 +9,13 @@ - - - - - - - + + + + + + + diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index e8d458b..09eed33 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -47,11 +47,7 @@ builder.Services.AddOpenTelemetry() .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation(options => - { - options.SetDbStatementForText = false; - options.SetDbStatementForStoredProcedure = false; - }); + .AddEntityFrameworkCoreInstrumentation(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index 247af3e..9500251 100644 --- a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj +++ b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj @@ -8,13 +8,13 @@ - - - - - - - + + + + + + + From ee506cfa50af92ccdd2766e2a4d4ae0a5527af85 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 14:19:44 +0800 Subject: [PATCH 38/56] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E6=96=B0?= =?UTF-8?q?=E7=89=88=20S3=20SDK=20=E7=AD=BE=E5=90=8D=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Providers/S3StorageProviderBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs b/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs index 6cc7773..3210f38 100644 --- a/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs +++ b/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs @@ -169,8 +169,7 @@ public abstract class S3StorageProviderBase : IObjectStorageProvider, IDisposabl { ServiceURL = ServiceUrl, ForcePathStyle = ForcePathStyle, - UseHttp = !UseHttps, - SignatureVersion = "4" + UseHttp = !UseHttps }; var credentials = new BasicAWSCredentials(AccessKey, SecretKey); From ddad5d1d4fd6abed170e9be6584bb5900f5d8cd4 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 14:46:02 +0800 Subject: [PATCH 39/56] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E5=8C=85=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TakeoutSaaS.AdminApi.csproj | Bin 2485 -> 5124 bytes .../TakeoutSaaS.MiniApi.csproj | Bin 1960 -> 4064 bytes .../TakeoutSaaS.UserApi.csproj | Bin 1436 -> 3004 bytes .../TakeoutSaaS.Application.csproj | Bin 1268 -> 2662 bytes .../Swagger/ConfigureSwaggerOptions.cs | 14 ++++++++------ .../TakeoutSaaS.Shared.Web.csproj | 9 +++++---- .../TakeoutSaaS.ApiGateway.csproj | Bin 308 -> 718 bytes .../TakeoutSaaS.Infrastructure.csproj | Bin 1722 -> 3590 bytes .../TakeoutSaaS.Module.Authorization.csproj | Bin 571 -> 1254 bytes .../TakeoutSaaS.Module.Storage.csproj | Bin 1002 -> 2134 bytes 10 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index e773c1f11270bd8823b3342ecd826fea325fee3e..e52a27b8c104dd342cd14fe7e803d0ac2e9cce76 100644 GIT binary patch literal 5124 zcmdUz%}?7v5XI+QssBUNJEl}sFHM6`2_FY2!fAWqV)6kg!HHt0E&TCqe{WZ=<0df% zqR17(A-gk^H#2W`U-TbP8_z>?(ERTnAv2`%3F3UV($ve)DHM7BCo#v;M1mHXn|WhJ7lD+nzqen zmwRL=eEd_JQKu0;mpuDKR8_KfinnM{jBpI13BIbt5GH9@Aw7<$;5B`jIdA0`*dA*WdZMm>bS&sQE`MR=U!HqT1h7MF z-(KL>({Sl?RP8a@JPnVZzV+bwL^Z^Q4o_7`+zu3>Ji8j*8R+@ zGRtbGs=dW}Q&`0Sp%Qx+`Te*$*A4p-d8PE}f2nnwUWgw$nHSI8ho&yC{uZ+kxqZ;QRrf8~#a#2~ks?E!Lzrvio(FRdx^OF^hL( zF{Vz~kS=@c^CQ$Z>wR(*6MUwc(7Vd{ zF7vH#rYDXK34OgHTYdYi+Sf(T`eqq&3smR4x2|(GT_#d@hM(qX<(`GGy69QwETvVY p8T4PAs;mr|+T5$Y*>%WC>MEuq)z6lqS9x}23dn1zc~#(*(;s?tXZ8R9 literal 2485 zcmcImQE%EX5Pt8khY_pvt63 zk?t<{-F?3M;^4uWUm(y6SKC2nCWST5tdY$p3q~gP6PBIfmFEQ%mw?6Hoi%BaS;)5F zHUhl0Jcbuzw?hrs>#}a(*ByEIJXMM>6%0XVufQ)1xqlibN(zbJoz#&lqH5+m_6{^y zjxaVNjX@(vY4ok6_`=<0 znTo9fHgd@v@Wxo^ct5~8Dq_&NX4mX<$MX*?{UsAvsZ7+I6eAO^HM zHhD$RSsjku8;8VXsj?pHePP|plTNiGS___=3vMge5Pd{IS1&5T&ZkB%clL(Q{Z6<4}mskx$iAgsaAbYlL|aT-xEgD65yKT z!1X<@x!S$aavtK)a$1lqGVrVUa%u7F3%l1pzakK)a*@kugIqB7L{BzuPYb>UlVZqu z$ifY`u*&rH9WxUyp`@i{{RhM-$>eh{gq4(M*e}MK>b2#WCJ0Pxjh*ef7<6VQZi1+*#G0a|X`wh@`yNiq5SDXL< diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index fd1fba49bc88ff04054afb0d954336b5cae07d3b..c5e376a694a3236022fdc7e2f4fa5881543d429e 100644 GIT binary patch literal 4064 zcmdUyPjAye5XI+=#CM3e*QJ0sg;W(JZ4W6`9m=H_*KtB=>O`>(Y+yC}W{Hh0vpXAFYVYiY9a&&6dDa+7E#`ff(bW2G zw6NTIc7WAeY>usAb-N_O(7s|HGg`9{k9%j`#xr)K85TW81G})Mb$EKKz&_)5ORR=H zw`X|Q{zM!(Y`IPB)Ws-lG-Ks0I~Fn4$1=AT|3&1L*mu4*215hf8aW~(WmUIBzP;X~ zMB(FKSV5fz_}p?&h^VTRzZ7rLVm`t-h(`FT5_e$v2x1NuD>ad8vh`5P; zAnO>sszAycz4g4dczX0X)&=82zKef}K>nWh^(@ux++llI!{H@JW7vuuk5{1UagALT zG4~oVq{IQpyZn|)LpYA8;1_r)oVW4|?1XnCI1%gboeC1CgRDGIoXX4c1jwa~)P`RH!t+7H8mgb>27ZXXKT`)BjTI zCR|A2>K%(+;a}^*wE73EE>dHJj$T!`N<2nKSLn-UCzb6BBbuorx87>Lj;Yt`9DU!& zvDWTlu*W$_DVo(nxl66pCD--8&AQ|4?rO8{7*PKkF5OGD-?*L6J$VdKP)-Mp_zSTK z>7v>bC3J>zKenB90F&yw9_gxX`nWt!h+pobru}O*&T8zRBC|G(bm2rA%V119)MY%) z+6C5j))}^aRyuXU-97g=%aC5Wc}=|aK4+sdh(5WVMDAnqNTwu*YdmZHQ?IM|JXn_hBRFJnSkcG2#v694-yCPLs~msCWu zg!E?o-puTLg!fwgK~7$r7X zq@`vBz9^l~B~mbjX*fAH)b4951ltOnqpUvRqycJsRFop)8T~SuOikl;1lPW|C{Z)Q z8^vvbk{D%5E;C0?+B)jfcK82q@-CY&A}rdu-7}uE6d!Ph8YM?Bl3dsXXMX&`%=6ar zTp(PTa*cGYH2U5nYU6^K`Im4BUwv=#R;4pcs57C`ewH6Fg=P^U)V+VzYQ zCu0sLosoM#fc0YP89;z1_p1v2v|%|aOEF_H#LrA)Qf;ggFmO{k(pJ0vL*lBe7Sds; zI+O$O4{NPdVg;IxPL06|jbW*qytBqJ72~2e5C@N{LQ1<8+m}^bo5obzxT_=izEADV h^kKC~?lh}587F(xjDt;W(ZgwKO~v6(Ro8ib@fSh4oe2N{ diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index 95002516553ed38390c432f9469d6626a7dd5706..08afe63c308bdc0d9d1515e303f5e661099df7a2 100644 GIT binary patch literal 3004 zcmdUxUvCmo5XH~4iQj?v-mNvpC!}eV{&{F^3hGN=w#!m$DG*rHetGqG?#&7n*tSR- z3}F}UotZsz=G>k6^|P*B{m_kis_C0@jWpJk2I}jrUT8;&UUILo(^tlNhuyv|!|p_d zy4u3(4K}-KsIF5Y4D=QIjNO`Acw7hTV?494JI12RZlV)4)#e_p5`D(+f>;eb*E76p ze*ku`IODuavxUedlQ-Ff_pJogFfAR&{Oj>}rn^MT~!< z2|V@ix!{@;(X5pJ9BP-S77-V*fyWz1<|Ul??yfdSX{r9xVhewbp~ED z&}YS~v99~vBl-gC3HwIAihqf~{;}@WRjTVKV7sZP;VDQns+ER1o`J5*cNDV7xYmea zCH6qx;lEYdqQ)rpW^AK^<26_MEm>!{z6g z?E%@Wh9{oB_2Bsc8&*S`s~NI3tbtpwHF*nIH~pYUpZ_tdhaexb9?!g)S;U=LdyVx| zVYLE`%Iclh_v3ipH0+1uRZyq@h3h7@us-}|o`2`wcj~6>Z*g|5#uOdBTH_h4()@io zIzwMEJF9F{D&mtm3~w`^*AaXz&oRE03#{F{dmEgCmEu!vl&f&<9#qv`9@cwiXP1Zd zo&^5S`TAO_{XEPS*W@ukK?VIO<RG;5cHe(5>SbI0w9Ue(!EsdqYEl~Z+`j(-EF CZ1x5K literal 1436 zcmcIk!A{#i6uk2lmhXK|stWbsL_#z`4x|XX^ulGc8G`He?rQg~Ab;OAuB%%Ws_d1<6UVswe4nC7to+foU--DMIuU%XE5@Bzf$!aJ;#6CfF86AXFN%kkDH<;imW!C#yzS4-s$FO|2&?OWau-nk=nW0-B zp014NnB4Q~?PIKc&if626!{gi{STgJnk(M)!@Bd&I`f~FU~$~P8>YS+ z&~1on&Gu0IYdW)MYaGMTKrqfP8)?)BY8 zOHsoFE`%tkcoYt+cEcQiNqQSex5o7^5@)tuATO(6KzRiIZmnyx3JQLp(_vJ`{ifyS RjnYtUknGEDYB%!qVx|(G+VMNEiVUR&9NiLvQvmp$7(Dime>GcC|(jL#RY zDG^PL>dE4*TdYRtgJ_1YDR2vxPp-1*nAb#Wx2_`FzNOmj8uip?#}ow33ZBCzMytqG zyuf?@taCa^5kA?b#4lso>&x+N4&aQoDnmJ0Vzwn-sp;<>;)mPOq+s1q3 zn%3JcLl9l@wd;TmPkc6}bj+1c z)jM)F!ExNUAAa H{-65;nxVk8 literal 1268 zcmb_c%WlFj5WMphQ9fWp+Dj`1B*df50fY$U#KkxZxFmLDZ`AVlwF`oThYF}tq(rlu znOW~7(O9cb*UG3R!Kgn43;FGa=S8-sKuy~>t;)h77Mn7?LK(2Ro9!mfQ(W*J*#*hcAYQE&iu^*Apw!3BL7Ay?+mbpksx!I_&VCd{ke#*A;+ z3MaTkjgq4mOP-Y}RtM~uVuaLH8sVL&fX>2@W}JkQ#`k8ZjeTnQPw)&)-wXCr-@@Gg zCY0`N196Iy1Z~0TR9Ii23E{)6UB=h;l&)EJe*@HUY_R@yU|u}q}Qk-!Dwqk+1_>p>z4YCAsP_%6j4HV!bi*HEQphGsrx8&oAF zj3uzcAjz-6z)fL!D|>oeWr)VGmEGDU3y0!=Dkh|~{Rl4U?Mrv7xfIv() } - }); + { new OpenApiSecuritySchemeReference { Id = "Bearer" }, new List() } + }; + options.SwaggerGeneratorOptions.SecurityRequirements.Add(requirement); } } } diff --git a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj index 94b4aeb..b387355 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj +++ b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj @@ -1,4 +1,4 @@ - + net10.0 enable @@ -12,9 +12,10 @@ - - - + + + + diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj index fda4d4da66f8b4f8c700067af9a4650b3eaa360d..463d6c9dc237fd0090c02da96961fc2236bfc252 100644 GIT binary patch literal 718 zcmaKqPfvqD6vXFj;&&k4rB)M9NKa~duxYevJW>ix6)EA5)-SLA-XgV)wg(oLnKwIc z_I-T@8fc|gW%BfWV$XYa4*T(tTlHRP00Gi6Osv{bsC>HTGFD-3Z| zP-{sn@N)0cwmhqU)b_JGWjnV`eSb!QhMbt-V0VIJx{1juaw*=zPI*e#XUyuMVRMj? z#EiYo$P?_6u6!B4o1x^#(~29j`FQ}_o~=_V&fHBH-rKlM>u8tP-)_4ZYiGsM_y(!G Bba(&& literal 308 zcmZXQ!H&W(3`Fn!iYOnDls%`t0qOySSQf-FgrN#elgdpO_ckrVpdM+?^~TvY^)~o||A>jmIg^i*<=f21%JETUmfZm}E@hwq diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index 4661491738b2452e01a7ae3a3e6351148b7e518c..ba314f274b97f023164f7b0013a7d88685d70d0e 100644 GIT binary patch literal 3590 zcmd56#X@E9&oSF6!cg7~2Q=>;|h!&dY@Y`wa(87J(o_X5)uEvMN*L|LDjDPHhb#CQ^1@{}3RuF+pfDvU(YRI$r5%XfG$oc$Iqn4b~% zobs8_qO#%NWbdX%*1{n@nsTo)Fr+UZEX&H{JjYtOwzsTvUAD_}RK+1rOz1&z29Fs{ z%v?p5#rM?i@iVhHV?G1gQ2j^DM3?WvMqWLe)JiSFMNTCStRjiw>7&`tV$IdZBX0Lb zoww^{2<1UlS48TFh;QnJZTa3JW`vN>JU3*gtdrZ2n3!-+%AKlT8}IOTt6tw87asIx|kX)v^C8*QhoZl&*HLKvr7@$ncci7Tq7X4Y9Gxmh5j9BL> zy*TB{kTvVtn{XopiCrZXi%MS{BL0-7qLwV;nozdDSFVYRv-VQ1Nk=*IHpK&F{;fvi zIHSDRvpE%`m+R+oLxX#!{8J41b$mobYr5>&>r-qm%sJ=@HA?tw)$U>?Q7 znn3AwoK4Qd7z4gZk{AnU>dgxx@pEOfywxTK)WzcMko6VoF8lw87}AP(p~K&2CRJRo zIrq{6PoGd;!DBw%Oy73Se~m&_(2V}~6G0t4McQiNKZi8;eJjHj5^==-k_eD+I-`_k!BKU+K zJsXVfoRKKH6T8=`7baG@HrFF|w&u#8YK6>=FyXuthvjw4-F&YbN4i_#5?RGjY({?@ yk@+4fj-_o3tG?;7K1G;C^S);Go#`k!P}g4k?+R`-<0RMwMj`mbWv&Da=X)=gcw)^j5E!IcaTDH2VU06$WaHtKQ7KHWf*_`z5-hGhSyPq1LbWNkIp9aj zm+)k4k!Zkfhjrp;|DbLkS4#1jf&{dG#L>1;pO2QMl0xDSCw1<&U3+COCOS|Pj}Vqk zZ4)jEp2I!N!GabLjI>Zy2F(i&HA-A<_1_u`Vek{IlSbc!{wk~!2GQoW7b3EIP#g)7 ztBb(LW$u0}c0?Buq^!vAqkatcW10R5sxjqE0r?F0ieB=my=Kx*8mGZVUOKVrD zaWnZ%qpfJ_OXvl&N~)|EXd2EV{S`(dXq-z0t`~3`*Xj>hvFSt0wUfRxj@o#2cA|ZE zPMS90B?Y>1zc;MQuCHI|_cLksadGUHiv{JDrAWyeK8pp{K7N^W4UYE|sqQ%x>B>={ z*(jrtP)6@e28vBhL2T^H6E+~wc7Nt58N$C_FO8Y<(p(LPi=tkBM-~NR5A++DC71eP$_p?R zrktnj#S#;)B)EpbC&2rqUwP{(wHVH*znaTC(;`V1cgM*0=326c?CK4}xR NJX~*o|Gi4C{sK6FM>+ri diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj b/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj index 9dfdb25626ebf650760f315a9a58b0567d532917..e1c880a90369295d51fb67a5619bba20cea9fee0 100644 GIT binary patch literal 1254 zcmb`HPfx-?5XIlw#P85>7xdz(LWrXAATffTyp&P|#nRBC;)hp%vzsC+Hby=4&vxF- z&U^E=uP>omdeXh73iO~vbLE<7rc?*2X-{?S@(hTiiupbyn(D?y7b-N;4yb)FEj1PD zoC-7Df{%#?>XC8h&}U@EE}DZF5v{AMj`}=3sz!xvZIKnMDrK&xLQ9>v+=a3=lt=az z5xW6d=$N+%K8YULWzODB`nAX&OpHya276X#lwJ7xT`kaOO3r|LLPb-fTv@%Zh>sOc z=_n)D6qwM6T@dEUSYwmc)+yiRZ z{HG`}WNqHqG|x7CGSo1=51sxAnzzX+Gk9N|*ZE16-fMiB_##`|tTLSlKMAru4HJuAjW0&9DyJ zYc9K&^SXAY;|~5aH@6=1ia4QmO2@pJD~7DTWB=FuI(nOnJR5V!`uwYudOs26r2PNn E4JQE5;Q#;t literal 571 zcma)(L2kk@5JmT#BFX`rulX%f}6y)#uK&l^tA~>5Rh8QlC3{u z|Nq8(h2|3^MOED{;#VcnII|;GF4xRUaW;!0?t3gC9bV83HYi?O3v7U%QS|U-u+22k zJY^|QriHP+9J)?yJIFwXjVEVetQWoQlvMQYly2O)Ya)j*$xRww5cHSXRzkL-fj8Iz zf|d{!T6RMXmw>smCD5ZmVEI6MgX%|+GCGbv0G&T9;s=(pc^oAt;{Uw5{3g)hwqs9V zLD$~?Q0OpWFS2~=2*vU2CPwMWh$mMxw`}8o-C<*F<5wWstVIiEph}2}ReOO@Us?ZE RXVO`3b1%_x`~fGM{Q{V@!IJ<0 diff --git a/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj index fbeba2babe5930264f5d61bc9a1ff36496312096..b8007bc279d1b63f438232e6f9b3f12e343077e2 100644 GIT binary patch literal 2134 zcmdUwQBUGf5QWe4CjN)UcdM+%C)5~mH^zuYP+xq(Qe0NH5L#6Jy!y>FRTOB5n&^hK z+}=Ag_sqFx<~}}DwWE9eQ%`|z6>F$eUG9kx#5Ub{VqwHh68NLME0?Rjce-?opt~=csc`byc=&?Ru)h4+ywH)Cvb zA^cupoB6EOQa+!~$)r$sQLmrF@Zy=&$jx+^xIRoPW7ka43I5Nlj>+k}GyfW6nWkoG z(0coH&3?6--sV)JGi5xx=qsTZ>VC2+3(mNC&|GACH+58YWP%emQDSg)$Zbkl zPBXm6B`OpQy+p90O0nnnp9mS=0_phPBPz|M==mMEg3GY$d%^bCzWKc1>q@|c5O8b8 zXa-W@!LAEvcOd-Ds=d^sH-o}a(J+u=$+JpPV_%`a&_+?_p7jAn7p%^Ih8SXrBDJcl z6XA^iH=s#*UgFz*O^x>2olT$*oFO%|FN8E3Grlq)3L4=%jbeSgK8Yg0++Hp}^BK)i gR)$b&bM;IWrVa0Z>I2iIh1o`;!Dw?@oXv6T9WriEI{*Lx From 6c0ec948a7641d1f96f04268e04398f4dfe3d1f6 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 14:59:15 +0800 Subject: [PATCH 40/56] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Swagger=20?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Swagger/ConfigureSwaggerOptions.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs index dc85906..e167f00 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; @@ -34,6 +35,8 @@ internal sealed class ConfigureSwaggerOptions( if (_settings.EnableAuthorization) { + const string bearerSchemeName = "Bearer"; + var scheme = new OpenApiSecurityScheme { Name = "Authorization", @@ -44,12 +47,16 @@ internal sealed class ConfigureSwaggerOptions( BearerFormat = "JWT" }; - options.SwaggerGeneratorOptions.SecuritySchemes["Bearer"] = scheme; - var requirement = new OpenApiSecurityRequirement + options.AddSecurityDefinition(bearerSchemeName, scheme); + options.AddSecurityRequirement(document => { - { new OpenApiSecuritySchemeReference { Id = "Bearer" }, new List() } - }; - options.SwaggerGeneratorOptions.SecurityRequirements.Add(requirement); + var requirement = new OpenApiSecurityRequirement + { + { new OpenApiSecuritySchemeReference(bearerSchemeName, document, null), new List() } + }; + + return requirement; + }); } } } From 188dc0ea9aeb11c0d8a73cae82402a416d7ee01b Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 15:02:58 +0800 Subject: [PATCH 41/56] =?UTF-8?q?fix:=E9=83=A8=E5=88=86=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Providers/S3StorageProviderBase.cs | 1 + .../TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs b/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs index 3210f38..afdfe96 100644 --- a/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs +++ b/src/Modules/TakeoutSaaS.Module.Storage/Providers/S3StorageProviderBase.cs @@ -188,5 +188,6 @@ public abstract class S3StorageProviderBase : IObjectStorageProvider, IDisposabl _disposed = true; _client?.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs index 59247c8..2a117bf 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs @@ -89,7 +89,7 @@ public sealed class TenantResolutionMiddleware( }); } - private TenantContext ResolveTenant(HttpContext context, TenantResolutionOptions options) + private static TenantContext ResolveTenant(HttpContext context, TenantResolutionOptions options) { var request = context.Request; From 5a4ce12d612490b745cc8cdce5926562d2c6917b Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 15:08:32 +0800 Subject: [PATCH 42/56] =?UTF-8?q?style:=20=E7=AE=80=E5=8C=96=20CORS=20?= =?UTF-8?q?=E6=BA=90=E7=A9=BA=E6=95=B0=E7=BB=84=E8=BF=94=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Api/TakeoutSaaS.AdminApi/Program.cs | 2 +- src/Api/TakeoutSaaS.MiniApi/Program.cs | 2 +- src/Api/TakeoutSaaS.UserApi/Program.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 245188d..0feed22 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -146,7 +146,7 @@ static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionK return origins? .Where(origin => !string.IsNullOrWhiteSpace(origin)) .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() ?? Array.Empty(); + .ToArray() ?? []; } static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index e99e957..0ae228f 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -120,7 +120,7 @@ static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionK return origins? .Where(origin => !string.IsNullOrWhiteSpace(origin)) .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() ?? Array.Empty(); + .ToArray() ?? []; } static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index 09eed33..7f76faa 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -108,7 +108,7 @@ static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionK return origins? .Where(origin => !string.IsNullOrWhiteSpace(origin)) .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() ?? Array.Empty(); + .ToArray() ?? []; } static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) From 8fbd40ecf26973d1227abbca287545495245eb22 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 15:49:04 +0800 Subject: [PATCH 43/56] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=9D=83=E9=99=90=E6=B4=9E=E5=AF=9F=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E4=B8=8E=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 7 ++ .../Controllers/AuthController.cs | 60 ++++++++++++++ .../Controllers/UserPermissionsController.cs | 71 +++++++++++++++++ .../Abstractions/IAdminAuthService.cs | 3 + .../Identity/Contracts/UserPermissionDto.cs | 54 +++++++++++++ .../GetUserPermissionsQueryHandler.cs | 42 ++++++++++ .../SearchUserPermissionsQueryHandler.cs | 67 ++++++++++++++++ .../Queries/GetUserPermissionsQuery.cs | 15 ++++ .../Queries/SearchUserPermissionsQuery.cs | 36 +++++++++ .../Identity/Services/AdminAuthService.cs | 79 ++++++++++++++++++- .../Repositories/IIdentityUserRepository.cs | 9 +++ .../Persistence/EfIdentityUserRepository.cs | 16 ++++ 12 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index 076085d..dac5cff 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -28,6 +28,13 @@ ## 4. 安全与合规 - [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 + - [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 + - [ ] 差距与步骤: + - [ ] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 + - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。 + - [ ] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。 + - [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。 + - [ ] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。 - [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 - [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 - [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index b406c49..72f23f4 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -55,6 +55,29 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr /// /// 获取当前用户信息 /// + /// + /// 示例: + /// + /// GET /api/admin/v1/auth/profile + /// Header: Authorization: Bearer <JWT> + /// 响应: + /// { + /// "success": true, + /// "code": 200, + /// "message": "操作成功", + /// "data": { + /// "userId": "900123456789012345", + /// "account": "admin@tenant1", + /// "displayName": "租户管理员", + /// "tenantId": "100000000000000001", + /// "merchantId": null, + /// "roles": ["TenantAdmin"], + /// "permissions": ["identity:permission:read", "merchant:read", "order:read"], + /// "avatar": "https://cdn.example.com/avatar.png" + /// } + /// } + /// + /// [HttpGet("profile")] [PermissionAuthorize("identity:profile:read")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] @@ -70,4 +93,41 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr var profile = await authService.GetProfileAsync(userId, cancellationToken); return ApiResponse.Ok(profile); } + + /// + /// 查询指定用户的角色与权限概览(当前租户范围)。 + /// + /// + /// 示例: + /// + /// GET /api/admin/v1/auth/permissions/900123456789012346 + /// Header: Authorization: Bearer <JWT> + /// 响应: + /// { + /// "success": true, + /// "code": 200, + /// "data": { + /// "userId": "900123456789012346", + /// "tenantId": "100000000000000001", + /// "merchantId": "200000000000000001", + /// "account": "ops.manager", + /// "displayName": "运营经理", + /// "roles": ["OpsManager", "Reporter"], + /// "permissions": ["delivery:read", "order:read", "payment:read"], + /// "createdAt": "2025-12-01T08:30:00Z" + /// } + /// } + /// + /// + [HttpGet("permissions/{userId:long}")] + [PermissionAuthorize("identity:permission:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> GetUserPermissions(long userId, CancellationToken cancellationToken) + { + var result = await authService.GetUserPermissionsAsync(userId, cancellationToken); + return result is null + ? ApiResponse.Error(ErrorCodes.NotFound, "用户不存在或不属于当前租户") + : ApiResponse.Ok(result); + } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs new file mode 100644 index 0000000..328499a --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 用户权限洞察接口。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/users/permissions")] +public sealed class UserPermissionsController(IAdminAuthService authService) : BaseApiController +{ + /// + /// 分页查询当前租户用户的角色与权限概览。 + /// + /// + /// 示例: + /// + /// GET /api/admin/v1/users/permissions?keyword=ops&page=1&pageSize=20&sortBy=createdAt&sortDescending=true + /// Header: Authorization: Bearer <JWT> + /// 响应: + /// { + /// "success": true, + /// "code": 200, + /// "data": { + /// "items": [ + /// { + /// "userId": "900123456789012346", + /// "tenantId": "100000000000000001", + /// "merchantId": "200000000000000001", + /// "account": "ops.manager", + /// "displayName": "运营经理", + /// "roles": ["OpsManager", "Reporter"], + /// "permissions": ["delivery:read", "order:read", "payment:read"], + /// "createdAt": "2025-12-01T08:30:00Z" + /// } + /// ], + /// "page": 1, + /// "pageSize": 20, + /// "totalCount": 1, + /// "totalPages": 1 + /// } + /// } + /// + /// + [HttpGet] + [PermissionAuthorize("identity:permission:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> Search( + [FromQuery] SearchUserPermissionsQuery query, + CancellationToken cancellationToken) + { + var result = await authService.SearchUserPermissionsAsync( + query.Keyword, + query.Page, + query.PageSize, + query.SortBy, + query.SortDescending, + cancellationToken); + + return ApiResponse>.Ok(result); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs index 28510e3..d1cd0a7 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using System.Threading.Tasks; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.Identity.Abstractions; @@ -13,4 +14,6 @@ public interface IAdminAuthService Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); Task GetProfileAsync(long userId, CancellationToken cancellationToken = default); + Task GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default); + Task> SearchUserPermissionsAsync(string? keyword, int page, int pageSize, string? sortBy, bool sortDescending, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs new file mode 100644 index 0000000..f3393ad --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs @@ -0,0 +1,54 @@ +using System; +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 用户权限概览 DTO。 +/// +public sealed class UserPermissionDto +{ + /// + /// 用户 ID(雪花,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long UserId { get; init; } + + /// + /// 租户 ID(雪花,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 商户 ID(雪花,序列化为字符串,可空)。 + /// + [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] + public long? MerchantId { get; init; } + + /// + /// 登录账号。 + /// + public string Account { get; init; } = string.Empty; + + /// + /// 展示名称。 + /// + public string DisplayName { get; init; } = string.Empty; + + /// + /// 角色集合。 + /// + public string[] Roles { get; init; } = Array.Empty(); + + /// + /// 权限集合。 + /// + public string[] Permissions { get; init; } = Array.Empty(); + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs new file mode 100644 index 0000000..cdc727b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs @@ -0,0 +1,42 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 按用户 ID 获取权限概览处理器。 +/// +public sealed class GetUserPermissionsQueryHandler( + IIdentityUserRepository identityUserRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var user = await _identityUserRepository.FindByIdAsync(request.UserId, cancellationToken); + if (user == null || user.TenantId != tenantId) + { + return null; + } + + return new UserPermissionDto + { + UserId = user.Id, + TenantId = user.TenantId, + MerchantId = user.MerchantId, + Account = user.Account, + DisplayName = user.DisplayName, + Roles = user.Roles, + Permissions = user.Permissions, + CreatedAt = user.CreatedAt + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs new file mode 100644 index 0000000..adfe198 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs @@ -0,0 +1,67 @@ +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 租户用户权限分页查询处理器。 +/// +public sealed class SearchUserPermissionsQueryHandler( + IIdentityUserRepository identityUserRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchUserPermissionsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var users = await _identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + + var sorted = SortUsers(users, request.SortBy, request.SortDescending); + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + var items = paged.Select(user => new UserPermissionDto + { + UserId = user.Id, + TenantId = user.TenantId, + MerchantId = user.MerchantId, + Account = user.Account, + DisplayName = user.DisplayName, + Roles = user.Roles, + Permissions = user.Permissions, + CreatedAt = user.CreatedAt + }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, users.Count); + } + + private static IOrderedEnumerable SortUsers( + IReadOnlyCollection users, + string? sortBy, + bool sortDescending) + { + return sortBy?.ToLowerInvariant() switch + { + "account" => sortDescending + ? users.OrderByDescending(x => x.Account) + : users.OrderBy(x => x.Account), + "displayname" => sortDescending + ? users.OrderByDescending(x => x.DisplayName) + : users.OrderBy(x => x.DisplayName), + _ => sortDescending + ? users.OrderByDescending(x => x.CreatedAt) + : users.OrderBy(x => x.CreatedAt) + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs new file mode 100644 index 0000000..84b11d2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Queries; + +/// +/// 按用户 ID 获取角色/权限概览。 +/// +public sealed class GetUserPermissionsQuery : IRequest +{ + /// + /// 用户 ID(雪花)。 + /// + public long UserId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs new file mode 100644 index 0000000..1a6d557 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs @@ -0,0 +1,36 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.Identity.Queries; + +/// +/// 按租户分页查询用户的角色/权限概览。 +/// +public sealed class SearchUserPermissionsQuery : IRequest> +{ + /// + /// 关键字(账号或展示名称)。 + /// + public string? Keyword { get; init; } + + /// + /// 页码,从 1 开始。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 排序字段(account/displayName/createdAt)。 + /// + public string? SortBy { get; init; } + + /// + /// 是否倒序。 + /// + public bool SortDescending { get; init; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 42418a4..e7b91fb 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -5,6 +5,8 @@ using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Services; @@ -15,8 +17,11 @@ public sealed class AdminAuthService( IIdentityUserRepository userRepository, IPasswordHasher passwordHasher, IJwtTokenService jwtTokenService, - IRefreshTokenStore refreshTokenStore) : IAdminAuthService + IRefreshTokenStore refreshTokenStore, + ITenantProvider tenantProvider) : IAdminAuthService { + private readonly ITenantProvider _tenantProvider = tenantProvider; + /// /// 管理后台登录:验证账号密码并生成令牌。 /// @@ -85,6 +90,78 @@ public sealed class AdminAuthService( return BuildProfile(user); } + /// + /// 获取指定用户的权限概览(校验当前租户)。 + /// + public async Task GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var user = await userRepository.FindByIdAsync(userId, cancellationToken); + if (user == null || user.TenantId != tenantId) + { + return null; + } + + return new UserPermissionDto + { + UserId = user.Id, + TenantId = user.TenantId, + MerchantId = user.MerchantId, + Account = user.Account, + DisplayName = user.DisplayName, + Roles = user.Roles, + Permissions = user.Permissions, + CreatedAt = user.CreatedAt + }; + } + + /// + /// 按租户分页查询用户权限概览。 + /// + public async Task> SearchUserPermissionsAsync( + string? keyword, + int page, + int pageSize, + string? sortBy, + bool sortDescending, + CancellationToken cancellationToken = default) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken); + + var sorted = sortBy?.ToLowerInvariant() switch + { + "account" => sortDescending + ? users.OrderByDescending(x => x.Account) + : users.OrderBy(x => x.Account), + "displayname" => sortDescending + ? users.OrderByDescending(x => x.DisplayName) + : users.OrderBy(x => x.DisplayName), + _ => sortDescending + ? users.OrderByDescending(x => x.CreatedAt) + : users.OrderBy(x => x.CreatedAt) + }; + + var paged = sorted + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToList(); + + var items = paged.Select(user => new UserPermissionDto + { + UserId = user.Id, + TenantId = user.TenantId, + MerchantId = user.MerchantId, + Account = user.Account, + DisplayName = user.DisplayName, + Roles = user.Roles, + Permissions = user.Permissions, + CreatedAt = user.CreatedAt + }).ToList(); + + return new PagedResult(items, page, pageSize, users.Count); + } + private static CurrentUserProfile BuildProfile(IdentityUser user) => new() { diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index cc74343..80d0a1e 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TakeoutSaaS.Domain.Identity.Entities; @@ -19,4 +20,12 @@ public interface IIdentityUserRepository /// 根据 ID 获取后台用户。 /// Task FindByIdAsync(long userId, CancellationToken cancellationToken = default); + + /// + /// 按租户与关键字查询后台用户列表(仅读)。 + /// + /// 租户 ID。 + /// 可选关键字(账号/名称)。 + /// 取消标记。 + Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index a355874..e1f657d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -18,4 +19,19 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde public Task FindByIdAsync(long userId, CancellationToken cancellationToken = default) => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); + + public async Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + { + var query = dbContext.IdentityUsers + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalized = keyword.Trim(); + query = query.Where(x => x.Account.Contains(normalized) || x.DisplayName.Contains(normalized)); + } + + return await query.ToListAsync(cancellationToken); + } } From 3d691514267ca559072f46571e70a316caf18b85 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 15:55:07 +0800 Subject: [PATCH 44/56] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E4=B8=8E=E5=90=88=E8=A7=84=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index dac5cff..a18b103 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -27,14 +27,15 @@ - [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 ## 4. 安全与合规 -- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 +- [x] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 - [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 - - [ ] 差距与步骤: - - [ ] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 + - [x] 差距与步骤: + - [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。 - - [ ] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。 + - [x] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。 - [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。 - - [ ] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。 + - [x] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。 + - [x] Step D 校验:Admin API 管道已在 Auth 之前使用 TenantResolution,中间件未忽略新接口;查询使用 TenantAwareDbContext + ITenantProvider 双重租户校验,暂无需调整。后续若加 Dapper 读侧需显式带 tenant 过滤。 - [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 - [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 - [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 From b459c7edbedf1297467bedc720472b23169e2efc Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 16:21:46 +0800 Subject: [PATCH 45/56] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=20RBAC1=20?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetUserPermissionsQueryHandler.cs | 50 ++++++- .../SearchUserPermissionsQueryHandler.cs | 68 ++++++++- .../Identity/Services/AdminAuthService.cs | 129 ++++++++++++++++-- .../Identity/Entities/IdentityUser.cs | 10 -- .../Identity/Entities/Permission.cs | 24 ++++ .../Identity/Entities/Role.cs | 24 ++++ .../Identity/Entities/RolePermission.cs | 19 +++ .../Identity/Entities/UserRole.cs | 19 +++ .../Repositories/IIdentityUserRepository.cs | 5 + .../Repositories/IPermissionRepository.cs | 21 +++ .../Repositories/IRolePermissionRepository.cs | 16 +++ .../Identity/Repositories/IRoleRepository.cs | 21 +++ .../Repositories/IUserRoleRepository.cs | 17 +++ .../Extensions/ServiceCollectionExtensions.cs | 4 + .../Persistence/EfIdentityUserRepository.cs | 6 + .../Persistence/EfPermissionRepository.cs | 60 ++++++++ .../Persistence/EfRolePermissionRepository.cs | 38 ++++++ .../Identity/Persistence/EfRoleRepository.cs | 60 ++++++++ .../Persistence/EfUserRoleRepository.cs | 44 ++++++ .../Persistence/IdentityDataSeeder.cs | 97 ++++++++++++- .../Identity/Persistence/IdentityDbContext.cs | 97 ++++++++++--- 21 files changed, 780 insertions(+), 49 deletions(-) create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs index cdc727b..a5a2a42 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; @@ -11,10 +13,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// public sealed class GetUserPermissionsQueryHandler( IIdentityUserRepository identityUserRepository, + IUserRoleRepository userRoleRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository, + IRolePermissionRepository rolePermissionRepository, ITenantProvider tenantProvider) : IRequestHandler { private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository; + private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; + private readonly IRoleRepository _roleRepository = roleRepository; + private readonly IPermissionRepository _permissionRepository = permissionRepository; + private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// @@ -27,6 +37,9 @@ public sealed class GetUserPermissionsQueryHandler( return null; } + var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + return new UserPermissionDto { UserId = user.Id, @@ -34,9 +47,42 @@ public sealed class GetUserPermissionsQueryHandler( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = roleCodes, + Permissions = permissionCodes, CreatedAt = user.CreatedAt }; } + + private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + if (permissionIds.Length == 0) + { + return Array.Empty(); + } + + var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs index adfe198..07e3595 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using MediatR; using TakeoutSaaS.Application.Identity.Contracts; @@ -13,10 +15,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// public sealed class SearchUserPermissionsQueryHandler( IIdentityUserRepository identityUserRepository, + IUserRoleRepository userRoleRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository, + IRolePermissionRepository rolePermissionRepository, ITenantProvider tenantProvider) : IRequestHandler> { private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository; + private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; + private readonly IRoleRepository _roleRepository = roleRepository; + private readonly IPermissionRepository _permissionRepository = permissionRepository; + private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; private readonly ITenantProvider _tenantProvider = tenantProvider; /// @@ -31,6 +41,7 @@ public sealed class SearchUserPermissionsQueryHandler( .Take(request.PageSize) .ToList(); + var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken); var items = paged.Select(user => new UserPermissionDto { UserId = user.Id, @@ -38,8 +49,8 @@ public sealed class SearchUserPermissionsQueryHandler( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = resolved[user.Id].roles, + Permissions = resolved[user.Id].permissions, CreatedAt = user.CreatedAt }).ToList(); @@ -64,4 +75,57 @@ public sealed class SearchUserPermissionsQueryHandler( : users.OrderBy(x => x.CreatedAt) }; } + + private async Task> ResolveRolesAndPermissionsAsync( + long tenantId, + IReadOnlyCollection users, + CancellationToken cancellationToken) + { + var userIds = users.Select(x => x.Id).ToArray(); + var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); + + var roles = roleIds.Length == 0 + ? Array.Empty() + : await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); + + var rolePermissions = roleIds.Length == 0 + ? Array.Empty() + : await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + + var permissions = permissionIds.Length == 0 + ? Array.Empty() + : await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); + + var rolePermissionsLookup = rolePermissions + .GroupBy(rp => rp.RoleId) + .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); + + var result = new Dictionary(); + foreach (var userId in userIds) + { + var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray(); + var roleCodes = rolesForUser + .Select(rid => roleCodeMap.GetValueOrDefault(rid)) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var permissionCodes = rolesForUser + .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) + .Select(pid => permissionCodeMap.GetValueOrDefault(pid)) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => code!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result[userId] = (roleCodes, permissionCodes); + } + + return result; + } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index e7b91fb..4836eb8 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Identity; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; @@ -15,12 +18,20 @@ namespace TakeoutSaaS.Application.Identity.Services; /// public sealed class AdminAuthService( IIdentityUserRepository userRepository, + IUserRoleRepository userRoleRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository, + IRolePermissionRepository rolePermissionRepository, IPasswordHasher passwordHasher, IJwtTokenService jwtTokenService, IRefreshTokenStore refreshTokenStore, ITenantProvider tenantProvider) : IAdminAuthService { private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; + private readonly IRoleRepository _roleRepository = roleRepository; + private readonly IPermissionRepository _permissionRepository = permissionRepository; + private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; /// /// 管理后台登录:验证账号密码并生成令牌。 @@ -43,7 +54,7 @@ public sealed class AdminAuthService( } // 3. 构建用户档案并生成令牌 - var profile = BuildProfile(user); + var profile = await BuildProfileAsync(user, cancellationToken); return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } @@ -71,7 +82,7 @@ public sealed class AdminAuthService( await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); // 4. 生成新的令牌对 - var profile = BuildProfile(user); + var profile = await BuildProfileAsync(user, cancellationToken); return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } @@ -87,7 +98,7 @@ public sealed class AdminAuthService( var user = await userRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); - return BuildProfile(user); + return await BuildProfileAsync(user, cancellationToken); } /// @@ -102,6 +113,9 @@ public sealed class AdminAuthService( return null; } + var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + return new UserPermissionDto { UserId = user.Id, @@ -109,8 +123,8 @@ public sealed class AdminAuthService( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = roleCodes, + Permissions = permissionCodes, CreatedAt = user.CreatedAt }; } @@ -147,6 +161,7 @@ public sealed class AdminAuthService( .Take(pageSize) .ToList(); + var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken); var items = paged.Select(user => new UserPermissionDto { UserId = user.Id, @@ -154,24 +169,116 @@ public sealed class AdminAuthService( MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = resolved[user.Id].roles, + Permissions = resolved[user.Id].permissions, CreatedAt = user.CreatedAt }).ToList(); return new PagedResult(items, page, pageSize, users.Count); } - private static CurrentUserProfile BuildProfile(IdentityUser user) - => new() + private async Task BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken) + { + var tenantId = user.TenantId; + var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + + return new CurrentUserProfile { UserId = user.Id, Account = user.Account, DisplayName = user.DisplayName, TenantId = user.TenantId, MerchantId = user.MerchantId, - Roles = user.Roles, - Permissions = user.Permissions, + Roles = roles, + Permissions = permissions, Avatar = user.Avatar }; + } + + private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) + { + var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); + if (roleIds.Length == 0) + { + return Array.Empty(); + } + + var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + if (permissionIds.Length == 0) + { + return Array.Empty(); + } + + var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private async Task> ResolveRolesAndPermissionsAsync( + long tenantId, + IReadOnlyCollection users, + CancellationToken cancellationToken) + { + var userIds = users.Select(x => x.Id).ToArray(); + var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); + + var roles = roleIds.Length == 0 + ? Array.Empty() + : await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); + + var rolePermissions = roleIds.Length == 0 + ? Array.Empty() + : await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + + var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); + var permissions = permissionIds.Length == 0 + ? Array.Empty() + : await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); + + var rolePermissionsLookup = rolePermissions + .GroupBy(rp => rp.RoleId) + .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); + + var result = new Dictionary(); + foreach (var userId in userIds) + { + var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray(); + var roleCodes = rolesForUser + .Select(rid => roleCodeMap.GetValueOrDefault(rid)) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var permissionCodes = rolesForUser + .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) + .Select(pid => permissionCodeMap.GetValueOrDefault(pid)) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => code!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result[userId] = (roleCodes, permissionCodes); + } + + return result; + } } diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs index 6080712..2bed359 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -27,16 +27,6 @@ public sealed class IdentityUser : MultiTenantEntityBase /// public long? MerchantId { get; set; } - /// - /// 角色集合。 - /// - public string[] Roles { get; set; } = Array.Empty(); - - /// - /// 权限集合。 - /// - public string[] Permissions { get; set; } = Array.Empty(); - /// /// 头像地址。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs new file mode 100644 index 0000000..53a4d0b --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs @@ -0,0 +1,24 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 权限定义。 +/// +public sealed class Permission : MultiTenantEntityBase +{ + /// + /// 权限名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 权限编码(租户内唯一)。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs new file mode 100644 index 0000000..356d8bc --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs @@ -0,0 +1,24 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 角色定义。 +/// +public sealed class Role : MultiTenantEntityBase +{ + /// + /// 角色名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 角色编码(租户内唯一)。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs new file mode 100644 index 0000000..55ac3e0 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 角色-权限关系。 +/// +public sealed class RolePermission : MultiTenantEntityBase +{ + /// + /// 角色 ID。 + /// + public long RoleId { get; set; } + + /// + /// 权限 ID。 + /// + public long PermissionId { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs new file mode 100644 index 0000000..eeb147c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 用户-角色关系。 +/// +public sealed class UserRole : MultiTenantEntityBase +{ + /// + /// 用户 ID。 + /// + public long UserId { get; set; } + + /// + /// 角色 ID。 + /// + public long RoleId { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index 80d0a1e..5b809cf 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -28,4 +28,9 @@ public interface IIdentityUserRepository /// 可选关键字(账号/名称)。 /// 取消标记。 Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + + /// + /// 获取指定租户、用户集合对应的用户(只读)。 + /// + Task> GetByIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs new file mode 100644 index 0000000..2bf97a0 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 权限仓储。 +/// +public interface IPermissionRepository +{ + Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default); + Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default); + Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default); + Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + Task AddAsync(Permission permission, CancellationToken cancellationToken = default); + Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default); + Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs new file mode 100644 index 0000000..6ace0ce --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 角色-权限关系仓储。 +/// +public interface IRolePermissionRepository +{ + Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs new file mode 100644 index 0000000..822266e --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 角色仓储。 +/// +public interface IRoleRepository +{ + Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default); + Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default); + Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + Task AddAsync(Role role, CancellationToken cancellationToken = default); + Task UpdateAsync(Role role, CancellationToken cancellationToken = default); + Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs new file mode 100644 index 0000000..aa9b9c8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TakeoutSaaS.Domain.Identity.Entities; + +namespace TakeoutSaaS.Domain.Identity.Repositories; + +/// +/// 用户-角色关系仓储。 +/// +public interface IUserRoleRepository +{ + Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default); + Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default); + Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs index 192b3a5..0c8dc63 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -51,6 +51,10 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index e1f657d..1f2bc95 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -34,4 +34,10 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde return await query.ToListAsync(cancellationToken); } + + public Task> GetByIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) + => dbContext.IdentityUsers.AsNoTracking() + .Where(x => x.TenantId == tenantId && userIds.Contains(x.Id)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs new file mode 100644 index 0000000..c07f15d --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 权限仓储。 +/// +public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository +{ + public Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); + + public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken); + + public Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default) + => dbContext.Permissions.AsNoTracking() + .Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + { + var query = dbContext.Permissions.AsNoTracking().Where(x => x.TenantId == tenantId); + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalized = keyword.Trim(); + query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); + } + + return query.ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + } + + public Task AddAsync(Permission permission, CancellationToken cancellationToken = default) + { + dbContext.Permissions.Add(permission); + return Task.CompletedTask; + } + + public Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default) + { + dbContext.Permissions.Update(permission); + return Task.CompletedTask; + } + + public async Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) + { + var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); + if (entity != null) + { + dbContext.Permissions.Remove(entity); + } + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs new file mode 100644 index 0000000..0409fff --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 角色-权限仓储。 +/// +public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IRolePermissionRepository +{ + public Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) + => dbContext.RolePermissions.AsNoTracking() + .Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default) + { + var existing = await dbContext.RolePermissions + .Where(x => x.TenantId == tenantId && x.RoleId == roleId) + .ToListAsync(cancellationToken); + + dbContext.RolePermissions.RemoveRange(existing); + + var toAdd = permissionIds.Distinct().Select(permissionId => new RolePermission + { + TenantId = tenantId, + RoleId = roleId, + PermissionId = permissionId + }); + + await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken); + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs new file mode 100644 index 0000000..a6c43c4 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 角色仓储。 +/// +public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleRepository +{ + public Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken); + + public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) + => dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken); + + public Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) + => dbContext.Roles.AsNoTracking() + .Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + { + var query = dbContext.Roles.AsNoTracking().Where(x => x.TenantId == tenantId); + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalized = keyword.Trim(); + query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); + } + + return query.ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + } + + public Task AddAsync(Role role, CancellationToken cancellationToken = default) + { + dbContext.Roles.Add(role); + return Task.CompletedTask; + } + + public Task UpdateAsync(Role role, CancellationToken cancellationToken = default) + { + dbContext.Roles.Update(role); + return Task.CompletedTask; + } + + public async Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) + { + var entity = await dbContext.Roles.FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken); + if (entity != null) + { + dbContext.Roles.Remove(entity); + } + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs new file mode 100644 index 0000000..bf3c334 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Infrastructure.Identity.Persistence; + +/// +/// EF 用户-角色仓储。 +/// +public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRoleRepository +{ + public Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) + => dbContext.UserRoles.AsNoTracking() + .Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId)) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default) + => dbContext.UserRoles.AsNoTracking() + .Where(x => x.TenantId == tenantId && x.UserId == userId) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + + public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default) + { + var existing = await dbContext.UserRoles + .Where(x => x.TenantId == tenantId && x.UserId == userId) + .ToListAsync(cancellationToken); + + dbContext.UserRoles.RemoveRange(existing); + + var toAdd = roleIds.Distinct().Select(roleId => new UserRole + { + TenantId = tenantId, + UserId = userId, + RoleId = roleId + }); + + await dbContext.UserRoles.AddRangeAsync(toAdd, cancellationToken); + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + => dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index b6ce20f..a9246dd 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -7,6 +9,10 @@ using Microsoft.Extensions.Options; using TakeoutSaaS.Infrastructure.Identity.Options; using TakeoutSaaS.Shared.Abstractions.Tenancy; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; +using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission; +using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role; +using DomainRolePermission = TakeoutSaaS.Domain.Identity.Entities.RolePermission; +using DomainUserRole = TakeoutSaaS.Domain.Identity.Entities.UserRole; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -47,9 +53,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger DisplayName = userOptions.DisplayName, TenantId = userOptions.TenantId, MerchantId = userOptions.MerchantId, - Avatar = null, - Roles = roles, - Permissions = permissions, + Avatar = null }; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); context.IdentityUsers.Add(user); @@ -60,11 +64,94 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger user.DisplayName = userOptions.DisplayName; user.TenantId = userOptions.TenantId; user.MerchantId = userOptions.MerchantId; - user.Roles = roles; - user.Permissions = permissions; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); logger.LogInformation("已更新后台账号 {Account}", user.Account); } + + // 确保角色存在 + var existingRoles = await context.Roles + .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) + .ToListAsync(cancellationToken); + var existingRoleCodes = existingRoles.Select(r => r.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var code in roles) + { + if (existingRoleCodes.Contains(code)) + { + continue; + } + + context.Roles.Add(new DomainRole + { + TenantId = userOptions.TenantId, + Code = code, + Name = code, + Description = $"Seed role {code}" + }); + } + + // 确保权限存在 + var existingPermissions = await context.Permissions + .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) + .ToListAsync(cancellationToken); + var existingPermissionCodes = existingPermissions.Select(p => p.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var code in permissions) + { + if (existingPermissionCodes.Contains(code)) + { + continue; + } + + context.Permissions.Add(new DomainPermission + { + TenantId = userOptions.TenantId, + Code = code, + Name = code, + Description = $"Seed permission {code}" + }); + } + + await context.SaveChangesAsync(cancellationToken); + + // 重新加载角色/权限以获取 Id + var roleEntities = await context.Roles + .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) + .ToListAsync(cancellationToken); + var permissionEntities = await context.Permissions + .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) + .ToListAsync(cancellationToken); + + // 重置用户角色 + var existingUserRoles = await context.UserRoles + .Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id) + .ToListAsync(cancellationToken); + context.UserRoles.RemoveRange(existingUserRoles); + + var roleIds = roleEntities.Select(r => r.Id).Distinct().ToArray(); + var userRoles = roleIds.Select(roleId => new DomainUserRole + { + TenantId = userOptions.TenantId, + UserId = user.Id, + RoleId = roleId + }); + await context.UserRoles.AddRangeAsync(userRoles, cancellationToken); + + // 为种子角色绑定种子权限 + if (permissions.Length > 0 && roleIds.Length > 0) + { + var permissionIds = permissionEntities.Select(p => p.Id).Distinct().ToArray(); + var existingRolePermissions = await context.RolePermissions + .Where(rp => rp.TenantId == userOptions.TenantId && roleIds.Contains(rp.RoleId)) + .ToListAsync(cancellationToken); + context.RolePermissions.RemoveRange(existingRolePermissions); + + var newRolePermissions = roleIds.SelectMany(roleId => permissionIds.Select(permissionId => new DomainRolePermission + { + TenantId = userOptions.TenantId, + RoleId = roleId, + PermissionId = permissionId + })); + await context.RolePermissions.AddRangeAsync(newRolePermissions, cancellationToken); + } } await context.SaveChangesAsync(cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 7f98396..dfe4d4f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -1,8 +1,6 @@ using System.Linq; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; @@ -31,6 +29,26 @@ public sealed class IdentityDbContext( /// public DbSet MiniUsers => Set(); + /// + /// 角色集合。 + /// + public DbSet Roles => Set(); + + /// + /// 权限集合。 + /// + public DbSet Permissions => Set(); + + /// + /// 用户-角色关系。 + /// + public DbSet UserRoles => Set(); + + /// + /// 角色-权限关系。 + /// + public DbSet RolePermissions => Set(); + /// /// 配置实体模型。 /// @@ -40,6 +58,10 @@ public sealed class IdentityDbContext( base.OnModelCreating(modelBuilder); ConfigureIdentityUser(modelBuilder.Entity()); ConfigureMiniUser(modelBuilder.Entity()); + ConfigureRole(modelBuilder.Entity()); + ConfigurePermission(modelBuilder.Entity()); + ConfigureUserRole(modelBuilder.Entity()); + ConfigureRolePermission(modelBuilder.Entity()); ApplyTenantQueryFilters(modelBuilder); } @@ -59,23 +81,6 @@ public sealed class IdentityDbContext( ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - var converter = new ValueConverter( - v => string.Join(',', v), - v => string.IsNullOrWhiteSpace(v) ? Array.Empty() : v.Split(',', StringSplitOptions.RemoveEmptyEntries)); - - var comparer = new ValueComparer( - (l, r) => (l == null && r == null) || (l != null && r != null && Enumerable.SequenceEqual(l, r)), - v => v.Aggregate(0, (current, item) => HashCode.Combine(current, item.GetHashCode())), - v => v.ToArray()); - - builder.Property(x => x.Roles) - .HasConversion(converter) - .Metadata.SetValueComparer(comparer); - - builder.Property(x => x.Permissions) - .HasConversion(converter) - .Metadata.SetValueComparer(comparer); - builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.TenantId, x.Account }).IsUnique(); } @@ -99,4 +104,58 @@ public sealed class IdentityDbContext( builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.TenantId, x.OpenId }).IsUnique(); } + + private static void ConfigureRole(EntityTypeBuilder builder) + { + builder.ToTable("roles"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(256); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigurePermission(EntityTypeBuilder builder) + { + builder.ToTable("permissions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Code).HasMaxLength(128).IsRequired(); + builder.Property(x => x.Description).HasMaxLength(256); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + } + + private static void ConfigureUserRole(EntityTypeBuilder builder) + { + builder.ToTable("user_roles"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.UserId).IsRequired(); + builder.Property(x => x.RoleId).IsRequired(); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique(); + } + + private static void ConfigureRolePermission(EntityTypeBuilder builder) + { + builder.ToTable("role_permissions"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.RoleId).IsRequired(); + builder.Property(x => x.PermissionId).IsRequired(); + ConfigureAuditableEntity(builder); + ConfigureSoftDeleteEntity(builder); + builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); + } } From 35b12fb0543437a164c4a85a8480010d3db4a273 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 16:43:46 +0800 Subject: [PATCH 46/56] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=A7=92?= =?UTF-8?q?=E8=89=B2/=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=20API=20?= =?UTF-8?q?=E4=B8=8E=E5=BA=94=E7=94=A8=E5=B1=82=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 2 +- .../Controllers/PermissionsController.cs | 78 ++++++++++++++++ .../Controllers/RolesController.cs | 93 +++++++++++++++++++ .../Commands/AssignUserRolesCommand.cs | 12 +++ .../Commands/BindRolePermissionsCommand.cs | 12 +++ .../Commands/CreatePermissionCommand.cs | 14 +++ .../Identity/Commands/CreateRoleCommand.cs | 14 +++ .../Commands/DeletePermissionCommand.cs | 11 +++ .../Identity/Commands/DeleteRoleCommand.cs | 11 +++ .../Commands/UpdatePermissionCommand.cs | 14 +++ .../Identity/Commands/UpdateRoleCommand.cs | 14 +++ .../Identity/Contracts/PermissionDto.cs | 37 ++++++++ .../Identity/Contracts/RoleDto.cs | 37 ++++++++ .../Handlers/AssignUserRolesCommandHandler.cs | 23 +++++ .../BindRolePermissionsCommandHandler.cs | 23 +++++ .../CreatePermissionCommandHandler.cs | 41 ++++++++ .../Handlers/CreateRoleCommandHandler.cs | 41 ++++++++ .../DeletePermissionCommandHandler.cs | 23 +++++ .../Handlers/DeleteRoleCommandHandler.cs | 23 +++++ .../Handlers/SearchPermissionsQueryHandler.cs | 54 +++++++++++ .../Handlers/SearchRolesQueryHandler.cs | 51 ++++++++++ .../UpdatePermissionCommandHandler.cs | 41 ++++++++ .../Handlers/UpdateRoleCommandHandler.cs | 41 ++++++++ .../Queries/SearchPermissionsQuery.cs | 17 ++++ .../Identity/Queries/SearchRolesQuery.cs | 17 ++++ 25 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index a18b103..02510ca 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -31,7 +31,7 @@ - [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 - [x] 差距与步骤: - [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 - - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。 + - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。【进行中:RBAC1 已落地,待补角色/权限管理 API 与 Swagger 示例】 - [x] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。 - [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。 - [x] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs new file mode 100644 index 0000000..26b8518 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs @@ -0,0 +1,78 @@ +using System.ComponentModel.DataAnnotations; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 权限管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/permissions")] +public sealed class PermissionsController(IMediator mediator) : BaseApiController +{ + /// + /// 分页查询权限。 + /// + /// + /// 示例:GET /api/admin/v1/permissions?keyword=order&page=1&pageSize=20 + /// + [HttpGet] + [PermissionAuthorize("identity:permission:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> Search([FromQuery] SearchPermissionsQuery query, CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 创建权限。 + /// + [HttpPost] + [PermissionAuthorize("identity:permission:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody, Required] CreatePermissionCommand command, CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新权限。 + /// + [HttpPut("{permissionId:long}")] + [PermissionAuthorize("identity:permission:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long permissionId, [FromBody, Required] UpdatePermissionCommand command, CancellationToken cancellationToken) + { + command = command with { PermissionId = permissionId }; + var result = await mediator.Send(command, cancellationToken); + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "权限不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除权限。 + /// + [HttpDelete("{permissionId:long}")] + [PermissionAuthorize("identity:permission:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Delete(long permissionId, CancellationToken cancellationToken) + { + var command = new DeletePermissionCommand { PermissionId = permissionId }; + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs new file mode 100644 index 0000000..a9f7148 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs @@ -0,0 +1,93 @@ +using System.ComponentModel.DataAnnotations; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 角色管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/roles")] +public sealed class RolesController(IMediator mediator) : BaseApiController +{ + /// + /// 分页查询角色。 + /// + /// + /// 示例: + /// GET /api/admin/v1/roles?keyword=ops&page=1&pageSize=20 + /// Header: Authorization: Bearer <JWT> + X-Tenant-Id + /// + [HttpGet] + [PermissionAuthorize("identity:role:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> Search([FromQuery] SearchRolesQuery query, CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 创建角色。 + /// + [HttpPost] + [PermissionAuthorize("identity:role:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody, Required] CreateRoleCommand command, CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新角色。 + /// + [HttpPut("{roleId:long}")] + [PermissionAuthorize("identity:role:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long roleId, [FromBody, Required] UpdateRoleCommand command, CancellationToken cancellationToken) + { + command = command with { RoleId = roleId }; + var result = await mediator.Send(command, cancellationToken); + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "角色不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除角色。 + /// + [HttpDelete("{roleId:long}")] + [PermissionAuthorize("identity:role:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Delete(long roleId, CancellationToken cancellationToken) + { + var command = new DeleteRoleCommand { RoleId = roleId }; + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 绑定角色权限(覆盖式)。 + /// + [HttpPut("{roleId:long}/permissions")] + [PermissionAuthorize("identity:role:bind-permission")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> BindPermissions(long roleId, [FromBody, Required] BindRolePermissionsCommand command, CancellationToken cancellationToken) + { + command = command with { RoleId = roleId }; + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs new file mode 100644 index 0000000..14672a3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 为用户分配角色(覆盖式)。 +/// +public sealed record AssignUserRolesCommand : IRequest +{ + public long UserId { get; init; } + public long[] RoleIds { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs new file mode 100644 index 0000000..aec3397 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 绑定角色权限(覆盖式)。 +/// +public sealed record BindRolePermissionsCommand : IRequest +{ + public long RoleId { get; init; } + public long[] PermissionIds { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs new file mode 100644 index 0000000..d554152 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs @@ -0,0 +1,14 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 创建权限。 +/// +public sealed record CreatePermissionCommand : IRequest +{ + public string Name { get; init; } = string.Empty; + public string Code { get; init; } = string.Empty; + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs new file mode 100644 index 0000000..dadc2a3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs @@ -0,0 +1,14 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 创建角色。 +/// +public sealed record CreateRoleCommand : IRequest +{ + public string Name { get; init; } = string.Empty; + public string Code { get; init; } = string.Empty; + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs new file mode 100644 index 0000000..ea91997 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 删除权限。 +/// +public sealed record DeletePermissionCommand : IRequest +{ + public long PermissionId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs new file mode 100644 index 0000000..09085c4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 删除角色。 +/// +public sealed record DeleteRoleCommand : IRequest +{ + public long RoleId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs new file mode 100644 index 0000000..edcb482 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs @@ -0,0 +1,14 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 更新权限。 +/// +public sealed record UpdatePermissionCommand : IRequest +{ + public long PermissionId { get; init; } + public string Name { get; init; } = string.Empty; + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs new file mode 100644 index 0000000..b4d58a2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs @@ -0,0 +1,14 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 更新角色。 +/// +public sealed record UpdateRoleCommand : IRequest +{ + public long RoleId { get; init; } + public string Name { get; init; } = string.Empty; + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs new file mode 100644 index 0000000..e9623bd --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 权限 DTO。 +/// +public sealed class PermissionDto +{ + /// + /// 权限 ID(雪花,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 权限名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 权限编码(租户内唯一)。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs new file mode 100644 index 0000000..31119d3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 角色 DTO。 +/// +public sealed class RoleDto +{ + /// + /// 角色 ID(雪花,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 角色名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 角色编码(租户内唯一)。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs new file mode 100644 index 0000000..8105f69 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs @@ -0,0 +1,23 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 用户角色分配处理器。 +/// +public sealed class AssignUserRolesCommandHandler( + IUserRoleRepository userRoleRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(AssignUserRolesCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + await userRoleRepository.ReplaceUserRolesAsync(tenantId, request.UserId, request.RoleIds, cancellationToken); + await userRoleRepository.SaveChangesAsync(cancellationToken); + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs new file mode 100644 index 0000000..eee1e9e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs @@ -0,0 +1,23 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 绑定角色权限处理器。 +/// +public sealed class BindRolePermissionsCommandHandler( + IRolePermissionRepository rolePermissionRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, request.PermissionIds, cancellationToken); + await rolePermissionRepository.SaveChangesAsync(cancellationToken); + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs new file mode 100644 index 0000000..275946e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 创建权限处理器。 +/// +public sealed class CreatePermissionCommandHandler( + IPermissionRepository permissionRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(CreatePermissionCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var permission = new Permission + { + TenantId = tenantId, + Name = request.Name, + Code = request.Code, + Description = request.Description + }; + + await permissionRepository.AddAsync(permission, cancellationToken); + await permissionRepository.SaveChangesAsync(cancellationToken); + + return new PermissionDto + { + Id = permission.Id, + TenantId = permission.TenantId, + Name = permission.Name, + Code = permission.Code, + Description = permission.Description + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs new file mode 100644 index 0000000..717393a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 创建角色处理器。 +/// +public sealed class CreateRoleCommandHandler( + IRoleRepository roleRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(CreateRoleCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var role = new Role + { + TenantId = tenantId, + Name = request.Name, + Code = request.Code, + Description = request.Description + }; + + await roleRepository.AddAsync(role, cancellationToken); + await roleRepository.SaveChangesAsync(cancellationToken); + + return new RoleDto + { + Id = role.Id, + TenantId = role.TenantId, + Name = role.Name, + Code = role.Code, + Description = role.Description + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs new file mode 100644 index 0000000..9dc2ce8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs @@ -0,0 +1,23 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 删除权限处理器。 +/// +public sealed class DeletePermissionCommandHandler( + IPermissionRepository permissionRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(DeletePermissionCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + await permissionRepository.DeleteAsync(request.PermissionId, tenantId, cancellationToken); + await permissionRepository.SaveChangesAsync(cancellationToken); + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs new file mode 100644 index 0000000..c45241a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs @@ -0,0 +1,23 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 删除角色处理器。 +/// +public sealed class DeleteRoleCommandHandler( + IRoleRepository roleRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(DeleteRoleCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken); + await roleRepository.SaveChangesAsync(cancellationToken); + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs new file mode 100644 index 0000000..97bdd1b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 权限分页查询处理器。 +/// +public sealed class SearchPermissionsQueryHandler( + IPermissionRepository permissionRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + public async Task> Handle(SearchPermissionsQuery request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + + var sorted = request.SortBy?.ToLowerInvariant() switch + { + "name" => request.SortDescending + ? permissions.OrderByDescending(x => x.Name) + : permissions.OrderBy(x => x.Name), + "code" => request.SortDescending + ? permissions.OrderByDescending(x => x.Code) + : permissions.OrderBy(x => x.Code), + _ => request.SortDescending + ? permissions.OrderByDescending(x => x.CreatedAt) + : permissions.OrderBy(x => x.CreatedAt) + }; + + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + var items = paged.Select(permission => new PermissionDto + { + Id = permission.Id, + TenantId = permission.TenantId, + Name = permission.Name, + Code = permission.Code, + Description = permission.Description + }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, permissions.Count); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs new file mode 100644 index 0000000..bd11a5d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 角色分页查询处理器。 +/// +public sealed class SearchRolesQueryHandler( + IRoleRepository roleRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + public async Task> Handle(SearchRolesQuery request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var roles = await roleRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + + var sorted = request.SortBy?.ToLowerInvariant() switch + { + "name" => request.SortDescending + ? roles.OrderByDescending(x => x.Name) + : roles.OrderBy(x => x.Name), + _ => request.SortDescending + ? roles.OrderByDescending(x => x.CreatedAt) + : roles.OrderBy(x => x.CreatedAt) + }; + + var paged = sorted + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + var items = paged.Select(role => new RoleDto + { + Id = role.Id, + TenantId = role.TenantId, + Name = role.Name, + Code = role.Code, + Description = role.Description + }).ToList(); + + return new PagedResult(items, request.Page, request.PageSize, roles.Count); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs new file mode 100644 index 0000000..b123164 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 更新权限处理器。 +/// +public sealed class UpdatePermissionCommandHandler( + IPermissionRepository permissionRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(UpdatePermissionCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var permission = await permissionRepository.FindByIdAsync(request.PermissionId, tenantId, cancellationToken); + if (permission == null) + { + return null; + } + + permission.Name = request.Name; + permission.Description = request.Description; + + await permissionRepository.UpdateAsync(permission, cancellationToken); + await permissionRepository.SaveChangesAsync(cancellationToken); + + return new PermissionDto + { + Id = permission.Id, + TenantId = permission.TenantId, + Name = permission.Name, + Code = permission.Code, + Description = permission.Description + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs new file mode 100644 index 0000000..c9b6a2d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 更新角色处理器。 +/// +public sealed class UpdateRoleCommandHandler( + IRoleRepository roleRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + public async Task Handle(UpdateRoleCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken); + if (role == null) + { + return null; + } + + role.Name = request.Name; + role.Description = request.Description; + + await roleRepository.UpdateAsync(role, cancellationToken); + await roleRepository.SaveChangesAsync(cancellationToken); + + return new RoleDto + { + Id = role.Id, + TenantId = role.TenantId, + Name = role.Name, + Code = role.Code, + Description = role.Description + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs new file mode 100644 index 0000000..8547d6e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs @@ -0,0 +1,17 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.Identity.Queries; + +/// +/// 分页查询权限。 +/// +public sealed class SearchPermissionsQuery : IRequest> +{ + public string? Keyword { get; init; } + public int Page { get; init; } = 1; + public int PageSize { get; init; } = 20; + public string? SortBy { get; init; } + public bool SortDescending { get; init; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs new file mode 100644 index 0000000..c4160a2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs @@ -0,0 +1,17 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.Identity.Queries; + +/// +/// 分页查询角色。 +/// +public sealed class SearchRolesQuery : IRequest> +{ + public string? Keyword { get; init; } + public int Page { get; init; } = 1; + public int PageSize { get; init; } = 20; + public string? SortBy { get; init; } + public bool SortDescending { get; init; } = true; +} From 3252b2a8a0f938773a6bba53592109dc65c6392c Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 16:50:05 +0800 Subject: [PATCH 47/56] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20RBAC1=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20251202084523_AddRbacModel.Designer.cs | 429 ++++++++++++++++++ .../IdentityDb/20251202084523_AddRbacModel.cs | 190 ++++++++ .../IdentityDbContextModelSnapshot.cs | 260 ++++++++++- 3 files changed, 869 insertions(+), 10 deletions(-) create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.cs diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.Designer.cs new file mode 100644 index 0000000..aeeb414 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.Designer.cs @@ -0,0 +1,429 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Identity.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20251202084523_AddRbacModel")] + partial class AddRbacModel + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Account") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("登录账号。"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("展示名称。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户(平台管理员为空)。"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("密码哈希。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Account") + .IsUnique(); + + b.ToTable("identity_users", null, t => + { + t.HasComment("管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("OpenId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 OpenId。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UnionId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 UnionId,可能为空。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "OpenId") + .IsUnique(); + + b.ToTable("mini_users", null, t => + { + t.HasComment("小程序用户实体。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("权限编码(租户内唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("权限名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("permissions", null, t => + { + t.HasComment("权限定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("角色编码(租户内唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("角色名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("roles", null, t => + { + t.HasComment("角色定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PermissionId") + .HasColumnType("bigint") + .HasComment("权限 ID。"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasComment("角色 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "RoleId", "PermissionId") + .IsUnique(); + + b.ToTable("role_permissions", null, t => + { + t.HasComment("角色-权限关系。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasComment("角色 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "UserId", "RoleId") + .IsUnique(); + + b.ToTable("user_roles", null, t => + { + t.HasComment("用户-角色关系。"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.cs new file mode 100644 index 0000000..aa64905 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251202084523_AddRbacModel.cs @@ -0,0 +1,190 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + /// + public partial class AddRbacModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Permissions", + table: "identity_users"); + + migrationBuilder.DropColumn( + name: "Roles", + table: "identity_users"); + + migrationBuilder.CreateTable( + name: "permissions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "权限名称。"), + Code = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "权限编码(租户内唯一)。"), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "描述。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_permissions", x => x.Id); + }, + comment: "权限定义。"); + + migrationBuilder.CreateTable( + name: "role_permissions", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "bigint", nullable: false, comment: "角色 ID。"), + PermissionId = table.Column(type: "bigint", nullable: false, comment: "权限 ID。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_role_permissions", x => x.Id); + }, + comment: "角色-权限关系。"); + + migrationBuilder.CreateTable( + name: "roles", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "角色名称。"), + Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "角色编码(租户内唯一)。"), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "描述。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_roles", x => x.Id); + }, + comment: "角色定义。"); + + migrationBuilder.CreateTable( + name: "user_roles", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "bigint", nullable: false, comment: "用户 ID。"), + RoleId = table.Column(type: "bigint", nullable: false, comment: "角色 ID。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_user_roles", x => x.Id); + }, + comment: "用户-角色关系。"); + + migrationBuilder.CreateIndex( + name: "IX_permissions_TenantId", + table: "permissions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_permissions_TenantId_Code", + table: "permissions", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_role_permissions_TenantId", + table: "role_permissions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_role_permissions_TenantId_RoleId_PermissionId", + table: "role_permissions", + columns: new[] { "TenantId", "RoleId", "PermissionId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_roles_TenantId", + table: "roles", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_roles_TenantId_Code", + table: "roles", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_user_roles_TenantId", + table: "user_roles", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_user_roles_TenantId_UserId_RoleId", + table: "user_roles", + columns: new[] { "TenantId", "UserId", "RoleId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "permissions"); + + migrationBuilder.DropTable( + name: "role_permissions"); + + migrationBuilder.DropTable( + name: "roles"); + + migrationBuilder.DropTable( + name: "user_roles"); + + migrationBuilder.AddColumn( + name: "Permissions", + table: "identity_users", + type: "text", + nullable: false, + defaultValue: "", + comment: "权限集合。"); + + migrationBuilder.AddColumn( + name: "Roles", + table: "identity_users", + type: "text", + nullable: false, + defaultValue: "", + comment: "角色集合。"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs index 477e211..a98f3c3 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs @@ -74,16 +74,6 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb .HasColumnType("character varying(256)") .HasComment("密码哈希。"); - b.Property("Permissions") - .IsRequired() - .HasColumnType("text") - .HasComment("权限集合。"); - - b.Property("Roles") - .IsRequired() - .HasColumnType("text") - .HasComment("角色集合。"); - b.Property("TenantId") .HasColumnType("bigint") .HasComment("所属租户 ID。"); @@ -180,6 +170,256 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb t.HasComment("小程序用户实体。"); }); }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("权限编码(租户内唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("权限名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("permissions", null, t => + { + t.HasComment("权限定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("角色编码(租户内唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("角色名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("roles", null, t => + { + t.HasComment("角色定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PermissionId") + .HasColumnType("bigint") + .HasComment("权限 ID。"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasComment("角色 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "RoleId", "PermissionId") + .IsUnique(); + + b.ToTable("role_permissions", null, t => + { + t.HasComment("角色-权限关系。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasComment("角色 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "UserId", "RoleId") + .IsUnique(); + + b.ToTable("user_roles", null, t => + { + t.HasComment("用户-角色关系。"); + }); + }); #pragma warning restore 612, 618 } } From 0d2ad0aecb89d39c93c2e77f5675ee21f3dd4130 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Tue, 2 Dec 2025 16:53:44 +0800 Subject: [PATCH 48/56] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20RBAC1=20?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/11_SystemTodo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index 02510ca..c0284c4 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -31,7 +31,7 @@ - [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 - [x] 差距与步骤: - [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 - - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。【进行中:RBAC1 已落地,待补角色/权限管理 API 与 Swagger 示例】 + - [x] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。【已落地:RBAC1 模型 + 角色/权限管理 API;Swagger 示例后续补充】 - [x] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。 - [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。 - [x] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。 From 2121432d5d0ea074d36223af46a4bafe35846140 Mon Sep 17 00:00:00 2001 From: msumshk Date: Tue, 2 Dec 2025 22:29:38 +0800 Subject: [PATCH 49/56] feat: add tracing enrichment and prometheus exporter --- Document/05_部署运维.md | 47 +++++++++--------- Document/11_SystemTodo.md | 6 +-- deploy/prometheus/alert.rules.yml | 34 +++++++++++++ deploy/prometheus/prometheus.yml | 28 +++++++++++ src/Api/TakeoutSaaS.AdminApi/Program.cs | 12 +++-- .../TakeoutSaaS.AdminApi.csproj | Bin 5124 -> 5262 bytes src/Api/TakeoutSaaS.MiniApi/Program.cs | 12 +++-- .../TakeoutSaaS.MiniApi.csproj | Bin 4064 -> 4212 bytes src/Api/TakeoutSaaS.UserApi/Program.cs | 12 +++-- .../TakeoutSaaS.UserApi.csproj | Bin 3004 -> 3164 bytes .../Diagnostics/TraceContext.cs | 18 ++++++- .../Middleware/CorrelationIdMiddleware.cs | 29 ++++++++++- .../Middleware/RequestLoggingMiddleware.cs | 6 ++- 13 files changed, 163 insertions(+), 41 deletions(-) create mode 100644 deploy/prometheus/alert.rules.yml create mode 100644 deploy/prometheus/prometheus.yml diff --git a/Document/05_部署运维.md b/Document/05_部署运维.md index 449ab67..2d90a48 100644 --- a/Document/05_部署运维.md +++ b/Document/05_部署运维.md @@ -709,35 +709,37 @@ scrape_configs: - targets: ['node-exporter:9100'] ``` -### 8.2 应用监控指标 +### 8.2 应用监控指标(OpenTelemetry + Prometheus Exporter) ```csharp -// Program.cs - 添加Prometheus监控 -builder.Services.AddPrometheusMetrics(); +// Program.cs - 指标与探针 +builder.Services.AddHealthChecks(); +builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddPrometheusExporter(); // /metrics + }); -app.UseMetricServer(); // /metrics端点 -app.UseHttpMetrics(); // HTTP请求指标 +var app = builder.Build(); +app.MapHealthChecks("/healthz"); // 存活/就绪探针 +app.MapPrometheusScrapingEndpoint(); // 默认 /metrics +``` -// 自定义指标 -public class MetricsService +自定义业务指标(使用 `System.Diagnostics.Metrics`,由 Prometheus Exporter 暴露): +```csharp +internal static class BusinessMetrics { - private static readonly Counter OrderCreatedCounter = Metrics - .CreateCounter("orders_created_total", "Total orders created"); - - private static readonly Histogram OrderProcessingDuration = Metrics - .CreateHistogram("order_processing_duration_seconds", "Order processing duration"); - - public void RecordOrderCreated() - { - OrderCreatedCounter.Inc(); - } - - public IDisposable MeasureOrderProcessing() - { - return OrderProcessingDuration.NewTimer(); - } + private static readonly Meter Meter = new("TakeoutSaaS.App", "1.0.0"); + public static readonly Counter OrdersCreated = Meter.CreateCounter("orders_created_total", "个", "订单创建计数"); + public static readonly Histogram OrderProcessingSeconds = Meter.CreateHistogram("order_processing_duration_seconds", "s", "订单处理耗时"); } ``` +Prometheus 抓取示例:见 `deploy/prometheus/prometheus.yml`,默认拉取 `/metrics`,告警规则见 `deploy/prometheus/alert.rules.yml`。 + ### 8.3 Grafana仪表板 ```json { @@ -1007,4 +1009,3 @@ docker-compose up -d --force-recreate --no-deps api docker pull takeout-saas-api:previous-version docker-compose up -d ``` - diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md index c0284c4..9d9adbe 100644 --- a/Document/11_SystemTodo.md +++ b/Document/11_SystemTodo.md @@ -28,7 +28,7 @@ ## 4. 安全与合规 - [x] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 - - [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 + - [x] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 - [x] 差距与步骤: - [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 - [x] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。【已落地:RBAC1 模型 + 角色/权限管理 API;Swagger 示例后续补充】 @@ -41,8 +41,8 @@ - [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 ## 5. 观测与运维 -- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。 -- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 +- [x] TraceId 贯通,Serilog 输出 Console/File(ELK 待后续配置)。 +- [x] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 - [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 ## 6. 业务能力补全 diff --git a/deploy/prometheus/alert.rules.yml b/deploy/prometheus/alert.rules.yml new file mode 100644 index 0000000..a74f845 --- /dev/null +++ b/deploy/prometheus/alert.rules.yml @@ -0,0 +1,34 @@ +groups: + - name: takeoutsaas-app + interval: 30s + rules: + - alert: HighErrorRate + expr: | + sum(rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m])) + / sum(rate(http_server_request_duration_seconds_count[5m])) > 0.05 + for: 5m + labels: + severity: critical + annotations: + summary: "API 5xx 错误率过高" + description: "过去 5 分钟 5xx 占比超过 5%,请检查依赖或发布" + + - alert: HighP95Latency + expr: | + histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le, service_name)) + > 1 + for: 5m + labels: + severity: warning + annotations: + summary: "API P95 延迟过高" + description: "过去 5 分钟 P95 超过 1s,请排查热点接口或依赖" + + - alert: InstanceDown + expr: up{job=~"admin-api|mini-api|user-api"} == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "实例不可达" + description: "Prometheus 抓取失败,实例处于 down 状态" diff --git a/deploy/prometheus/prometheus.yml b/deploy/prometheus/prometheus.yml new file mode 100644 index 0000000..3385f12 --- /dev/null +++ b/deploy/prometheus/prometheus.yml @@ -0,0 +1,28 @@ +global: + scrape_interval: 15s + evaluation_interval: 30s + +rule_files: + - alert.rules.yml + +scrape_configs: + - job_name: admin-api + metrics_path: /metrics + static_configs: + - targets: ["admin-api:8080"] + labels: + service: admin-api + + - job_name: mini-api + metrics_path: /metrics + static_configs: + - targets: ["mini-api:8080"] + labels: + service: mini-api + + - job_name: user-api + metrics_path: /metrics + static_configs: + - targets: ["user-api:8080"] + labels: + service: user-api diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 0feed22..32b6036 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -27,6 +27,7 @@ using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); +const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; builder.Configuration .AddJsonFile("appsettings.Seed.json", optional: true, reloadOnChange: true) @@ -37,12 +38,13 @@ builder.Host.UseSerilog((context, _, configuration) => configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "AdminApi") - .WriteTo.Console() + .WriteTo.Console(outputTemplate: logTemplate) .WriteTo.File( "logs/admin-api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, - shared: true); + shared: true, + outputTemplate: logTemplate); }); builder.Services.AddSharedWebCore(); @@ -68,6 +70,7 @@ builder.Services.AddSmsApplication(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddMessagingApplication(); builder.Services.AddSchedulerModule(builder.Configuration); +builder.Services.AddHealthChecks(); var otelSection = builder.Configuration.GetSection("Otel"); var otelEndpoint = otelSection.GetValue("Endpoint"); var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); @@ -102,7 +105,8 @@ builder.Services.AddOpenTelemetry() metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation(); + .AddRuntimeInstrumentation() + .AddPrometheusExporter(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { @@ -137,6 +141,8 @@ app.UseAuthorization(); app.UseSharedSwagger(); app.UseSchedulerDashboard(builder.Configuration); +app.MapHealthChecks("/healthz"); +app.MapPrometheusScrapingEndpoint(); app.MapControllers(); app.Run(); diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index e52a27b8c104dd342cd14fe7e803d0ac2e9cce76..57391fc397c5fd11a3c454326f36530a043a25b9 100644 GIT binary patch delta 303 zcmZqC=+m4qYvLT0iHG7QD=|7uPGO9gypOSAvICPEI~Rij5ZX+>$R#>?5u3y0V@zRS zI*!>7NXJa(Vgb=LldrMZf$2%CMqt{BEe=R;0pgg+6&ym7kFke9*8@YKIxEK_G(1t;O;zC)VU^h32 zT*%=#c^gw0l;y+h2WBNs{>AJCX2nikz!C>%DNL?nHA3QPup!Gav0H%U@+Yrg4}r3> zI6z{Boa=$4JXbJ?&2<(?+HxBL$y{!EAPI4aKA-R8FWgl?K8Ur8rw7WK#5)hn`U)nS zCokhqgR=Srl7OuD0wD9=3$#vl5J~{D<_q10%KsCN0 { configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "MiniApi") - .WriteTo.Console() + .WriteTo.Console(outputTemplate: logTemplate) .WriteTo.File( "logs/mini-api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, - shared: true); + shared: true, + outputTemplate: logTemplate); }); builder.Services.AddSharedWebCore(); @@ -45,6 +47,7 @@ builder.Services.AddSmsModule(builder.Configuration); builder.Services.AddSmsApplication(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddMessagingApplication(); +builder.Services.AddHealthChecks(); var otelSection = builder.Configuration.GetSection("Otel"); var otelEndpoint = otelSection.GetValue("Endpoint"); var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); @@ -79,7 +82,8 @@ builder.Services.AddOpenTelemetry() metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation(); + .AddRuntimeInstrumentation() + .AddPrometheusExporter(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { @@ -111,6 +115,8 @@ app.UseTenantResolution(); app.UseSharedWebCore(); app.UseSharedSwagger(); +app.MapHealthChecks("/healthz"); +app.MapPrometheusScrapingEndpoint(); app.MapControllers(); app.Run(); diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index c5e376a694a3236022fdc7e2f4fa5881543d429e..0b5e0ba555ac630fdc3cc0baf896a5ac9f9e1cab 100644 GIT binary patch delta 285 zcmaDL|3zWKtci0}CLW5LtiCQE$N+Ig N7SR4=pgTZz0RTySP?`V$ delta 296 zcmeyO@IZdTEJogmGgX1a-grjd$F-%jHk@ z;0S@TxHv&#f?V=I5@NR?x9{W`TvZ@Z?werJcd`_(+2kCaBsi}D&a#*+#}@(TnN8N? s=bOyN?*kMG diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index 7f76faa..98ed208 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -11,18 +11,20 @@ using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); +const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; builder.Host.UseSerilog((_, _, configuration) => { configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "UserApi") - .WriteTo.Console() + .WriteTo.Console(outputTemplate: logTemplate) .WriteTo.File( "logs/user-api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, - shared: true); + shared: true, + outputTemplate: logTemplate); }); builder.Services.AddSharedWebCore(); @@ -33,6 +35,7 @@ builder.Services.AddSharedSwagger(options => options.EnableAuthorization = true; }); builder.Services.AddTenantResolution(builder.Configuration); +builder.Services.AddHealthChecks(); var otelSection = builder.Configuration.GetSection("Otel"); var otelEndpoint = otelSection.GetValue("Endpoint"); var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); @@ -67,7 +70,8 @@ builder.Services.AddOpenTelemetry() metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation(); + .AddRuntimeInstrumentation() + .AddPrometheusExporter(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { @@ -99,6 +103,8 @@ app.UseTenantResolution(); app.UseSharedWebCore(); app.UseSharedSwagger(); +app.MapHealthChecks("/healthz"); +app.MapPrometheusScrapingEndpoint(); app.MapControllers(); app.Run(); diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index 08afe63c308bdc0d9d1515e303f5e661099df7a2..b5f3476cfdae878b68d3fd34e022ff5dafb02903 100644 GIT binary patch delta 189 zcmdlZen(=$tci0}CLW5Lti9JF&$9=`BDUGr598X!0@k5GdOVCMF!fP{feWkjs$DP{NP_ zBug2JC*Nk$pWMJE#%ai)#Gt^S&tNyXky{>YI}_)z$xNI+lf}4t!1M>M36q<+@fqh>e9@zLS4&R)IvhZh}eQ$ -/// 轻量级 TraceId 上下文,便于跨层访问当前请求的追踪标识。 +/// 轻量级 TraceId/SpanId 上下文,便于跨层访问当前请求的追踪标识。 /// public static class TraceContext { private static readonly AsyncLocal TraceIdHolder = new(); + private static readonly AsyncLocal SpanIdHolder = new(); /// /// 当前请求的 TraceId。 @@ -18,8 +19,21 @@ public static class TraceContext set => TraceIdHolder.Value = value; } + /// + /// 当前请求的 SpanId。 + /// + public static string? SpanId + { + get => SpanIdHolder.Value; + set => SpanIdHolder.Value = value; + } + /// /// 清理 TraceId,避免 AsyncLocal 污染其它请求。 /// - public static void Clear() => TraceIdHolder.Value = null; + public static void Clear() + { + TraceIdHolder.Value = null; + SpanIdHolder.Value = null; + } } diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs index 07740f7..e1dadd9 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs +++ b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -14,23 +15,43 @@ namespace TakeoutSaaS.Shared.Web.Middleware; public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger logger, IIdGenerator idGenerator) { private const string TraceHeader = "X-Trace-Id"; + private const string SpanHeader = "X-Span-Id"; private const string RequestHeader = "X-Request-Id"; public async Task InvokeAsync(HttpContext context) { - var traceId = ResolveTraceId(context); + var ownsActivity = Activity.Current is null; + var activity = Activity.Current ?? new Activity("TakeoutSaaS.Request"); + + if (activity.Id is null) + { + activity.SetIdFormat(ActivityIdFormat.W3C); + activity.Start(); + } + + var traceId = activity.TraceId.ToString(); + var spanId = activity.SpanId.ToString(); + + if (string.IsNullOrWhiteSpace(traceId)) + { + traceId = ResolveTraceId(context); + } + context.TraceIdentifier = traceId; TraceContext.TraceId = traceId; + TraceContext.SpanId = spanId; context.Response.OnStarting(() => { context.Response.Headers[TraceHeader] = traceId; + context.Response.Headers[SpanHeader] = spanId; return Task.CompletedTask; }); using (logger.BeginScope(new Dictionary { - ["TraceId"] = traceId + ["TraceId"] = traceId, + ["SpanId"] = spanId })) { try @@ -40,6 +61,10 @@ public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger {StatusCode} ({Elapsed} ms) TraceId:{TraceId}", + "HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId} SpanId:{SpanId}", context.Request.Method, context.Request.Path, context.Response.StatusCode, stopwatch.Elapsed.TotalMilliseconds, - traceId); + traceId, + spanId); } } } From ff0858a57bc04b1684e2beae6ff7dc9fc0be7445 Mon Sep 17 00:00:00 2001 From: msumshk Date: Tue, 2 Dec 2025 22:39:32 +0800 Subject: [PATCH 50/56] docs: add api boundaries and checklists --- Document/API边界与自检清单.md | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 Document/API边界与自检清单.md diff --git a/Document/API边界与自检清单.md b/Document/API边界与自检清单.md new file mode 100644 index 0000000..d1fa76d --- /dev/null +++ b/Document/API边界与自检清单.md @@ -0,0 +1,52 @@ +# API 边界与自检清单 + +> 目的:明确 Admin/User/Mini 三个 API 的职责边界,避免跨端耦合。开发新接口或改动现有控制器时请对照自检,确保租户、安全、DTO、路由符合约定。 + +## 1. AdminApi(管理后台) +- **面向对象**:运营、客服、商户管理员。 +- **职责**:租户/门店/商品/订单/支付/配送/字典/权限/RBAC/审计/任务调度等后台管理与洞察。 +- **鉴权**:JWT + RBAC(`[Authorize]` + `PermissionAuthorize`),必须带租户头 `X-Tenant-Id/Code`。 +- **路由前缀**:`api/admin/v{version}/...`。 +- **DTO/约束**:仅管理字段,禁止返回 C 端敏感信息;long -> string;严禁实体直接返回。 +- **现有控制器**:`AuthController`、`DeliveriesController`、`DictionaryController`、`FilesController`、`MerchantsController`、`OrdersController`、`PaymentsController`、`PermissionsController`、`RolesController`、`StoresController`、`SystemParametersController`、`UserPermissionsController`、`HealthController`。 +- **自检清单**: + 1. 是否需要权限/租户过滤?未加则补 `[Authorize]` + 租户解析。 + 2. 是否调用了应用层 CQRS,而非在 Controller 写业务? + 3. DTO 是否按管理口径,未暴露用户端字段? + 4. 是否使用参数化/AsNoTracking/投影,避免 N+1? + 5. 路由和 Swagger 示例是否含租户/权限说明? + +## 2. UserApi(C 端用户) +- **面向对象**:App/H5 普通用户。 +- **职责**:菜单浏览、下单、支付、评价、地址、售后、订单查询、支付/配送回调(验证签名)等用户闭环。 +- **鉴权**:用户 JWT,租户隔离;幂等接口需校验。 +- **路由前缀**:`api/user/v{version}/...`。 +- **DTO/约束**:仅用户侧可见字段,屏蔽后台配置字段;long -> string。 +- **现有控制器**:当前仅 `HealthController`(业务接口待补)。 +- **自检清单**: + 1. 是否暴露给用户的纯前台功能?后台配置请放 AdminApi。 + 2. 是否做租户隔离、用户鉴权、签名/幂等校验? + 3. 响应是否脱敏且只含用户需要的字段? + 4. 是否避免跨端复用后台 DTO/命令? + 5. 回调路由是否验证签名/防重放? + +## 3. MiniApi(小程序端) +- **面向对象**:微信/小程序前端。 +- **职责**:小程序登录/刷新、当前用户档案、订阅消息、直传凭证、小程序场景特定的下单/浏览等。 +- **鉴权**:小程序登录态/Token,租户隔离;必要时区分渠道。 +- **路由前缀**:`api/mini/v{version}/...`。 +- **DTO/约束**:遵循小程序接口规范,错误码与前端对齐;long -> string。 +- **现有控制器**:`AuthController`、`MeController`、`FilesController`、`HealthController`。 +- **自检清单**: + 1. 是否为小程序特有流程(code2session、订阅消息、直传等)?通用用户接口放 UserApi。 + 2. 是否完成租户/鉴权校验,区分渠道标识? + 3. 请求/响应是否符合小程序对错误码与字段的约定? + 4. 是否避免使用后台管理 DTO/权限模型? + 5. 上传/直传接口是否限制 MIME/大小并做鉴权? + +## 4. 共通约束 +- **分层**:Controller 仅做路由/DTO 转换,业务放 Application 层 Handler。 +- **租户**:所有写/读需租户过滤;严禁跨租户访问。 +- **日志/观测**:TraceId/SpanId 已贯通;/metrics、/healthz 按服务暴露。 +- **命名**:输入 `XxxRequest`、输出 `XxxDto`;文件名与类名一致;布尔属性加 `Is/Has`。 +- **发布前检查**:运行 `dotnet build`,必要时补 Swagger 示例、单元测试(核心逻辑 100% 覆盖,服务 ≥70%)。 From eedac9ae634b133a7268f78c107b8d38caf6a8b7 Mon Sep 17 00:00:00 2001 From: msumshk Date: Tue, 2 Dec 2025 23:15:22 +0800 Subject: [PATCH 51/56] 1 --- Document/09_服务器文档.md | 11 ++++++----- src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep diff --git a/Document/09_服务器文档.md b/Document/09_服务器文档.md index 6d60349..a682d88 100644 --- a/Document/09_服务器文档.md +++ b/Document/09_服务器文档.md @@ -39,7 +39,7 @@ - 安全组/开放端口: 待补充 - 变更记录: 待补充 -## 3. 天翼云主机 PostgreSQL 服务器 +## 3. 天翼云主机 应用服务器 ### 基础信息 - IP: 49.7.179.246 @@ -47,15 +47,16 @@ - 密码: 7zE&84XI6~w57W7N - 配置: 4 核 CPU / 8 GB 内存(天翼云) - 地点: 北京 -- 用途: PostgreSQL 服务器 +- 用途: 主应用服务器(承载 Admin/User/Mini API 或网关实例) - 到期时间: 2027-10-04 17:17:57 ### 建议补充 - 系统版本: 待补充(执行 `cat /etc/os-release`) - 带宽/磁盘: 待补充 -- 数据目录: 待补充(示例 `/var/lib/postgresql`) -- 数据备份/监控: 待补充 -- 安全组/开放端口: 待补充 +- 部署路径: 待补充(示例 `/opt/takeoutsaas`) +- 进程/端口: 待补充(示例 `AdminApi/UserApi/MiniApi`、`:8080`) +- 日志/监控: 待补充(Serilog 文件目录、进程监控方式) +- 安全组/开放端口: 待补充(按 API/网关暴露的 HTTP/HTTPS 端口) - 变更记录: 待补充 ## 4. 腾讯云 Redis/RabbitMQ 服务器 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep b/src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - From 871e06c4721e1c39af76f2bef8f11058a47881c4 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 10:29:34 +0800 Subject: [PATCH 52/56] =?UTF-8?q?docs:=E6=96=B0=E5=A2=9E=E6=B5=81=E6=B0=B4?= =?UTF-8?q?=E7=BA=BF=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/CI_CD流水线.md | 134 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 Document/CI_CD流水线.md diff --git a/Document/CI_CD流水线.md b/Document/CI_CD流水线.md new file mode 100644 index 0000000..6be352f --- /dev/null +++ b/Document/CI_CD流水线.md @@ -0,0 +1,134 @@ +# CI/CD 流水线(云效,dev 合并 master 触发) + +## 触发规则 +- 分支触发:仅 `master`。 +- 校验来源:流水线脚本内检查 `GIT_BRANCH == master` 且 `GIT_PREVIOUS_BRANCH == dev`,否则退出。 + +## 必填变量(云效“变量/密钥”) +- 字符变量: + - `REGISTRY=crpi-z1i5bludyfuvzo9o.cn-beijing.personal.cr.aliyuncs.com` + - `REGISTRY_USERNAME=heaize404@163.com` + - `DEPLOY_HOST=49.7.179.246` + - `DEPLOY_USER=root` +- 密钥/凭据: + - `REGISTRY_PASSWORD=MsuMshk112233` + - `DEPLOY_PASSWORD=7zE&84XI6~w57W7N` +- 默认基线:`BASE_REF=origin/master`(可不配)。 + +## Docker 端口约定 +- Admin:7801 +- Mini:7701 +- User:7901 + +## 完整流水线 YAML +```yaml +version: 1.0 +name: takeoutsaas-ci-cd +displayName: TakeoutSaaS CI/CD +triggers: + push: + branches: + include: + - master + +stages: + - stage: DetectChanges + name: DetectChanges + steps: + - step: Checkout + name: Checkout + checkout: self + - step: Detect + name: Detect + script: | + set -e + if [ "$GIT_BRANCH" != "master" ] || [ "$GIT_PREVIOUS_BRANCH" != "dev" ]; then + echo "非 dev->master,跳过流水线"; exit 0; fi + + git fetch origin master --depth=1 + BASE=${BASE_REF:-origin/master} + CHANGED=$(git diff --name-only "$(git merge-base $BASE HEAD)" HEAD) + echo "变更文件:" + echo "$CHANGED" + + deploy_all=false + services=() + hit(){ echo "$CHANGED" | grep -qE "$1"; } + + if hit '^src/(Domain|Application|Infrastructure|Core|Modules)/'; then deploy_all=true; fi + hit '^Directory.Build.props$' && deploy_all=true + + hit '^src/Api/TakeoutSaaS.AdminApi/' && services+=("admin-api") + hit '^src/Api/TakeoutSaaS.MiniApi/' && services+=("mini-api") + hit '^src/Api/TakeoutSaaS.UserApi/' && services+=("user-api") + + if $deploy_all || [ ${#services[@]} -eq 0 ]; then + services=("admin-api" "mini-api" "user-api") + fi + + echo "SERVICES=${services[*]}" >> "$ACROSS_STAGES_ENV_FILE" + + - stage: BuildPush + name: BuildPush + steps: + - step: DockerBuildPush + name: DockerBuildPush + script: | + set -e + IFS=' ' read -ra svcs <<< "$SERVICES" + REGISTRY=${REGISTRY:?需要配置 REGISTRY} + TAG=${TAG:-$(date +%Y%m%d%H%M%S)} + + echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin + + for svc in "${svcs[@]}"; do + case "$svc" in + admin-api) dockerfile="src/Api/TakeoutSaaS.AdminApi/Dockerfile"; image="$REGISTRY/admin-api:$TAG" ;; + mini-api) dockerfile="src/Api/TakeoutSaaS.MiniApi/Dockerfile"; image="$REGISTRY/mini-api:$TAG" ;; + user-api) dockerfile="src/Api/TakeoutSaaS.UserApi/Dockerfile"; image="$REGISTRY/user-api:$TAG" ;; + esac + echo "构建并推送 $image" + docker build -f "$dockerfile" -t "$image" . + docker push "$image" + done + echo "IMAGE_TAG=$TAG" >> "$ACROSS_STAGES_ENV_FILE" + + - stage: Deploy + name: Deploy + steps: + - step: DockerDeploy + name: DockerDeploy + script: | + set -e + command -v sshpass >/dev/null 2>&1 || (sudo apt-get update && sudo apt-get install -y sshpass) + + IFS=' ' read -ra svcs <<< "$SERVICES" + TAG="$IMAGE_TAG" + REGISTRY=${REGISTRY:?} + DEPLOY_HOST=${DEPLOY_HOST:?} + DEPLOY_USER=${DEPLOY_USER:-root} + DEPLOY_PASSWORD=${DEPLOY_PASSWORD:?} + + for svc in "${svcs[@]}"; do + case "$svc" in + admin-api) image="$REGISTRY/admin-api:$TAG"; port=7801 ;; + mini-api) image="$REGISTRY/mini-api:$TAG"; port=7701 ;; + user-api) image="$REGISTRY/user-api:$TAG"; port=7901 ;; + esac + + echo "部署 $svc -> $image" + sshpass -p "$DEPLOY_PASSWORD" ssh -o StrictHostKeyChecking=no "$DEPLOY_USER@$DEPLOY_HOST" "set -e; docker pull $image; docker stop $svc 2>/dev/null || true; docker rm $svc 2>/dev/null || true; docker run -d --name $svc --restart=always -p $port:$port $image" + done +``` + +## 注意事项 +- 以上 YAML 如仍报 YAML 校验错误,可将 `triggers` 改为: + ```yaml + on: + push: + branches: + - master + ``` + 其余保持不变。 +- 如果云效的分支变量名与 `GIT_BRANCH` / `GIT_PREVIOUS_BRANCH` 不同,请在 Detect 步骤替换为实际变量名。 +- 所有密码、密钥务必放在“密钥/凭据”类型变量中,不要写入代码库。 From 13e0eed6cef779f013deca7465b9c7c3e1a92b07 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 12:32:01 +0800 Subject: [PATCH 53/56] ci: add github actions workflow and dockerfiles --- .github/workflows/ci-cd.yml | 180 ++++++++++++++++++++++++ src/Api/TakeoutSaaS.AdminApi/Dockerfile | 12 ++ src/Api/TakeoutSaaS.MiniApi/Dockerfile | 12 ++ src/Api/TakeoutSaaS.UserApi/Dockerfile | 12 ++ 4 files changed, 216 insertions(+) create mode 100644 .github/workflows/ci-cd.yml create mode 100644 src/Api/TakeoutSaaS.AdminApi/Dockerfile create mode 100644 src/Api/TakeoutSaaS.MiniApi/Dockerfile create mode 100644 src/Api/TakeoutSaaS.UserApi/Dockerfile diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..9dd470e --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,180 @@ +name: TakeoutSaaS CI/CD + +on: + push: + branches: + - master + workflow_dispatch: + +env: + REGISTRY: ${{ secrets.REGISTRY }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PASSWORD: ${{ secrets.DEPLOY_PASSWORD }} + +jobs: + detect: + runs-on: ubuntu-latest + outputs: + services: ${{ steps.collect.outputs.services }} + image_tag: ${{ steps.collect.outputs.image_tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: collect + shell: bash + run: | + set -euo pipefail + + BASE="${{ github.event.before }}" + if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then + if git rev-parse HEAD^ >/dev/null 2>&1; then + BASE="$(git rev-parse HEAD^)" + else + BASE="" + fi + fi + + if [ -z "$BASE" ]; then + CHANGED=$(git ls-tree -r --name-only HEAD) + else + CHANGED=$(git diff --name-only "$BASE" HEAD || true) + fi + + echo "本次变更文件:" + echo "$CHANGED" + + deploy_all=false + services=() + + hit() { echo "$CHANGED" | grep -qE "$1"; } + + if hit '^src/(Domain|Application|Infrastructure|Core|Modules)/'; then deploy_all=true; fi + if hit '^Directory\.Build\.props$'; then deploy_all=true; fi + + if hit '^src/Api/TakeoutSaaS.AdminApi/'; then services+=("admin-api"); fi + if hit '^src/Api/TakeoutSaaS.MiniApi/'; then services+=("mini-api"); fi + if hit '^src/Api/TakeoutSaaS.UserApi/'; then services+=("user-api"); fi + + if $deploy_all || [ ${#services[@]} -eq 0 ]; then + services=("admin-api" "mini-api" "user-api") + fi + + printf '需要处理的服务: %s\n' "${services[*]}" + + SERVICES_LIST="${services[*]}" + export SERVICES_LIST + SERVICES_JSON=$(python - <<'PY' +import json, os +raw = os.environ.get("SERVICES_LIST", "").split() +print(json.dumps(raw)) +PY +) + + echo "services=$SERVICES_JSON" >> "$GITHUB_OUTPUT" + TAG=$(date +%Y%m%d%H%M%S) + echo "image_tag=$TAG" >> "$GITHUB_OUTPUT" + + build: + runs-on: ubuntu-latest + needs: detect + if: needs.detect.outputs.services != '[]' + strategy: + matrix: + service: ${{ fromJson(needs.detect.outputs.services) }} + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ env.REGISTRY_PASSWORD }} + + - name: Build and push ${{ matrix.service }} + env: + SERVICE: ${{ matrix.service }} + IMAGE_TAG: ${{ needs.detect.outputs.image_tag }} + run: | + set -euo pipefail + + case "$SERVICE" in + admin-api) + DOCKERFILE="src/Api/TakeoutSaaS.AdminApi/Dockerfile" + IMAGE="$REGISTRY/admin-api:$IMAGE_TAG" + ;; + mini-api) + DOCKERFILE="src/Api/TakeoutSaaS.MiniApi/Dockerfile" + IMAGE="$REGISTRY/mini-api:$IMAGE_TAG" + ;; + user-api) + DOCKERFILE="src/Api/TakeoutSaaS.UserApi/Dockerfile" + IMAGE="$REGISTRY/user-api:$IMAGE_TAG" + ;; + *) + echo "未知服务:$SERVICE" + exit 1 + ;; + esac + + if [ ! -f "$DOCKERFILE" ]; then + echo "未找到 Dockerfile: $DOCKERFILE" + exit 1 + fi + + docker build -f "$DOCKERFILE" -t "$IMAGE" . + docker push "$IMAGE" + + deploy: + runs-on: ubuntu-latest + needs: + - detect + - build + if: needs.detect.outputs.services != '[]' + strategy: + matrix: + service: ${{ fromJson(needs.detect.outputs.services) }} + steps: + - name: Install sshpass + run: sudo apt-get update && sudo apt-get install -y sshpass + + - name: Deploy ${{ matrix.service }} + env: + SERVICE: ${{ matrix.service }} + IMAGE_TAG: ${{ needs.detect.outputs.image_tag }} + run: | + set -euo pipefail + + case "$SERVICE" in + admin-api) + IMAGE="$REGISTRY/admin-api:$IMAGE_TAG" + PORT=7801 + ;; + mini-api) + IMAGE="$REGISTRY/mini-api:$IMAGE_TAG" + PORT=7701 + ;; + user-api) + IMAGE="$REGISTRY/user-api:$IMAGE_TAG" + PORT=7901 + ;; + *) + echo "未知服务:$SERVICE" + exit 1 + ;; + esac + + sshpass -p "$DEPLOY_PASSWORD" ssh -o StrictHostKeyChecking=no "$DEPLOY_USER@$DEPLOY_HOST" </dev/null || true +docker rm $SERVICE 2>/dev/null || true +docker run -d --name $SERVICE --restart=always -p $PORT:$PORT $IMAGE +EOF diff --git a/src/Api/TakeoutSaaS.AdminApi/Dockerfile b/src/Api/TakeoutSaaS.AdminApi/Dockerfile new file mode 100644 index 0000000..27f5733 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +RUN dotnet publish src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 7801 +ENV ASPNETCORE_URLS=http://+:7801 +ENTRYPOINT ["dotnet", "TakeoutSaaS.AdminApi.dll"] diff --git a/src/Api/TakeoutSaaS.MiniApi/Dockerfile b/src/Api/TakeoutSaaS.MiniApi/Dockerfile new file mode 100644 index 0000000..f737a92 --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj +RUN dotnet publish src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 7701 +ENV ASPNETCORE_URLS=http://+:7701 +ENTRYPOINT ["dotnet", "TakeoutSaaS.MiniApi.dll"] diff --git a/src/Api/TakeoutSaaS.UserApi/Dockerfile b/src/Api/TakeoutSaaS.UserApi/Dockerfile new file mode 100644 index 0000000..22e0ab8 --- /dev/null +++ b/src/Api/TakeoutSaaS.UserApi/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj +RUN dotnet publish src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 7901 +ENV ASPNETCORE_URLS=http://+:7901 +ENTRYPOINT ["dotnet", "TakeoutSaaS.UserApi.dll"] From 4f65d33cf3fb6f9c790b29e6ffb281edd3742da1 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 12:42:20 +0800 Subject: [PATCH 54/56] =?UTF-8?q?ci:=20=E4=BF=AE=E5=A4=8D=20workflow=20pyt?= =?UTF-8?q?hon=20=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9dd470e..4de513e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -68,12 +68,7 @@ jobs: SERVICES_LIST="${services[*]}" export SERVICES_LIST - SERVICES_JSON=$(python - <<'PY' -import json, os -raw = os.environ.get("SERVICES_LIST", "").split() -print(json.dumps(raw)) -PY -) + SERVICES_JSON=$(python -c "import json, os; print(json.dumps(os.environ.get('SERVICES_LIST','').split()))") echo "services=$SERVICES_JSON" >> "$GITHUB_OUTPUT" TAG=$(date +%Y%m%d%H%M%S) From c16311dcd455235971c8ea31508a08fba7fe1356 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 12:48:56 +0800 Subject: [PATCH 55/56] =?UTF-8?q?ci:=20=E8=B0=83=E6=95=B4=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=E9=81=BF=E5=85=8D=20heredoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4de513e..4055b77 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -165,11 +165,11 @@ jobs: ;; esac - sshpass -p "$DEPLOY_PASSWORD" ssh -o StrictHostKeyChecking=no "$DEPLOY_USER@$DEPLOY_HOST" </dev/null || true docker rm $SERVICE 2>/dev/null || true docker run -d --name $SERVICE --restart=always -p $PORT:$PORT $IMAGE -EOF +" From eb1d07d382b7919ea2ab8f1fb2c8eabcddd2132c Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 12:53:50 +0800 Subject: [PATCH 56/56] =?UTF-8?q?ci:=20=E4=BF=AE=E5=A4=8D=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=91=BD=E4=BB=A4=E7=BC=A9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4055b77..138c139 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -166,10 +166,10 @@ jobs: esac sshpass -p "$DEPLOY_PASSWORD" ssh -o StrictHostKeyChecking=no "$DEPLOY_USER@$DEPLOY_HOST" " -set -e -echo \"$REGISTRY_PASSWORD\" | docker login \"$REGISTRY\" -u \"$REGISTRY_USERNAME\" --password-stdin -docker pull $IMAGE -docker stop $SERVICE 2>/dev/null || true -docker rm $SERVICE 2>/dev/null || true -docker run -d --name $SERVICE --restart=always -p $PORT:$PORT $IMAGE -" + set -e + echo \"$REGISTRY_PASSWORD\" | docker login \"$REGISTRY\" -u \"$REGISTRY_USERNAME\" --password-stdin + docker pull $IMAGE + docker stop $SERVICE 2>/dev/null || true + docker rm $SERVICE 2>/dev/null || true + docker run -d --name $SERVICE --restart=always -p $PORT:$PORT $IMAGE + "