diff --git a/Document/Completed/后端套餐管理.md b/Document/Completed/后端套餐管理.md new file mode 100644 index 0000000..23c0496 --- /dev/null +++ b/Document/Completed/后端套餐管理.md @@ -0,0 +1,79 @@ +# TakeoutSaaS 开发 TODO(自动维护) + +> 说明:该文档用于记录本仓库待办与进度。我会在完成每个任务后更新标记状态,并尽量保持“一个功能点一个原子提交”。 +> +> 状态标记:[x] 已完成 / [~] 部分完成 / [ ] 未开始 + +## 一期:套餐管理 MVP(已落地 + 待收口) + +[x] 1. 套餐增删改:新建/编辑/复制套餐,支持草稿保存 + +- [x] 新建套餐(表单:基础信息 + 定价 + 权益配额) +- [x] 编辑套餐(同新增表单) +- [x] 复制套餐(基于现有套餐快速创建新套餐) +- [x] 删除套餐(软删) +- [x] 草稿保存(草稿/发布状态、草稿发布、回滚草稿) +- [x] 修复“保存草稿却变发布”根因(EF 默认值哨兵导致 insert 省略字段,已将发布状态默认值调整为草稿并补齐哨兵配置) + +[x] 2. 上架体系:上架/下架、是否对外可见、是否允许新租户购买 + +- [x] 上架/下架(启用/禁用套餐,含二次确认与提示) +- [x] 是否对外可见(展示与可售解耦) +- [x] 是否允许新租户购买(可售开关与可见开关解耦) +- [x] 修复“新增时开关 false 无法落库”根因(EF 默认值哨兵导致 insert 省略字段,已将哨兵改为 true) + +[~] 3. 价格与计费周期:月付/年付、阶梯价/按量计费 + +- [x] 月付/年付价格(`monthlyPrice` / `yearlyPrice`) +- [ ] 阶梯价/按量计费 + +[x] 4. 权益/配额配置:功能开关 + 数值配额(门店/账号/存储/短信/配送单量/更多) + +- [x] 数值配额(门店数、账号数、存储、短信、配送单量) +- [x] 功能策略(`featurePoliciesJson`:可视化编辑 + JSON 预览,保留未知字段) +- [x] 商品/菜单上限 +- [x] API 调用次数 +- [x] 报表/导出权限 +- [x] 打印/小票能力 +- [x] 营销功能(优惠券/满减/会员/积分等开关) + +[~] 5. 展示配置:卖点文案、推荐标识、排序、标签、对比页字段 + +- [x] 排序(`sortOrder`) +- [~] 卖点/描述(`description`) +- [x] 推荐标识(Recommended) +- [x] 标签(推荐/性价比/旗舰) +- [ ] (已移除)对比页展示字段配置(对比维度/顺序) +- [ ] 自助入驻选套餐页展示推荐/标签(公共套餐列表接口返回 isRecommended/tags + 前端展示/置顶) + +[x] 6. 订阅关联视图:当前使用该套餐的租户数量、MRR/ARR 粗看、到期分布(运营常用) + +- [x] 订阅/租户数量:活跃订阅数、总订阅数、使用租户数 +- [x] 使用租户列表入口:抽屉列表(支持分页与搜索) +- [x] MRR/ARR 粗看 +- [x] 到期分布(7/15/30 天到期租户数、到期列表入口) + +[x] 7. 后端接口补齐(套餐使用统计/使用租户分页查询等) + +[x] 8. 前端联调与回归(包含权限、空态、错误提示) + +## 二期:上架与配置完善(建议) + +- [x] 1. 草稿保存与发布流程(草稿/发布、回滚到草稿) +- [x] 2. 可见性/可售开关拆分(对外可见、允许新租户购买、已订阅不受影响说明) +- [x] 4. 权益/配额可视化编辑器(JSON 结构化编辑、Schema 校验、预设模板) +- [x] 5. 更多常用配额字段补齐(商品/菜单、API 次数、导出/报表、打印等) + +## 三期:计费与权益策略(建议) + +- [ ] 1. 阶梯价/按量计费(超配计费、账单明细) +- [ ] 2. 权益变更影响说明:升配/降配规则(立即/次周期)、影响范围提示 +- [ ] 3. 超配策略(禁止/只读/按量计费/宽限期) + +## 四期:商业化与合规增强(建议) + +- [ ] 1. 附加计费(Add-ons):短信包、存储包、额外门店/账号包、配送单量包(可叠加、单独定价) +- [ ] 2. 版本与历史:套餐版本号、变更记录、回滚、对已订阅租户的影响范围提示 +- [ ] 3. 购买限制:可购买地区/行业、仅邀请可见、最大购买数量、是否允许叠加订阅 +- [ ] 4. 订阅关联视图增强:MRR/ARR、到期分布、续费转化漏斗(运营常用) +- [ ] 5. 操作审计:谁改了套餐、何时改、改了什么(合规必备) diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs new file mode 100644 index 0000000..97d43df --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs @@ -0,0 +1,121 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Billings.Commands; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Application.App.Billings.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}/bills")] +public sealed class BillingsController(IMediator mediator) : BaseApiController +{ + /// + /// 分页查询账单列表。 + /// + /// 账单分页结果。 + [HttpGet] + [PermissionAuthorize("bill:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetList([FromQuery] GetBillListQuery query, CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 获取账单详情。 + /// + /// 账单 ID。 + /// 取消标记。 + /// 账单详情。 + [HttpGet("{id:long}")] + [PermissionAuthorize("bill:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> GetDetail(long id, CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetBillDetailQuery { BillId = id }, cancellationToken); + + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "账单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 手动创建账单。 + /// + /// 创建账单命令。 + /// 取消标记。 + /// 创建的账单信息。 + [HttpPost] + [PermissionAuthorize("bill:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody, Required] CreateBillCommand command, CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新账单状态。 + /// + /// 账单 ID。 + /// 更新状态命令。 + /// 取消标记。 + /// 更新后的账单信息。 + [HttpPut("{id:long}/status")] + [PermissionAuthorize("bill:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> UpdateStatus(long id, [FromBody, Required] UpdateBillStatusCommand command, CancellationToken cancellationToken) + { + command = command with { BillId = id }; + var result = await mediator.Send(command, cancellationToken); + + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "账单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 获取账单支付记录。 + /// + /// 账单 ID。 + /// 取消标记。 + /// 支付记录列表。 + [HttpGet("{billId:long}/payments")] + [PermissionAuthorize("bill:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetPayments(long billId, CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetTenantPaymentsQuery { BillId = billId }, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 记录支付(线下支付确认)。 + /// + /// 账单 ID。 + /// 记录支付命令。 + /// 取消标记。 + /// 支付记录信息。 + [HttpPost("{billId:long}/payments")] + [PermissionAuthorize("bill:pay")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> RecordPayment(long billId, [FromBody, Required] RecordPaymentCommand command, CancellationToken cancellationToken) + { + command = command with { BillId = billId }; + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs new file mode 100644 index 0000000..b6a3ee2 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/QuotaPackagesController.cs @@ -0,0 +1,198 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.QuotaPackages.Commands; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Application.App.QuotaPackages.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}/quota-packages")] +public sealed class QuotaPackagesController(IMediator mediator) : BaseApiController +{ + /// + /// 配额包列表。 + /// + /// 查询条件。 + /// 取消标记。 + /// 配额包分页结果。 + [HttpGet] + [PermissionAuthorize("quota-package:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] GetQuotaPackageListQuery query, CancellationToken cancellationToken) + { + // 1. 查询配额包分页 + var result = await mediator.Send(query, cancellationToken); + + // 2. 返回结果 + return ApiResponse>.Ok(result); + } + + /// + /// 创建配额包。 + /// + /// 创建命令。 + /// 取消标记。 + /// 创建后的配额包。 + [HttpPost] + [PermissionAuthorize("quota-package:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody, Required] CreateQuotaPackageCommand command, CancellationToken cancellationToken) + { + // 1. 执行创建 + var result = await mediator.Send(command, cancellationToken); + + // 2. 返回创建结果 + return ApiResponse.Ok(result); + } + + /// + /// 更新配额包。 + /// + /// 配额包 ID。 + /// 更新命令。 + /// 取消标记。 + /// 更新后的配额包或未找到。 + [HttpPut("{quotaPackageId:long}")] + [PermissionAuthorize("quota-package:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long quotaPackageId, [FromBody, Required] UpdateQuotaPackageCommand command, CancellationToken cancellationToken) + { + // 1. 绑定路由 ID + command = command with { QuotaPackageId = quotaPackageId }; + + // 2. 执行更新 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回更新结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "配额包不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除配额包。 + /// + /// 配额包 ID。 + /// 取消标记。 + /// 删除结果。 + [HttpDelete("{quotaPackageId:long}")] + [PermissionAuthorize("quota-package:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Delete(long quotaPackageId, CancellationToken cancellationToken) + { + // 1. 构建删除命令 + var command = new DeleteQuotaPackageCommand { QuotaPackageId = quotaPackageId }; + + // 2. 执行删除并返回 + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 上架/下架配额包。 + /// + /// 配额包 ID。 + /// 状态更新命令。 + /// 取消标记。 + /// 更新结果。 + [HttpPut("{quotaPackageId:long}/status")] + [PermissionAuthorize("quota-package:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> UpdateStatus(long quotaPackageId, [FromBody, Required] UpdateQuotaPackageStatusCommand command, CancellationToken cancellationToken) + { + // 1. 绑定路由 ID + command = command with { QuotaPackageId = quotaPackageId }; + + // 2. 执行状态更新 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回结果 + return ApiResponse.Ok(result); + } + + /// + /// 为租户购买配额包。 + /// + /// 租户 ID。 + /// 购买命令。 + /// 取消标记。 + /// 购买记录。 + [HttpPost("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-packages")] + [PermissionAuthorize("tenant:quota:purchase")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> PurchaseForTenant( + long tenantId, + [FromBody, Required] PurchaseQuotaPackageCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定租户 ID + command = command with { TenantId = tenantId }; + + // 2. 执行购买 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回购买结果 + return ApiResponse.Ok(result); + } + + /// + /// 租户配额使用情况。 + /// + /// 租户 ID。 + /// 查询条件。 + /// 取消标记。 + /// 配额使用情况列表。 + [HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-usage")] + [PermissionAuthorize("tenant:quota:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetTenantQuotaUsage( + long tenantId, + [FromQuery] GetTenantQuotaUsageQuery query, + CancellationToken cancellationToken) + { + // 1. 绑定租户 ID + query = query with { TenantId = tenantId }; + + // 2. 查询配额使用情况 + var result = await mediator.Send(query, cancellationToken); + + // 3. 返回结果 + return ApiResponse>.Ok(result); + } + + /// + /// 租户配额购买记录。 + /// + /// 租户 ID。 + /// 查询条件。 + /// 取消标记。 + /// 购买记录分页结果。 + [HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-purchases")] + [PermissionAuthorize("tenant:quota:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetTenantQuotaPurchases( + long tenantId, + [FromQuery] GetTenantQuotaPurchasesQuery query, + CancellationToken cancellationToken) + { + // 1. 绑定租户 ID + query = query with { TenantId = tenantId }; + + // 2. 查询购买记录 + var result = await mediator.Send(query, cancellationToken); + + // 3. 返回结果 + return ApiResponse>.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs new file mode 100644 index 0000000..cf4dd7f --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StatisticsController.cs @@ -0,0 +1,96 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Statistics.Dto; +using TakeoutSaaS.Application.App.Statistics.Queries; +using TakeoutSaaS.Domain.Tenants.Enums; +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}/statistics")] +public sealed class StatisticsController(IMediator mediator) : BaseApiController +{ + /// + /// 获取订阅概览统计。 + /// + /// 取消标记。 + /// 订阅概览数据。 + [HttpGet("subscription-overview")] + [PermissionAuthorize("statistics:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> GetSubscriptionOverview(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetSubscriptionOverviewQuery(), cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 获取配额使用排行。 + /// + /// 配额类型。 + /// 返回前N条记录,默认10。 + /// 取消标记。 + /// 配额使用排行数据。 + [HttpGet("quota-ranking")] + [PermissionAuthorize("statistics:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> GetQuotaRanking( + [FromQuery] TenantQuotaType quotaType, + [FromQuery] int topN = 10, + CancellationToken cancellationToken = default) + { + var query = new GetQuotaUsageRankingQuery { QuotaType = quotaType, TopN = topN }; + var result = await mediator.Send(query, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 获取收入统计。 + /// + /// 统计月份数量,默认12个月。 + /// 取消标记。 + /// 收入统计数据。 + [HttpGet("revenue")] + [PermissionAuthorize("statistics:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> GetRevenue( + [FromQuery] int monthsCount = 12, + CancellationToken cancellationToken = default) + { + var query = new GetRevenueStatisticsQuery { MonthsCount = monthsCount }; + var result = await mediator.Send(query, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 获取即将到期的订阅列表。 + /// + /// 筛选天数,默认7天内到期。 + /// 是否只返回未开启自动续费的订阅。 + /// 取消标记。 + /// 即将到期的订阅列表。 + [HttpGet("expiring-subscriptions")] + [PermissionAuthorize("statistics:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetExpiringSubscriptions( + [FromQuery] int daysAhead = 7, + [FromQuery] bool onlyWithoutAutoRenew = false, + CancellationToken cancellationToken = default) + { + var query = new GetExpiringSubscriptionsQuery + { + DaysAhead = daysAhead, + OnlyWithoutAutoRenew = onlyWithoutAutoRenew + }; + var result = await mediator.Send(query, cancellationToken); + return ApiResponse>.Ok(result); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs new file mode 100644 index 0000000..75336d1 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs @@ -0,0 +1,210 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.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}/subscriptions")] +public sealed class SubscriptionsController(IMediator mediator) : BaseApiController +{ + /// + /// 分页查询订阅列表(支持按状态、套餐、到期时间筛选)。 + /// + /// 查询条件。 + /// 取消标记。 + /// 订阅分页结果。 + [HttpGet] + [PermissionAuthorize("subscription:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + [FromQuery] GetSubscriptionListQuery query, + CancellationToken cancellationToken) + { + // 1. 查询订阅分页 + var result = await mediator.Send(query, cancellationToken); + + // 2. 返回结果 + return ApiResponse>.Ok(result); + } + + /// + /// 查看订阅详情(含套餐信息、配额使用、变更历史)。 + /// + /// 订阅 ID。 + /// 取消标记。 + /// 订阅详情或未找到。 + [HttpGet("{subscriptionId:long}")] + [PermissionAuthorize("subscription:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail( + long subscriptionId, + CancellationToken cancellationToken) + { + // 1. 查询订阅详情 + var result = await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscriptionId }, cancellationToken); + + // 2. 返回查询结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新订阅基础信息(备注、自动续费等)。 + /// + /// 订阅 ID。 + /// 更新命令。 + /// 取消标记。 + /// 更新后的订阅详情或未找到。 + [HttpPut("{subscriptionId:long}")] + [PermissionAuthorize("subscription:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update( + long subscriptionId, + [FromBody, Required] UpdateSubscriptionCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定路由 ID + command = command with { SubscriptionId = subscriptionId }; + + // 2. 执行更新 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回更新结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") + : ApiResponse.Ok(result); + } + + /// + /// 延期订阅(增加订阅时长)。 + /// + /// 订阅 ID。 + /// 延期命令。 + /// 取消标记。 + /// 延期后的订阅详情或未找到。 + [HttpPost("{subscriptionId:long}/extend")] + [PermissionAuthorize("subscription:extend")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Extend( + long subscriptionId, + [FromBody, Required] ExtendSubscriptionCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定路由 ID + command = command with { SubscriptionId = subscriptionId }; + + // 2. 执行延期 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回延期结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") + : ApiResponse.Ok(result); + } + + /// + /// 变更套餐(支持立即生效或下周期生效)。 + /// + /// 订阅 ID。 + /// 变更套餐命令。 + /// 取消标记。 + /// 变更后的订阅详情或未找到。 + [HttpPost("{subscriptionId:long}/change-plan")] + [PermissionAuthorize("subscription:change-plan")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> ChangePlan( + long subscriptionId, + [FromBody, Required] ChangeSubscriptionPlanCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定路由 ID + command = command with { SubscriptionId = subscriptionId }; + + // 2. 执行套餐变更 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回变更结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") + : ApiResponse.Ok(result); + } + + /// + /// 变更订阅状态。 + /// + /// 订阅 ID。 + /// 状态变更命令。 + /// 取消标记。 + /// 变更后的订阅详情或未找到。 + [HttpPost("{subscriptionId:long}/status")] + [PermissionAuthorize("subscription:update-status")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> UpdateStatus( + long subscriptionId, + [FromBody, Required] UpdateSubscriptionStatusCommand command, + CancellationToken cancellationToken) + { + // 1. 绑定路由 ID + command = command with { SubscriptionId = subscriptionId }; + + // 2. 执行状态变更 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回变更结果或 404 + return result is null + ? ApiResponse.Error(StatusCodes.Status404NotFound, "订阅不存在") + : ApiResponse.Ok(result); + } + + /// + /// 批量延期订阅。 + /// + /// 批量延期命令。 + /// 取消标记。 + /// 批量延期结果。 + [HttpPost("batch-extend")] + [PermissionAuthorize("subscription:batch-extend")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> BatchExtend( + [FromBody, Required] BatchExtendSubscriptionsCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 批量发送续费提醒。 + /// + /// 批量发送提醒命令。 + /// 取消标记。 + /// 批量发送提醒结果。 + [HttpPost("batch-remind")] + [PermissionAuthorize("subscription:batch-remind")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> BatchRemind( + [FromBody, Required] BatchSendReminderCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs b/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs new file mode 100644 index 0000000..6a8c797 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs @@ -0,0 +1,80 @@ +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Entities; + +namespace TakeoutSaaS.Application.App.Billings; + +/// +/// 账单 DTO 映射助手。 +/// +internal static class BillingMapping +{ + /// + /// 将账单实体映射为账单 DTO。 + /// + /// 账单实体。 + /// 租户名称。 + /// 账单 DTO。 + public static BillDto ToDto(this TenantBillingStatement bill, string? tenantName = null) + => new() + { + Id = bill.Id, + TenantId = bill.TenantId, + TenantName = tenantName, + StatementNo = bill.StatementNo, + PeriodStart = bill.PeriodStart, + PeriodEnd = bill.PeriodEnd, + AmountDue = bill.AmountDue, + AmountPaid = bill.AmountPaid, + Status = bill.Status, + DueDate = bill.DueDate, + CreatedAt = bill.CreatedAt + }; + + /// + /// 将账单实体与支付记录映射为账单详情 DTO。 + /// + /// 账单实体。 + /// 支付记录列表。 + /// 租户名称。 + /// 账单详情 DTO。 + public static BillDetailDto ToDetailDto( + this TenantBillingStatement bill, + List payments, + string? tenantName = null) + => new() + { + Id = bill.Id, + TenantId = bill.TenantId, + TenantName = tenantName, + StatementNo = bill.StatementNo, + PeriodStart = bill.PeriodStart, + PeriodEnd = bill.PeriodEnd, + AmountDue = bill.AmountDue, + AmountPaid = bill.AmountPaid, + Status = bill.Status, + DueDate = bill.DueDate, + LineItemsJson = bill.LineItemsJson, + CreatedAt = bill.CreatedAt, + Payments = payments.Select(p => p.ToDto()).ToList() + }; + + /// + /// 将支付记录实体映射为支付 DTO。 + /// + /// 支付记录实体。 + /// 支付 DTO。 + public static PaymentDto ToDto(this TenantPayment payment) + => new() + { + Id = payment.Id, + BillingStatementId = payment.BillingStatementId, + Amount = payment.Amount, + Method = payment.Method, + Status = payment.Status, + TransactionNo = payment.TransactionNo, + ProofUrl = payment.ProofUrl, + PaidAt = payment.PaidAt, + Notes = payment.Notes, + CreatedAt = payment.CreatedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs new file mode 100644 index 0000000..d22ecde --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/CreateBillCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; + +namespace TakeoutSaaS.Application.App.Billings.Commands; + +/// +/// 创建账单命令。 +/// +public sealed record CreateBillCommand : IRequest +{ + /// + /// 租户 ID(雪花算法)。 + /// + public long TenantId { get; init; } + + /// + /// 应付金额。 + /// + public decimal AmountDue { get; init; } + + /// + /// 到期日(UTC)。 + /// + public DateTime DueDate { get; init; } + + /// + /// 备注信息。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs new file mode 100644 index 0000000..0d81407 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/RecordPaymentCommand.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.Billings.Commands; + +/// +/// 记录支付命令。 +/// +public sealed record RecordPaymentCommand : IRequest +{ + /// + /// 账单 ID(雪花算法)。 + /// + public long BillId { get; init; } + + /// + /// 支付金额。 + /// + public decimal Amount { get; init; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; init; } + + /// + /// 交易号。 + /// + public string? TransactionNo { get; init; } + + /// + /// 支付凭证 URL。 + /// + public string? ProofUrl { get; init; } + + /// + /// 备注信息。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs new file mode 100644 index 0000000..fcbff76 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Commands/UpdateBillStatusCommand.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.Billings.Commands; + +/// +/// 更新账单状态命令。 +/// +public sealed record UpdateBillStatusCommand : IRequest +{ + /// + /// 账单 ID(雪花算法)。 + /// + public long BillId { get; init; } + + /// + /// 新状态。 + /// + public TenantBillingStatus Status { get; init; } + + /// + /// 备注信息。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs new file mode 100644 index 0000000..36b6851 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDetailDto.cs @@ -0,0 +1,78 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Billings.Dto; + +/// +/// 账单详情 DTO。 +/// +public sealed record BillDetailDto +{ + /// + /// 账单 ID(雪花算法,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID(雪花算法,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 租户名称。 + /// + public string? TenantName { get; init; } + + /// + /// 账单编号。 + /// + public string StatementNo { get; init; } = string.Empty; + + /// + /// 计费周期开始时间(UTC)。 + /// + public DateTime PeriodStart { get; init; } + + /// + /// 计费周期结束时间(UTC)。 + /// + public DateTime PeriodEnd { get; init; } + + /// + /// 应付金额。 + /// + public decimal AmountDue { get; init; } + + /// + /// 已付金额。 + /// + public decimal AmountPaid { get; init; } + + /// + /// 账单状态。 + /// + public TenantBillingStatus Status { get; init; } + + /// + /// 到期日(UTC)。 + /// + public DateTime DueDate { get; init; } + + /// + /// 账单明细 JSON。 + /// + public string? LineItemsJson { get; init; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; init; } + + /// + /// 支付记录列表。 + /// + public List Payments { get; init; } = new(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs new file mode 100644 index 0000000..dfc76d1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/BillDto.cs @@ -0,0 +1,68 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Billings.Dto; + +/// +/// 账单 DTO。 +/// +public sealed record BillDto +{ + /// + /// 账单 ID(雪花算法,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID(雪花算法,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 租户名称。 + /// + public string? TenantName { get; init; } + + /// + /// 账单编号。 + /// + public string StatementNo { get; init; } = string.Empty; + + /// + /// 计费周期开始时间(UTC)。 + /// + public DateTime PeriodStart { get; init; } + + /// + /// 计费周期结束时间(UTC)。 + /// + public DateTime PeriodEnd { get; init; } + + /// + /// 应付金额。 + /// + public decimal AmountDue { get; init; } + + /// + /// 已付金额。 + /// + public decimal AmountPaid { get; init; } + + /// + /// 账单状态。 + /// + public TenantBillingStatus Status { get; init; } + + /// + /// 到期日(UTC)。 + /// + public DateTime DueDate { get; init; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs new file mode 100644 index 0000000..41e5784 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Dto/PaymentDto.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Billings.Dto; + +/// +/// 支付记录 DTO。 +/// +public sealed record PaymentDto +{ + /// + /// 支付记录 ID(雪花算法,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 账单 ID(雪花算法,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long BillingStatementId { get; init; } + + /// + /// 支付金额。 + /// + public decimal Amount { get; init; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; init; } + + /// + /// 交易号。 + /// + public string? TransactionNo { get; init; } + + /// + /// 支付凭证 URL。 + /// + public string? ProofUrl { get; init; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; init; } + + /// + /// 备注信息。 + /// + public string? Notes { get; init; } + + /// + /// 创建时间(UTC)。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs new file mode 100644 index 0000000..bbcf044 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/CreateBillCommandHandler.cs @@ -0,0 +1,61 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Commands; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Billings.Handlers; + +/// +/// 创建账单处理器。 +/// +public sealed class CreateBillCommandHandler( + ITenantBillingRepository billingRepository, + ITenantRepository tenantRepository, + IIdGenerator idGenerator) + : IRequestHandler +{ + /// + /// 处理创建账单请求。 + /// + /// 创建命令。 + /// 取消标记。 + /// 账单 DTO。 + public async Task Handle(CreateBillCommand request, CancellationToken cancellationToken) + { + // 1. 验证租户存在 + var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken); + if (tenant is null) + { + throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); + } + + // 2. 生成账单编号 + var statementNo = $"BILL-{DateTime.UtcNow:yyyyMMdd}-{idGenerator.NextId()}"; + + // 3. 构建账单实体 + var bill = new TenantBillingStatement + { + TenantId = request.TenantId, + StatementNo = statementNo, + PeriodStart = DateTime.UtcNow, + PeriodEnd = DateTime.UtcNow, + AmountDue = request.AmountDue, + AmountPaid = 0, + Status = TenantBillingStatus.Pending, + DueDate = request.DueDate, + LineItemsJson = request.Notes + }; + + // 4. 持久化账单 + await billingRepository.AddAsync(bill, cancellationToken); + await billingRepository.SaveChangesAsync(cancellationToken); + + // 5. 返回 DTO + return bill.ToDto(tenant.Name); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs new file mode 100644 index 0000000..c368fac --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillDetailQueryHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Application.App.Billings.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Billings.Handlers; + +/// +/// 获取账单详情查询处理器。 +/// +public sealed class GetBillDetailQueryHandler( + ITenantBillingRepository billingRepository, + ITenantPaymentRepository paymentRepository, + ITenantRepository tenantRepository) + : IRequestHandler +{ + /// + /// 处理获取账单详情请求。 + /// + /// 查询请求。 + /// 取消标记。 + /// 账单详情或 null。 + public async Task Handle(GetBillDetailQuery request, CancellationToken cancellationToken) + { + // 1. 查询账单 + var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken); + if (bill is null) + { + return null; + } + + // 2. 查询支付记录 + var payments = await paymentRepository.GetByBillingIdAsync(request.BillId, cancellationToken); + + // 3. 查询租户名称 + var tenant = await tenantRepository.FindByIdAsync(bill.TenantId, cancellationToken); + + // 4. 返回详情 DTO + return bill.ToDetailDto(payments.ToList(), tenant?.Name); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs new file mode 100644 index 0000000..f63d55a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetBillListQueryHandler.cs @@ -0,0 +1,53 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Application.App.Billings.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.Billings.Handlers; + +/// +/// 获取账单列表查询处理器。 +/// +public sealed class GetBillListQueryHandler( + ITenantBillingRepository billingRepository, + ITenantRepository tenantRepository) + : IRequestHandler> +{ + /// + /// 处理获取账单列表请求。 + /// + /// 查询请求。 + /// 取消标记。 + /// 分页账单列表。 + public async Task> Handle(GetBillListQuery request, CancellationToken cancellationToken) + { + // 1. 分页查询账单 + var (bills, total) = await billingRepository.SearchPagedAsync( + request.TenantId, + request.Status, + request.StartDate, + request.EndDate, + request.Keyword, + request.PageNumber, + request.PageSize, + cancellationToken); + + // 2. 无数据直接返回 + if (bills.Count == 0) + { + return new PagedResult([], request.PageNumber, request.PageSize, total); + } + + // 3. 批量查询租户信息 + var tenantIds = bills.Select(b => b.TenantId).Distinct().ToArray(); + var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken); + var tenantDict = tenants.ToDictionary(t => t.Id, t => t.Name); + + // 4. 映射 DTO + var result = bills.Select(b => b.ToDto(tenantDict.GetValueOrDefault(b.TenantId))).ToList(); + + // 5. 返回分页结果 + return new PagedResult(result, request.PageNumber, request.PageSize, total); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetTenantPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetTenantPaymentsQueryHandler.cs new file mode 100644 index 0000000..80d1538 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetTenantPaymentsQueryHandler.cs @@ -0,0 +1,28 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Application.App.Billings.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Billings.Handlers; + +/// +/// 获取租户支付记录查询处理器。 +/// +public sealed class GetTenantPaymentsQueryHandler(ITenantPaymentRepository paymentRepository) + : IRequestHandler> +{ + /// + /// 处理获取支付记录请求。 + /// + /// 查询请求。 + /// 取消标记。 + /// 支付记录列表。 + public async Task> Handle(GetTenantPaymentsQuery request, CancellationToken cancellationToken) + { + // 1. 查询支付记录 + var payments = await paymentRepository.GetByBillingIdAsync(request.BillId, cancellationToken); + + // 2. 映射并返回 DTO + return payments.Select(p => p.ToDto()).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs new file mode 100644 index 0000000..c993631 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/RecordPaymentCommandHandler.cs @@ -0,0 +1,66 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Commands; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Application.App.Billings.Handlers; + +/// +/// 记录支付处理器。 +/// +public sealed class RecordPaymentCommandHandler( + ITenantBillingRepository billingRepository, + ITenantPaymentRepository paymentRepository) + : IRequestHandler +{ + /// + /// 处理记录支付请求。 + /// + /// 记录支付命令。 + /// 取消标记。 + /// 支付 DTO。 + public async Task Handle(RecordPaymentCommand request, CancellationToken cancellationToken) + { + // 1. 查询账单 + var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken); + if (bill is null) + { + throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); + } + + // 2. 构建支付记录 + var payment = new TenantPayment + { + TenantId = bill.TenantId, + BillingStatementId = request.BillId, + Amount = request.Amount, + Method = request.Method, + Status = PaymentStatus.Success, + TransactionNo = request.TransactionNo, + ProofUrl = request.ProofUrl, + PaidAt = DateTime.UtcNow, + Notes = request.Notes + }; + + // 3. 更新账单已付金额 + bill.AmountPaid += request.Amount; + + // 4. 如果已付金额 >= 应付金额,标记为已支付 + if (bill.AmountPaid >= bill.AmountDue) + { + bill.Status = TenantBillingStatus.Paid; + } + + // 5. 持久化变更 + await paymentRepository.AddAsync(payment, cancellationToken); + await billingRepository.UpdateAsync(bill, cancellationToken); + await paymentRepository.SaveChangesAsync(cancellationToken); + + // 6. 返回 DTO + return payment.ToDto(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs new file mode 100644 index 0000000..9a6f33b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/UpdateBillStatusCommandHandler.cs @@ -0,0 +1,50 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Commands; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Application.App.Billings.Handlers; + +/// +/// 更新账单状态处理器。 +/// +public sealed class UpdateBillStatusCommandHandler( + ITenantBillingRepository billingRepository, + ITenantRepository tenantRepository) + : IRequestHandler +{ + /// + /// 处理更新账单状态请求。 + /// + /// 更新命令。 + /// 取消标记。 + /// 账单 DTO 或 null。 + public async Task Handle(UpdateBillStatusCommand request, CancellationToken cancellationToken) + { + // 1. 查询账单 + var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken); + if (bill is null) + { + return null; + } + + // 2. 更新状态 + bill.Status = request.Status; + if (!string.IsNullOrWhiteSpace(request.Notes)) + { + bill.LineItemsJson = request.Notes; + } + + // 3. 持久化变更 + await billingRepository.UpdateAsync(bill, cancellationToken); + await billingRepository.SaveChangesAsync(cancellationToken); + + // 4. 查询租户名称 + var tenant = await tenantRepository.FindByIdAsync(bill.TenantId, cancellationToken); + + // 5. 返回 DTO + return bill.ToDto(tenant?.Name); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs new file mode 100644 index 0000000..d155acd --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillDetailQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; + +namespace TakeoutSaaS.Application.App.Billings.Queries; + +/// +/// 获取账单详情查询。 +/// +public sealed record GetBillDetailQuery : IRequest +{ + /// + /// 账单 ID(雪花算法)。 + /// + public long BillId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs new file mode 100644 index 0000000..903c333 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetBillListQuery.cs @@ -0,0 +1,47 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.Billings.Queries; + +/// +/// 获取账单列表查询。 +/// +public sealed record GetBillListQuery : IRequest> +{ + /// + /// 页码(从 1 开始)。 + /// + public int PageNumber { get; init; } = 1; + + /// + /// 页大小。 + /// + public int PageSize { get; init; } = 20; + + /// + /// 租户 ID 筛选(可选)。 + /// + public long? TenantId { get; init; } + + /// + /// 状态筛选(可选)。 + /// + public TenantBillingStatus? Status { get; init; } + + /// + /// 开始日期筛选(可选)。 + /// + public DateTime? StartDate { get; init; } + + /// + /// 结束日期筛选(可选)。 + /// + public DateTime? EndDate { get; init; } + + /// + /// 搜索关键词(账单号或租户名)。 + /// + public string? Keyword { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs new file mode 100644 index 0000000..986ca41 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Billings/Queries/GetTenantPaymentsQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Billings.Dto; + +namespace TakeoutSaaS.Application.App.Billings.Queries; + +/// +/// 获取租户支付记录查询。 +/// +public sealed record GetTenantPaymentsQuery : IRequest> +{ + /// + /// 账单 ID(雪花算法)。 + /// + public long BillId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs new file mode 100644 index 0000000..7c8de9e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/CreateQuotaPackageCommand.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; + +/// +/// 创建配额包命令。 +/// +public sealed record CreateQuotaPackageCommand : IRequest +{ + /// + /// 配额包名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 配额数值。 + /// + public decimal QuotaValue { get; init; } + + /// + /// 价格。 + /// + public decimal Price { get; init; } + + /// + /// 是否上架。 + /// + public bool IsActive { get; init; } = true; + + /// + /// 排序。 + /// + public int SortOrder { get; init; } = 0; + + /// + /// 描述。 + /// + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs new file mode 100644 index 0000000..62c8f3f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/DeleteQuotaPackageCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; + +/// +/// 删除配额包命令。 +/// +public sealed record DeleteQuotaPackageCommand : IRequest +{ + /// + /// 配额包 ID。 + /// + public long QuotaPackageId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs new file mode 100644 index 0000000..3485036 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/PurchaseQuotaPackageCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; + +/// +/// 为租户购买配额包命令。 +/// +public sealed record PurchaseQuotaPackageCommand : IRequest +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 配额包 ID。 + /// + public long QuotaPackageId { get; init; } + + /// + /// 过期时间(可选)。 + /// + public DateTime? ExpiredAt { get; init; } + + /// + /// 备注。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs new file mode 100644 index 0000000..f6f43c3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageCommand.cs @@ -0,0 +1,51 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; + +/// +/// 更新配额包命令。 +/// +public sealed record UpdateQuotaPackageCommand : IRequest +{ + /// + /// 配额包 ID。 + /// + public long QuotaPackageId { get; init; } + + /// + /// 配额包名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 配额数值。 + /// + public decimal QuotaValue { get; init; } + + /// + /// 价格。 + /// + public decimal Price { get; init; } + + /// + /// 是否上架。 + /// + public bool IsActive { get; init; } = true; + + /// + /// 排序。 + /// + public int SortOrder { get; init; } = 0; + + /// + /// 描述。 + /// + public string? Description { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs new file mode 100644 index 0000000..4a0fdb0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Commands/UpdateQuotaPackageStatusCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Commands; + +/// +/// 更新配额包状态命令(上架/下架)。 +/// +public sealed record UpdateQuotaPackageStatusCommand : IRequest +{ + /// + /// 配额包 ID。 + /// + public long QuotaPackageId { get; init; } + + /// + /// 是否上架。 + /// + public bool IsActive { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs new file mode 100644 index 0000000..f710e8c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageDto.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; + +/// +/// 配额包 DTO。 +/// +public sealed record QuotaPackageDto +{ + /// + /// 配额包 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 配额包名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 配额数值。 + /// + public decimal QuotaValue { get; init; } + + /// + /// 价格。 + /// + public decimal Price { get; init; } + + /// + /// 是否上架。 + /// + public bool IsActive { get; init; } + + /// + /// 排序。 + /// + public int SortOrder { get; init; } + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } + + /// + /// 更新时间。 + /// + public DateTime? UpdatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs new file mode 100644 index 0000000..9dd8fdf --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/QuotaPackageListDto.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; + +/// +/// 配额包列表 DTO。 +/// +public sealed record QuotaPackageListDto +{ + /// + /// 配额包 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 配额包名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 配额数值。 + /// + public decimal QuotaValue { get; init; } + + /// + /// 价格。 + /// + public decimal Price { get; init; } + + /// + /// 是否上架。 + /// + public bool IsActive { get; init; } + + /// + /// 排序。 + /// + public int SortOrder { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs new file mode 100644 index 0000000..2ce1bb4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaPurchaseDto.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; + +/// +/// 租户配额购买记录 DTO。 +/// +public sealed record TenantQuotaPurchaseDto +{ + /// + /// 购买记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 配额包 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long QuotaPackageId { get; init; } + + /// + /// 配额包名称。 + /// + public string QuotaPackageName { get; init; } = string.Empty; + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 购买时的配额值。 + /// + public decimal QuotaValue { get; init; } + + /// + /// 购买价格。 + /// + public decimal Price { get; init; } + + /// + /// 购买时间。 + /// + public DateTime PurchasedAt { get; init; } + + /// + /// 过期时间(可选)。 + /// + public DateTime? ExpiredAt { get; init; } + + /// + /// 备注。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs new file mode 100644 index 0000000..a651fd8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Dto/TenantQuotaUsageDto.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Dto; + +/// +/// 租户配额使用情况 DTO。 +/// +public sealed record TenantQuotaUsageDto +{ + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 配额上限。 + /// + public decimal LimitValue { get; init; } + + /// + /// 已使用值。 + /// + public decimal UsedValue { get; init; } + + /// + /// 剩余值。 + /// + public decimal RemainingValue { get; init; } + + /// + /// 配额刷新周期。 + /// + public string? ResetCycle { get; init; } + + /// + /// 最近一次重置时间。 + /// + public DateTime? LastResetAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs new file mode 100644 index 0000000..28c9b3a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/CreateQuotaPackageCommandHandler.cs @@ -0,0 +1,54 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Commands; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 创建配额包命令处理器。 +/// +public sealed class CreateQuotaPackageCommandHandler( + IQuotaPackageRepository quotaPackageRepository, + IIdGenerator idGenerator) + : IRequestHandler +{ + /// + public async Task Handle(CreateQuotaPackageCommand request, CancellationToken cancellationToken) + { + // 1. 创建配额包实体 + var quotaPackage = new QuotaPackage + { + Id = idGenerator.NextId(), + Name = request.Name, + QuotaType = request.QuotaType, + QuotaValue = request.QuotaValue, + Price = request.Price, + IsActive = request.IsActive, + SortOrder = request.SortOrder, + Description = request.Description, + CreatedAt = DateTime.UtcNow + }; + + // 2. 保存到数据库 + await quotaPackageRepository.AddAsync(quotaPackage, cancellationToken); + await quotaPackageRepository.SaveChangesAsync(cancellationToken); + + // 3. 返回 DTO + return new QuotaPackageDto + { + Id = quotaPackage.Id, + Name = quotaPackage.Name, + QuotaType = quotaPackage.QuotaType, + QuotaValue = quotaPackage.QuotaValue, + Price = quotaPackage.Price, + IsActive = quotaPackage.IsActive, + SortOrder = quotaPackage.SortOrder, + Description = quotaPackage.Description, + CreatedAt = quotaPackage.CreatedAt, + UpdatedAt = quotaPackage.UpdatedAt + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs new file mode 100644 index 0000000..1180d7e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/DeleteQuotaPackageCommandHandler.cs @@ -0,0 +1,29 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Commands; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 删除配额包命令处理器。 +/// +public sealed class DeleteQuotaPackageCommandHandler(IQuotaPackageRepository quotaPackageRepository) + : IRequestHandler +{ + /// + public async Task Handle(DeleteQuotaPackageCommand request, CancellationToken cancellationToken) + { + // 1. 软删除配额包 + var deleted = await quotaPackageRepository.SoftDeleteAsync(request.QuotaPackageId, cancellationToken); + + if (!deleted) + { + return false; + } + + // 2. 保存变更 + await quotaPackageRepository.SaveChangesAsync(cancellationToken); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs new file mode 100644 index 0000000..a61d883 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetQuotaPackageListQueryHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Application.App.QuotaPackages.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 获取配额包列表查询处理器。 +/// +public sealed class GetQuotaPackageListQueryHandler(IQuotaPackageRepository quotaPackageRepository) + : IRequestHandler> +{ + /// + public async Task> Handle(GetQuotaPackageListQuery request, CancellationToken cancellationToken) + { + // 1. 分页查询 + var (items, total) = await quotaPackageRepository.SearchPagedAsync( + request.QuotaType, + request.IsActive, + request.Page, + request.PageSize, + cancellationToken); + + // 2. 映射为 DTO + var dtos = items.Select(x => new QuotaPackageListDto + { + Id = x.Id, + Name = x.Name, + QuotaType = x.QuotaType, + QuotaValue = x.QuotaValue, + Price = x.Price, + IsActive = x.IsActive, + SortOrder = x.SortOrder + }).ToList(); + + // 3. 返回分页结果 + return new PagedResult(dtos, request.Page, request.PageSize, total); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs new file mode 100644 index 0000000..1b232d3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaPurchasesQueryHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Application.App.QuotaPackages.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 获取租户配额购买记录查询处理器。 +/// +public sealed class GetTenantQuotaPurchasesQueryHandler(IQuotaPackageRepository quotaPackageRepository) + : IRequestHandler> +{ + /// + public async Task> Handle(GetTenantQuotaPurchasesQuery request, CancellationToken cancellationToken) + { + // 1. 分页查询购买记录 + var (items, total) = await quotaPackageRepository.GetPurchasesPagedAsync( + request.TenantId, + request.Page, + request.PageSize, + cancellationToken); + + // 2. 映射为 DTO + var dtos = items.Select(x => new TenantQuotaPurchaseDto + { + Id = x.Purchase.Id, + TenantId = x.Purchase.TenantId, + QuotaPackageId = x.Purchase.QuotaPackageId, + QuotaPackageName = x.Package.Name, + QuotaType = x.Package.QuotaType, + QuotaValue = x.Purchase.QuotaValue, + Price = x.Purchase.Price, + PurchasedAt = x.Purchase.PurchasedAt, + ExpiredAt = x.Purchase.ExpiredAt, + Notes = x.Purchase.Notes + }).ToList(); + + // 3. 返回分页结果 + return new PagedResult(dtos, request.Page, request.PageSize, total); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs new file mode 100644 index 0000000..ee246b6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/GetTenantQuotaUsageQueryHandler.cs @@ -0,0 +1,35 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Application.App.QuotaPackages.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 获取租户配额使用情况查询处理器。 +/// +public sealed class GetTenantQuotaUsageQueryHandler(IQuotaPackageRepository quotaPackageRepository) + : IRequestHandler> +{ + /// + public async Task> Handle(GetTenantQuotaUsageQuery request, CancellationToken cancellationToken) + { + // 1. 查询配额使用情况 + var items = await quotaPackageRepository.GetUsageByTenantAsync( + request.TenantId, + request.QuotaType, + cancellationToken); + + // 2. 映射为 DTO + return items.Select(x => new TenantQuotaUsageDto + { + TenantId = x.TenantId, + QuotaType = x.QuotaType, + LimitValue = x.LimitValue, + UsedValue = x.UsedValue, + RemainingValue = x.LimitValue - x.UsedValue, + ResetCycle = x.ResetCycle, + LastResetAt = x.LastResetAt + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs new file mode 100644 index 0000000..28c4e37 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/PurchaseQuotaPackageCommandHandler.cs @@ -0,0 +1,72 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Commands; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 购买配额包命令处理器。 +/// +public sealed class PurchaseQuotaPackageCommandHandler( + IQuotaPackageRepository quotaPackageRepository, + IIdGenerator idGenerator) + : IRequestHandler +{ + /// + public async Task Handle(PurchaseQuotaPackageCommand request, CancellationToken cancellationToken) + { + // 1. 查找配额包 + var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken); + + if (quotaPackage == null) + { + throw new InvalidOperationException("配额包不存在"); + } + + // 2. 创建购买记录 + var purchase = new TenantQuotaPackagePurchase + { + Id = idGenerator.NextId(), + TenantId = request.TenantId, + QuotaPackageId = request.QuotaPackageId, + QuotaValue = quotaPackage.QuotaValue, + Price = quotaPackage.Price, + PurchasedAt = DateTime.UtcNow, + ExpiredAt = request.ExpiredAt, + Notes = request.Notes, + CreatedAt = DateTime.UtcNow + }; + + // 3. 保存购买记录 + await quotaPackageRepository.AddPurchaseAsync(purchase, cancellationToken); + + // 4. 更新租户配额(根据配额类型更新对应配额) + var quotaUsage = await quotaPackageRepository.FindUsageAsync(request.TenantId, quotaPackage.QuotaType, cancellationToken); + + if (quotaUsage != null) + { + quotaUsage.LimitValue += quotaPackage.QuotaValue; + await quotaPackageRepository.UpdateUsageAsync(quotaUsage, cancellationToken); + } + + await quotaPackageRepository.SaveChangesAsync(cancellationToken); + + // 5. 返回 DTO + return new TenantQuotaPurchaseDto + { + Id = purchase.Id, + TenantId = purchase.TenantId, + QuotaPackageId = purchase.QuotaPackageId, + QuotaPackageName = quotaPackage.Name, + QuotaType = quotaPackage.QuotaType, + QuotaValue = purchase.QuotaValue, + Price = purchase.Price, + PurchasedAt = purchase.PurchasedAt, + ExpiredAt = purchase.ExpiredAt, + Notes = purchase.Notes + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs new file mode 100644 index 0000000..5e57207 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageCommandHandler.cs @@ -0,0 +1,54 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Commands; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 更新配额包命令处理器。 +/// +public sealed class UpdateQuotaPackageCommandHandler(IQuotaPackageRepository quotaPackageRepository) + : IRequestHandler +{ + /// + public async Task Handle(UpdateQuotaPackageCommand request, CancellationToken cancellationToken) + { + // 1. 查找配额包 + var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken); + + if (quotaPackage == null) + { + return null; + } + + // 2. 更新配额包 + quotaPackage.Name = request.Name; + quotaPackage.QuotaType = request.QuotaType; + quotaPackage.QuotaValue = request.QuotaValue; + quotaPackage.Price = request.Price; + quotaPackage.IsActive = request.IsActive; + quotaPackage.SortOrder = request.SortOrder; + quotaPackage.Description = request.Description; + quotaPackage.UpdatedAt = DateTime.UtcNow; + + // 3. 保存到数据库 + await quotaPackageRepository.UpdateAsync(quotaPackage, cancellationToken); + await quotaPackageRepository.SaveChangesAsync(cancellationToken); + + // 4. 返回 DTO + return new QuotaPackageDto + { + Id = quotaPackage.Id, + Name = quotaPackage.Name, + QuotaType = quotaPackage.QuotaType, + QuotaValue = quotaPackage.QuotaValue, + Price = quotaPackage.Price, + IsActive = quotaPackage.IsActive, + SortOrder = quotaPackage.SortOrder, + Description = quotaPackage.Description, + CreatedAt = quotaPackage.CreatedAt, + UpdatedAt = quotaPackage.UpdatedAt + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs new file mode 100644 index 0000000..e5ea40b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Handlers/UpdateQuotaPackageStatusCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Commands; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers; + +/// +/// 更新配额包状态命令处理器。 +/// +public sealed class UpdateQuotaPackageStatusCommandHandler(IQuotaPackageRepository quotaPackageRepository) + : IRequestHandler +{ + /// + public async Task Handle(UpdateQuotaPackageStatusCommand request, CancellationToken cancellationToken) + { + // 1. 查找配额包 + var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken); + + if (quotaPackage == null) + { + return false; + } + + // 2. 更新状态 + quotaPackage.IsActive = request.IsActive; + quotaPackage.UpdatedAt = DateTime.UtcNow; + + // 3. 保存到数据库 + await quotaPackageRepository.UpdateAsync(quotaPackage, cancellationToken); + await quotaPackageRepository.SaveChangesAsync(cancellationToken); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs new file mode 100644 index 0000000..538d1ef --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetQuotaPackageListQuery.cs @@ -0,0 +1,32 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Queries; + +/// +/// 获取配额包列表查询。 +/// +public sealed record GetQuotaPackageListQuery : IRequest> +{ + /// + /// 配额类型(可选筛选)。 + /// + public TenantQuotaType? QuotaType { get; init; } + + /// + /// 状态(可选筛选)。 + /// + public bool? IsActive { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页大小。 + /// + public int PageSize { get; init; } = 20; +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs new file mode 100644 index 0000000..56348e6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaPurchasesQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Queries; + +/// +/// 获取租户配额购买记录查询。 +/// +public sealed record GetTenantQuotaPurchasesQuery : IRequest> +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页大小。 + /// + public int PageSize { get; init; } = 20; +} diff --git a/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs new file mode 100644 index 0000000..f5b395c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/QuotaPackages/Queries/GetTenantQuotaUsageQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.QuotaPackages.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.QuotaPackages.Queries; + +/// +/// 获取租户配额使用情况查询。 +/// +public sealed record GetTenantQuotaUsageQuery : IRequest> +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 配额类型(可选筛选)。 + /// + public TenantQuotaType? QuotaType { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs new file mode 100644 index 0000000..c9a6d30 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/ExpiringSubscriptionDto.cs @@ -0,0 +1,49 @@ +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.Statistics.Dto; + +/// +/// 即将到期的订阅项。 +/// +public record ExpiringSubscriptionDto +{ + /// + /// 订阅ID。 + /// + public long Id { get; init; } + + /// + /// 租户ID。 + /// + public string TenantId { get; init; } = string.Empty; + + /// + /// 租户名称。 + /// + public string TenantName { get; init; } = string.Empty; + + /// + /// 套餐名称。 + /// + public string PackageName { get; init; } = string.Empty; + + /// + /// 订阅状态。 + /// + public SubscriptionStatus Status { get; init; } + + /// + /// 到期时间。 + /// + public DateTime EffectiveTo { get; init; } + + /// + /// 剩余天数。 + /// + public int DaysRemaining { get; init; } + + /// + /// 是否开启自动续费。 + /// + public bool AutoRenew { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs new file mode 100644 index 0000000..57f53c2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/QuotaUsageRankingDto.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.Statistics.Dto; + +/// +/// 配额使用排行。 +/// +public record QuotaUsageRankingDto +{ + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 排行列表。 + /// + public IReadOnlyList Rankings { get; init; } = Array.Empty(); +} + +/// +/// 配额使用排行项。 +/// +public record QuotaUsageRankItem +{ + /// + /// 租户ID。 + /// + public string TenantId { get; init; } = string.Empty; + + /// + /// 租户名称。 + /// + public string TenantName { get; init; } = string.Empty; + + /// + /// 已使用值。 + /// + public decimal UsedValue { get; init; } + + /// + /// 限制值。 + /// + public decimal LimitValue { get; init; } + + /// + /// 使用百分比。 + /// + public decimal UsagePercentage { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs new file mode 100644 index 0000000..ceb604f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/RevenueStatisticsDto.cs @@ -0,0 +1,53 @@ +namespace TakeoutSaaS.Application.App.Statistics.Dto; + +/// +/// 收入统计。 +/// +public record RevenueStatisticsDto +{ + /// + /// 总收入。 + /// + public decimal TotalRevenue { get; init; } + + /// + /// 本月收入。 + /// + public decimal MonthlyRevenue { get; init; } + + /// + /// 本季度收入。 + /// + public decimal QuarterlyRevenue { get; init; } + + /// + /// 月度收入明细。 + /// + public IReadOnlyList MonthlyDetails { get; init; } = Array.Empty(); +} + +/// +/// 月度收入项。 +/// +public record MonthlyRevenueItem +{ + /// + /// 年份。 + /// + public int Year { get; init; } + + /// + /// 月份。 + /// + public int Month { get; init; } + + /// + /// 收入金额。 + /// + public decimal Amount { get; init; } + + /// + /// 账单数量。 + /// + public int BillCount { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs new file mode 100644 index 0000000..9323faa --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Dto/SubscriptionOverviewDto.cs @@ -0,0 +1,42 @@ +namespace TakeoutSaaS.Application.App.Statistics.Dto; + +/// +/// 订阅概览。 +/// +public record SubscriptionOverviewDto +{ + /// + /// 活跃订阅总数。 + /// + public int TotalActive { get; init; } + + /// + /// 7天内到期数量。 + /// + public int ExpiringIn7Days { get; init; } + + /// + /// 3天内到期数量。 + /// + public int ExpiringIn3Days { get; init; } + + /// + /// 1天内到期数量。 + /// + public int ExpiringIn1Day { get; init; } + + /// + /// 已过期数量。 + /// + public int Expired { get; init; } + + /// + /// 待激活数量。 + /// + public int Pending { get; init; } + + /// + /// 已暂停数量。 + /// + public int Suspended { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs new file mode 100644 index 0000000..a913aac --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetExpiringSubscriptionsQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; +using TakeoutSaaS.Application.App.Statistics.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Statistics.Handlers; + +/// +/// 获取即将到期的订阅列表处理器。 +/// +public sealed class GetExpiringSubscriptionsQueryHandler(IStatisticsRepository statisticsRepository) + : IRequestHandler> +{ + /// + public async Task> Handle(GetExpiringSubscriptionsQuery request, CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + + // 查询即将到期的订阅 + var items = await statisticsRepository.GetExpiringSubscriptionsAsync( + request.DaysAhead, + request.OnlyWithoutAutoRenew, + cancellationToken); + + // 映射为 DTO + return items.Select(x => new ExpiringSubscriptionDto + { + Id = x.Subscription.Id, + TenantId = x.Subscription.TenantId.ToString(), + TenantName = x.TenantName, + PackageName = x.PackageName, + Status = x.Subscription.Status, + EffectiveTo = x.Subscription.EffectiveTo, + DaysRemaining = (int)(x.Subscription.EffectiveTo - now).TotalDays, + AutoRenew = x.Subscription.AutoRenew + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs new file mode 100644 index 0000000..b6fc116 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetQuotaUsageRankingQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; +using TakeoutSaaS.Application.App.Statistics.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Statistics.Handlers; + +/// +/// 获取配额使用排行处理器。 +/// +public sealed class GetQuotaUsageRankingQueryHandler(IStatisticsRepository statisticsRepository) + : IRequestHandler +{ + /// + public async Task Handle(GetQuotaUsageRankingQuery request, CancellationToken cancellationToken) + { + // 查询指定类型的配额使用排行 + var items = await statisticsRepository.GetQuotaUsageRankingAsync( + request.QuotaType, + request.TopN, + cancellationToken); + + // 映射为 DTO + var rankings = items.Select(x => new QuotaUsageRankItem + { + TenantId = x.TenantId.ToString(), + TenantName = x.TenantName, + UsedValue = x.UsedValue, + LimitValue = x.LimitValue, + UsagePercentage = x.UsagePercentage + }).ToList(); + + return new QuotaUsageRankingDto + { + QuotaType = request.QuotaType, + Rankings = rankings + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs new file mode 100644 index 0000000..55cee9f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetRevenueStatisticsQueryHandler.cs @@ -0,0 +1,71 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; +using TakeoutSaaS.Application.App.Statistics.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Statistics.Handlers; + +/// +/// 获取收入统计处理器。 +/// +public sealed class GetRevenueStatisticsQueryHandler(IStatisticsRepository statisticsRepository) + : IRequestHandler +{ + /// + public async Task Handle(GetRevenueStatisticsQuery request, CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + var currentMonth = new DateTime(now.Year, now.Month, 1); + var currentQuarter = GetQuarterStart(now); + var startMonth = currentMonth.AddMonths(-request.MonthsCount + 1); + + // 查询所有已付款的账单 + var bills = await statisticsRepository.GetPaidBillsAsync(cancellationToken); + + // 总收入 + var totalRevenue = bills.Sum(b => b.AmountPaid); + + // 本月收入 + var monthlyRevenue = bills + .Where(b => b.PeriodStart >= currentMonth) + .Sum(b => b.AmountPaid); + + // 本季度收入 + var quarterlyRevenue = bills + .Where(b => b.PeriodStart >= currentQuarter) + .Sum(b => b.AmountPaid); + + // 月度收入明细 + var monthlyDetails = bills + .Where(b => b.PeriodStart >= startMonth) + .GroupBy(b => new { b.PeriodStart.Year, b.PeriodStart.Month }) + .Select(g => new MonthlyRevenueItem + { + Year = g.Key.Year, + Month = g.Key.Month, + Amount = g.Sum(b => b.AmountPaid), + BillCount = g.Count() + }) + .OrderBy(m => m.Year) + .ThenBy(m => m.Month) + .ToList(); + + return new RevenueStatisticsDto + { + TotalRevenue = totalRevenue, + MonthlyRevenue = monthlyRevenue, + QuarterlyRevenue = quarterlyRevenue, + MonthlyDetails = monthlyDetails + }; + } + + /// + /// 获取季度开始时间。 + /// + private static DateTime GetQuarterStart(DateTime date) + { + var quarter = (date.Month - 1) / 3; + var quarterStartMonth = quarter * 3 + 1; + return new DateTime(date.Year, quarterStartMonth, 1); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs new file mode 100644 index 0000000..054044b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Handlers/GetSubscriptionOverviewQueryHandler.cs @@ -0,0 +1,49 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; +using TakeoutSaaS.Application.App.Statistics.Queries; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Statistics.Handlers; + +/// +/// 获取订阅概览统计处理器。 +/// +public sealed class GetSubscriptionOverviewQueryHandler(IStatisticsRepository statisticsRepository) + : IRequestHandler +{ + /// + public async Task Handle(GetSubscriptionOverviewQuery request, CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + var in7Days = now.AddDays(7); + var in3Days = now.AddDays(3); + var in1Day = now.AddDays(1); + + // 查询所有订阅 + var subscriptions = await statisticsRepository.GetAllSubscriptionsAsync(cancellationToken); + + // 统计各项数据 + var overview = new SubscriptionOverviewDto + { + TotalActive = subscriptions.Count(s => s.Status == SubscriptionStatus.Active), + ExpiringIn7Days = subscriptions.Count(s => + s.Status == SubscriptionStatus.Active && + s.EffectiveTo >= now && + s.EffectiveTo <= in7Days), + ExpiringIn3Days = subscriptions.Count(s => + s.Status == SubscriptionStatus.Active && + s.EffectiveTo >= now && + s.EffectiveTo <= in3Days), + ExpiringIn1Day = subscriptions.Count(s => + s.Status == SubscriptionStatus.Active && + s.EffectiveTo >= now && + s.EffectiveTo <= in1Day), + Expired = subscriptions.Count(s => s.Status == SubscriptionStatus.GracePeriod), + Pending = subscriptions.Count(s => s.Status == SubscriptionStatus.Pending), + Suspended = subscriptions.Count(s => s.Status == SubscriptionStatus.Suspended) + }; + + return overview; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs new file mode 100644 index 0000000..3fcd2e6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetExpiringSubscriptionsQuery.cs @@ -0,0 +1,20 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; + +namespace TakeoutSaaS.Application.App.Statistics.Queries; + +/// +/// 获取即将到期的订阅列表。 +/// +public sealed record GetExpiringSubscriptionsQuery : IRequest> +{ + /// + /// 筛选天数,默认7天内到期。 + /// + public int DaysAhead { get; init; } = 7; + + /// + /// 是否只返回未开启自动续费的订阅。 + /// + public bool OnlyWithoutAutoRenew { get; init; } = false; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs new file mode 100644 index 0000000..af8d0cc --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetQuotaUsageRankingQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.Statistics.Queries; + +/// +/// 获取配额使用排行。 +/// +public sealed record GetQuotaUsageRankingQuery : IRequest +{ + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 返回前N条记录,默认前10。 + /// + public int TopN { get; init; } = 10; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs new file mode 100644 index 0000000..bda07b6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetRevenueStatisticsQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; + +namespace TakeoutSaaS.Application.App.Statistics.Queries; + +/// +/// 获取收入统计。 +/// +public sealed record GetRevenueStatisticsQuery : IRequest +{ + /// + /// 统计月份数量,默认12个月。 + /// + public int MonthsCount { get; init; } = 12; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs new file mode 100644 index 0000000..68b356a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Statistics/Queries/GetSubscriptionOverviewQuery.cs @@ -0,0 +1,11 @@ +using MediatR; +using TakeoutSaaS.Application.App.Statistics.Dto; + +namespace TakeoutSaaS.Application.App.Statistics.Queries; + +/// +/// 获取订阅概览统计。 +/// +public sealed record GetSubscriptionOverviewQuery : IRequest +{ +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs new file mode 100644 index 0000000..0f2954b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchExtendSubscriptionsCommand.cs @@ -0,0 +1,72 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.App.Subscriptions.Commands; + +/// +/// 批量延期订阅命令。 +/// +public sealed record BatchExtendSubscriptionsCommand : IRequest +{ + /// + /// 订阅ID列表。 + /// + [Required] + [MinLength(1, ErrorMessage = "至少需要选择一个订阅")] + public IReadOnlyList SubscriptionIds { get; init; } = Array.Empty(); + + /// + /// 延期时长(天)。 + /// + [Range(1, 3650, ErrorMessage = "延期天数必须在1-3650天之间")] + public int? DurationDays { get; init; } + + /// + /// 延期时长(月)。 + /// + [Range(1, 120, ErrorMessage = "延期月数必须在1-120月之间")] + public int? DurationMonths { get; init; } + + /// + /// 备注信息。 + /// + [MaxLength(500)] + public string? Notes { get; init; } +} + +/// +/// 批量延期结果。 +/// +public record BatchExtendResult +{ + /// + /// 成功数量。 + /// + public int SuccessCount { get; init; } + + /// + /// 失败数量。 + /// + public int FailureCount { get; init; } + + /// + /// 失败详情列表。 + /// + public IReadOnlyList Failures { get; init; } = Array.Empty(); +} + +/// +/// 批量操作失败项。 +/// +public record BatchFailureItem +{ + /// + /// 订阅ID。 + /// + public long SubscriptionId { get; init; } + + /// + /// 失败原因。 + /// + public string Reason { get; init; } = string.Empty; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs new file mode 100644 index 0000000..8c41be6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/BatchSendReminderCommand.cs @@ -0,0 +1,45 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace TakeoutSaaS.Application.App.Subscriptions.Commands; + +/// +/// 批量发送续费提醒命令。 +/// +public sealed record BatchSendReminderCommand : IRequest +{ + /// + /// 订阅ID列表。 + /// + [Required] + [MinLength(1, ErrorMessage = "至少需要选择一个订阅")] + public IReadOnlyList SubscriptionIds { get; init; } = Array.Empty(); + + /// + /// 提醒内容。 + /// + [Required(ErrorMessage = "提醒内容不能为空")] + [MaxLength(1000, ErrorMessage = "提醒内容不能超过1000字符")] + public string ReminderContent { get; init; } = string.Empty; +} + +/// +/// 批量发送提醒结果。 +/// +public record BatchSendReminderResult +{ + /// + /// 成功发送数量。 + /// + public int SuccessCount { get; init; } + + /// + /// 发送失败数量。 + /// + public int FailureCount { get; init; } + + /// + /// 失败详情列表。 + /// + public IReadOnlyList Failures { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs new file mode 100644 index 0000000..3b85ae5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ChangeSubscriptionPlanCommand.cs @@ -0,0 +1,34 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Subscriptions.Dto; + +namespace TakeoutSaaS.Application.App.Subscriptions.Commands; + +/// +/// 变更套餐命令。 +/// +public sealed record ChangeSubscriptionPlanCommand : IRequest +{ + /// + /// 订阅 ID(从路由参数绑定)。 + /// + [Required] + public long SubscriptionId { get; init; } + + /// + /// 目标套餐 ID。 + /// + [Required] + public long TargetPackageId { get; init; } + + /// + /// 是否立即生效,否则在下周期生效。 + /// + public bool Immediate { get; init; } + + /// + /// 备注信息。 + /// + [MaxLength(500)] + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs new file mode 100644 index 0000000..6034e01 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/ExtendSubscriptionCommand.cs @@ -0,0 +1,29 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Subscriptions.Dto; + +namespace TakeoutSaaS.Application.App.Subscriptions.Commands; + +/// +/// 延期订阅命令。 +/// +public sealed record ExtendSubscriptionCommand : IRequest +{ + /// + /// 订阅 ID(从路由参数绑定)。 + /// + [Required] + public long SubscriptionId { get; init; } + + /// + /// 延期时长(月)。 + /// + [Range(1, 120)] + public int DurationMonths { get; init; } + + /// + /// 备注信息。 + /// + [MaxLength(500)] + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs new file mode 100644 index 0000000..dd16fc5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionCommand.cs @@ -0,0 +1,28 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Subscriptions.Dto; + +namespace TakeoutSaaS.Application.App.Subscriptions.Commands; + +/// +/// 更新订阅基础信息命令。 +/// +public sealed record UpdateSubscriptionCommand : IRequest +{ + /// + /// 订阅 ID(从路由参数绑定)。 + /// + [Required] + public long SubscriptionId { get; init; } + + /// + /// 是否自动续费。 + /// + public bool? AutoRenew { get; init; } + + /// + /// 运营备注信息。 + /// + [MaxLength(500)] + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs new file mode 100644 index 0000000..4e8240b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Commands/UpdateSubscriptionStatusCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Application.App.Subscriptions.Commands; + +/// +/// 更新订阅状态命令。 +/// +public sealed record UpdateSubscriptionStatusCommand : IRequest +{ + /// + /// 订阅 ID(从路由参数绑定)。 + /// + [Required] + public long SubscriptionId { get; init; } + + /// + /// 目标状态。 + /// + [Required] + public SubscriptionStatus Status { get; init; } + + /// + /// 备注信息。 + /// + [MaxLength(500)] + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs new file mode 100644 index 0000000..0f36ea6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/QuotaUsageDto.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Subscriptions.Dto; + +/// +/// 配额使用 DTO。 +/// +public sealed record QuotaUsageDto +{ + /// + /// 配额 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; init; } + + /// + /// 配额上限。 + /// + public decimal LimitValue { get; init; } + + /// + /// 已使用量。 + /// + public decimal UsedValue { get; init; } + + /// + /// 使用率(百分比)。 + /// + public decimal UsagePercentage => LimitValue > 0 ? Math.Round(UsedValue / LimitValue * 100, 2) : 0; + + /// + /// 剩余额度。 + /// + public decimal RemainingValue => Math.Max(0, LimitValue - UsedValue); + + /// + /// 重置周期描述。 + /// + public string? ResetCycle { get; init; } + + /// + /// 最近一次重置时间。 + /// + public DateTime? LastResetAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs new file mode 100644 index 0000000..2a91720 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionDetailDto.cs @@ -0,0 +1,106 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Application.App.Tenants.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Subscriptions.Dto; + +/// +/// 订阅详情 DTO。 +/// +public sealed record SubscriptionDetailDto +{ + /// + /// 订阅 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 租户名称。 + /// + public string TenantName { get; init; } = string.Empty; + + /// + /// 租户编码。 + /// + public string TenantCode { get; init; } = string.Empty; + + /// + /// 当前套餐 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantPackageId { get; init; } + + /// + /// 当前套餐信息。 + /// + public TenantPackageDto? Package { get; init; } + + /// + /// 排期套餐 ID(下周期生效)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? ScheduledPackageId { get; init; } + + /// + /// 排期套餐信息。 + /// + public TenantPackageDto? ScheduledPackage { get; init; } + + /// + /// 订阅状态。 + /// + public SubscriptionStatus Status { get; init; } + + /// + /// 生效时间(UTC)。 + /// + public DateTime EffectiveFrom { get; init; } + + /// + /// 到期时间(UTC)。 + /// + public DateTime EffectiveTo { get; init; } + + /// + /// 下次计费时间。 + /// + public DateTime? NextBillingDate { get; init; } + + /// + /// 是否自动续费。 + /// + public bool AutoRenew { get; init; } + + /// + /// 备注信息。 + /// + public string? Notes { get; init; } + + /// + /// 配额使用情况列表。 + /// + public IReadOnlyList QuotaUsages { get; init; } = []; + + /// + /// 订阅变更历史列表。 + /// + public IReadOnlyList ChangeHistory { get; init; } = []; + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } + + /// + /// 更新时间。 + /// + public DateTime? UpdatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs new file mode 100644 index 0000000..c513263 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionHistoryDto.cs @@ -0,0 +1,80 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Subscriptions.Dto; + +/// +/// 订阅变更历史 DTO。 +/// +public sealed record SubscriptionHistoryDto +{ + /// + /// 历史记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订阅 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantSubscriptionId { get; init; } + + /// + /// 原套餐 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long FromPackageId { get; init; } + + /// + /// 原套餐名称。 + /// + public string FromPackageName { get; init; } = string.Empty; + + /// + /// 新套餐 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long ToPackageId { get; init; } + + /// + /// 新套餐名称。 + /// + public string ToPackageName { get; init; } = string.Empty; + + /// + /// 变更类型。 + /// + public SubscriptionChangeType ChangeType { get; init; } + + /// + /// 生效时间。 + /// + public DateTime EffectiveFrom { get; init; } + + /// + /// 到期时间。 + /// + public DateTime EffectiveTo { get; init; } + + /// + /// 相关费用。 + /// + public decimal? Amount { get; init; } + + /// + /// 币种。 + /// + public string? Currency { get; init; } + + /// + /// 备注。 + /// + public string? Notes { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs new file mode 100644 index 0000000..1cb4d05 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Dto/SubscriptionListDto.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Subscriptions.Dto; + +/// +/// 订阅列表 DTO。 +/// +public sealed record SubscriptionListDto +{ + /// + /// 订阅 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 租户名称。 + /// + public string TenantName { get; init; } = string.Empty; + + /// + /// 租户编码。 + /// + public string TenantCode { get; init; } = string.Empty; + + /// + /// 当前套餐 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantPackageId { get; init; } + + /// + /// 当前套餐名称。 + /// + public string PackageName { get; init; } = string.Empty; + + /// + /// 排期套餐 ID(下周期生效)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? ScheduledPackageId { get; init; } + + /// + /// 排期套餐名称。 + /// + public string? ScheduledPackageName { get; init; } + + /// + /// 订阅状态。 + /// + public SubscriptionStatus Status { get; init; } + + /// + /// 生效时间(UTC)。 + /// + public DateTime EffectiveFrom { get; init; } + + /// + /// 到期时间(UTC)。 + /// + public DateTime EffectiveTo { get; init; } + + /// + /// 下次计费时间。 + /// + public DateTime? NextBillingDate { get; init; } + + /// + /// 是否自动续费。 + /// + public bool AutoRenew { get; init; } + + /// + /// 备注信息。 + /// + public string? Notes { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } + + /// + /// 更新时间。 + /// + public DateTime? UpdatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs new file mode 100644 index 0000000..3a63b63 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs @@ -0,0 +1,133 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 批量延期订阅命令处理器。 +/// +public sealed class BatchExtendSubscriptionsCommandHandler( + ISubscriptionRepository subscriptionRepository, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken) + { + var successCount = 0; + var failures = new List(); + + // 验证参数 + if (!request.DurationDays.HasValue && !request.DurationMonths.HasValue) + { + throw new InvalidOperationException("必须指定延期天数或延期月数"); + } + + // 计算延期时间 + var extendDays = request.DurationDays ?? 0; + var extendMonths = request.DurationMonths ?? 0; + + // 查询所有订阅 + var subscriptions = await subscriptionRepository.FindByIdsAsync(request.SubscriptionIds, cancellationToken); + + foreach (var subscriptionId in request.SubscriptionIds) + { + try + { + var subscription = subscriptions.FirstOrDefault(s => s.Id == subscriptionId); + if (subscription == null) + { + failures.Add(new BatchFailureItem + { + SubscriptionId = subscriptionId, + Reason = "订阅不存在" + }); + continue; + } + + // 记录原始到期时间 + var originalEffectiveTo = subscription.EffectiveTo; + + // 计算新的到期时间 + var newEffectiveTo = subscription.EffectiveTo; + if (extendMonths > 0) + { + newEffectiveTo = newEffectiveTo.AddMonths(extendMonths); + } + if (extendDays > 0) + { + newEffectiveTo = newEffectiveTo.AddDays(extendDays); + } + + subscription.EffectiveTo = newEffectiveTo; + + // 更新备注 + if (!string.IsNullOrWhiteSpace(request.Notes)) + { + subscription.Notes = request.Notes; + } + + // 记录变更历史 + var history = new TenantSubscriptionHistory + { + Id = idGenerator.NextId(), + TenantId = subscription.TenantId, + TenantSubscriptionId = subscription.Id, + FromPackageId = subscription.TenantPackageId, + ToPackageId = subscription.TenantPackageId, + ChangeType = SubscriptionChangeType.Renew, + EffectiveFrom = originalEffectiveTo, + EffectiveTo = newEffectiveTo, + Amount = null, + Currency = null, + Notes = request.Notes ?? $"批量延期: {(extendMonths > 0 ? $"{extendMonths}个月" : "")}{(extendDays > 0 ? $"{extendDays}天" : "")}" + }; + + await subscriptionRepository.AddHistoryAsync(history, cancellationToken); + await subscriptionRepository.UpdateAsync(subscription, cancellationToken); + successCount++; + } + catch (Exception ex) + { + logger.LogError(ex, "批量延期订阅失败: SubscriptionId={SubscriptionId}", subscriptionId); + failures.Add(new BatchFailureItem + { + SubscriptionId = subscriptionId, + Reason = $"处理失败: {ex.Message}" + }); + } + } + + // 记录操作日志 + var operationLog = new OperationLog + { + Id = idGenerator.NextId(), + OperationType = "BatchExtend", + TargetType = "Subscription", + TargetIds = JsonSerializer.Serialize(request.SubscriptionIds), + Parameters = JsonSerializer.Serialize(new { request.DurationDays, request.DurationMonths, request.Notes }), + Result = JsonSerializer.Serialize(new { SuccessCount = successCount, FailureCount = failures.Count }), + Success = failures.Count == 0, + CreatedAt = DateTime.UtcNow + }; + + await subscriptionRepository.AddOperationLogAsync(operationLog, cancellationToken); + + // 保存所有更改 + await subscriptionRepository.SaveChangesAsync(cancellationToken); + + return new BatchExtendResult + { + SuccessCount = successCount, + FailureCount = failures.Count, + Failures = failures + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs new file mode 100644 index 0000000..95f36b1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs @@ -0,0 +1,104 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 批量发送续费提醒命令处理器。 +/// +public sealed class BatchSendReminderCommandHandler( + ISubscriptionRepository subscriptionRepository, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(BatchSendReminderCommand request, CancellationToken cancellationToken) + { + var successCount = 0; + var failures = new List(); + + // 查询所有订阅及租户信息 + var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync( + request.SubscriptionIds, + cancellationToken); + + foreach (var subscriptionId in request.SubscriptionIds) + { + try + { + var item = subscriptions.FirstOrDefault(s => s.Subscription.Id == subscriptionId); + if (item == null) + { + failures.Add(new BatchFailureItem + { + SubscriptionId = subscriptionId, + Reason = "订阅不存在" + }); + continue; + } + + // 创建通知记录 + var notification = new TenantNotification + { + Id = idGenerator.NextId(), + TenantId = item.Subscription.TenantId, + Title = "续费提醒", + Message = request.ReminderContent, + Severity = TenantNotificationSeverity.Warning, + Channel = TenantNotificationChannel.InApp, + SentAt = DateTime.UtcNow, + ReadAt = null, + CreatedAt = DateTime.UtcNow + }; + + await subscriptionRepository.AddNotificationAsync(notification, cancellationToken); + successCount++; + + logger.LogInformation( + "发送续费提醒: SubscriptionId={SubscriptionId}, TenantId={TenantId}, TenantName={TenantName}", + subscriptionId, item.Subscription.TenantId, item.Tenant.Name); + } + catch (Exception ex) + { + logger.LogError(ex, "发送续费提醒失败: SubscriptionId={SubscriptionId}", subscriptionId); + failures.Add(new BatchFailureItem + { + SubscriptionId = subscriptionId, + Reason = $"发送失败: {ex.Message}" + }); + } + } + + // 记录操作日志 + var operationLog = new OperationLog + { + Id = idGenerator.NextId(), + OperationType = "BatchRemind", + TargetType = "Subscription", + TargetIds = JsonSerializer.Serialize(request.SubscriptionIds), + Parameters = JsonSerializer.Serialize(new { request.ReminderContent }), + Result = JsonSerializer.Serialize(new { SuccessCount = successCount, FailureCount = failures.Count }), + Success = failures.Count == 0, + CreatedAt = DateTime.UtcNow + }; + + await subscriptionRepository.AddOperationLogAsync(operationLog, cancellationToken); + + // 保存所有更改 + await subscriptionRepository.SaveChangesAsync(cancellationToken); + + return new BatchSendReminderResult + { + SuccessCount = successCount, + FailureCount = failures.Count, + Failures = failures + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs new file mode 100644 index 0000000..5a238a2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs @@ -0,0 +1,93 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.Queries; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 变更套餐命令处理器。 +/// +public sealed class ChangeSubscriptionPlanCommandHandler( + ISubscriptionRepository subscriptionRepository, + IIdGenerator idGenerator, + IMediator mediator) + : IRequestHandler +{ + /// + public async Task Handle(ChangeSubscriptionPlanCommand request, CancellationToken cancellationToken) + { + // 1. 查询订阅 + var subscription = await subscriptionRepository.FindByIdAsync(request.SubscriptionId, cancellationToken); + + if (subscription == null) + { + return null; + } + + // 2. 记录原套餐ID + var previousPackageId = subscription.TenantPackageId; + + // 3. 根据是否立即生效更新订阅 + if (request.Immediate) + { + // 立即生效:直接更新当前套餐 + subscription.TenantPackageId = request.TargetPackageId; + subscription.ScheduledPackageId = null; + } + else + { + // 下周期生效:设置排期套餐 + subscription.ScheduledPackageId = request.TargetPackageId; + } + + // 4. 更新备注 + if (!string.IsNullOrWhiteSpace(request.Notes)) + { + subscription.Notes = request.Notes; + } + + // 5. 判断变更类型(升级或降级) + var fromPackage = await subscriptionRepository.FindPackageByIdAsync(previousPackageId, cancellationToken); + var toPackage = await subscriptionRepository.FindPackageByIdAsync(request.TargetPackageId, cancellationToken); + + var changeType = SubscriptionChangeType.Upgrade; + if (fromPackage != null && toPackage != null) + { + // 简单根据价格判断升降级 + if (toPackage.MonthlyPrice < fromPackage.MonthlyPrice) + { + changeType = SubscriptionChangeType.Downgrade; + } + } + + // 6. 记录变更历史 + var history = new TenantSubscriptionHistory + { + Id = idGenerator.NextId(), + TenantId = subscription.TenantId, + TenantSubscriptionId = subscription.Id, + FromPackageId = previousPackageId, + ToPackageId = request.TargetPackageId, + ChangeType = changeType, + EffectiveFrom = request.Immediate ? DateTime.UtcNow : subscription.EffectiveTo, + EffectiveTo = subscription.EffectiveTo, + Amount = null, + Currency = null, + Notes = request.Notes ?? (request.Immediate ? "套餐立即变更" : "套餐排期变更") + }; + + await subscriptionRepository.AddHistoryAsync(history, cancellationToken); + + // 7. 保存更改 + await subscriptionRepository.UpdateAsync(subscription, cancellationToken); + await subscriptionRepository.SaveChangesAsync(cancellationToken); + + // 8. 返回更新后的详情 + return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs new file mode 100644 index 0000000..98fc903 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs @@ -0,0 +1,67 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.Queries; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 延期订阅命令处理器。 +/// +public sealed class ExtendSubscriptionCommandHandler( + ISubscriptionRepository subscriptionRepository, + IIdGenerator idGenerator, + IMediator mediator) + : IRequestHandler +{ + /// + public async Task Handle(ExtendSubscriptionCommand request, CancellationToken cancellationToken) + { + // 1. 查询订阅 + var subscription = await subscriptionRepository.FindByIdAsync(request.SubscriptionId, cancellationToken); + + if (subscription == null) + { + return null; + } + + // 2. 计算新的到期时间(从当前到期时间延长) + var originalEffectiveTo = subscription.EffectiveTo; + subscription.EffectiveTo = subscription.EffectiveTo.AddMonths(request.DurationMonths); + + // 3. 更新备注 + if (!string.IsNullOrWhiteSpace(request.Notes)) + { + subscription.Notes = request.Notes; + } + + // 4. 记录变更历史(使用 Renew 类型表示延期) + var history = new TenantSubscriptionHistory + { + Id = idGenerator.NextId(), + TenantId = subscription.TenantId, + TenantSubscriptionId = subscription.Id, + FromPackageId = subscription.TenantPackageId, + ToPackageId = subscription.TenantPackageId, + ChangeType = SubscriptionChangeType.Renew, + EffectiveFrom = originalEffectiveTo, + EffectiveTo = subscription.EffectiveTo, + Amount = null, + Currency = null, + Notes = request.Notes ?? $"延期 {request.DurationMonths} 个月" + }; + + await subscriptionRepository.AddHistoryAsync(history, cancellationToken); + + // 5. 保存更改 + await subscriptionRepository.UpdateAsync(subscription, cancellationToken); + await subscriptionRepository.SaveChangesAsync(cancellationToken); + + // 6. 返回更新后的详情 + return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs new file mode 100644 index 0000000..6bb13ab --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs @@ -0,0 +1,84 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.Queries; +using TakeoutSaaS.Application.App.Tenants; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 订阅详情查询处理器。 +/// +public sealed class GetSubscriptionDetailQueryHandler(ISubscriptionRepository subscriptionRepository) + : IRequestHandler +{ + /// + public async Task Handle(GetSubscriptionDetailQuery request, CancellationToken cancellationToken) + { + // 1. 查询订阅基础信息 + var detail = await subscriptionRepository.GetDetailAsync(request.SubscriptionId, cancellationToken); + + if (detail == null) + { + return null; + } + + // 2. 查询配额使用情况 + var quotaUsages = await subscriptionRepository.GetQuotaUsagesAsync( + detail.Subscription.TenantId, + cancellationToken); + + var quotaUsageDtos = quotaUsages.Select(q => new QuotaUsageDto + { + Id = q.Id, + QuotaType = q.QuotaType, + LimitValue = q.LimitValue, + UsedValue = q.UsedValue, + ResetCycle = q.ResetCycle, + LastResetAt = q.LastResetAt + }).ToList(); + + // 3. 查询订阅变更历史(关联套餐信息) + var histories = await subscriptionRepository.GetHistoryAsync(request.SubscriptionId, cancellationToken); + + var historyDtos = histories.Select(h => new SubscriptionHistoryDto + { + Id = h.History.Id, + TenantSubscriptionId = h.History.TenantSubscriptionId, + FromPackageId = h.History.FromPackageId, + FromPackageName = h.FromPackageName, + ToPackageId = h.History.ToPackageId, + ToPackageName = h.ToPackageName, + ChangeType = h.History.ChangeType, + EffectiveFrom = h.History.EffectiveFrom, + EffectiveTo = h.History.EffectiveTo, + Amount = h.History.Amount, + Currency = h.History.Currency, + Notes = h.History.Notes, + CreatedAt = h.History.CreatedAt + }).ToList(); + + // 4. 构建返回结果 + return new SubscriptionDetailDto + { + Id = detail.Subscription.Id, + TenantId = detail.Subscription.TenantId, + TenantName = detail.TenantName, + TenantCode = detail.TenantCode, + TenantPackageId = detail.Subscription.TenantPackageId, + Package = detail.Package?.ToDto(), + ScheduledPackageId = detail.Subscription.ScheduledPackageId, + ScheduledPackage = detail.ScheduledPackage?.ToDto(), + Status = detail.Subscription.Status, + EffectiveFrom = detail.Subscription.EffectiveFrom, + EffectiveTo = detail.Subscription.EffectiveTo, + NextBillingDate = detail.Subscription.NextBillingDate, + AutoRenew = detail.Subscription.AutoRenew, + Notes = detail.Subscription.Notes, + QuotaUsages = quotaUsageDtos, + ChangeHistory = historyDtos, + CreatedAt = detail.Subscription.CreatedAt, + UpdatedAt = detail.Subscription.UpdatedAt + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs new file mode 100644 index 0000000..75146b7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs @@ -0,0 +1,58 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 订阅分页查询处理器。 +/// +public sealed class GetSubscriptionListQueryHandler(ISubscriptionRepository subscriptionRepository) + : IRequestHandler> +{ + /// + public async Task> Handle(GetSubscriptionListQuery request, CancellationToken cancellationToken) + { + // 1. 构建查询过滤条件 + var filter = new SubscriptionSearchFilter + { + Status = request.Status, + TenantPackageId = request.TenantPackageId, + TenantId = request.TenantId, + TenantKeyword = request.TenantKeyword, + ExpiringWithinDays = request.ExpiringWithinDays, + AutoRenew = request.AutoRenew, + Page = request.Page, + PageSize = request.PageSize + }; + + // 2. 执行分页查询 + var (items, total) = await subscriptionRepository.SearchPagedAsync(filter, cancellationToken); + + // 3. 映射为 DTO + var dtos = items.Select(x => new SubscriptionListDto + { + Id = x.Subscription.Id, + TenantId = x.Subscription.TenantId, + TenantName = x.TenantName, + TenantCode = x.TenantCode, + TenantPackageId = x.Subscription.TenantPackageId, + PackageName = x.PackageName, + ScheduledPackageId = x.Subscription.ScheduledPackageId, + ScheduledPackageName = x.ScheduledPackageName, + Status = x.Subscription.Status, + EffectiveFrom = x.Subscription.EffectiveFrom, + EffectiveTo = x.Subscription.EffectiveTo, + NextBillingDate = x.Subscription.NextBillingDate, + AutoRenew = x.Subscription.AutoRenew, + Notes = x.Subscription.Notes, + CreatedAt = x.Subscription.CreatedAt, + UpdatedAt = x.Subscription.UpdatedAt + }).ToList(); + + // 4. 返回分页结果 + return new PagedResult(dtos, request.Page, request.PageSize, total); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs new file mode 100644 index 0000000..a3d531f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 更新订阅基础信息命令处理器。 +/// +public sealed class UpdateSubscriptionCommandHandler( + ISubscriptionRepository subscriptionRepository, + IMediator mediator) + : IRequestHandler +{ + /// + public async Task Handle(UpdateSubscriptionCommand request, CancellationToken cancellationToken) + { + // 1. 查询订阅 + var subscription = await subscriptionRepository.FindByIdAsync(request.SubscriptionId, cancellationToken); + + if (subscription == null) + { + return null; + } + + // 2. 更新字段 + if (request.AutoRenew.HasValue) + { + subscription.AutoRenew = request.AutoRenew.Value; + } + + if (request.Notes != null) + { + subscription.Notes = request.Notes; + } + + // 3. 保存更改 + await subscriptionRepository.UpdateAsync(subscription, cancellationToken); + await subscriptionRepository.SaveChangesAsync(cancellationToken); + + // 4. 返回更新后的详情 + return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs new file mode 100644 index 0000000..b75e50d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Application.App.Subscriptions.Queries; +using TakeoutSaaS.Domain.Tenants.Repositories; + +namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; + +/// +/// 更新订阅状态命令处理器。 +/// +public sealed class UpdateSubscriptionStatusCommandHandler( + ISubscriptionRepository subscriptionRepository, + IMediator mediator) + : IRequestHandler +{ + /// + public async Task Handle(UpdateSubscriptionStatusCommand request, CancellationToken cancellationToken) + { + // 1. 查询订阅 + var subscription = await subscriptionRepository.FindByIdAsync(request.SubscriptionId, cancellationToken); + + if (subscription == null) + { + return null; + } + + // 2. 更新状态 + subscription.Status = request.Status; + + // 3. 更新备注 + if (!string.IsNullOrWhiteSpace(request.Notes)) + { + subscription.Notes = request.Notes; + } + + // 4. 保存更改 + await subscriptionRepository.UpdateAsync(subscription, cancellationToken); + await subscriptionRepository.SaveChangesAsync(cancellationToken); + + // 5. 返回更新后的详情 + return await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscription.Id }, cancellationToken); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs new file mode 100644 index 0000000..c69c3e4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Dto; + +namespace TakeoutSaaS.Application.App.Subscriptions.Queries; + +/// +/// 查询订阅详情(含套餐信息、配额使用、变更历史)。 +/// +public sealed record GetSubscriptionDetailQuery : IRequest +{ + /// + /// 订阅 ID。 + /// + public long SubscriptionId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs new file mode 100644 index 0000000..e7a8cd4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs @@ -0,0 +1,52 @@ +using MediatR; +using TakeoutSaaS.Application.App.Subscriptions.Dto; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.Subscriptions.Queries; + +/// +/// 订阅分页查询。 +/// +public sealed record GetSubscriptionListQuery : IRequest> +{ + /// + /// 订阅状态(精确匹配)。 + /// + public SubscriptionStatus? Status { get; init; } + + /// + /// 套餐 ID(精确匹配)。 + /// + public long? TenantPackageId { get; init; } + + /// + /// 租户 ID(精确匹配)。 + /// + public long? TenantId { get; init; } + + /// + /// 租户关键词(名称或编码模糊匹配)。 + /// + public string? TenantKeyword { get; init; } + + /// + /// 到期时间筛选:未来 N 天内到期。 + /// + public int? ExpiringWithinDays { get; init; } + + /// + /// 是否自动续费筛选。 + /// + public bool? AutoRenew { get; init; } + + /// + /// 页码(从 1 开始)。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页大小。 + /// + public int PageSize { get; init; } = 20; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/OperationLog.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/OperationLog.cs new file mode 100644 index 0000000..15131ae --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/OperationLog.cs @@ -0,0 +1,49 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 运营操作日志。 +/// +public sealed class OperationLog : AuditableEntityBase +{ + /// + /// 操作类型:BatchExtend, BatchRemind, StatusChange 等。 + /// + public string OperationType { get; set; } = string.Empty; + + /// + /// 目标类型:Subscription, Bill 等。 + /// + public string TargetType { get; set; } = string.Empty; + + /// + /// 目标ID列表(JSON)。 + /// + public string? TargetIds { get; set; } + + /// + /// 操作人ID。 + /// + public string? OperatorId { get; set; } + + /// + /// 操作人名称。 + /// + public string? OperatorName { get; set; } + + /// + /// 操作参数(JSON)。 + /// + public string? Parameters { get; set; } + + /// + /// 操作结果(JSON)。 + /// + public string? Result { get; set; } + + /// + /// 是否成功。 + /// + public bool Success { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/QuotaPackage.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/QuotaPackage.cs new file mode 100644 index 0000000..c480561 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/QuotaPackage.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 配额包定义(平台提供的可购买配额包)。 +/// +public sealed class QuotaPackage : AuditableEntityBase +{ + /// + /// 配额包名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 配额类型。 + /// + public TenantQuotaType QuotaType { get; set; } + + /// + /// 配额数值。 + /// + public decimal QuotaValue { get; set; } + + /// + /// 价格。 + /// + public decimal Price { get; set; } + + /// + /// 是否上架。 + /// + public bool IsActive { get; set; } = true; + + /// + /// 排序。 + /// + public int SortOrder { get; set; } = 0; + + /// + /// 描述。 + /// + public string? Description { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPayment.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPayment.cs new file mode 100644 index 0000000..7db207f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPayment.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户支付记录。 +/// +public sealed class TenantPayment : MultiTenantEntityBase +{ + /// + /// 关联的账单 ID。 + /// + public long BillingStatementId { get; set; } + + /// + /// 支付金额。 + /// + public decimal Amount { get; set; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; set; } + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; set; } + + /// + /// 交易号。 + /// + public string? TransactionNo { get; set; } + + /// + /// 支付凭证 URL。 + /// + public string? ProofUrl { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 备注信息。 + /// + public string? Notes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaPackagePurchase.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaPackagePurchase.cs new file mode 100644 index 0000000..bd3bc51 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantQuotaPackagePurchase.cs @@ -0,0 +1,39 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户配额包购买记录。 +/// +public sealed class TenantQuotaPackagePurchase : MultiTenantEntityBase +{ + /// + /// 配额包 ID。 + /// + public long QuotaPackageId { get; set; } + + /// + /// 购买时的配额值。 + /// + public decimal QuotaValue { get; set; } + + /// + /// 购买价格。 + /// + public decimal Price { get; set; } + + /// + /// 购买时间。 + /// + public DateTime PurchasedAt { get; set; } + + /// + /// 过期时间(可选)。 + /// + public DateTime? ExpiredAt { get; set; } + + /// + /// 备注。 + /// + public string? Notes { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/PaymentMethod.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/PaymentMethod.cs new file mode 100644 index 0000000..e73b78d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/PaymentMethod.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 支付方式。 +/// +public enum PaymentMethod +{ + /// + /// 线上支付。 + /// + Online = 0, + + /// + /// 银行转账。 + /// + BankTransfer = 1, + + /// + /// 其他方式。 + /// + Other = 2 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/PaymentStatus.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/PaymentStatus.cs new file mode 100644 index 0000000..97b539a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/PaymentStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 支付状态。 +/// +public enum PaymentStatus +{ + /// + /// 待支付。 + /// + Pending = 0, + + /// + /// 支付成功。 + /// + Success = 1, + + /// + /// 支付失败。 + /// + Failed = 2, + + /// + /// 已退款。 + /// + Refunded = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/IQuotaPackageRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/IQuotaPackageRepository.cs new file mode 100644 index 0000000..49aa91d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/IQuotaPackageRepository.cs @@ -0,0 +1,131 @@ +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Domain.Tenants.Repositories; + +/// +/// 配额包仓储。 +/// +public interface IQuotaPackageRepository +{ + #region 配额包定义 + + /// + /// 按 ID 查找配额包。 + /// + /// 配额包 ID(雪花算法)。 + /// 取消标记。 + /// 配额包实体,未找到返回 null。 + Task FindByIdAsync(long id, CancellationToken cancellationToken = default); + + /// + /// 分页查询配额包。 + /// + /// 配额类型,为空不按类型过滤。 + /// 启用状态,为空不按状态过滤。 + /// 页码(从 1 开始)。 + /// 每页大小。 + /// 取消标记。 + /// 分页数据与总数。 + Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + TenantQuotaType? quotaType, + bool? isActive, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 新增配额包。 + /// + /// 配额包实体。 + /// 取消标记。 + /// 异步任务。 + Task AddAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default); + + /// + /// 更新配额包。 + /// + /// 配额包实体。 + /// 取消标记。 + /// 异步任务。 + Task UpdateAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default); + + /// + /// 软删除配额包。 + /// + /// 配额包 ID(雪花算法)。 + /// 取消标记。 + /// 删除成功返回 true,未找到返回 false。 + Task SoftDeleteAsync(long id, CancellationToken cancellationToken = default); + + #endregion + + #region 配额包购买记录 + + /// + /// 分页查询租户配额购买记录(包含配额包信息)。 + /// + /// 租户 ID(雪花算法)。 + /// 页码(从 1 开始)。 + /// 每页大小。 + /// 取消标记。 + /// 分页数据与总数。 + Task<(IReadOnlyList<(TenantQuotaPackagePurchase Purchase, QuotaPackage Package)> Items, int Total)> GetPurchasesPagedAsync( + long tenantId, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 新增配额购买记录。 + /// + /// 购买记录实体。 + /// 取消标记。 + /// 异步任务。 + Task AddPurchaseAsync(TenantQuotaPackagePurchase purchase, CancellationToken cancellationToken = default); + + #endregion + + #region 配额使用情况 + + /// + /// 查询租户配额使用情况。 + /// + /// 租户 ID(雪花算法)。 + /// 配额类型,为空查询全部。 + /// 取消标记。 + /// 配额使用情况列表。 + Task> GetUsageByTenantAsync( + long tenantId, + TenantQuotaType? quotaType, + CancellationToken cancellationToken = default); + + /// + /// 查找特定配额使用记录。 + /// + /// 租户 ID(雪花算法)。 + /// 配额类型。 + /// 取消标记。 + /// 配额使用记录,未找到返回 null。 + Task FindUsageAsync( + long tenantId, + TenantQuotaType quotaType, + CancellationToken cancellationToken = default); + + /// + /// 更新配额使用情况。 + /// + /// 配额使用实体。 + /// 取消标记。 + /// 异步任务。 + Task UpdateUsageAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default); + + #endregion + + /// + /// 持久化。 + /// + /// 取消标记。 + /// 异步任务。 + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/IStatisticsRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/IStatisticsRepository.cs new file mode 100644 index 0000000..806d440 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/IStatisticsRepository.cs @@ -0,0 +1,112 @@ +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Domain.Tenants.Repositories; + +/// +/// 统计数据仓储接口。 +/// +public interface IStatisticsRepository +{ + #region 订阅统计 + + /// + /// 获取所有订阅(用于统计)。 + /// + /// 取消标记。 + /// 所有订阅记录。 + Task> GetAllSubscriptionsAsync(CancellationToken cancellationToken = default); + + /// + /// 获取即将到期的订阅(含租户和套餐信息)。 + /// + /// 到期天数。 + /// 是否仅查询未开启自动续费的。 + /// 取消标记。 + /// 即将到期的订阅信息列表。 + Task> GetExpiringSubscriptionsAsync( + int daysAhead, + bool onlyWithoutAutoRenew, + CancellationToken cancellationToken = default); + + #endregion + + #region 收入统计 + + /// + /// 获取所有已付款账单(用于收入统计)。 + /// + /// 取消标记。 + /// 已付款账单列表。 + Task> GetPaidBillsAsync(CancellationToken cancellationToken = default); + + #endregion + + #region 配额使用排行 + + /// + /// 获取配额使用排行(含租户信息)。 + /// + /// 配额类型。 + /// 前 N 名。 + /// 取消标记。 + /// 配额使用排行列表。 + Task> GetQuotaUsageRankingAsync( + TenantQuotaType quotaType, + int topN, + CancellationToken cancellationToken = default); + + #endregion +} + +/// +/// 即将到期的订阅信息(含关联数据)。 +/// +public record ExpiringSubscriptionInfo +{ + /// + /// 订阅实体。 + /// + public required TenantSubscription Subscription { get; init; } + + /// + /// 租户名称。 + /// + public required string TenantName { get; init; } + + /// + /// 套餐名称。 + /// + public required string PackageName { get; init; } +} + +/// +/// 配额使用排行信息(含租户名称)。 +/// +public record QuotaUsageRankInfo +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 租户名称。 + /// + public required string TenantName { get; init; } + + /// + /// 已使用值。 + /// + public decimal UsedValue { get; init; } + + /// + /// 限制值。 + /// + public decimal LimitValue { get; init; } + + /// + /// 使用百分比。 + /// + public decimal UsagePercentage { get; init; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs new file mode 100644 index 0000000..54ee43d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs @@ -0,0 +1,295 @@ +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; + +namespace TakeoutSaaS.Domain.Tenants.Repositories; + +/// +/// 订阅管理仓储接口。 +/// +public interface ISubscriptionRepository +{ + #region 订阅查询 + + /// + /// 按 ID 查询订阅。 + /// + /// 订阅 ID。 + /// 取消标记。 + /// 订阅实体,未找到返回 null。 + Task FindByIdAsync(long subscriptionId, CancellationToken cancellationToken = default); + + /// + /// 按 ID 列表批量查询订阅。 + /// + /// 订阅 ID 列表。 + /// 取消标记。 + /// 订阅实体列表。 + Task> FindByIdsAsync( + IEnumerable subscriptionIds, + CancellationToken cancellationToken = default); + + /// + /// 分页查询订阅列表(含关联信息)。 + /// + /// 查询过滤条件。 + /// 取消标记。 + /// 分页结果。 + Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + SubscriptionSearchFilter filter, + CancellationToken cancellationToken = default); + + /// + /// 获取订阅详情(含关联信息)。 + /// + /// 订阅 ID。 + /// 取消标记。 + /// 订阅详情信息。 + Task GetDetailAsync(long subscriptionId, CancellationToken cancellationToken = default); + + /// + /// 按 ID 列表批量查询订阅(含租户信息)。 + /// + /// 订阅 ID 列表。 + /// 取消标记。 + /// 订阅与租户信息列表。 + Task> FindByIdsWithTenantAsync( + IEnumerable subscriptionIds, + CancellationToken cancellationToken = default); + + #endregion + + #region 套餐查询 + + /// + /// 按 ID 查询套餐。 + /// + /// 套餐 ID。 + /// 取消标记。 + /// 套餐实体,未找到返回 null。 + Task FindPackageByIdAsync(long packageId, CancellationToken cancellationToken = default); + + #endregion + + #region 订阅更新 + + /// + /// 更新订阅。 + /// + /// 订阅实体。 + /// 取消标记。 + Task UpdateAsync(TenantSubscription subscription, CancellationToken cancellationToken = default); + + #endregion + + #region 订阅历史 + + /// + /// 添加订阅变更历史。 + /// + /// 历史记录实体。 + /// 取消标记。 + Task AddHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default); + + /// + /// 获取订阅变更历史(含套餐名称)。 + /// + /// 订阅 ID。 + /// 取消标记。 + /// 历史记录列表。 + Task> GetHistoryAsync( + long subscriptionId, + CancellationToken cancellationToken = default); + + #endregion + + #region 配额使用 + + /// + /// 获取租户配额使用情况。 + /// + /// 租户 ID。 + /// 取消标记。 + /// 配额使用列表。 + Task> GetQuotaUsagesAsync( + long tenantId, + CancellationToken cancellationToken = default); + + #endregion + + #region 通知 + + /// + /// 添加租户通知。 + /// + /// 通知实体。 + /// 取消标记。 + Task AddNotificationAsync(TenantNotification notification, CancellationToken cancellationToken = default); + + #endregion + + #region 操作日志 + + /// + /// 添加操作日志。 + /// + /// 日志实体。 + /// 取消标记。 + Task AddOperationLogAsync(OperationLog log, CancellationToken cancellationToken = default); + + #endregion + + /// + /// 保存变更。 + /// + /// 取消标记。 + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} + +#region 查询过滤与结果类型 + +/// +/// 订阅查询过滤条件。 +/// +public record SubscriptionSearchFilter +{ + /// + /// 订阅状态。 + /// + public SubscriptionStatus? Status { get; init; } + + /// + /// 套餐 ID。 + /// + public long? TenantPackageId { get; init; } + + /// + /// 租户 ID。 + /// + public long? TenantId { get; init; } + + /// + /// 租户关键词(名称或编码)。 + /// + public string? TenantKeyword { get; init; } + + /// + /// 即将到期天数。 + /// + public int? ExpiringWithinDays { get; init; } + + /// + /// 自动续费状态。 + /// + public bool? AutoRenew { get; init; } + + /// + /// 页码(从 1 开始)。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页数量。 + /// + public int PageSize { get; init; } = 20; +} + +/// +/// 订阅及关联信息。 +/// +public record SubscriptionWithRelations +{ + /// + /// 订阅实体。 + /// + public required TenantSubscription Subscription { get; init; } + + /// + /// 租户名称。 + /// + public required string TenantName { get; init; } + + /// + /// 租户编码。 + /// + public required string TenantCode { get; init; } + + /// + /// 套餐名称。 + /// + public required string PackageName { get; init; } + + /// + /// 排期套餐名称(可选)。 + /// + public string? ScheduledPackageName { get; init; } +} + +/// +/// 订阅详情信息。 +/// +public record SubscriptionDetailInfo +{ + /// + /// 订阅实体。 + /// + public required TenantSubscription Subscription { get; init; } + + /// + /// 租户名称。 + /// + public required string TenantName { get; init; } + + /// + /// 租户编码。 + /// + public required string TenantCode { get; init; } + + /// + /// 当前套餐。 + /// + public TenantPackage? Package { get; init; } + + /// + /// 排期套餐。 + /// + public TenantPackage? ScheduledPackage { get; init; } +} + +/// +/// 订阅与租户信息。 +/// +public record SubscriptionWithTenant +{ + /// + /// 订阅实体。 + /// + public required TenantSubscription Subscription { get; init; } + + /// + /// 租户实体。 + /// + public required Tenant Tenant { get; init; } +} + +/// +/// 订阅历史(含套餐名称)。 +/// +public record SubscriptionHistoryWithPackageNames +{ + /// + /// 历史记录实体。 + /// + public required TenantSubscriptionHistory History { get; init; } + + /// + /// 原套餐名称。 + /// + public required string FromPackageName { get; init; } + + /// + /// 目标套餐名称。 + /// + public required string ToPackageName { get; init; } +} + +#endregion diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs index 9edf1a4..81e222d 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.cs @@ -64,4 +64,34 @@ public interface ITenantBillingRepository /// 取消标记。 /// 异步任务。 Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 管理员端分页查询账单列表(跨租户)。 + /// + /// 租户 ID 筛选(可选)。 + /// 账单状态筛选(可选)。 + /// 开始时间(UTC,可选)。 + /// 结束时间(UTC,可选)。 + /// 关键词搜索(账单号或租户名)。 + /// 页码(从 1 开始)。 + /// 页大小。 + /// 取消标记。 + /// 账单集合与总数。 + Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + long? tenantId, + TenantBillingStatus? status, + DateTime? from, + DateTime? to, + string? keyword, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 按 ID 获取账单(不限租户,管理员端使用)。 + /// + /// 账单 ID。 + /// 取消标记。 + /// 账单实体或 null。 + Task FindByIdAsync(long billingId, CancellationToken cancellationToken = default); } diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantPaymentRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantPaymentRepository.cs new file mode 100644 index 0000000..bb2214c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantPaymentRepository.cs @@ -0,0 +1,48 @@ +using TakeoutSaaS.Domain.Tenants.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Repositories; + +/// +/// 租户支付记录仓储。 +/// +public interface ITenantPaymentRepository +{ + /// + /// 查询指定账单的支付记录列表。 + /// + /// 账单 ID。 + /// 取消标记。 + /// 支付记录集合。 + Task> GetByBillingIdAsync(long billingStatementId, CancellationToken cancellationToken = default); + + /// + /// 按 ID 获取支付记录。 + /// + /// 支付记录 ID。 + /// 取消标记。 + /// 支付记录实体或 null。 + Task FindByIdAsync(long paymentId, CancellationToken cancellationToken = default); + + /// + /// 新增支付记录。 + /// + /// 支付记录实体。 + /// 取消标记。 + /// 异步任务。 + Task AddAsync(TenantPayment payment, CancellationToken cancellationToken = default); + + /// + /// 更新支付记录。 + /// + /// 支付记录实体。 + /// 取消标记。 + /// 异步任务。 + Task UpdateAsync(TenantPayment payment, CancellationToken cancellationToken = default); + + /// + /// 保存变更。 + /// + /// 取消标记。 + /// 异步任务。 + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs index 6a1fa4d..c8c6a3f 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs @@ -16,6 +16,14 @@ public interface ITenantRepository /// 租户实体,未找到返回 null。 Task FindByIdAsync(long tenantId, CancellationToken cancellationToken = default); + /// + /// 批量获取租户。 + /// + /// 租户 ID 列表。 + /// 取消标记。 + /// 租户列表(仅返回找到的租户)。 + Task> FindByIdsAsync(IReadOnlyCollection tenantIds, CancellationToken cancellationToken = default); + /// /// 按状态与关键词查询租户列表。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs index 7333909..ab6cb73 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs @@ -41,12 +41,16 @@ public static class AppServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddOptions() .Bind(configuration.GetSection(AppSeedOptions.SectionName)) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 01dd78f..73cf234 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -62,6 +62,10 @@ public sealed class TakeoutAppDbContext( /// public DbSet TenantBillingStatements => Set(); /// + /// 租户支付记录。 + /// + public DbSet TenantPayments => Set(); + /// /// 租户通知。 /// public DbSet TenantNotifications => Set(); @@ -86,6 +90,18 @@ public sealed class TakeoutAppDbContext( /// public DbSet TenantReviewClaims => Set(); /// + /// 运营操作日志。 + /// + public DbSet OperationLogs => Set(); + /// + /// 配额包定义。 + /// + public DbSet QuotaPackages => Set(); + /// + /// 租户配额包购买记录。 + /// + public DbSet TenantQuotaPackagePurchases => Set(); + /// /// 商户实体。 /// public DbSet Merchants => Set(); @@ -374,12 +390,16 @@ public sealed class TakeoutAppDbContext( ConfigureTenantSubscriptionHistory(modelBuilder.Entity()); ConfigureTenantQuotaUsage(modelBuilder.Entity()); ConfigureTenantBilling(modelBuilder.Entity()); + ConfigureTenantPayment(modelBuilder.Entity()); ConfigureTenantNotification(modelBuilder.Entity()); ConfigureTenantAnnouncement(modelBuilder.Entity()); ConfigureTenantAnnouncementRead(modelBuilder.Entity()); ConfigureTenantVerificationProfile(modelBuilder.Entity()); ConfigureTenantAuditLog(modelBuilder.Entity()); ConfigureTenantReviewClaim(modelBuilder.Entity()); + ConfigureOperationLog(modelBuilder.Entity()); + ConfigureQuotaPackage(modelBuilder.Entity()); + ConfigureTenantQuotaPackagePurchase(modelBuilder.Entity()); ConfigureMerchantDocument(modelBuilder.Entity()); ConfigureMerchantContract(modelBuilder.Entity()); ConfigureMerchantStaff(modelBuilder.Entity()); @@ -511,6 +531,22 @@ public sealed class TakeoutAppDbContext( builder.HasIndex(x => x.TenantId).IsUnique().HasFilter("\"ReleasedAt\" IS NULL AND \"DeletedAt\" IS NULL"); } + private static void ConfigureOperationLog(EntityTypeBuilder builder) + { + builder.ToTable("operation_logs"); + builder.HasKey(x => x.Id); + builder.Property(x => x.OperationType).HasMaxLength(64).IsRequired(); + builder.Property(x => x.TargetType).HasMaxLength(64).IsRequired(); + builder.Property(x => x.TargetIds).HasColumnType("text"); + builder.Property(x => x.OperatorId).HasMaxLength(64); + builder.Property(x => x.OperatorName).HasMaxLength(128); + builder.Property(x => x.Parameters).HasColumnType("text"); + builder.Property(x => x.Result).HasColumnType("text"); + builder.Property(x => x.Success).IsRequired(); + builder.HasIndex(x => new { x.OperationType, x.CreatedAt }); + builder.HasIndex(x => x.CreatedAt); + } + private static void ConfigureTenantSubscriptionHistory(EntityTypeBuilder builder) { builder.ToTable("tenant_subscription_histories"); @@ -736,6 +772,20 @@ public sealed class TakeoutAppDbContext( builder.HasIndex(x => new { x.TenantId, x.StatementNo }).IsUnique(); } + private static void ConfigureTenantPayment(EntityTypeBuilder builder) + { + builder.ToTable("tenant_payments"); + builder.HasKey(x => x.Id); + builder.Property(x => x.BillingStatementId).IsRequired(); + builder.Property(x => x.Amount).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.Method).HasConversion(); + builder.Property(x => x.Status).HasConversion(); + builder.Property(x => x.TransactionNo).HasMaxLength(64); + builder.Property(x => x.ProofUrl).HasMaxLength(512); + builder.Property(x => x.Notes).HasMaxLength(512); + builder.HasIndex(x => new { x.TenantId, x.BillingStatementId }); + } + private static void ConfigureTenantNotification(EntityTypeBuilder builder) { builder.ToTable("tenant_notifications"); @@ -1413,4 +1463,31 @@ public sealed class TakeoutAppDbContext( builder.Property(x => x.NotificationChannels).HasMaxLength(256); builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.Severity }); } + + private static void ConfigureQuotaPackage(EntityTypeBuilder builder) + { + builder.ToTable("quota_packages"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(128).IsRequired(); + builder.Property(x => x.QuotaType).HasConversion().IsRequired(); + builder.Property(x => x.QuotaValue).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.Price).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.IsActive).IsRequired(); + builder.Property(x => x.SortOrder).HasDefaultValue(0); + builder.Property(x => x.Description).HasMaxLength(512); + builder.HasIndex(x => new { x.QuotaType, x.IsActive, x.SortOrder }); + } + + private static void ConfigureTenantQuotaPackagePurchase(EntityTypeBuilder builder) + { + builder.ToTable("tenant_quota_package_purchases"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.QuotaPackageId).IsRequired(); + builder.Property(x => x.QuotaValue).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.Price).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.PurchasedAt).IsRequired(); + builder.Property(x => x.Notes).HasMaxLength(512); + builder.HasIndex(x => new { x.TenantId, x.QuotaPackageId, x.PurchasedAt }); + } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs new file mode 100644 index 0000000..80b29b6 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfQuotaPackageRepository.cs @@ -0,0 +1,164 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// EF 配额包仓储实现。 +/// +public sealed class EfQuotaPackageRepository(TakeoutAppDbContext context) : IQuotaPackageRepository +{ + #region 配额包定义 + + /// + public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) + { + return context.QuotaPackages + .FirstOrDefaultAsync(x => x.Id == id && x.DeletedAt == null, cancellationToken); + } + + /// + public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + TenantQuotaType? quotaType, + bool? isActive, + int page, + int pageSize, + CancellationToken cancellationToken = default) + { + var query = context.QuotaPackages.AsNoTracking() + .Where(x => x.DeletedAt == null); + + if (quotaType.HasValue) + { + query = query.Where(x => x.QuotaType == quotaType.Value); + } + + if (isActive.HasValue) + { + query = query.Where(x => x.IsActive == isActive.Value); + } + + var total = await query.CountAsync(cancellationToken); + + var items = await query + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (items, total); + } + + /// + public Task AddAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default) + { + return context.QuotaPackages.AddAsync(quotaPackage, cancellationToken).AsTask(); + } + + /// + public Task UpdateAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default) + { + context.QuotaPackages.Update(quotaPackage); + return Task.CompletedTask; + } + + /// + public async Task SoftDeleteAsync(long id, CancellationToken cancellationToken = default) + { + var quotaPackage = await context.QuotaPackages + .FirstOrDefaultAsync(x => x.Id == id && x.DeletedAt == null, cancellationToken); + + if (quotaPackage == null) + { + return false; + } + + quotaPackage.DeletedAt = DateTime.UtcNow; + return true; + } + + #endregion + + #region 配额包购买记录 + + /// + public async Task<(IReadOnlyList<(TenantQuotaPackagePurchase Purchase, QuotaPackage Package)> Items, int Total)> GetPurchasesPagedAsync( + long tenantId, + int page, + int pageSize, + CancellationToken cancellationToken = default) + { + var query = context.TenantQuotaPackagePurchases.AsNoTracking() + .Where(x => x.TenantId == tenantId && x.DeletedAt == null); + + var total = await query.CountAsync(cancellationToken); + + var items = await query + .OrderByDescending(x => x.PurchasedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Join(context.QuotaPackages.AsNoTracking(), + purchase => purchase.QuotaPackageId, + package => package.Id, + (purchase, package) => new { Purchase = purchase, Package = package }) + .ToListAsync(cancellationToken); + + return (items.Select(x => (x.Purchase, x.Package)).ToList(), total); + } + + /// + public Task AddPurchaseAsync(TenantQuotaPackagePurchase purchase, CancellationToken cancellationToken = default) + { + return context.TenantQuotaPackagePurchases.AddAsync(purchase, cancellationToken).AsTask(); + } + + #endregion + + #region 配额使用情况 + + /// + public async Task> GetUsageByTenantAsync( + long tenantId, + TenantQuotaType? quotaType, + CancellationToken cancellationToken = default) + { + var query = context.TenantQuotaUsages.AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (quotaType.HasValue) + { + query = query.Where(x => x.QuotaType == quotaType.Value); + } + + return await query.ToListAsync(cancellationToken); + } + + /// + public Task FindUsageAsync( + long tenantId, + TenantQuotaType quotaType, + CancellationToken cancellationToken = default) + { + return context.TenantQuotaUsages + .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.QuotaType == quotaType, cancellationToken); + } + + /// + public Task UpdateUsageAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default) + { + context.TenantQuotaUsages.Update(usage); + return Task.CompletedTask; + } + + #endregion + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs new file mode 100644 index 0000000..694fdb7 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStatisticsRepository.cs @@ -0,0 +1,116 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 统计数据仓储实现。 +/// +public sealed class EfStatisticsRepository(TakeoutAppDbContext dbContext) : IStatisticsRepository +{ + #region 订阅统计 + + /// + public async Task> GetAllSubscriptionsAsync(CancellationToken cancellationToken = default) + { + return await dbContext.TenantSubscriptions + .AsNoTracking() + .ToListAsync(cancellationToken); + } + + /// + public async Task> GetExpiringSubscriptionsAsync( + int daysAhead, + bool onlyWithoutAutoRenew, + CancellationToken cancellationToken = default) + { + var now = DateTime.UtcNow; + var targetDate = now.AddDays(daysAhead); + + // 构建基础查询 + var query = dbContext.TenantSubscriptions + .AsNoTracking() + .Where(s => s.Status == SubscriptionStatus.Active + && s.EffectiveTo >= now + && s.EffectiveTo <= targetDate); + + // 如果只查询未开启自动续费的 + if (onlyWithoutAutoRenew) + { + query = query.Where(s => !s.AutoRenew); + } + + // 连接租户和套餐信息 + var result = await query + .Join( + dbContext.Tenants, + sub => sub.TenantId, + tenant => tenant.Id, + (sub, tenant) => new { Subscription = sub, Tenant = tenant } + ) + .Join( + dbContext.TenantPackages, + combined => combined.Subscription.TenantPackageId, + package => package.Id, + (combined, package) => new ExpiringSubscriptionInfo + { + Subscription = combined.Subscription, + TenantName = combined.Tenant.Name, + PackageName = package.Name + } + ) + .OrderBy(x => x.Subscription.EffectiveTo) + .ToListAsync(cancellationToken); + + return result; + } + + #endregion + + #region 收入统计 + + /// + public async Task> GetPaidBillsAsync(CancellationToken cancellationToken = default) + { + return await dbContext.TenantBillingStatements + .AsNoTracking() + .Where(b => b.Status == TenantBillingStatus.Paid) + .ToListAsync(cancellationToken); + } + + #endregion + + #region 配额使用排行 + + /// + public async Task> GetQuotaUsageRankingAsync( + TenantQuotaType quotaType, + int topN, + CancellationToken cancellationToken = default) + { + return await dbContext.TenantQuotaUsages + .AsNoTracking() + .Where(q => q.QuotaType == quotaType && q.LimitValue > 0) + .Join( + dbContext.Tenants, + quota => quota.TenantId, + tenant => tenant.Id, + (quota, tenant) => new QuotaUsageRankInfo + { + TenantId = quota.TenantId, + TenantName = tenant.Name, + UsedValue = quota.UsedValue, + LimitValue = quota.LimitValue, + UsagePercentage = quota.LimitValue > 0 ? (quota.UsedValue / quota.LimitValue * 100) : 0 + } + ) + .OrderByDescending(x => x.UsagePercentage) + .Take(topN) + .ToListAsync(cancellationToken); + } + + #endregion +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs new file mode 100644 index 0000000..281c941 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs @@ -0,0 +1,270 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 订阅管理仓储实现。 +/// +public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext) : ISubscriptionRepository +{ + #region 订阅查询 + + /// + public async Task FindByIdAsync(long subscriptionId, CancellationToken cancellationToken = default) + { + return await dbContext.TenantSubscriptions + .FirstOrDefaultAsync(s => s.Id == subscriptionId, cancellationToken); + } + + /// + public async Task> FindByIdsAsync( + IEnumerable subscriptionIds, + CancellationToken cancellationToken = default) + { + var ids = subscriptionIds.ToList(); + return await dbContext.TenantSubscriptions + .Where(s => ids.Contains(s.Id)) + .ToListAsync(cancellationToken); + } + + /// + public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + SubscriptionSearchFilter filter, + CancellationToken cancellationToken = default) + { + // 1. 构建基础查询 + var query = dbContext.TenantSubscriptions + .AsNoTracking() + .Join( + dbContext.Tenants, + sub => sub.TenantId, + tenant => tenant.Id, + (sub, tenant) => new { Subscription = sub, Tenant = tenant } + ) + .Join( + dbContext.TenantPackages, + combined => combined.Subscription.TenantPackageId, + package => package.Id, + (combined, package) => new { combined.Subscription, combined.Tenant, Package = package } + ) + .GroupJoin( + dbContext.TenantPackages, + combined => combined.Subscription.ScheduledPackageId, + scheduledPackage => scheduledPackage.Id, + (combined, scheduledPackages) => new { combined.Subscription, combined.Tenant, combined.Package, ScheduledPackage = scheduledPackages.FirstOrDefault() } + ); + + // 2. 应用过滤条件 + if (filter.Status.HasValue) + { + query = query.Where(x => x.Subscription.Status == filter.Status.Value); + } + + if (filter.TenantPackageId.HasValue) + { + query = query.Where(x => x.Subscription.TenantPackageId == filter.TenantPackageId.Value); + } + + if (filter.TenantId.HasValue) + { + query = query.Where(x => x.Subscription.TenantId == filter.TenantId.Value); + } + + if (!string.IsNullOrWhiteSpace(filter.TenantKeyword)) + { + var keyword = filter.TenantKeyword.Trim().ToLower(); + query = query.Where(x => x.Tenant.Name.ToLower().Contains(keyword) || x.Tenant.Code.ToLower().Contains(keyword)); + } + + if (filter.ExpiringWithinDays.HasValue) + { + var expiryDate = DateTime.UtcNow.AddDays(filter.ExpiringWithinDays.Value); + query = query.Where(x => x.Subscription.EffectiveTo <= expiryDate && x.Subscription.EffectiveTo >= DateTime.UtcNow); + } + + if (filter.AutoRenew.HasValue) + { + query = query.Where(x => x.Subscription.AutoRenew == filter.AutoRenew.Value); + } + + // 3. 获取总数 + var total = await query.CountAsync(cancellationToken); + + // 4. 排序和分页 + var items = await query + .OrderByDescending(x => x.Subscription.CreatedAt) + .Skip((filter.Page - 1) * filter.PageSize) + .Take(filter.PageSize) + .Select(x => new SubscriptionWithRelations + { + Subscription = x.Subscription, + TenantName = x.Tenant.Name, + TenantCode = x.Tenant.Code, + PackageName = x.Package.Name, + ScheduledPackageName = x.ScheduledPackage != null ? x.ScheduledPackage.Name : null + }) + .ToListAsync(cancellationToken); + + return (items, total); + } + + /// + public async Task GetDetailAsync(long subscriptionId, CancellationToken cancellationToken = default) + { + var result = await dbContext.TenantSubscriptions + .AsNoTracking() + .Where(s => s.Id == subscriptionId) + .Select(s => new + { + Subscription = s, + Tenant = dbContext.Tenants.FirstOrDefault(t => t.Id == s.TenantId), + Package = dbContext.TenantPackages.FirstOrDefault(p => p.Id == s.TenantPackageId), + ScheduledPackage = s.ScheduledPackageId.HasValue + ? dbContext.TenantPackages.FirstOrDefault(p => p.Id == s.ScheduledPackageId) + : null + }) + .FirstOrDefaultAsync(cancellationToken); + + if (result == null) + { + return null; + } + + return new SubscriptionDetailInfo + { + Subscription = result.Subscription, + TenantName = result.Tenant?.Name ?? "", + TenantCode = result.Tenant?.Code ?? "", + Package = result.Package, + ScheduledPackage = result.ScheduledPackage + }; + } + + /// + public async Task> FindByIdsWithTenantAsync( + IEnumerable subscriptionIds, + CancellationToken cancellationToken = default) + { + var ids = subscriptionIds.ToList(); + return await dbContext.TenantSubscriptions + .Where(s => ids.Contains(s.Id)) + .Join( + dbContext.Tenants, + sub => sub.TenantId, + tenant => tenant.Id, + (sub, tenant) => new SubscriptionWithTenant + { + Subscription = sub, + Tenant = tenant + } + ) + .ToListAsync(cancellationToken); + } + + #endregion + + #region 套餐查询 + + /// + public async Task FindPackageByIdAsync(long packageId, CancellationToken cancellationToken = default) + { + return await dbContext.TenantPackages + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + } + + #endregion + + #region 订阅更新 + + /// + public Task UpdateAsync(TenantSubscription subscription, CancellationToken cancellationToken = default) + { + dbContext.TenantSubscriptions.Update(subscription); + return Task.CompletedTask; + } + + #endregion + + #region 订阅历史 + + /// + public Task AddHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default) + { + dbContext.TenantSubscriptionHistories.Add(history); + return Task.CompletedTask; + } + + /// + public async Task> GetHistoryAsync( + long subscriptionId, + CancellationToken cancellationToken = default) + { + return await dbContext.TenantSubscriptionHistories + .AsNoTracking() + .Where(h => h.TenantSubscriptionId == subscriptionId) + .OrderByDescending(h => h.CreatedAt) + .Select(h => new SubscriptionHistoryWithPackageNames + { + History = h, + FromPackageName = dbContext.TenantPackages + .Where(p => p.Id == h.FromPackageId) + .Select(p => p.Name) + .FirstOrDefault() ?? "", + ToPackageName = dbContext.TenantPackages + .Where(p => p.Id == h.ToPackageId) + .Select(p => p.Name) + .FirstOrDefault() ?? "" + }) + .ToListAsync(cancellationToken); + } + + #endregion + + #region 配额使用 + + /// + public async Task> GetQuotaUsagesAsync( + long tenantId, + CancellationToken cancellationToken = default) + { + return await dbContext.TenantQuotaUsages + .AsNoTracking() + .Where(q => q.TenantId == tenantId) + .ToListAsync(cancellationToken); + } + + #endregion + + #region 通知 + + /// + public Task AddNotificationAsync(TenantNotification notification, CancellationToken cancellationToken = default) + { + dbContext.TenantNotifications.Add(notification); + return Task.CompletedTask; + } + + #endregion + + #region 操作日志 + + /// + public Task AddOperationLogAsync(OperationLog log, CancellationToken cancellationToken = default) + { + dbContext.Set().Add(log); + return Task.CompletedTask; + } + + #endregion + + /// + public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantBillingRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantBillingRepository.cs index 7b939d5..c71e7a9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantBillingRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantBillingRepository.cs @@ -43,6 +43,69 @@ public sealed class EfTenantBillingRepository(TakeoutAppDbContext context) : ITe .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } + /// + public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( + long? tenantId, + TenantBillingStatus? status, + DateTime? from, + DateTime? to, + string? keyword, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default) + { + var query = context.TenantBillingStatements.AsNoTracking(); + + // 1. 按租户过滤(可选) + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 2. 按状态过滤 + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + // 3. 按日期范围过滤 + if (from.HasValue) + { + query = query.Where(x => x.PeriodStart >= from.Value); + } + + if (to.HasValue) + { + query = query.Where(x => x.PeriodEnd <= to.Value); + } + + // 4. 按关键字过滤(账单编号) + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalizedKeyword = keyword.Trim(); + query = query.Where(x => EF.Functions.ILike(x.StatementNo, $"%{normalizedKeyword}%")); + } + + // 5. 统计总数 + var total = await query.CountAsync(cancellationToken); + + // 6. 分页查询 + var items = await query + .OrderByDescending(x => x.PeriodEnd) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (items, total); + } + + /// + public Task FindByIdAsync(long billingId, CancellationToken cancellationToken = default) + { + return context.TenantBillingStatements.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == billingId, cancellationToken); + } + /// public Task FindByIdAsync(long tenantId, long billingId, CancellationToken cancellationToken = default) { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPaymentRepository.cs new file mode 100644 index 0000000..935580a --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPaymentRepository.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// EF 租户支付记录仓储。 +/// +public sealed class EfTenantPaymentRepository(TakeoutAppDbContext context) : ITenantPaymentRepository +{ + /// + public async Task> GetByBillingIdAsync(long billingStatementId, CancellationToken cancellationToken = default) + { + return await context.TenantPayments.AsNoTracking() + .Where(x => x.BillingStatementId == billingStatementId) + .OrderByDescending(x => x.PaidAt) + .ToListAsync(cancellationToken); + } + + /// + public Task FindByIdAsync(long paymentId, CancellationToken cancellationToken = default) + { + return context.TenantPayments.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == paymentId, cancellationToken); + } + + /// + public Task AddAsync(TenantPayment payment, CancellationToken cancellationToken = default) + { + return context.TenantPayments.AddAsync(payment, cancellationToken).AsTask(); + } + + /// + public Task UpdateAsync(TenantPayment payment, CancellationToken cancellationToken = default) + { + context.TenantPayments.Update(payment); + return Task.CompletedTask; + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs index 17f3dee..e67e3f5 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs @@ -20,6 +20,20 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context) : ITenantRep .FirstOrDefaultAsync(x => x.Id == tenantId, cancellationToken); } + /// + public async Task> FindByIdsAsync(IReadOnlyCollection tenantIds, CancellationToken cancellationToken = default) + { + if (tenantIds.Count == 0) + { + return Array.Empty(); + } + + return await context.Tenants + .AsNoTracking() + .Where(x => tenantIds.Contains(x.Id)) + .ToListAsync(cancellationToken); + } + /// public async Task> SearchAsync( TenantStatus? status, diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs new file mode 100644 index 0000000..9eb39db --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/AutoRenewalService.cs @@ -0,0 +1,176 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Infrastructure.App.Persistence; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Infrastructure.BackgroundServices; + +/// +/// 自动续费后台服务。 +/// 定期检查开启自动续费的订阅,在到期前自动生成续费账单。 +/// +public sealed class AutoRenewalService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly AutoRenewalOptions _options; + + public AutoRenewalService( + IServiceProvider serviceProvider, + ILogger logger, + IOptions options) + { + _serviceProvider = serviceProvider; + _logger = logger; + _options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("自动续费服务已启动"); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + // 计算下次执行时间(每天执行) + var now = DateTime.UtcNow; + var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); + var delay = nextRun - now; + + _logger.LogInformation("自动续费服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); + + await Task.Delay(delay, stoppingToken); + + if (stoppingToken.IsCancellationRequested) + break; + + await ProcessAutoRenewalsAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "自动续费服务执行异常"); + // 出错后等待一段时间再重试 + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + } + } + + _logger.LogInformation("自动续费服务已停止"); + } + + private async Task ProcessAutoRenewalsAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("开始处理自动续费"); + + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var idGenerator = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + var renewalThreshold = now.AddDays(_options.RenewalDaysBeforeExpiry); + var billsCreated = 0; + + try + { + // 查询开启自动续费且即将到期的活跃订阅 + var autoRenewSubscriptions = await dbContext.TenantSubscriptions + .Where(s => s.Status == SubscriptionStatus.Active + && s.AutoRenew + && s.EffectiveTo <= renewalThreshold + && s.EffectiveTo > now) + .Join( + dbContext.TenantPackages, + sub => sub.TenantPackageId, + package => package.Id, + (sub, package) => new { Subscription = sub, Package = package } + ) + .ToListAsync(cancellationToken); + + foreach (var item in autoRenewSubscriptions) + { + // 检查是否已为本次到期生成过账单 + var existingBill = await dbContext.TenantBillingStatements + .AnyAsync(b => b.TenantId == item.Subscription.TenantId + && b.PeriodStart >= item.Subscription.EffectiveTo + && b.Status != TenantBillingStatus.Cancelled, + cancellationToken); + + if (existingBill) + { + _logger.LogInformation( + "订阅 {SubscriptionId} 已存在续费账单,跳过", + item.Subscription.Id); + continue; + } + + // 生成续费账单 + var billNo = $"BILL-{DateTime.UtcNow:yyyyMMddHHmmss}-{item.Subscription.TenantId}"; + var periodStart = item.Subscription.EffectiveTo; + + // 从当前订阅计算续费周期(月数) + var currentDurationMonths = ((item.Subscription.EffectiveTo.Year - item.Subscription.EffectiveFrom.Year) * 12) + + item.Subscription.EffectiveTo.Month - item.Subscription.EffectiveFrom.Month; + if (currentDurationMonths <= 0) currentDurationMonths = 1; // 至少1个月 + + var periodEnd = periodStart.AddMonths(currentDurationMonths); + + // 根据续费周期计算价格(年付优惠) + var renewalPrice = currentDurationMonths >= 12 + ? (item.Package.YearlyPrice ?? item.Package.MonthlyPrice * 12 ?? 0) + : (item.Package.MonthlyPrice ?? 0) * currentDurationMonths; + + var bill = new TenantBillingStatement + { + Id = idGenerator.NextId(), + TenantId = item.Subscription.TenantId, + StatementNo = billNo, + PeriodStart = periodStart, + PeriodEnd = periodEnd, + AmountDue = renewalPrice, + AmountPaid = 0, + Status = TenantBillingStatus.Pending, + DueDate = periodStart.AddDays(-1), // 到期前一天为付款截止日 + LineItemsJson = $"{{\"套餐名称\":\"{item.Package.Name}\",\"续费周期\":\"{currentDurationMonths}个月\"}}", + CreatedAt = DateTime.UtcNow + }; + + dbContext.TenantBillingStatements.Add(bill); + billsCreated++; + + _logger.LogInformation( + "为订阅 {SubscriptionId} (租户 {TenantId}) 生成自动续费账单 {BillNo},金额 {Amount}", + item.Subscription.Id, item.Subscription.TenantId, billNo, renewalPrice); + } + + await dbContext.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("自动续费处理完成,共生成 {Count} 张账单", billsCreated); + } + catch (Exception ex) + { + _logger.LogError(ex, "自动续费处理失败"); + throw; + } + } +} + +/// +/// 自动续费配置选项。 +/// +public sealed class AutoRenewalOptions +{ + /// + /// 执行时间(小时,UTC时间),默认凌晨1点。 + /// + public int ExecuteHour { get; set; } = 1; + + /// + /// 在到期前N天生成续费账单,默认3天。 + /// + public int RenewalDaysBeforeExpiry { get; set; } = 3; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs new file mode 100644 index 0000000..ced3034 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/RenewalReminderService.cs @@ -0,0 +1,171 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Infrastructure.App.Persistence; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Infrastructure.BackgroundServices; + +/// +/// 续费提醒后台服务。 +/// 定期检查即将到期的订阅,发送续费提醒通知。 +/// +public sealed class RenewalReminderService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly RenewalReminderOptions _options; + + public RenewalReminderService( + IServiceProvider serviceProvider, + ILogger logger, + IOptions options) + { + _serviceProvider = serviceProvider; + _logger = logger; + _options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("续费提醒服务已启动"); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + // 计算下次执行时间(每天执行) + var now = DateTime.UtcNow; + var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); + var delay = nextRun - now; + + _logger.LogInformation("续费提醒服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); + + await Task.Delay(delay, stoppingToken); + + if (stoppingToken.IsCancellationRequested) + break; + + await SendRenewalRemindersAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "续费提醒服务执行异常"); + // 出错后等待一段时间再重试 + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + } + } + + _logger.LogInformation("续费提醒服务已停止"); + } + + private async Task SendRenewalRemindersAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("开始发送续费提醒"); + + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var idGenerator = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + var remindersSent = 0; + + try + { + // 遍历配置的提醒时间点(例如:到期前7天、3天、1天) + foreach (var daysBeforeExpiry in _options.ReminderDaysBeforeExpiry) + { + var targetDate = now.AddDays(daysBeforeExpiry); + var startOfDay = targetDate.Date; + var endOfDay = startOfDay.AddDays(1); + + // 查询即将到期的活跃订阅(且未开启自动续费) + var expiringSubscriptions = await dbContext.TenantSubscriptions + .Where(s => s.Status == SubscriptionStatus.Active + && !s.AutoRenew + && s.EffectiveTo >= startOfDay + && s.EffectiveTo < endOfDay) + .Join( + dbContext.Tenants, + sub => sub.TenantId, + tenant => tenant.Id, + (sub, tenant) => new { Subscription = sub, Tenant = tenant } + ) + .Join( + dbContext.TenantPackages, + combined => combined.Subscription.TenantPackageId, + package => package.Id, + (combined, package) => new { combined.Subscription, combined.Tenant, Package = package } + ) + .ToListAsync(cancellationToken); + + foreach (var item in expiringSubscriptions) + { + // 检查是否已发送过相同天数的提醒(避免重复发送) + var alreadySent = await dbContext.TenantNotifications + .AnyAsync(n => n.TenantId == item.Subscription.TenantId + && n.Message.Contains($"{daysBeforeExpiry}天内到期") + && n.SentAt >= now.AddHours(-24), // 24小时内已发送过 + cancellationToken); + + if (alreadySent) + { + continue; + } + + // 创建续费提醒通知 + var notification = new TenantNotification + { + Id = idGenerator.NextId(), + TenantId = item.Subscription.TenantId, + Title = "订阅续费提醒", + Message = $"您的订阅套餐「{item.Package.Name}」将在 {daysBeforeExpiry} 天内到期(到期时间:{item.Subscription.EffectiveTo:yyyy-MM-dd HH:mm}),请及时续费以免影响使用。", + Severity = daysBeforeExpiry <= 1 + ? TenantNotificationSeverity.Critical + : TenantNotificationSeverity.Warning, + Channel = TenantNotificationChannel.InApp, + SentAt = DateTime.UtcNow, + ReadAt = null, + CreatedAt = DateTime.UtcNow + }; + + dbContext.TenantNotifications.Add(notification); + remindersSent++; + + _logger.LogInformation( + "发送续费提醒: 租户 {TenantName} ({TenantId}), 套餐 {PackageName}, 剩余 {Days} 天", + item.Tenant.Name, item.Subscription.TenantId, item.Package.Name, daysBeforeExpiry); + } + } + + await dbContext.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("续费提醒发送完成,共发送 {Count} 条提醒", remindersSent); + } + catch (Exception ex) + { + _logger.LogError(ex, "发送续费提醒失败"); + throw; + } + } +} + +/// +/// 续费提醒配置选项。 +/// +public sealed class RenewalReminderOptions +{ + /// + /// 执行时间(小时,UTC时间),默认上午10点。 + /// + public int ExecuteHour { get; set; } = 10; + + /// + /// 提醒时间点(到期前N天),默认7天、3天、1天。 + /// + public int[] ReminderDaysBeforeExpiry { get; set; } = { 7, 3, 1 }; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs new file mode 100644 index 0000000..a743e66 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs @@ -0,0 +1,132 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.BackgroundServices; + +/// +/// 订阅到期检查后台服务。 +/// 每天凌晨执行,检查即将到期和已到期的订阅,自动更新状态。 +/// +public sealed class SubscriptionExpiryCheckService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly SubscriptionExpiryCheckOptions _options; + + public SubscriptionExpiryCheckService( + IServiceProvider serviceProvider, + ILogger logger, + IOptions options) + { + _serviceProvider = serviceProvider; + _logger = logger; + _options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("订阅到期检查服务已启动"); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + // 计算下次执行时间(每天凌晨) + var now = DateTime.UtcNow; + var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); + var delay = nextRun - now; + + _logger.LogInformation("订阅到期检查服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); + + await Task.Delay(delay, stoppingToken); + + if (stoppingToken.IsCancellationRequested) + break; + + await CheckExpiringSubscriptionsAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "订阅到期检查服务执行异常"); + // 出错后等待一段时间再重试 + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + } + } + + _logger.LogInformation("订阅到期检查服务已停止"); + } + + private async Task CheckExpiringSubscriptionsAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("开始执行订阅到期检查"); + + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + var gracePeriodDays = _options.GracePeriodDays; + + try + { + // 1. 检查活跃订阅中已到期的,转为宽限期 + var expiredActive = await dbContext.TenantSubscriptions + .Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now) + .ToListAsync(cancellationToken); + + foreach (var subscription in expiredActive) + { + subscription.Status = SubscriptionStatus.GracePeriod; + _logger.LogInformation( + "订阅 {SubscriptionId} (租户 {TenantId}) 已到期,进入宽限期", + subscription.Id, subscription.TenantId); + } + + // 2. 检查宽限期订阅中超过宽限期的,转为暂停 + var gracePeriodExpired = await dbContext.TenantSubscriptions + .Where(s => s.Status == SubscriptionStatus.GracePeriod + && s.EffectiveTo.AddDays(gracePeriodDays) < now) + .ToListAsync(cancellationToken); + + foreach (var subscription in gracePeriodExpired) + { + subscription.Status = SubscriptionStatus.Suspended; + _logger.LogInformation( + "订阅 {SubscriptionId} (租户 {TenantId}) 宽限期已结束,已暂停", + subscription.Id, subscription.TenantId); + } + + // 3. 保存更改 + var changedCount = await dbContext.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "订阅到期检查完成,共更新 {Count} 条记录 (到期转宽限期: {ExpiredCount}, 宽限期转暂停: {SuspendedCount})", + changedCount, expiredActive.Count, gracePeriodExpired.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "订阅到期检查失败"); + throw; + } + } +} + +/// +/// 订阅到期检查配置选项。 +/// +public sealed class SubscriptionExpiryCheckOptions +{ + /// + /// 执行时间(小时,UTC时间),默认凌晨2点。 + /// + public int ExecuteHour { get; set; } = 2; + + /// + /// 宽限期天数,默认7天。 + /// + public int GracePeriodDays { get; set; } = 7; +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json new file mode 100644 index 0000000..7d4bda2 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/appsettings.backgroundservices.json @@ -0,0 +1,16 @@ +{ + "BackgroundServices": { + "SubscriptionExpiryCheck": { + "ExecuteHour": 2, + "GracePeriodDays": 7 + }, + "RenewalReminder": { + "ExecuteHour": 10, + "ReminderDaysBeforeExpiry": [7, 3, 1] + }, + "AutoRenewal": { + "ExecuteHour": 1, + "RenewalDaysBeforeExpiry": 3 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251217092209_AddQuotaPackagesAndPayments.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251217092209_AddQuotaPackagesAndPayments.Designer.cs new file mode 100644 index 0000000..7d2d1ea --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251217092209_AddQuotaPackagesAndPayments.Designer.cs @@ -0,0 +1,7101 @@ +// +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.Migrations +{ + [DbContext(typeof(TakeoutAppDbContext))] + [Migration("20251217092209_AddQuotaPackagesAndPayments")] + partial class AddQuotaPackagesAndPayments + { + /// + 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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionJson") + .IsRequired() + .HasColumnType("text") + .HasComment("触发条件 JSON。"); + + 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("Enabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("MetricDefinitionId") + .HasColumnType("bigint") + .HasComment("关联指标。"); + + b.Property("NotificationChannels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("通知渠道。"); + + b.Property("Severity") + .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", "MetricDefinitionId", "Severity"); + + b.ToTable("metric_alert_rules", null, t => + { + t.HasComment("指标告警规则。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", 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("DefaultAggregation") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("默认聚合方式。"); + + 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("DimensionsJson") + .HasColumnType("text") + .HasComment("维度描述 JSON。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .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", "Code") + .IsUnique(); + + b.ToTable("metric_definitions", null, t => + { + t.HasComment("指标定义,描述可观测的数据点。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", 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("DimensionKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("维度键(JSON)。"); + + b.Property("MetricDefinitionId") + .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("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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("券码或序列号。"); + + b.Property("CouponTemplateId") + .HasColumnType("bigint") + .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("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("发放时间。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单 ID(已使用时记录)。"); + + b.Property("Status") + .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.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间。"); + + b.Property("UserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("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("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("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TotalQuantity") + .HasColumnType("integer") + .HasComment("总发放数量上限。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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.ToTable("promotion_campaigns", null, t => + { + t.HasComment("营销活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSessionId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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", "ChatSessionId", "CreatedAt"); + + b.ToTable("chat_messages", null, t => + { + t.HasComment("会话消息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("bigint") + .HasComment("顾客用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .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", "SessionCode") + .IsUnique(); + + b.ToTable("chat_sessions", null, t => + { + t.HasComment("客服会话。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAgentId") + .HasColumnType("bigint") + .HasComment("指派的客服。"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("关闭时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("bigint") + .HasComment("客户用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("工单详情。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachmentsJson") + .HasColumnType("text") + .HasComment("附件 JSON。"); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsInternal") + .HasColumnType("boolean") + .HasComment("是否内部备注。"); + + b.Property("SupportTicketId") + .HasColumnType("bigint") + .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", "SupportTicketId"); + + b.ToTable("ticket_comments", null, t => + { + t.HasComment("工单评论/流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", 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("DeliveryOrderId") + .HasColumnType("bigint") + .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("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", "DeliveryOrderId", "EventType"); + + b.ToTable("delivery_events", null, t => + { + t.HasComment("配送状态事件流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("获取或设置关联订单 ID。"); + + 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("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", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", null, t => + { + t.HasComment("配送单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") + .HasComment("推广人标识。"); + + b.Property("BuyerUserId") + .HasColumnType("bigint") + .HasComment("用户 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("EstimatedCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("预计佣金。"); + + b.Property("OrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("订单金额。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单。"); + + b.Property("SettledAt") + .HasColumnType("timestamp with time zone") + .HasComment("结算完成时间。"); + + b.Property("Status") + .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", "AffiliatePartnerId", "OrderId") + .IsUnique(); + + b.ToTable("affiliate_orders", null, t => + { + t.HasComment("分销订单记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("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("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("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", "DisplayName"); + + b.ToTable("affiliate_partners", null, t => + { + t.HasComment("分销/推广合作伙伴。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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", "AffiliatePartnerId", "Period") + .IsUnique(); + + b.ToTable("affiliate_payouts", null, t => + { + t.HasComment("佣金结算记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowMakeupCount") + .HasColumnType("integer") + .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("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("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", "Name"); + + b.ToTable("checkin_campaigns", null, t => + { + t.HasComment("签到活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CheckInCampaignId") + .HasColumnType("bigint") + .HasComment("活动标识。"); + + b.Property("CheckInDate") + .HasColumnType("timestamp with time zone") + .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("IsMakeup") + .HasColumnType("boolean") + .HasComment("是否补签。"); + + b.Property("RewardJson") + .IsRequired() + .HasColumnType("text") + .HasComment("获得奖励 JSON。"); + + 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("用户标识。"); + + 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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasComment("状态。"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级评论 ID。"); + + b.Property("PostId") + .HasColumnType("bigint") + .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", "PostId", "CreatedAt"); + + b.ToTable("community_comments", null, t => + { + t.HasComment("社区评论。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .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("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("PostId") + .HasColumnType("bigint") + .HasComment("动态 ID。"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone") + .HasComment("时间戳。"); + + b.Property("ReactionType") + .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.Property("UserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentCount") + .HasColumnType("integer") + .HasComment("当前已参与人数。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("团长用户 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("关联商品或套餐。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("拼团状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasComment("成团时间。"); + + b.Property("TargetCount") + .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", "GroupOrderNo") + .IsUnique(); + + b.ToTable("group_orders", null, t => + { + t.HasComment("拼单活动。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", 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("GroupOrderId") + .HasColumnType("bigint") + .HasComment("拼单活动标识。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("参与时间。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("对应订单标识。"); + + b.Property("Status") + .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.Property("UserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdjustmentType") + .HasColumnType("integer") + .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("InventoryItemId") + .HasColumnType("bigint") + .HasComment("对应的库存记录标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("调整数量,正数增加,负数减少。"); + + b.Property("Reason") + .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", "InventoryItemId", "OccurredAt"); + + b.ToTable("inventory_adjustments", null, t => + { + t.HasComment("库存调整记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .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("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BatchConsumeStrategy") + .HasColumnType("integer") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("IsPresale") + .HasColumnType("boolean") + .HasComment("是否预售商品。"); + + b.Property("IsSoldOut") + .HasColumnType("boolean") + .HasComment("是否标记售罄。"); + + b.Property("Location") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("储位或仓位信息。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("单品限购(覆盖商品级 MaxQuantityPerOrder)。"); + + b.Property("PresaleCapacity") + .HasColumnType("integer") + .HasComment("预售名额(上限)。"); + + b.Property("PresaleEndTime") + .HasColumnType("timestamp with time zone") + .HasComment("预售结束时间(UTC)。"); + + b.Property("PresaleLocked") + .HasColumnType("integer") + .HasComment("当前预售已锁定数量。"); + + b.Property("PresaleStartTime") + .HasColumnType("timestamp with time zone") + .HasComment("预售开始时间(UTC)。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("QuantityOnHand") + .HasColumnType("integer") + .HasComment("可用库存。"); + + b.Property("QuantityReserved") + .HasColumnType("integer") + .HasComment("已锁定库存(订单占用)。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("SafetyStock") + .HasColumnType("integer") + .HasComment("安全库存阈值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "ProductSkuId", "BatchNumber"); + + b.ToTable("inventory_items", null, t => + { + t.HasComment("SKU 在门店的库存信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryLockRecord", 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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("幂等键。"); + + b.Property("IsPresale") + .HasColumnType("boolean") + .HasComment("是否预售锁定。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU ID。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("锁定数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("锁定状态。"); + + b.Property("StoreId") + .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", "IdempotencyKey") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "Status"); + + b.ToTable("inventory_lock_records", null, t => + { + t.HasComment("库存锁定记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChangeValue") + .HasColumnType("integer") + .HasComment("变动数量。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentValue") + .HasColumnType("integer") + .HasComment("当前成长值。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .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", "MemberId", "OccurredAt"); + + b.ToTable("member_growth_logs", null, t => + { + t.HasComment("成长值变动日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(如适用)。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Reason") + .HasColumnType("integer") + .HasComment("变动原因。"); + + b.Property("SourceId") + .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", "MemberId", "OccurredAt"); + + b.ToTable("member_point_ledgers", null, t => + { + t.HasComment("积分变动流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GrowthValue") + .HasColumnType("integer") + .HasComment("成长值/经验值。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("注册时间。"); + + b.Property("MemberTierId") + .HasColumnType("bigint") + .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("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("用户标识。"); + + 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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BenefitsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("等级权益(JSON)。"); + + 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("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("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", "Name") + .IsUnique(); + + b.ToTable("member_tiers", null, t => + { + t.HasComment("会员等级定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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.ToTable("merchants", null, t => + { + t.HasComment("商户主体信息,承载入驻和资质审核结果。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .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(1024) + .HasColumnType("character varying(1024)") + .HasComment("详情描述。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("商户标识。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人 ID。"); + + b.Property("OperatorName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作人名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId"); + + b.ToTable("merchant_audit_logs", null, t => + { + t.HasComment("商户入驻审核日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantCategory", 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("DisplayOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("显示顺序,越小越靠前。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .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", "Name") + .IsUnique(); + + b.ToTable("merchant_categories", null, t => + { + t.HasComment("商户可选类目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .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("bigint") + .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("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("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("bigint") + .HasComment("所属商户标识。"); + + b.Property("Remarks") + .HasColumnType("text") + .HasComment("审核备注或驳回原因。"); + + b.Property("Status") + .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", "MerchantId", "DocumentType"); + + b.ToTable("merchant_documents", null, t => + { + t.HasComment("商户提交的资质或证照材料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", 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("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("邮箱地址。"); + + b.Property("IdentityUserId") + .HasColumnType("bigint") + .HasComment("登录账号 ID(指向统一身份体系)。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .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("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", "MerchantId", "Phone"); + + b.ToTable("merchant_staff", null, t => + { + t.HasComment("商户员工账号,支持门店维度分配。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("关联门店 ID,可空表示独立 POI。"); + + 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", "StoreId"); + + b.ToTable("map_locations", null, t => + { + t.HasComment("地图 POI 信息,用于门店定位和推荐。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("来源通道(小程序、H5 等)。"); + + 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("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("请求时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TargetApp") + .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.Property("UserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("扩展 JSON(规格、加料选项等)。"); + + 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("ProductId") + .HasColumnType("bigint") + .HasComment("商品或 SKU 标识。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称快照。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("自定义备注(口味要求)。"); + + b.Property("ShoppingCartId") + .HasColumnType("bigint") + .HasComment("所属购物车标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("选项 ID(可对应 ProductAddonOption)。"); + + 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.ToTable("cart_item_addons", null, t => + { + t.HasComment("购物车条目的加料/附加项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", 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("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("bigint") + .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("UserId") + .HasColumnType("bigint") + .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("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("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("bigint") + .HasComment("门店标识。"); + + b.Property("TableContext") + .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.Property("UserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .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("bigint") + .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("bigint") + .HasComment("预约 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店。"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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", "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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("自定义属性 JSON。"); + + 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("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .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("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("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注信息。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人标识(可为空表示系统)。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单标识。"); + + b.Property("Status") + .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", "OrderId", "OccurredAt"); + + b.ToTable("order_status_histories", null, t => + { + t.HasComment("订单状态流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .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("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", "RefundNo") + .IsUnique(); + + b.ToTable("refund_requests", null, t => + { + t.HasComment("售后/退款申请。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单标识。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("渠道返回的原始数据 JSON。"); + + b.Property("PaymentRecordId") + .HasColumnType("bigint") + .HasComment("原支付记录标识。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款请求时间。"); + + b.Property("Status") + .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", "PaymentRecordId"); + + b.ToTable("payment_refund_records", null, t => + { + t.HasComment("支付渠道退款流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("所属门店。"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("副标题/卖点。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .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("bigint") + .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("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("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("bigint") + .HasComment("所属商品。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型。"); + + b.Property("SortOrder") + .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", "ProductId", "Name"); + + b.ToTable("product_addon_groups", null, t => + { + t.HasComment("加料/做法分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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.ToTable("product_addon_options", null, t => + { + t.HasComment("加料选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", 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("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称,例如“辣度”“份量”。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型(单选/多选)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("显示排序。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Name"); + + b.ToTable("product_attribute_groups", null, t => + { + t.HasComment("商品规格/属性分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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", "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("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(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("bigint") + .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", "StoreId"); + + b.ToTable("product_categories", null, t => + { + t.HasComment("商品分类。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Caption") + .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("MediaType") + .HasColumnType("integer") + .HasComment("媒体类型。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品标识。"); + + b.Property("SortOrder") + .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.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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("条件描述(JSON),如会员等级、渠道等。"); + + 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("EndTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效结束时间。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("特殊价格。"); + + 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("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("所属商品标识。"); + + b.Property("SkuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("SKU 编码。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StockQuantity") + .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.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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .HasComment("获取或设置所属门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .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("bigint") + .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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .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("bigint") + .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("bigint") + .HasComment("门店。"); + + b.Property("TablePreference") + .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", "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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .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("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", "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("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapacityLimit") + .HasColumnType("integer") + .HasComment("最大接待容量或单量限制。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期几,0 表示周日。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("bigint") + .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", "StoreId", "DayOfWeek"); + + b.ToTable("store_business_hours", null, t => + { + t.HasComment("门店营业时段配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", 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("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("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("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("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("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("bigint") + .HasComment("员工标识。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "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("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("Date") + .HasColumnType("timestamp with time zone") + .HasComment("日期。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsClosed") + .HasColumnType("boolean") + .HasComment("是否全天闭店。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("说明内容。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Date") + .IsUnique(); + + b.ToTable("store_holidays", null, t => + { + t.HasComment("门店休息日或特殊营业日。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StorePickupSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowDaysAhead") + .HasColumnType("integer") + .HasComment("可预约天数(含当天)。"); + + b.Property("AllowToday") + .HasColumnType("boolean") + .HasComment("是否允许当天自提。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultCutoffMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("默认截单分钟(开始前多少分钟截止)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("单笔自提最大份数。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId") + .IsUnique(); + + b.ToTable("store_pickup_settings", null, t => + { + t.HasComment("门店自提配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StorePickupSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasComment("容量(份数)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CutoffMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("截单分钟(开始前多少分钟截止)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("当天结束时间(UTC)。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("档期名称。"); + + b.Property("ReservedCount") + .HasColumnType("integer") + .HasComment("已占用数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("当天开始时间(UTC)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("Weekdays") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("适用星期(逗号分隔 1-7)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("store_pickup_slots", null, t => + { + t.HasComment("门店自提档期。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AreaId") + .HasColumnType("bigint") + .HasComment("所在区域 ID。"); + + b.Property("Capacity") + .HasColumnType("integer") + .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("QrCodeUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("桌码二维码地址。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前桌台状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("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", "StoreId", "TableCode") + .IsUnique(); + + b.ToTable("store_tables", null, t => + { + t.HasComment("桌台信息与二维码绑定。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", 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(256) + .HasColumnType("character varying(256)") + .HasComment("区域描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Name") + .IsUnique(); + + b.ToTable("store_table_areas", null, t => + { + t.HasComment("门店桌台区域配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.OperationLog", 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("OperationType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作类型:BatchExtend, BatchRemind, StatusChange 等。"); + + b.Property("OperatorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作人ID。"); + + b.Property("OperatorName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("操作人名称。"); + + b.Property("Parameters") + .HasColumnType("text") + .HasComment("操作参数(JSON)。"); + + b.Property("Result") + .HasColumnType("text") + .HasComment("操作结果(JSON)。"); + + b.Property("Success") + .HasColumnType("boolean") + .HasComment("是否成功。"); + + b.Property("TargetIds") + .HasColumnType("text") + .HasComment("目标ID列表(JSON)。"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("目标类型:Subscription, Bill 等。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("OperationType", "CreatedAt"); + + b.ToTable("operation_logs", null, t => + { + t.HasComment("运营操作日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.QuotaPackage", 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("IsActive") + .HasColumnType("boolean") + .HasComment("是否上架。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("配额包名称。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("价格。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配额数值。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("排序。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("QuotaType", "IsActive", "SortOrder"); + + b.ToTable("quota_packages", null, t => + { + t.HasComment("配额包定义(平台提供的可购买配额包)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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") + .HasColumnType("text") + .HasComment("LOGO 图片地址。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("租户全称或品牌名称。"); + + b.Property("PrimaryOwnerUserId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Website") + .HasColumnType("text") + .HasComment("官网或主要宣传链接。"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ContactPhone") + .IsUnique(); + + b.ToTable("tenants", null, t => + { + t.HasComment("平台租户信息,描述租户的生命周期与基础资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnnouncementType") + .HasColumnType("integer") + .HasComment("公告类型。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("公告正文(可为 Markdown/HTML,前端自行渲染)。"); + + 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("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("失效时间(UTC),为空表示长期有效。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("展示优先级,数值越大越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AnnouncementType", "IsActive"); + + b.HasIndex("TenantId", "EffectiveFrom", "EffectiveTo"); + + b.ToTable("tenant_announcements", null, t => + { + t.HasComment("租户公告。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncementRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnnouncementId") + .HasColumnType("bigint") + .HasComment("公告 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("ReadAt") + .HasColumnType("timestamp with time zone") + .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("UserId") + .HasColumnType("bigint") + .HasComment("已读用户 ID(后台账号),为空表示租户级已读。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AnnouncementId", "UserId") + .IsUnique(); + + b.ToTable("tenant_announcement_reads", null, t => + { + t.HasComment("租户公告已读记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasComment("操作类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentStatus") + .HasColumnType("integer") + .HasComment("新状态。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("详细描述。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人 ID。"); + + b.Property("OperatorName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作人名称。"); + + b.Property("PreviousStatus") + .HasColumnType("integer") + .HasComment("原状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("关联的租户标识。"); + + 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("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("tenant_audit_logs", null, t => + { + t.HasComment("租户运营审核日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .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("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", "StatementNo") + .IsUnique(); + + b.ToTable("tenant_billing_statements", null, t => + { + t.HasComment("租户账单,用于呈现周期性收费。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("integer") + .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("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("bigint") + .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("bigint") + .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("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("FeaturePoliciesJson") + .HasColumnType("text") + .HasComment("权益明细 JSON,记录自定义特性开关。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否仍启用(平台控制)。"); + + b.Property("IsAllowNewTenantPurchase") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否允许新租户购买/选择(仅影响新购)。"); + + b.Property("IsPublicVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否对外可见(展示页/套餐列表可见性)。"); + + b.Property("IsRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .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("PublishStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("发布状态:0=草稿,1=已发布。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("展示排序,数值越小越靠前。"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasComment("套餐标签(用于展示与对比页)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("YearlyPrice") + .HasColumnType("numeric") + .HasComment("年付价格,单位:人民币元。"); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder"); + + b.HasIndex("PublishStatus", "IsActive", "IsPublicVisible", "IsAllowNewTenantPurchase", "SortOrder"); + + b.ToTable("tenant_packages", null, t => + { + t.HasComment("平台提供的租户套餐定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("BillingStatementId") + .HasColumnType("bigint") + .HasComment("关联的账单 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("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("ProofUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("支付凭证 URL。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TransactionNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "BillingStatementId"); + + b.ToTable("tenant_payments", null, t => + { + t.HasComment("租户支付记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaPackagePurchase", 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("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(可选)。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("购买价格。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间。"); + + b.Property("QuotaPackageId") + .HasColumnType("bigint") + .HasComment("配额包 ID。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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", "QuotaPackageId", "PurchasedAt"); + + b.ToTable("tenant_quota_package_purchases", null, t => + { + t.HasComment("租户配额包购买记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", 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("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("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .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.TenantReviewClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone") + .HasComment("领取时间(UTC)。"); + + b.Property("ClaimedBy") + .HasColumnType("bigint") + .HasComment("领取人用户 ID。"); + + b.Property("ClaimedByName") + .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("ReleasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("释放时间(UTC),未释放时为 null。"); + + 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("ClaimedBy"); + + b.HasIndex("TenantId") + .IsUnique() + .HasFilter("\"ReleasedAt\" IS NULL AND \"DeletedAt\" IS NULL"); + + b.ToTable("tenant_review_claims", null, t => + { + t.HasComment("租户入驻审核领取记录(防止多管理员并发审核)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoRenew") + .HasColumnType("boolean") + .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("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("bigint") + .HasComment("若已排期升降配,对应的新套餐 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("订阅当前状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TenantPackageId") + .HasColumnType("bigint") + .HasComment("当前订阅关联的套餐标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantPackageId"); + + b.ToTable("tenant_subscriptions", null, t => + { + t.HasComment("租户套餐订阅记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscriptionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric") + .HasComment("相关费用。"); + + b.Property("ChangeType") + .HasColumnType("integer") + .HasComment("变更类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Currency") + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasComment("币种。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效时间。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("FromPackageId") + .HasColumnType("bigint") + .HasComment("原套餐 ID。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("租户标识。"); + + b.Property("TenantSubscriptionId") + .HasColumnType("bigint") + .HasComment("对应的订阅 ID。"); + + b.Property("ToPackageId") + .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", "TenantSubscriptionId"); + + b.ToTable("tenant_subscription_histories", null, t => + { + t.HasComment("租户套餐订阅变更记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVerificationProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalDataJson") + .HasColumnType("text") + .HasComment("附加资料(JSON)。"); + + b.Property("BankAccountName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("开户名。"); + + b.Property("BankAccountNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("银行账号。"); + + b.Property("BankName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("银行名称。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("营业执照编号。"); + + b.Property("BusinessLicenseUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .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("LegalPersonIdBackUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("法人身份证反面。"); + + b.Property("LegalPersonIdFrontUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("法人身份证正面。"); + + b.Property("LegalPersonIdNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("法人身份证号。"); + + b.Property("LegalPersonName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("法人姓名。"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("审核备注。"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核时间。"); + + b.Property("ReviewedBy") + .HasColumnType("bigint") + .HasComment("审核人 ID。"); + + b.Property("ReviewedByName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("审核人姓名。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("实名状态。"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasComment("提交时间。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("对应的租户标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId") + .IsUnique(); + + b.ToTable("tenant_verification_profiles", 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/Migrations/20251217092209_AddQuotaPackagesAndPayments.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251217092209_AddQuotaPackagesAndPayments.cs new file mode 100644 index 0000000..79238e8 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251217092209_AddQuotaPackagesAndPayments.cs @@ -0,0 +1,164 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations +{ + /// + public partial class AddQuotaPackagesAndPayments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "operation_logs", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OperationType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "操作类型:BatchExtend, BatchRemind, StatusChange 等。"), + TargetType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "目标类型:Subscription, Bill 等。"), + TargetIds = table.Column(type: "text", nullable: true, comment: "目标ID列表(JSON)。"), + OperatorId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "操作人ID。"), + OperatorName = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "操作人名称。"), + Parameters = table.Column(type: "text", nullable: true, comment: "操作参数(JSON)。"), + Result = table.Column(type: "text", nullable: true, comment: "操作结果(JSON)。"), + Success = 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_operation_logs", x => x.Id); + }, + comment: "运营操作日志。"); + + migrationBuilder.CreateTable( + name: "quota_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: "配额包名称。"), + QuotaType = table.Column(type: "integer", nullable: false, comment: "配额类型。"), + QuotaValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "配额数值。"), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "价格。"), + IsActive = table.Column(type: "boolean", nullable: false, comment: "是否上架。"), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 0, 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。") + }, + constraints: table => + { + table.PrimaryKey("PK_quota_packages", x => x.Id); + }, + comment: "配额包定义(平台提供的可购买配额包)。"); + + migrationBuilder.CreateTable( + name: "tenant_payments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + BillingStatementId = table.Column(type: "bigint", nullable: false, comment: "关联的账单 ID。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "支付金额。"), + Method = table.Column(type: "integer", nullable: false, comment: "支付方式。"), + Status = table.Column(type: "integer", nullable: false, comment: "支付状态。"), + TransactionNo = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "交易号。"), + ProofUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "支付凭证 URL。"), + PaidAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "支付时间。"), + Notes = 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_tenant_payments", x => x.Id); + }, + comment: "租户支付记录。"); + + migrationBuilder.CreateTable( + name: "tenant_quota_package_purchases", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + QuotaPackageId = table.Column(type: "bigint", nullable: false, comment: "配额包 ID。"), + QuotaValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "购买时的配额值。"), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "购买价格。"), + PurchasedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "购买时间。"), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "过期时间(可选)。"), + Notes = 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_tenant_quota_package_purchases", x => x.Id); + }, + comment: "租户配额包购买记录。"); + + migrationBuilder.CreateIndex( + name: "IX_operation_logs_CreatedAt", + table: "operation_logs", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_operation_logs_OperationType_CreatedAt", + table: "operation_logs", + columns: new[] { "OperationType", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_quota_packages_QuotaType_IsActive_SortOrder", + table: "quota_packages", + columns: new[] { "QuotaType", "IsActive", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_tenant_payments_TenantId_BillingStatementId", + table: "tenant_payments", + columns: new[] { "TenantId", "BillingStatementId" }); + + migrationBuilder.CreateIndex( + name: "IX_tenant_quota_package_purchases_TenantId_QuotaPackageId_Purc~", + table: "tenant_quota_package_purchases", + columns: new[] { "TenantId", "QuotaPackageId", "PurchasedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "operation_logs"); + + migrationBuilder.DropTable( + name: "quota_packages"); + + migrationBuilder.DropTable( + name: "tenant_payments"); + + migrationBuilder.DropTable( + name: "tenant_quota_package_purchases"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251217092230_FixIdentitySchemaMismatch.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251217092230_FixIdentitySchemaMismatch.Designer.cs new file mode 100644 index 0000000..6acfbd3 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251217092230_FixIdentitySchemaMismatch.Designer.cs @@ -0,0 +1,681 @@ +// +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("20251217092230_FixIdentitySchemaMismatch")] + partial class FixIdentitySchemaMismatch + { + /// + 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") + .HasColumnType("text") + .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.MenuDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthListJson") + .HasColumnType("text") + .HasComment("按钮权限列表 JSON(存储 MenuAuthItemDto 数组)。"); + + b.Property("Component") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("组件路径(不含 .vue)。"); + + 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("Icon") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("图标标识。"); + + b.Property("IsIframe") + .HasColumnType("boolean") + .HasComment("是否 iframe。"); + + b.Property("KeepAlive") + .HasColumnType("boolean") + .HasComment("是否缓存。"); + + b.Property("Link") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("外链或 iframe 地址。"); + + b.Property("MetaPermissions") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Meta.permissions(逗号分隔)。"); + + b.Property("MetaRoles") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Meta.roles(逗号分隔)。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("菜单名称(前端路由 name)。"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级菜单 ID,根节点为 0。"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("路由路径。"); + + b.Property("RequiredPermissions") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("访问该菜单所需的权限集合(逗号分隔)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .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("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ParentId", "SortOrder"); + + b.ToTable("menu_definitions", 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") + .HasColumnType("text") + .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("ParentId") + .HasColumnType("bigint") + .HasComment("父级权限 ID,根节点为 0。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值,值越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("权限类型(group/leaf)。"); + + 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.HasIndex("TenantId", "ParentId", "SortOrder"); + + 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.RoleTemplate", 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(256) + .HasColumnType("character varying(256)") + .HasComment("模板描述。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("模板名称。"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("模板编码(唯一)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TemplateCode") + .IsUnique(); + + b.ToTable("role_templates", null, t => + { + t.HasComment("角色模板定义(平台级)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RoleTemplatePermission", 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("PermissionCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("权限编码。"); + + b.Property("RoleTemplateId") + .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("RoleTemplateId", "PermissionCode") + .IsUnique(); + + b.ToTable("role_template_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/20251217092230_FixIdentitySchemaMismatch.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251217092230_FixIdentitySchemaMismatch.cs new file mode 100644 index 0000000..9bd30e4 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251217092230_FixIdentitySchemaMismatch.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + /// + public partial class FixIdentitySchemaMismatch : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Avatar", + table: "mini_users", + type: "text", + nullable: true, + comment: "头像地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "头像地址。"); + + migrationBuilder.AlterColumn( + name: "Avatar", + table: "identity_users", + type: "text", + nullable: true, + comment: "头像地址。", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true, + oldComment: "头像地址。"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Avatar", + table: "mini_users", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "头像地址。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "头像地址。"); + + migrationBuilder.AlterColumn( + name: "Avatar", + table: "identity_users", + type: "character varying(256)", + maxLength: 256, + nullable: true, + comment: "头像地址。", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "头像地址。"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs index 21b8b40..e9b454c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs @@ -38,8 +38,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb .HasComment("登录账号。"); b.Property("Avatar") - .HasMaxLength(256) - .HasColumnType("character varying(256)") + .HasColumnType("text") .HasComment("头像地址。"); b.Property("CreatedAt") @@ -228,8 +227,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Avatar") - .HasMaxLength(256) - .HasColumnType("character varying(256)") + .HasColumnType("text") .HasComment("头像地址。"); b.Property("CreatedAt") diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs index e68d586..441723a 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs @@ -5680,6 +5680,167 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.OperationLog", 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("OperationType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作类型:BatchExtend, BatchRemind, StatusChange 等。"); + + b.Property("OperatorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作人ID。"); + + b.Property("OperatorName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("操作人名称。"); + + b.Property("Parameters") + .HasColumnType("text") + .HasComment("操作参数(JSON)。"); + + b.Property("Result") + .HasColumnType("text") + .HasComment("操作结果(JSON)。"); + + b.Property("Success") + .HasColumnType("boolean") + .HasComment("是否成功。"); + + b.Property("TargetIds") + .HasColumnType("text") + .HasComment("目标ID列表(JSON)。"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("目标类型:Subscription, Bill 等。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("OperationType", "CreatedAt"); + + b.ToTable("operation_logs", null, t => + { + t.HasComment("运营操作日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.QuotaPackage", 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("IsActive") + .HasColumnType("boolean") + .HasComment("是否上架。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("配额包名称。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("价格。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配额数值。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("排序。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("QuotaType", "IsActive", "SortOrder"); + + b.ToTable("quota_packages", null, t => + { + t.HasComment("配额包定义(平台提供的可购买配额包)。"); + }); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => { b.Property("Id") @@ -6342,6 +6503,163 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("BillingStatementId") + .HasColumnType("bigint") + .HasComment("关联的账单 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("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("ProofUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("支付凭证 URL。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TransactionNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "BillingStatementId"); + + b.ToTable("tenant_payments", null, t => + { + t.HasComment("租户支付记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaPackagePurchase", 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("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(可选)。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("购买价格。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间。"); + + b.Property("QuotaPackageId") + .HasColumnType("bigint") + .HasComment("配额包 ID。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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", "QuotaPackageId", "PurchasedAt"); + + b.ToTable("tenant_quota_package_purchases", null, t => + { + t.HasComment("租户配额包购买记录。"); + }); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => { b.Property("Id")