From c5e2501e1aa6c527c26e8cf63198e5e00d10a6a2 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 03:07:01 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8A=BD=E7=A6=BB=20Docs/BuildingB?= =?UTF-8?q?locks=20=E5=AD=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 6 + Directory.Build.props | 2 +- Document/01_项目概述.md | 188 - Document/02_技术架构.md | 253 - Document/03_数据库设计.md | 641 - Document/04_API接口设计.md | 885 - Document/05_部署运维.md | 1058 - Document/06_开发规范.md | 395 - Document/07_系统架构图.md | 321 - Document/08_AI精简开发规范.md | 145 - Document/09_服务器文档.md | 79 - Document/10_设计期DbContext配置指引.md | 131 - Document/11_SystemTodo.md | 67 - Document/12_BusinessTodo.md | 73 - Document/13_AppSeed说明.md | 62 - Document/14_OpenTelemetry接入指引.md | 40 - Document/15_API边界与自检清单.md | 53 - Document/Completed/后端套餐管理.md | 79 - Document/README.md | 195 - Document/infra/postgres_redis.md | 101 - Document/swagger/swagger20251215095917.json | 19254 ---------------- Document/xmind_小程序版模块规划.md | 89 - Document/xmind_小程序版模块规划.xmind | Bin 341473 -> 0 bytes README.md | 232 +- TakeoutSaaS.BuildingBlocks | 1 + TakeoutSaaS.Docs | 1 + TakeoutSaaS.sln | 6 +- deploy/dbhub/dbhub.toml | 15 - deploy/postgres/README.md | 47 - deploy/postgres/bootstrap.ps1 | 37 - deploy/postgres/create_databases.sql | 102 - deploy/postgres/migrate_logs_to_logs_db.sql | 89 - deploy/prometheus/alert.rules.yml | 34 - deploy/prometheus/prometheus.yml | 28 - deploy/redis/README.md | 34 - deploy/redis/redis.conf | 25 - scripts/build-adminapi-forlinux.sh | 35 - scripts/build-adminapi.ps1 | 46 - src/Api/TakeoutSaaS.AdminApi/Dockerfile | 29 +- .../TakeoutSaaS.AdminApi.csproj | 2 +- .../TakeoutSaaS.MiniApi.csproj | Bin 4588 -> 4730 bytes .../TakeoutSaaS.UserApi.csproj | Bin 3904 -> 4046 bytes .../TakeoutSaaS.Application.csproj | Bin 2924 -> 2992 bytes .../Constants/DatabaseConstants.cs | 27 - .../Constants/ErrorCodes.cs | 47 - .../Data/DatabaseConnectionRole.cs | 17 - .../Data/IDapperExecutor.cs | 48 - .../Diagnostics/TraceContext.cs | 37 - .../Entities/AuditableEntityBase.cs | 37 - .../Entities/EntityBase.cs | 12 - .../Entities/IAuditableEntity.cs | 37 - .../Entities/IMultiTenantEntity.cs | 12 - .../Entities/ISoftDeleteEntity.cs | 12 - .../Entities/MultiTenantEntityBase.cs | 12 - .../Exceptions/BusinessException.cs | 13 - .../Exceptions/ValidationException.cs | 13 - .../Ids/IIdGenerator.cs | 13 - .../Ids/IdGeneratorOptions.cs | 26 - .../Results/ApiResponse.NonGeneric.cs | 43 - .../Results/ApiResponse.cs | 203 - .../Results/PagedResult.cs | 36 - .../Security/ICurrentUserAccessor.cs | 17 - .../Serialization/SnowflakeIdJsonConverter.cs | 52 - .../TakeoutSaaS.Shared.Abstractions.csproj | 13 - .../Tenancy/ITenantContextAccessor.cs | 12 - .../Tenancy/ITenantProvider.cs | 13 - .../Tenancy/TenantConstants.cs | 12 - .../Tenancy/TenantContext.cs | 38 - .../Ids/SnowflakeIdGenerator.cs | 104 - .../TakeoutSaaS.Shared.Kernel.csproj | 14 - .../Api/BaseApiController.cs | 15 - .../ApplicationBuilderExtensions.cs | 22 - .../Extensions/ServiceCollectionExtensions.cs | 53 - .../Filters/ApiResponseResultFilter.cs | 115 - .../Filters/ValidateModelAttribute.cs | 33 - .../Middleware/CorrelationIdMiddleware.cs | 108 - .../Middleware/ExceptionHandlingMiddleware.cs | 139 - .../Middleware/RequestLoggingMiddleware.cs | 41 - .../Middleware/SecurityHeadersMiddleware.cs | 25 - .../Security/ClaimsPrincipalExtensions.cs | 27 - .../HttpContextCurrentUserAccessor.cs | 35 - .../Security/TenantHttpContextExtensions.cs | 31 - .../Swagger/ConfigureSwaggerOptions.cs | 60 - .../Swagger/SwaggerDocumentSettings.cs | 22 - .../Swagger/SwaggerExtensions.cs | 87 - .../TakeoutSaaS.Shared.Web.csproj | 30 - .../TakeoutSaaS.Domain.csproj | 2 +- .../TakeoutSaaS.Infrastructure.csproj | Bin 4616 -> 4752 bytes .../TakeoutSaaS.Module.Authorization.csproj | Bin 1460 -> 1528 bytes .../TakeoutSaaS.Module.Delivery.csproj | 2 +- .../TakeoutSaaS.Module.Dictionary.csproj | 2 +- .../TakeoutSaaS.Module.Messaging.csproj | 2 +- .../TakeoutSaaS.Module.Scheduler.csproj | 2 +- .../TakeoutSaaS.Module.Sms.csproj | 2 +- .../TakeoutSaaS.Module.Storage.csproj | Bin 2336 -> 2404 bytes .../TakeoutSaaS.Module.Tenancy.csproj | 2 +- 商品模块_API设计_v1.md | 426 - 97 files changed, 57 insertions(+), 27026 deletions(-) create mode 100644 .gitmodules delete mode 100644 Document/01_项目概述.md delete mode 100644 Document/02_技术架构.md delete mode 100644 Document/03_数据库设计.md delete mode 100644 Document/04_API接口设计.md delete mode 100644 Document/05_部署运维.md delete mode 100644 Document/06_开发规范.md delete mode 100644 Document/07_系统架构图.md delete mode 100644 Document/08_AI精简开发规范.md delete mode 100644 Document/09_服务器文档.md delete mode 100644 Document/10_设计期DbContext配置指引.md delete mode 100644 Document/11_SystemTodo.md delete mode 100644 Document/12_BusinessTodo.md delete mode 100644 Document/13_AppSeed说明.md delete mode 100644 Document/14_OpenTelemetry接入指引.md delete mode 100644 Document/15_API边界与自检清单.md delete mode 100644 Document/Completed/后端套餐管理.md delete mode 100644 Document/README.md delete mode 100644 Document/infra/postgres_redis.md delete mode 100644 Document/swagger/swagger20251215095917.json delete mode 100644 Document/xmind_小程序版模块规划.md delete mode 100644 Document/xmind_小程序版模块规划.xmind create mode 160000 TakeoutSaaS.BuildingBlocks create mode 160000 TakeoutSaaS.Docs delete mode 100644 deploy/dbhub/dbhub.toml delete mode 100644 deploy/postgres/README.md delete mode 100644 deploy/postgres/bootstrap.ps1 delete mode 100644 deploy/postgres/create_databases.sql delete mode 100644 deploy/postgres/migrate_logs_to_logs_db.sql delete mode 100644 deploy/prometheus/alert.rules.yml delete mode 100644 deploy/prometheus/prometheus.yml delete mode 100644 deploy/redis/README.md delete mode 100644 deploy/redis/redis.conf delete mode 100755 scripts/build-adminapi-forlinux.sh delete mode 100644 scripts/build-adminapi.ps1 delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Constants/DatabaseConstants.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs delete mode 100644 src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj delete mode 100644 商品模块_API设计_v1.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c0e417a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "TakeoutSaaS.BuildingBlocks"] + path = TakeoutSaaS.BuildingBlocks + url = git@github.com:msumshk/TakeoutSaaS.BuildingBlocks.git +[submodule "TakeoutSaaS.Docs"] + path = TakeoutSaaS.Docs + url = git@github.com:msumshk/TakeoutSaaS.Docs.git diff --git a/Directory.Build.props b/Directory.Build.props index da983d4..e175c9b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ - + diff --git a/Document/01_项目概述.md b/Document/01_项目概述.md deleted file mode 100644 index 3d8920c..0000000 --- a/Document/01_项目概述.md +++ /dev/null @@ -1,188 +0,0 @@ -# 外卖SaaS系统 - 项目概述 - -## 1. 项目简介 - -### 1.1 项目背景 -外卖SaaS系统是一个面向餐饮企业的多租户外卖管理平台,旨在为中小型餐饮企业提供完整的外卖业务解决方案。系统支持商家入驻、菜品管理、订单处理、配送管理等核心功能。 - -### 1.2 项目目标 -- 提供稳定、高效的外卖业务管理平台 -- 支持多租户架构,实现数据隔离和资源共享 -- 提供完善的商家管理和运营工具 -- 支持灵活的配送模式(自配送、第三方配送) -- 提供实时数据分析和报表功能 - -### 1.3 核心价值 -- **降低成本**:SaaS模式降低企业IT投入成本 -- **快速上线**:开箱即用,快速开展外卖业务 -- **灵活扩展**:支持业务增长和功能定制 -- **数据驱动**:提供数据分析,辅助经营决策 - -## 2. 业务模块 - -### 2.1 租户管理模块 -- 租户注册与认证 -- 租户信息管理 -- 套餐订阅管理 -- 权限与配额管理 - -### 2.2 商家管理模块 -- 商家入驻审核 -- 商家信息管理 -- 门店管理(支持多门店) -- 营业时间设置 -- 配送范围设置 - -### 2.3 菜品管理模块 -- 菜品分类管理 -- 菜品信息管理(名称、价格、图片、描述) -- 菜品规格管理(大份、小份等) -- 菜品库存管理 -- 菜品上下架管理 - -### 2.4 订单管理模块 -- 订单创建与支付 -- 订单状态流转(待支付、待接单、制作中、配送中、已完成、已取消) -- 订单查询与筛选 -- 订单退款处理 -- 订单统计分析 - -### 2.5 配送管理模块 -- 配送员管理 -- 配送任务分配 -- 配送路线规划 -- 配送状态跟踪 -- 配送费用计算 - -### 2.6 用户管理模块 -- 用户注册与登录 -- 用户信息管理 -- 收货地址管理 -- 用户订单历史 -- 用户评价管理 - -### 2.7 支付管理模块 -- 多支付方式支持(微信、支付宝、余额) -- 支付回调处理 -- 退款处理 -- 账单管理 - -### 2.8 营销管理模块 -- 优惠券管理 -- 满减活动 -- 会员积分 -- 推广活动 - -### 2.9 数据分析模块 -- 销售数据统计 -- 订单趋势分析 -- 用户行为分析 -- 商家经营报表 -- 平台运营大盘 - -### 2.10 系统管理模块 -- 系统配置管理 -- 日志管理 -- 权限管理 -- 消息通知管理 - -## 3. 用户角色 - -### 3.1 平台管理员(Web管理端) -- 管理所有租户和商家 -- 系统配置和维护 -- 数据监控和分析 -- 审核商家入驻 -- 平台运营管理 - -### 3.2 租户管理员(Web管理端) -- 管理租户下的所有商家 -- 查看租户数据报表 -- 管理租户套餐和权限 -- 租户配置管理 - -### 3.3 商家管理员(Web管理端) -- 管理门店信息 -- 管理菜品和订单 -- 查看经营数据 -- 管理配送(自配送或第三方配送对接) -- 营销活动管理 - -### 3.4 商家员工(Web管理端) -- 处理订单(接单/出餐/发货) -- 更新菜品状态 -- 订单打印与出餐看板 - -### 3.5 普通用户/消费者(小程序端 + Web用户端) -- 浏览商家和菜品 -- 下单和支付 -- 查看订单状态 -- 评价和反馈 -- 收货地址管理 -- 优惠券领取和使用 - -## 4. 系统特性 - -### 4.1 多租户架构 -- 数据隔离:每个租户数据完全隔离 -- 资源共享:共享基础设施,降低成本 -- 灵活配置:支持租户级别的个性化配置 - -### 4.2 高可用性 -- 服务高可用:支持集群部署 -- 数据高可用:数据库主从复制 -- 故障自动恢复 - -### 4.3 高性能 -- 缓存策略:Redis缓存热点数据 -- 数据库优化:索引优化、查询优化 -- 异步处理:消息队列处理耗时任务 - -### 4.4 安全性 -- 身份认证:JWT Token认证 -- 权限控制:基于角色的访问控制(RBAC) -- 数据加密:敏感数据加密存储 -- 接口防护:限流、防重放攻击 - -### 4.5 可扩展性 -- 微服务架构:支持服务独立扩展 -- 插件化设计:支持功能模块插拔 -- API开放:提供开放API接口 - -## 5. 技术选型 - -- **后端框架**:.NET 10 -- **ORM框架**:Entity Framework Core 10 + Dapper -- **数据库**:PostgreSQL 16+ -- **缓存**:Redis 7.0+ -- **消息队列**:RabbitMQ 3.12+ -- **API文档**:Swagger/OpenAPI -- **日志**:Serilog -- **认证授权**:JWT + OAuth2.0 - -## 6. 项目里程碑 - -### Phase 1:基础功能(1-2个月) -- 租户管理 -- 商家管理 -- 菜品管理 -- 订单管理(基础流程) - -### Phase 2:核心功能(2-3个月) -- 配送管理 -- 支付集成 -- 用户管理 -- 基础营销功能 - -### Phase 3:高级功能(3-4个月) -- 数据分析 -- 高级营销 -- 系统优化 -- 性能调优 - -### Phase 4:完善与上线(1个月) -- 测试与修复 -- 文档完善 -- 部署上线 -- 运维监控 - diff --git a/Document/02_技术架构.md b/Document/02_技术架构.md deleted file mode 100644 index e7d1861..0000000 --- a/Document/02_技术架构.md +++ /dev/null @@ -1,253 +0,0 @@ -# 外卖SaaS系统 - 技术架构 - -## 1. 技术栈 - -### 1.1 后端技术栈 -- **.NET 10**:最新的.NET平台,提供高性能和现代化开发体验 -- **ASP.NET Core Web API**:构建RESTful API服务 -- **Entity Framework Core 10**:最新ORM框架,用于复杂查询和实体管理 -- **Dapper 2.1+**:轻量级ORM,用于高性能查询和批量操作 -- **PostgreSQL 16+**:主数据库,支持JSON、全文搜索等高级特性 -- **Redis 7.0+**:缓存和会话存储 -- **RabbitMQ 3.12+**:消息队列,处理异步任务 - -### 1.2 开发工具和框架 -- **AutoMapper**:对象映射 -- **FluentValidation**:数据验证 -- **Serilog**:结构化日志 -- **MediatR**:CQRS和中介者模式实现 -- **Hangfire**:后台任务调度 -- **Polly**:弹性和瞬态故障处理 -- **Swagger/Swashbuckle**:API文档生成 - -### 1.3 认证授权 -- **JWT (JSON Web Token)**:无状态身份认证 -- **IdentityServer/Duende IdentityServer**:OAuth2.0和OpenID Connect -- **ASP.NET Core Identity**:用户身份管理 - -### 1.4 测试框架 -- **xUnit**:单元测试框架 -- **Moq**:Mock框架 -- **FluentAssertions**:断言库 -- **Testcontainers**:集成测试容器化 - -### 1.5 DevOps工具 -- **Docker**:容器化部署 -- **Docker Compose**:本地开发环境 -- **GitHub Actions/GitLab CI**:CI/CD流水线 -- **Nginx**:反向代理和负载均衡 - -## 2. 系统架构 - -### 2.1 整体架构 -``` -┌─────────────────────────────────────────────────────────────┐ -│ 客户端层 │ -│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ -│ │ Web管理端 │ │ Web用户端 │ │ 小程序端(用户) │ │ -│ └──────────┘ └──────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ API网关层 │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ Nginx / API Gateway (路由、限流、认证、日志) │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 应用服务层 │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │租户服务 │ │商家服务 │ │订单服务 │ │配送服务 │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │用户服务 │ │支付服务 │ │营销服务 │ │通知服务 │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 基础设施层 │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │PostgreSQL │ │ Redis │ │ RabbitMQ │ │ MinIO │ │ -│ │ (主库) │ │ (缓存) │ │ (消息队列)│ │(对象存储) │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 2.2 分层架构 - -#### 2.2.1 表现层 (Presentation Layer) -- **TakeoutSaaS.AdminApi**:管理后台 Web API 项目(/api/admin/v1) - - Controllers:后台管理API控制器 - - Filters:过滤器(异常处理、日志、验证) - - Middleware:中间件(认证、租户识别、RBAC) - - Models:请求/响应DTO -- **TakeoutSaaS.MiniApi**:小程序/用户端 Web API 项目(/api/mini/v1) - - Controllers:用户端API控制器 - - Filters:过滤器(异常处理、限流、签名校验) - - Middleware:中间件(小程序登录态、租户识别、CORS) - - Models:请求/响应DTO - -#### 2.2.2 应用层 (Application Layer) -- **TakeoutSaaS.Application**:应用逻辑 - - Services:应用服务 - - DTOs:数据传输对象 - - Interfaces:服务接口 - - Validators:FluentValidation验证器 - - Mappings:AutoMapper配置 - - Commands/Queries:CQRS命令和查询 - -#### 2.2.3 领域层 (Domain Layer) -- **TakeoutSaaS.Domain**:领域模型 - - Entities:实体类 - - ValueObjects:值对象 - - Enums:枚举 - - Events:领域事件 - - Interfaces:仓储接口 - - Specifications:规约模式 - -#### 2.2.4 基础设施层 (Infrastructure Layer) -- **TakeoutSaaS.Infrastructure**:基础设施实现 - - Data:数据访问 - - EFCore:EF Core DbContext和配置 - - Dapper:Dapper查询实现 - - Repositories:仓储实现 - - Migrations:数据库迁移 - - Cache:Redis缓存实现 - - MessageQueue:RabbitMQ实现 - - ExternalServices:第三方服务集成 - -#### 2.2.5 共享层 (Shared Layer) -- **TakeoutSaaS.Shared**:共享组件 - - Constants:常量定义 - - Exceptions:自定义异常 - - Extensions:扩展方法 - - Helpers:辅助类 - - Results:统一返回结果 - -## 3. 核心设计模式 - -### 3.1 多租户模式 -- **数据隔离策略**:每个租户独立Schema -- **租户识别**:通过HTTP Header或JWT Token识别租户 -- **动态切换**:运行时动态切换数据库连接 - -### 3.2 CQRS模式 -- **命令(Command)**:处理写操作,修改数据 -- **查询(Query)**:处理读操作,不修改数据 -- **分离优势**:读写分离,优化性能 - -### 3.3 仓储模式 -- **抽象数据访问**:统一数据访问接口 -- **EF Core仓储**:复杂查询和事务处理 -- **Dapper仓储**:高性能查询和批量操作 - -### 3.4 工作单元模式 -- **事务管理**:统一管理数据库事务 -- **批量提交**:减少数据库往返次数 - -### 3.5 领域驱动设计(DDD) -- **聚合根**:定义实体边界 -- **值对象**:不可变对象 -- **领域事件**:解耦业务逻辑 - -## 4. 数据访问策略 - -### 4.1 EF Core使用场景 -- 复杂的实体关系查询 -- 需要变更跟踪的操作 -- 事务性操作 -- 数据库迁移管理 - -### 4.2 Dapper使用场景 -- 高性能查询(大数据量) -- 复杂SQL查询 -- 批量插入/更新 -- 报表统计查询 -- 存储过程调用 - -### 4.3 混合使用策略 -```csharp -// EF Core - 复杂查询和实体管理 -public async Task GetOrderWithDetailsAsync(Guid orderId) -{ - return await _dbContext.Orders - .Include(o => o.OrderItems) - .Include(o => o.Customer) - .FirstOrDefaultAsync(o => o.Id == orderId); -} - -// Dapper - 高性能统计查询 -public async Task GetOrderStatisticsAsync(DateTime startDate, DateTime endDate) -{ - var sql = @" - SELECT - COUNT(*) as TotalOrders, - SUM(total_amount) as TotalAmount, - AVG(total_amount) as AvgAmount - FROM orders - WHERE created_at BETWEEN @StartDate AND @EndDate"; - - return await _connection.QueryFirstOrDefaultAsync(sql, - new { StartDate = startDate, EndDate = endDate }); -} -``` - -## 5. 缓存策略 - -### 5.1 缓存层次 -- **L1缓存**:内存缓存(IMemoryCache)- 进程内缓存 -- **L2缓存**:Redis缓存 - 分布式缓存 - -### 5.2 缓存场景 -- 商家信息缓存(30分钟) -- 菜品信息缓存(15分钟) -- 用户会话缓存(2小时) -- 配置信息缓存(1小时) -- 热点数据缓存(动态过期) - -### 5.3 缓存更新策略 -- **Cache-Aside**:旁路缓存,先查缓存,未命中查数据库 -- **Write-Through**:写入时同步更新缓存 -- **Write-Behind**:异步更新缓存 - -## 6. 消息队列应用 - -### 6.1 异步任务 -- 订单状态变更通知 -- 短信/邮件发送 -- 数据统计计算 -- 日志持久化 - -### 6.2 事件驱动 -- 订单创建事件 -- 支付成功事件 -- 配送状态变更事件 - -## 7. 安全设计 - -### 7.1 认证机制 -- JWT Token认证 -- Refresh Token刷新 -- Token过期管理 - -### 7.2 授权机制 -- 基于角色的访问控制(RBAC) -- 基于策略的授权 -- 资源级权限控制 - -### 7.3 数据安全 -- 敏感数据加密(密码、支付信息) -- HTTPS传输加密 -- SQL注入防护 -- XSS防护 - -### 7.4 接口安全 -- 请求签名验证 -- 接口限流(Rate Limiting) -- 防重放攻击 -- CORS跨域配置 - diff --git a/Document/03_数据库设计.md b/Document/03_数据库设计.md deleted file mode 100644 index 93074df..0000000 --- a/Document/03_数据库设计.md +++ /dev/null @@ -1,641 +0,0 @@ -# 外卖SaaS系统 - 数据库设计 - -## 1. 数据库设计原则 - -### 1.1 命名规范 -- **表名**:小写字母,下划线分隔,复数形式(如:`orders`, `order_items`) -- **字段名**:小写字母,下划线分隔(如:`created_at`, `total_amount`) -- **主键**:统一使用 `id`,类型为 UUID -- **外键**:`表名_id`(如:`order_id`, `merchant_id`) -- **索引**:`idx_表名_字段名`(如:`idx_orders_merchant_id`) - -### 1.2 通用字段 -所有表都包含以下字段: -- `id`:UUID,主键 -- `created_at`:TIMESTAMP,创建时间 -- `updated_at`:TIMESTAMP,更新时间 -- `deleted_at`:TIMESTAMP,软删除时间(可选) -- `tenant_id`:UUID,租户ID(多租户隔离) - -### 1.3 数据类型规范 -- **金额**:DECIMAL(18,2) -- **时间**:TIMESTAMP WITH TIME ZONE -- **布尔**:BOOLEAN -- **枚举**:VARCHAR 或 INTEGER -- **JSON数据**:JSONB - -## 2. 核心表结构 - -### 2.1 租户管理 - -#### tenants(租户表) -```sql -CREATE TABLE tenants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(100) NOT NULL, - code VARCHAR(50) UNIQUE NOT NULL, - contact_name VARCHAR(50), - contact_phone VARCHAR(20), - contact_email VARCHAR(100), - status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:冻结 3:过期 - subscription_plan VARCHAR(50), -- 订阅套餐 - subscription_start_date TIMESTAMP WITH TIME ZONE, - subscription_end_date TIMESTAMP WITH TIME ZONE, - max_merchants INTEGER DEFAULT 10, -- 最大商家数 - max_orders_per_day INTEGER DEFAULT 1000, -- 每日订单限额 - settings JSONB, -- 租户配置 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_tenants_code ON tenants(code); -CREATE INDEX idx_tenants_status ON tenants(status); -``` - -### 2.2 商家管理 - -#### merchants(商家表) -```sql -CREATE TABLE merchants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - name VARCHAR(100) NOT NULL, - logo_url VARCHAR(500), - description TEXT, - contact_phone VARCHAR(20), - contact_person VARCHAR(50), - business_license VARCHAR(100), -- 营业执照号 - status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:休息 3:停业 - rating DECIMAL(3,2) DEFAULT 0, -- 评分 - total_sales INTEGER DEFAULT 0, -- 总销量 - settings JSONB, -- 商家配置 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_merchants_tenant_id ON merchants(tenant_id); -CREATE INDEX idx_merchants_status ON merchants(status); -``` - -#### merchant_stores(门店表) -```sql -CREATE TABLE merchant_stores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - merchant_id UUID NOT NULL REFERENCES merchants(id), - name VARCHAR(100) NOT NULL, - address VARCHAR(500) NOT NULL, - latitude DECIMAL(10,7), -- 纬度 - longitude DECIMAL(10,7), -- 经度 - phone VARCHAR(20), - business_hours JSONB, -- 营业时间 {"monday": {"open": "09:00", "close": "22:00"}} - delivery_range INTEGER DEFAULT 3000, -- 配送范围(米) - min_order_amount DECIMAL(18,2) DEFAULT 0, -- 起送价 - delivery_fee DECIMAL(18,2) DEFAULT 0, -- 配送费 - status INTEGER NOT NULL DEFAULT 1, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_merchant_stores_merchant_id ON merchant_stores(merchant_id); -CREATE INDEX idx_merchant_stores_location ON merchant_stores USING GIST(point(longitude, latitude)); -``` - -### 2.3 菜品管理 - -#### categories(菜品分类表) -```sql -CREATE TABLE categories ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - merchant_id UUID NOT NULL REFERENCES merchants(id), - name VARCHAR(50) NOT NULL, - sort_order INTEGER DEFAULT 0, - status INTEGER NOT NULL DEFAULT 1, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_categories_merchant_id ON categories(merchant_id); -``` - -#### dishes(菜品表) -```sql -CREATE TABLE dishes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - merchant_id UUID NOT NULL REFERENCES merchants(id), - category_id UUID REFERENCES categories(id), - name VARCHAR(100) NOT NULL, - description TEXT, - image_url VARCHAR(500), - price DECIMAL(18,2) NOT NULL, - original_price DECIMAL(18,2), -- 原价 - unit VARCHAR(20) DEFAULT '份', -- 单位 - stock INTEGER, -- 库存(NULL表示不限) - sales_count INTEGER DEFAULT 0, -- 销量 - rating DECIMAL(3,2) DEFAULT 0, -- 评分 - sort_order INTEGER DEFAULT 0, - status INTEGER NOT NULL DEFAULT 1, -- 1:上架 2:下架 - tags JSONB, -- 标签 ["热销", "新品"] - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_dishes_merchant_id ON dishes(merchant_id); -CREATE INDEX idx_dishes_category_id ON dishes(category_id); -CREATE INDEX idx_dishes_status ON dishes(status); -``` - -#### dish_specs(菜品规格表) -```sql -CREATE TABLE dish_specs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - dish_id UUID NOT NULL REFERENCES dishes(id), - name VARCHAR(50) NOT NULL, -- 规格名称(如:大份、小份) - price DECIMAL(18,2) NOT NULL, - stock INTEGER, - status INTEGER NOT NULL DEFAULT 1, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_dish_specs_dish_id ON dish_specs(dish_id); -``` - -### 2.4 用户管理 - -#### users(用户表) -```sql -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - phone VARCHAR(20) UNIQUE NOT NULL, - nickname VARCHAR(50), - avatar_url VARCHAR(500), - gender INTEGER, -- 0:未知 1:男 2:女 - birthday DATE, - balance DECIMAL(18,2) DEFAULT 0, -- 余额 - points INTEGER DEFAULT 0, -- 积分 - status INTEGER NOT NULL DEFAULT 1, - last_login_at TIMESTAMP WITH TIME ZONE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_users_phone ON users(phone); -``` - -#### user_addresses(用户地址表) -```sql -CREATE TABLE user_addresses ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - contact_name VARCHAR(50) NOT NULL, - contact_phone VARCHAR(20) NOT NULL, - province VARCHAR(50), - city VARCHAR(50), - district VARCHAR(50), - address VARCHAR(500) NOT NULL, - house_number VARCHAR(50), -- 门牌号 - latitude DECIMAL(10,7), - longitude DECIMAL(10,7), - is_default BOOLEAN DEFAULT FALSE, - label VARCHAR(20), -- 标签:家、公司等 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_user_addresses_user_id ON user_addresses(user_id); -``` - -### 2.5 订单管理 - -#### orders(订单表) -```sql -CREATE TABLE orders ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - order_no VARCHAR(50) UNIQUE NOT NULL, -- 订单号 - merchant_id UUID NOT NULL REFERENCES merchants(id), - store_id UUID NOT NULL REFERENCES merchant_stores(id), - user_id UUID NOT NULL REFERENCES users(id), - - -- 收货信息 - delivery_address VARCHAR(500) NOT NULL, - delivery_latitude DECIMAL(10,7), - delivery_longitude DECIMAL(10,7), - contact_name VARCHAR(50) NOT NULL, - contact_phone VARCHAR(20) NOT NULL, - - -- 金额信息 - dish_amount DECIMAL(18,2) NOT NULL, -- 菜品金额 - delivery_fee DECIMAL(18,2) DEFAULT 0, -- 配送费 - package_fee DECIMAL(18,2) DEFAULT 0, -- 打包费 - discount_amount DECIMAL(18,2) DEFAULT 0, -- 优惠金额 - total_amount DECIMAL(18,2) NOT NULL, -- 总金额 - actual_amount DECIMAL(18,2) NOT NULL, -- 实付金额 - - -- 订单状态 - status INTEGER NOT NULL DEFAULT 1, -- 1:待支付 2:待接单 3:制作中 4:待配送 5:配送中 6:已完成 7:已取消 - payment_status INTEGER DEFAULT 0, -- 0:未支付 1:已支付 2:已退款 - payment_method VARCHAR(20), -- 支付方式 - payment_time TIMESTAMP WITH TIME ZONE, - - -- 时间信息 - estimated_delivery_time TIMESTAMP WITH TIME ZONE, -- 预计送达时间 - accepted_at TIMESTAMP WITH TIME ZONE, -- 接单时间 - cooking_at TIMESTAMP WITH TIME ZONE, -- 开始制作时间 - delivered_at TIMESTAMP WITH TIME ZONE, -- 送达时间 - completed_at TIMESTAMP WITH TIME ZONE, -- 完成时间 - cancelled_at TIMESTAMP WITH TIME ZONE, -- 取消时间 - - remark TEXT, -- 备注 - cancel_reason TEXT, -- 取消原因 - - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_orders_tenant_id ON orders(tenant_id); -CREATE INDEX idx_orders_order_no ON orders(order_no); -CREATE INDEX idx_orders_merchant_id ON orders(merchant_id); -CREATE INDEX idx_orders_user_id ON orders(user_id); -CREATE INDEX idx_orders_status ON orders(status); -CREATE INDEX idx_orders_created_at ON orders(created_at); -``` - -#### order_items(订单明细表) -```sql -CREATE TABLE order_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - order_id UUID NOT NULL REFERENCES orders(id), - dish_id UUID NOT NULL REFERENCES dishes(id), - dish_name VARCHAR(100) NOT NULL, -- 冗余字段,防止菜品被删除 - dish_image_url VARCHAR(500), - spec_id UUID REFERENCES dish_specs(id), - spec_name VARCHAR(50), - price DECIMAL(18,2) NOT NULL, -- 单价 - quantity INTEGER NOT NULL, -- 数量 - amount DECIMAL(18,2) NOT NULL, -- 小计 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_order_items_order_id ON order_items(order_id); -CREATE INDEX idx_order_items_dish_id ON order_items(dish_id); -``` - -### 2.6 配送管理 - -#### delivery_drivers(配送员表) -```sql -CREATE TABLE delivery_drivers ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - merchant_id UUID REFERENCES merchants(id), -- NULL表示平台配送员 - name VARCHAR(50) NOT NULL, - phone VARCHAR(20) UNIQUE NOT NULL, - id_card VARCHAR(18), -- 身份证号 - vehicle_type VARCHAR(20), -- 车辆类型:电动车、摩托车 - vehicle_number VARCHAR(20), -- 车牌号 - status INTEGER NOT NULL DEFAULT 1, -- 1:空闲 2:配送中 3:休息 4:离线 - current_latitude DECIMAL(10,7), -- 当前位置 - current_longitude DECIMAL(10,7), - rating DECIMAL(3,2) DEFAULT 0, - total_deliveries INTEGER DEFAULT 0, -- 总配送单数 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_delivery_drivers_merchant_id ON delivery_drivers(merchant_id); -CREATE INDEX idx_delivery_drivers_status ON delivery_drivers(status); -``` - -#### delivery_tasks(配送任务表) -```sql -CREATE TABLE delivery_tasks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - order_id UUID NOT NULL REFERENCES orders(id), - driver_id UUID REFERENCES delivery_drivers(id), - pickup_address VARCHAR(500) NOT NULL, -- 取餐地址 - pickup_latitude DECIMAL(10,7), - pickup_longitude DECIMAL(10,7), - delivery_address VARCHAR(500) NOT NULL, -- 送餐地址 - delivery_latitude DECIMAL(10,7), - delivery_longitude DECIMAL(10,7), - distance INTEGER, -- 配送距离(米) - estimated_time INTEGER, -- 预计时长(分钟) - status INTEGER NOT NULL DEFAULT 1, -- 1:待分配 2:待取餐 3:配送中 4:已送达 5:异常 - assigned_at TIMESTAMP WITH TIME ZONE, -- 分配时间 - picked_at TIMESTAMP WITH TIME ZONE, -- 取餐时间 - delivered_at TIMESTAMP WITH TIME ZONE, -- 送达时间 - delivery_fee DECIMAL(18,2) DEFAULT 0, -- 配送费 - remark TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_delivery_tasks_order_id ON delivery_tasks(order_id); -CREATE INDEX idx_delivery_tasks_driver_id ON delivery_tasks(driver_id); -CREATE INDEX idx_delivery_tasks_status ON delivery_tasks(status); -``` - -### 2.7 支付管理 - -#### payments(支付记录表) -```sql -CREATE TABLE payments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - order_id UUID NOT NULL REFERENCES orders(id), - user_id UUID NOT NULL REFERENCES users(id), - payment_no VARCHAR(50) UNIQUE NOT NULL, -- 支付单号 - payment_method VARCHAR(20) NOT NULL, -- 支付方式:wechat、alipay、balance - amount DECIMAL(18,2) NOT NULL, - status INTEGER NOT NULL DEFAULT 0, -- 0:待支付 1:支付中 2:成功 3:失败 4:已退款 - third_party_no VARCHAR(100), -- 第三方支付单号 - paid_at TIMESTAMP WITH TIME ZONE, - callback_data JSONB, -- 回调数据 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_payments_order_id ON payments(order_id); -CREATE INDEX idx_payments_payment_no ON payments(payment_no); -CREATE INDEX idx_payments_user_id ON payments(user_id); -``` - -#### refunds(退款记录表) -```sql -CREATE TABLE refunds ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - order_id UUID NOT NULL REFERENCES orders(id), - payment_id UUID NOT NULL REFERENCES payments(id), - refund_no VARCHAR(50) UNIQUE NOT NULL, - amount DECIMAL(18,2) NOT NULL, - reason TEXT, - status INTEGER NOT NULL DEFAULT 0, -- 0:待审核 1:退款中 2:成功 3:失败 - third_party_no VARCHAR(100), - refunded_at TIMESTAMP WITH TIME ZONE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_refunds_order_id ON refunds(order_id); -CREATE INDEX idx_refunds_payment_id ON refunds(payment_id); -``` - -### 2.8 营销管理 - -#### coupons(优惠券表) -```sql -CREATE TABLE coupons ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - merchant_id UUID REFERENCES merchants(id), -- NULL表示平台券 - name VARCHAR(100) NOT NULL, - type INTEGER NOT NULL, -- 1:满减券 2:折扣券 3:代金券 - discount_type INTEGER NOT NULL, -- 1:固定金额 2:百分比 - discount_value DECIMAL(18,2) NOT NULL, -- 优惠值 - min_order_amount DECIMAL(18,2) DEFAULT 0, -- 最低消费 - max_discount_amount DECIMAL(18,2), -- 最大优惠金额(折扣券用) - total_quantity INTEGER NOT NULL, -- 总数量 - received_quantity INTEGER DEFAULT 0, -- 已领取数量 - used_quantity INTEGER DEFAULT 0, -- 已使用数量 - valid_start_time TIMESTAMP WITH TIME ZONE NOT NULL, - valid_end_time TIMESTAMP WITH TIME ZONE NOT NULL, - status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:停用 - description TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_coupons_merchant_id ON coupons(merchant_id); -CREATE INDEX idx_coupons_status ON coupons(status); -``` - -#### user_coupons(用户优惠券表) -```sql -CREATE TABLE user_coupons ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - coupon_id UUID NOT NULL REFERENCES coupons(id), - status INTEGER NOT NULL DEFAULT 1, -- 1:未使用 2:已使用 3:已过期 - used_order_id UUID REFERENCES orders(id), - received_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - used_at TIMESTAMP WITH TIME ZONE, - expired_at TIMESTAMP WITH TIME ZONE NOT NULL -); - -CREATE INDEX idx_user_coupons_user_id ON user_coupons(user_id); -CREATE INDEX idx_user_coupons_coupon_id ON user_coupons(coupon_id); -CREATE INDEX idx_user_coupons_status ON user_coupons(status); -``` - -### 2.9 评价管理 - -#### reviews(评价表) -```sql -CREATE TABLE reviews ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES tenants(id), - order_id UUID NOT NULL REFERENCES orders(id), - user_id UUID NOT NULL REFERENCES users(id), - merchant_id UUID NOT NULL REFERENCES merchants(id), - rating INTEGER NOT NULL, -- 评分 1-5 - taste_rating INTEGER, -- 口味评分 - package_rating INTEGER, -- 包装评分 - delivery_rating INTEGER, -- 配送评分 - content TEXT, - images JSONB, -- 评价图片 - is_anonymous BOOLEAN DEFAULT FALSE, - reply_content TEXT, -- 商家回复 - reply_at TIMESTAMP WITH TIME ZONE, - status INTEGER NOT NULL DEFAULT 1, -- 1:正常 2:隐藏 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_reviews_order_id ON reviews(order_id); -CREATE INDEX idx_reviews_user_id ON reviews(user_id); -CREATE INDEX idx_reviews_merchant_id ON reviews(merchant_id); -``` - -### 2.10 系统管理 - -#### system_users(系统用户表) -```sql -CREATE TABLE system_users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID REFERENCES tenants(id), -- NULL表示平台管理员 - merchant_id UUID REFERENCES merchants(id), -- NULL表示租户管理员 - username VARCHAR(50) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - real_name VARCHAR(50), - phone VARCHAR(20), - email VARCHAR(100), - role_id UUID REFERENCES roles(id), - status INTEGER NOT NULL DEFAULT 1, - last_login_at TIMESTAMP WITH TIME ZONE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_system_users_username ON system_users(username); -CREATE INDEX idx_system_users_tenant_id ON system_users(tenant_id); -``` - -#### roles(角色表) -```sql -CREATE TABLE roles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID REFERENCES tenants(id), - name VARCHAR(50) NOT NULL, - code VARCHAR(50) NOT NULL, - description TEXT, - permissions JSONB, -- 权限列表 - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP WITH TIME ZONE -); - -CREATE INDEX idx_roles_tenant_id ON roles(tenant_id); -``` - -#### operation_logs(操作日志表) -```sql -CREATE TABLE operation_logs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID REFERENCES tenants(id), - user_id UUID, - user_type VARCHAR(20), -- system_user, merchant_user, customer - module VARCHAR(50), -- 模块 - action VARCHAR(50), -- 操作 - description TEXT, - ip_address VARCHAR(50), - user_agent TEXT, - request_data JSONB, - response_data JSONB, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_operation_logs_tenant_id ON operation_logs(tenant_id); -CREATE INDEX idx_operation_logs_user_id ON operation_logs(user_id); -CREATE INDEX idx_operation_logs_created_at ON operation_logs(created_at); -``` - -## 3. 数据库索引策略 - -### 3.1 主键索引 -- 所有表使用UUID作为主键,自动创建主键索引 - -### 3.2 外键索引 -- 所有外键字段创建索引,提升关联查询性能 - -### 3.3 业务索引 -- 订单号、支付单号等唯一业务字段创建唯一索引 -- 状态字段创建普通索引 -- 时间字段(created_at)创建索引,支持时间范围查询 - -### 3.4 复合索引 -```sql --- 订单查询常用复合索引 -CREATE INDEX idx_orders_merchant_status_created ON orders(merchant_id, status, created_at DESC); - --- 用户订单查询 -CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC); -``` - -### 3.5 地理位置索引 -```sql --- 使用PostGIS扩展支持地理位置查询 -CREATE EXTENSION IF NOT EXISTS postgis; - --- 门店位置索引 -CREATE INDEX idx_merchant_stores_location ON merchant_stores - USING GIST(ST_MakePoint(longitude, latitude)); -``` - -## 4. 数据库优化 - -### 4.1 分区策略 -```sql --- 订单表按月分区 -CREATE TABLE orders_2024_01 PARTITION OF orders - FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); - -CREATE TABLE orders_2024_02 PARTITION OF orders - FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); -``` - -### 4.2 物化视图 -```sql --- 商家统计物化视图 -CREATE MATERIALIZED VIEW merchant_statistics AS -SELECT - m.id as merchant_id, - m.name, - COUNT(DISTINCT o.id) as total_orders, - SUM(o.actual_amount) as total_revenue, - AVG(r.rating) as avg_rating -FROM merchants m -LEFT JOIN orders o ON m.id = o.merchant_id AND o.status = 6 -LEFT JOIN reviews r ON m.id = r.merchant_id -GROUP BY m.id, m.name; - -CREATE UNIQUE INDEX ON merchant_statistics(merchant_id); -``` - -### 4.3 查询优化建议 -- 避免SELECT *,只查询需要的字段 -- 使用EXPLAIN分析查询计划 -- 合理使用JOIN,避免过多关联 -- 大数据量查询使用分页 -- 使用prepared statement防止SQL注入 - -## 5. 数据备份策略 - -### 5.1 备份方案 -- **全量备份**:每天凌晨2点执行 -- **增量备份**:每4小时执行一次 -- **WAL归档**:实时归档,支持PITR - -### 5.2 备份脚本示例 -```bash -#!/bin/bash -# 全量备份 -pg_dump -h localhost -U postgres -d takeout_saas -F c -f /backup/full_$(date +%Y%m%d).dump - -# 保留最近30天的备份 -find /backup -name "full_*.dump" -mtime +30 -delete -``` - -## 6. 数据迁移 - -### 6.1 EF Core Migrations -```bash -# 添加迁移 -dotnet ef migrations add InitialCreate --project TakeoutSaaS.Infrastructure - -# 更新数据库 -dotnet ef database update --project TakeoutSaaS.Infrastructure -``` - -### 6.2 版本控制 -- 所有数据库变更通过Migration管理 -- Migration文件纳入版本控制 -- 生产环境变更需要审核 - diff --git a/Document/04_API接口设计.md b/Document/04_API接口设计.md deleted file mode 100644 index 1f3735d..0000000 --- a/Document/04_API接口设计.md +++ /dev/null @@ -1,885 +0,0 @@ -# 外卖SaaS系统 - API接口设计 - -## 1. API设计规范 - -### 1.1 RESTful规范 -- 使用标准HTTP方法:GET、POST、PUT、DELETE、PATCH -- URL使用名词复数形式,如:`/api/orders` -- 使用HTTP状态码表示请求结果 -- 版本控制:`/api/v1/orders` - -### 1.2 请求规范 -- **Content-Type**:`application/json` -- **认证方式**:Bearer Token (JWT) -- **租户识别**:通过Header `X-Tenant-Id` 或从Token中解析 - -### 1.3 响应规范 -```json -{ - "success": true, - "code": 200, - "message": "操作成功", - "data": {}, - "timestamp": "2024-01-01T12:00:00Z" -} -``` - -### 1.4 错误响应 -```json -{ - "success": false, - "code": 400, - "message": "参数错误", - "errors": [ - { - "field": "phone", - "message": "手机号格式不正确" - } - ], - "timestamp": "2024-01-01T12:00:00Z" -} -``` - -### 1.5 HTTP状态码 -- **200 OK**:请求成功 -- **201 Created**:创建成功 -- **204 No Content**:删除成功 -- **400 Bad Request**:参数错误 -- **401 Unauthorized**:未认证 -- **403 Forbidden**:无权限 -- **404 Not Found**:资源不存在 -- **409 Conflict**:资源冲突 -- **422 Unprocessable Entity**:业务逻辑错误 -- **500 Internal Server Error**:服务器错误 - -### 1.6 分页规范 -```json -// 请求参数 -{ - "pageIndex": 1, - "pageSize": 20, - "sortBy": "createdAt", - "sortOrder": "desc" -} - -// 响应格式 -{ - "success": true, - "data": { - "items": [], - "totalCount": 100, - "pageIndex": 1, - "pageSize": 20, - "totalPages": 5 - } -} -``` - -## 2. 认证授权接口 - -### 2.1 用户登录 -```http -POST /api/v1/auth/login -Content-Type: application/json - -{ - "phone": "13800138000", - "password": "password123", - "loginType": "customer" // customer, merchant, system -} - -Response: -{ - "success": true, - "data": { - "accessToken": "eyJhbGciOiJIUzI1NiIs...", - "refreshToken": "eyJhbGciOiJIUzI1NiIs...", - "expiresIn": 7200, - "tokenType": "Bearer", - "userInfo": { - "id": "uuid", - "phone": "13800138000", - "nickname": "张三", - "avatar": "https://..." - } - } -} -``` - -### 2.2 刷新Token -```http -POST /api/v1/auth/refresh -Content-Type: application/json - -{ - "refreshToken": "eyJhbGciOiJIUzI1NiIs..." -} - -Response: -{ - "success": true, - "data": { - "accessToken": "eyJhbGciOiJIUzI1NiIs...", - "expiresIn": 7200 - } -} -``` - -### 2.3 用户注册 -```http -POST /api/v1/auth/register -Content-Type: application/json - -{ - "phone": "13800138000", - "password": "password123", - "verificationCode": "123456", - "nickname": "张三" -} -``` - -### 2.4 发送验证码 -```http -POST /api/v1/auth/send-code -Content-Type: application/json - -{ - "phone": "13800138000", - "type": "register" // register, login, reset_password -} -``` - -## 3. 商家管理接口 - -### 3.1 获取商家列表 -```http -GET /api/v1/merchants?pageIndex=1&pageSize=20&keyword=&status=1 -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "items": [ - { - "id": "uuid", - "name": "美味餐厅", - "logo": "https://...", - "rating": 4.5, - "totalSales": 1000, - "status": 1, - "createdAt": "2024-01-01T12:00:00Z" - } - ], - "totalCount": 50, - "pageIndex": 1, - "pageSize": 20 - } -} -``` - -### 3.2 获取商家详情 -```http -GET /api/v1/merchants/{id} -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "id": "uuid", - "name": "美味餐厅", - "logo": "https://...", - "description": "专注美食20年", - "contactPhone": "400-123-4567", - "rating": 4.5, - "totalSales": 1000, - "status": 1, - "stores": [ - { - "id": "uuid", - "name": "总店", - "address": "北京市朝阳区...", - "phone": "010-12345678" - } - ] - } -} -``` - -### 3.3 创建商家 -```http -POST /api/v1/merchants -Authorization: Bearer {token} -Content-Type: application/json - -{ - "name": "美味餐厅", - "logo": "https://...", - "description": "专注美食20年", - "contactPhone": "400-123-4567", - "contactPerson": "张三", - "businessLicense": "91110000..." -} -``` - -### 3.4 更新商家信息 -```http -PUT /api/v1/merchants/{id} -Authorization: Bearer {token} -Content-Type: application/json - -{ - "name": "美味餐厅", - "logo": "https://...", - "description": "专注美食20年" -} -``` - -### 3.5 删除商家 -```http -DELETE /api/v1/merchants/{id} -Authorization: Bearer {token} -``` - -## 4. 菜品管理接口 - -### 4.1 获取菜品列表 -```http -GET /api/v1/dishes?merchantId={merchantId}&categoryId={categoryId}&keyword=&status=1&pageIndex=1&pageSize=20 -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "items": [ - { - "id": "uuid", - "name": "宫保鸡丁", - "description": "经典川菜", - "image": "https://...", - "price": 38.00, - "originalPrice": 48.00, - "salesCount": 500, - "rating": 4.8, - "status": 1, - "tags": ["热销", "招牌菜"] - } - ], - "totalCount": 100 - } -} -``` - -### 4.2 获取菜品详情 -```http -GET /api/v1/dishes/{id} -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "id": "uuid", - "name": "宫保鸡丁", - "description": "经典川菜,选用优质鸡肉...", - "image": "https://...", - "price": 38.00, - "originalPrice": 48.00, - "unit": "份", - "stock": 100, - "salesCount": 500, - "rating": 4.8, - "status": 1, - "tags": ["热销", "招牌菜"], - "specs": [ - { - "id": "uuid", - "name": "大份", - "price": 48.00, - "stock": 50 - }, - { - "id": "uuid", - "name": "小份", - "price": 28.00, - "stock": 50 - } - ] - } -} -``` - -### 4.3 创建菜品 -```http -POST /api/v1/dishes -Authorization: Bearer {token} -Content-Type: application/json - -{ - "merchantId": "uuid", - "categoryId": "uuid", - "name": "宫保鸡丁", - "description": "经典川菜", - "image": "https://...", - "price": 38.00, - "originalPrice": 48.00, - "unit": "份", - "stock": 100, - "tags": ["热销", "招牌菜"], - "specs": [ - { - "name": "大份", - "price": 48.00, - "stock": 50 - } - ] -} -``` - -### 4.4 更新菜品 -```http -PUT /api/v1/dishes/{id} -Authorization: Bearer {token} -Content-Type: application/json - -{ - "name": "宫保鸡丁", - "price": 38.00, - "stock": 100, - "status": 1 -} -``` - -### 4.5 批量上下架 -```http -PATCH /api/v1/dishes/batch-status -Authorization: Bearer {token} -Content-Type: application/json - -{ - "dishIds": ["uuid1", "uuid2"], - "status": 1 // 1:上架 2:下架 -} -``` - -## 5. 订单管理接口 - -### 5.1 创建订单 -```http -POST /api/v1/orders -Authorization: Bearer {token} -Content-Type: application/json - -{ - "merchantId": "uuid", - "storeId": "uuid", - "items": [ - { - "dishId": "uuid", - "specId": "uuid", - "quantity": 2, - "price": 38.00 - } - ], - "deliveryAddress": { - "contactName": "张三", - "contactPhone": "13800138000", - "address": "北京市朝阳区...", - "latitude": 39.9042, - "longitude": 116.4074 - }, - "remark": "少辣", - "couponId": "uuid" -} - -Response: -{ - "success": true, - "data": { - "orderId": "uuid", - "orderNo": "202401010001", - "totalAmount": 76.00, - "deliveryFee": 5.00, - "discountAmount": 10.00, - "actualAmount": 71.00, - "paymentInfo": { - "paymentNo": "PAY202401010001", - "qrCode": "https://..." // 支付二维码 - } - } -} -``` - -### 5.2 获取订单列表 -```http -GET /api/v1/orders?status=&startDate=&endDate=&pageIndex=1&pageSize=20 -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "items": [ - { - "id": "uuid", - "orderNo": "202401010001", - "merchantName": "美味餐厅", - "totalAmount": 76.00, - "actualAmount": 71.00, - "status": 2, - "statusText": "待接单", - "createdAt": "2024-01-01T12:00:00Z", - "items": [ - { - "dishName": "宫保鸡丁", - "specName": "大份", - "quantity": 2, - "price": 38.00 - } - ] - } - ], - "totalCount": 50 - } -} -``` - -### 5.3 获取订单详情 -```http -GET /api/v1/orders/{id} -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "id": "uuid", - "orderNo": "202401010001", - "merchant": { - "id": "uuid", - "name": "美味餐厅", - "phone": "400-123-4567" - }, - "items": [ - { - "dishName": "宫保鸡丁", - "dishImage": "https://...", - "specName": "大份", - "quantity": 2, - "price": 38.00, - "amount": 76.00 - } - ], - "deliveryAddress": { - "contactName": "张三", - "contactPhone": "13800138000", - "address": "北京市朝阳区..." - }, - "dishAmount": 76.00, - "deliveryFee": 5.00, - "packageFee": 2.00, - "discountAmount": 10.00, - "totalAmount": 83.00, - "actualAmount": 73.00, - "status": 2, - "statusText": "待接单", - "paymentStatus": 1, - "paymentMethod": "wechat", - "estimatedDeliveryTime": "2024-01-01T13:00:00Z", - "createdAt": "2024-01-01T12:00:00Z", - "paidAt": "2024-01-01T12:05:00Z", - "remark": "少辣", - "timeline": [ - { - "status": "created", - "statusText": "订单创建", - "time": "2024-01-01T12:00:00Z" - }, - { - "status": "paid", - "statusText": "支付成功", - "time": "2024-01-01T12:05:00Z" - } - ] - } -} -``` - -### 5.4 商家接单 -```http -POST /api/v1/orders/{id}/accept -Authorization: Bearer {token} -Content-Type: application/json - -{ - "estimatedTime": 30 // 预计制作时长(分钟) -} -``` - -### 5.5 开始制作 -```http -POST /api/v1/orders/{id}/cooking -Authorization: Bearer {token} -``` - -### 5.6 订单完成 -```http -POST /api/v1/orders/{id}/complete -Authorization: Bearer {token} -``` - -### 5.7 取消订单 -```http -POST /api/v1/orders/{id}/cancel -Authorization: Bearer {token} -Content-Type: application/json - -{ - "reason": "用户取消", - "cancelBy": "customer" // customer, merchant, system -} -``` - -## 6. 支付接口 - -### 6.1 创建支付 -```http -POST /api/v1/payments -Authorization: Bearer {token} -Content-Type: application/json - -{ - "orderId": "uuid", - "paymentMethod": "wechat", // wechat, alipay, balance - "amount": 71.00 -} - -Response: -{ - "success": true, - "data": { - "paymentNo": "PAY202401010001", - "qrCode": "https://...", // 支付二维码 - "deepLink": "weixin://..." // 唤起支付的深度链接 - } -} -``` - -### 6.2 查询支付状态 -```http -GET /api/v1/payments/{paymentNo} -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "paymentNo": "PAY202401010001", - "status": 2, // 0:待支付 1:支付中 2:成功 3:失败 - "amount": 71.00, - "paidAt": "2024-01-01T12:05:00Z" - } -} -``` - -### 6.3 支付回调(第三方调用) -```http -POST /api/v1/payments/callback/wechat -Content-Type: application/json - -{ - "out_trade_no": "PAY202401010001", - "transaction_id": "4200001234567890", - "total_fee": 7100, - "result_code": "SUCCESS" -} -``` - -### 6.4 申请退款 -```http -POST /api/v1/refunds -Authorization: Bearer {token} -Content-Type: application/json - -{ - "orderId": "uuid", - "amount": 71.00, - "reason": "不想要了" -} -``` - -## 7. 配送管理接口 - -### 7.1 获取配送任务列表 -```http -GET /api/v1/delivery-tasks?status=&driverId=&pageIndex=1&pageSize=20 -Authorization: Bearer {token} -``` - -### 7.2 分配配送员 -```http -POST /api/v1/delivery-tasks/{id}/assign -Authorization: Bearer {token} -Content-Type: application/json - -{ - "driverId": "uuid" -} -``` - -### 7.3 配送员接单 -```http -POST /api/v1/delivery-tasks/{id}/accept -Authorization: Bearer {token} -``` - -### 7.4 确认取餐 -```http -POST /api/v1/delivery-tasks/{id}/pickup -Authorization: Bearer {token} -``` - -### 7.5 确认送达 -```http -POST /api/v1/delivery-tasks/{id}/deliver -Authorization: Bearer {token} -Content-Type: application/json - -{ - "deliveryCode": "123456" // 取餐码 -} -``` - -### 7.6 更新配送员位置 -```http -POST /api/v1/delivery-drivers/location -Authorization: Bearer {token} -Content-Type: application/json - -{ - "latitude": 39.9042, - "longitude": 116.4074 -} -``` - -## 8. 营销管理接口 - -### 8.1 获取优惠券列表 -```http -GET /api/v1/coupons?merchantId=&status=1&pageIndex=1&pageSize=20 -Authorization: Bearer {token} -``` - -### 8.2 领取优惠券 -```http -POST /api/v1/coupons/{id}/receive -Authorization: Bearer {token} -``` - -### 8.3 获取用户优惠券 -```http -GET /api/v1/user-coupons?status=1&pageIndex=1&pageSize=20 -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "items": [ - { - "id": "uuid", - "couponName": "满50减10", - "discountValue": 10.00, - "minOrderAmount": 50.00, - "status": 1, - "expiredAt": "2024-12-31T23:59:59Z" - } - ] - } -} -``` - -### 8.4 获取可用优惠券 -```http -GET /api/v1/user-coupons/available?merchantId={merchantId}&amount={amount} -Authorization: Bearer {token} -``` - -## 9. 评价管理接口 - -### 9.1 创建评价 -```http -POST /api/v1/reviews -Authorization: Bearer {token} -Content-Type: application/json - -{ - "orderId": "uuid", - "rating": 5, - "tasteRating": 5, - "packageRating": 5, - "deliveryRating": 5, - "content": "非常好吃", - "images": ["https://...", "https://..."], - "isAnonymous": false -} -``` - -### 9.2 获取商家评价列表 -```http -GET /api/v1/reviews?merchantId={merchantId}&rating=&pageIndex=1&pageSize=20 - -Response: -{ - "success": true, - "data": { - "items": [ - { - "id": "uuid", - "userName": "张三", - "userAvatar": "https://...", - "rating": 5, - "content": "非常好吃", - "images": ["https://..."], - "createdAt": "2024-01-01T12:00:00Z", - "replyContent": "感谢支持", - "replyAt": "2024-01-01T13:00:00Z" - } - ], - "totalCount": 100, - "statistics": { - "avgRating": 4.8, - "totalReviews": 100, - "rating5Count": 80, - "rating4Count": 15, - "rating3Count": 3, - "rating2Count": 1, - "rating1Count": 1 - } - } -} -``` - -### 9.3 商家回复评价 -```http -POST /api/v1/reviews/{id}/reply -Authorization: Bearer {token} -Content-Type: application/json - -{ - "replyContent": "感谢您的支持" -} -``` - -## 10. 数据统计接口 - -### 10.1 商家数据概览 -```http -GET /api/v1/statistics/merchant/overview?merchantId={merchantId}&startDate=&endDate= -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "totalOrders": 1000, - "totalRevenue": 50000.00, - "avgOrderAmount": 50.00, - "completionRate": 0.95, - "todayOrders": 50, - "todayRevenue": 2500.00, - "orderTrend": [ - { - "date": "2024-01-01", - "orders": 50, - "revenue": 2500.00 - } - ], - "topDishes": [ - { - "dishId": "uuid", - "dishName": "宫保鸡丁", - "salesCount": 200, - "revenue": 7600.00 - } - ] - } -} -``` - -### 10.2 平台数据大盘 -```http -GET /api/v1/statistics/platform/dashboard?startDate=&endDate= -Authorization: Bearer {token} - -Response: -{ - "success": true, - "data": { - "totalMerchants": 100, - "totalUsers": 10000, - "totalOrders": 50000, - "totalRevenue": 2500000.00, - "activeMerchants": 80, - "activeUsers": 5000, - "todayOrders": 500, - "todayRevenue": 25000.00 - } -} -``` - -## 11. 文件上传接口 - -### 11.1 上传图片 -```http -POST /api/v1/files/upload -Authorization: Bearer {token} -Content-Type: multipart/form-data - -file: -type: dish_image // dish_image, merchant_logo, user_avatar, review_image - -Response: -{ - "success": true, - "data": { - "url": "https://cdn.example.com/images/xxx.jpg", - "fileName": "xxx.jpg", - "fileSize": 102400 - } -} -``` - -## 12. WebSocket实时通知 - -### 12.1 连接WebSocket -```javascript -// 连接地址 -ws://api.example.com/ws?token={jwt_token} - -// 订阅主题 -{ - "action": "subscribe", - "topics": ["order.new", "order.status", "delivery.location"] -} - -// 接收消息 -{ - "topic": "order.new", - "data": { - "orderId": "uuid", - "orderNo": "202401010001", - "merchantId": "uuid" - }, - "timestamp": "2024-01-01T12:00:00Z" -} -``` - -### 12.2 消息主题 -- `order.new`:新订单通知 -- `order.status`:订单状态变更 -- `delivery.location`:配送员位置更新 -- `payment.success`:支付成功通知 - diff --git a/Document/05_部署运维.md b/Document/05_部署运维.md deleted file mode 100644 index f92e303..0000000 --- a/Document/05_部署运维.md +++ /dev/null @@ -1,1058 +0,0 @@ -# 外卖SaaS系统 - 部署运维 - -## 1. 环境要求 - -### 1.1 开发环境 -- **.NET SDK**:10.0 或更高版本 -- **IDE**:Visual Studio 2022 / JetBrains Rider / VS Code -- **数据库**:PostgreSQL 16+ -- **缓存**:Redis 7.0+ -- **消息队列**:RabbitMQ 3.12+ -- **Git**:版本控制 -- **Docker Desktop**:容器化开发(可选) - -### 1.2 生产环境 -- **操作系统**:Linux (Ubuntu 22.04 LTS / CentOS 8+) -- **运行时**:.NET Runtime 10.0 -- **Web服务器**:Nginx 1.24+ -- **数据库**:PostgreSQL 16+ (主从复制) -- **缓存**:Redis 7.0+ (哨兵模式) -- **消息队列**:RabbitMQ 3.12+ (集群模式) -- **对象存储**:MinIO / 阿里云OSS / 腾讯云COS -- **监控**:Prometheus + Grafana -- **日志**:ELK Stack (Elasticsearch + Logstash + Kibana) - -### 1.3 硬件要求(生产环境) -- **应用服务器**:4核8GB内存(最低),推荐8核16GB -- **数据库服务器**:8核16GB内存,SSD存储 -- **Redis服务器**:4核8GB内存 -- **负载均衡器**:2核4GB内存 - -## 2. 本地开发环境搭建 - -### 2.1 安装.NET SDK -```bash -# Windows -# 从官网下载安装:https://dotnet.microsoft.com/download - -# Linux (Ubuntu) -wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -sudo dpkg -i packages-microsoft-prod.deb -sudo apt-get update -sudo apt-get install -y dotnet-sdk-10.0 - -# 验证安装 -dotnet --version -``` - -### 2.2 安装PostgreSQL -```bash -# Ubuntu -sudo apt-get update -sudo apt-get install -y postgresql-16 postgresql-contrib-16 - -# 启动服务 -sudo systemctl start postgresql -sudo systemctl enable postgresql - -# 创建数据库 -sudo -u postgres psql -CREATE DATABASE takeout_saas; -CREATE USER takeout_user WITH PASSWORD 'your_password'; -GRANT ALL PRIVILEGES ON DATABASE takeout_saas TO takeout_user; -\q -``` - -### 2.3 安装Redis -```bash -# Ubuntu -sudo apt-get install -y redis-server - -# 启动服务 -sudo systemctl start redis-server -sudo systemctl enable redis-server - -# 测试连接 -redis-cli ping -``` - -### 2.4 安装RabbitMQ -```bash -# Ubuntu -sudo apt-get install -y rabbitmq-server - -# 启动服务 -sudo systemctl start rabbitmq-server -sudo systemctl enable rabbitmq-server - -# 启用管理插件 -sudo rabbitmq-plugins enable rabbitmq_management - -# 创建用户 -sudo rabbitmqctl add_user admin password -sudo rabbitmqctl set_user_tags admin administrator -sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*" - -# 访问管理界面:http://localhost:15672 -``` - -### 2.5 使用Docker Compose(推荐) -```yaml -# docker-compose.yml -version: '3.8' - -services: - postgres: - image: postgres:16 - container_name: takeout_postgres - environment: - POSTGRES_DB: takeout_saas - POSTGRES_USER: takeout_user - POSTGRES_PASSWORD: your_password - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - redis: - image: redis:7-alpine - container_name: takeout_redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - - rabbitmq: - image: rabbitmq:3.12-management - container_name: takeout_rabbitmq - environment: - RABBITMQ_DEFAULT_USER: admin - RABBITMQ_DEFAULT_PASS: password - ports: - - "5672:5672" - - "15672:15672" - volumes: - - rabbitmq_data:/var/lib/rabbitmq - - minio: - image: minio/minio:latest - container_name: takeout_minio - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: admin - MINIO_ROOT_PASSWORD: password123 - ports: - - "9000:9000" - - "9001:9001" - volumes: - - minio_data:/data - -volumes: - postgres_data: - redis_data: - rabbitmq_data: - minio_data: -``` - -```bash -# 启动所有服务 -docker-compose up -d - -# 查看服务状态 -docker-compose ps - -# 停止服务 -docker-compose down -``` - -### 2.6 配置项目 -```bash -# 克隆项目 -git clone https://github.com/your-org/takeout-saas.git -cd takeout-saas - -# 还原依赖 -dotnet restore - -# 配置appsettings.Development.json -{ - "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password" - }, - "Redis": { - "Configuration": "localhost:6379" - }, - "RabbitMQ": { - "Host": "localhost", - "Port": 5672, - "Username": "admin", - "Password": "password" - } -} - -# 执行数据库迁移 -cd src/TakeoutSaaS.Api -dotnet ef database update - -# 运行项目 -dotnet run -``` - -### 2.7 EF Core 迁移基线 - -现已内置 `dotnet-ef` 本地工具与设计时 DbContext 工厂,可直接在命令行生成/更新数据库。运行前可通过环境变量 `TAKEOUTSAAS_APP_CONNECTION`、`TAKEOUTSAAS_IDENTITY_CONNECTION` 覆盖默认连接串(默认指向本地 PostgreSQL)。 - -```powershell -# 业务主库(TakeoutAppDbContext,含租户/商户/门店/商品/订单等) -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext - -# 身份库(IdentityDbContext) -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext - -# 业务/字典库(DictionaryDbContext,归属 AppDatabase) -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext -``` - -> Hangfire 使用 Scheduler.ConnectionString 指向的数据库,首次启动服务会自动建表;只需提前创建空数据库并授予账号权限。 - -## 3. Docker部署 - -### 3.1 创建Dockerfile -```dockerfile -# Dockerfile -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY ["src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj", "src/TakeoutSaaS.Api/"] -COPY ["src/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj", "src/TakeoutSaaS.Application/"] -COPY ["src/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj", "src/TakeoutSaaS.Domain/"] -COPY ["src/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj", "src/TakeoutSaaS.Infrastructure/"] -COPY ["src/TakeoutSaaS.Shared/TakeoutSaaS.Shared.csproj", "src/TakeoutSaaS.Shared/"] -RUN dotnet restore "src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj" -COPY . . -WORKDIR "/src/src/TakeoutSaaS.Api" -RUN dotnet build "TakeoutSaaS.Api.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "TakeoutSaaS.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "TakeoutSaaS.Api.dll"] -``` - -### 3.2 构建镜像 -```bash -# 构建镜像 -docker build -t takeout-saas-api:latest . - -# 查看镜像 -docker images | grep takeout-saas - -# 运行容器 -docker run -d \ - --name takeout-api \ - -p 8080:80 \ - -e ASPNETCORE_ENVIRONMENT=Production \ - -e ConnectionStrings__DefaultConnection="Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password" \ - takeout-saas-api:latest -``` - -### 3.3 生产环境Docker Compose -```yaml -# docker-compose.prod.yml -version: '3.8' - -services: - api: - image: takeout-saas-api:latest - container_name: takeout_api - restart: always - environment: - ASPNETCORE_ENVIRONMENT: Production - ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=${DB_PASSWORD}" - Redis__Configuration: "redis:6379" - RabbitMQ__Host: "rabbitmq" - ports: - - "8080:80" - depends_on: - - postgres - - redis - - rabbitmq - networks: - - takeout_network - - nginx: - image: nginx:latest - container_name: takeout_nginx - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf - - ./nginx/ssl:/etc/nginx/ssl - depends_on: - - api - networks: - - takeout_network - -networks: - takeout_network: - driver: bridge -``` - -## 4. Nginx配置 - -### 4.1 基础配置 -```nginx -# /etc/nginx/nginx.conf -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - - # Gzip压缩 - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types text/plain text/css text/xml text/javascript - application/json application/javascript application/xml+rss - application/rss+xml font/truetype font/opentype - application/vnd.ms-fontobject image/svg+xml; - - # 限流配置 - limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s; - limit_conn_zone $binary_remote_addr zone=conn_limit:10m; - - include /etc/nginx/conf.d/*.conf; -} -``` - -### 4.2 API服务配置 -```nginx -# /etc/nginx/conf.d/api.conf -upstream api_backend { - least_conn; - server api1:80 weight=1 max_fails=3 fail_timeout=30s; - server api2:80 weight=1 max_fails=3 fail_timeout=30s; - keepalive 32; -} - -server { - listen 80; - server_name api.example.com; - - # 重定向到HTTPS - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl http2; - server_name api.example.com; - - # SSL证书配置 - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - - # 安全头 - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # 客户端请求体大小限制 - client_max_body_size 10M; - - # API接口 - location /api/ { - # 限流 - limit_req zone=api_limit burst=20 nodelay; - limit_conn conn_limit 10; - - proxy_pass http://api_backend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # 超时设置 - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # 缓冲设置 - proxy_buffering on; - proxy_buffer_size 4k; - proxy_buffers 8 4k; - proxy_busy_buffers_size 8k; - } - - # WebSocket - location /ws { - proxy_pass http://api_backend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # WebSocket超时 - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - } - - # 健康检查 - location /health { - proxy_pass http://api_backend; - access_log off; - } - - # 静态文件缓存 - location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { - proxy_pass http://api_backend; - expires 30d; - add_header Cache-Control "public, immutable"; - } -} -``` - -## 5. 数据库部署 - -### 5.1 PostgreSQL主从复制 -```bash -# 主库配置 (postgresql.conf) -listen_addresses = '*' -wal_level = replica -max_wal_senders = 10 -wal_keep_size = 64MB -hot_standby = on - -# 主库配置 (pg_hba.conf) -host replication replicator 192.168.1.0/24 md5 - -# 创建复制用户 -CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'repl_password'; - -# 从库配置 -# 1. 停止从库 -sudo systemctl stop postgresql - -# 2. 清空从库数据目录 -rm -rf /var/lib/postgresql/16/main/* - -# 3. 从主库复制数据 -pg_basebackup -h master_ip -D /var/lib/postgresql/16/main -U replicator -P -v -R -X stream -C -S replica1 - -# 4. 启动从库 -sudo systemctl start postgresql - -# 5. 验证复制状态 -# 主库执行 -SELECT * FROM pg_stat_replication; -``` - -### 5.2 数据库备份脚本 -```bash -#!/bin/bash -# backup_db.sh - -BACKUP_DIR="/backup/postgres" -DATE=$(date +%Y%m%d_%H%M%S) -DB_NAME="takeout_saas" -DB_USER="takeout_user" -RETENTION_DAYS=30 - -# 创建备份目录 -mkdir -p $BACKUP_DIR - -# 全量备份 -pg_dump -h localhost -U $DB_USER -d $DB_NAME -F c -f $BACKUP_DIR/full_$DATE.dump - -# 压缩备份 -gzip $BACKUP_DIR/full_$DATE.dump - -# 删除过期备份 -find $BACKUP_DIR -name "full_*.dump.gz" -mtime +$RETENTION_DAYS -delete - -# 上传到对象存储(可选) -# aws s3 cp $BACKUP_DIR/full_$DATE.dump.gz s3://your-bucket/backups/ - -echo "Backup completed: full_$DATE.dump.gz" -``` - -### 5.3 定时备份(Crontab) -```bash -# 编辑crontab -crontab -e - -# 每天凌晨2点执行备份 -0 2 * * * /path/to/backup_db.sh >> /var/log/backup.log 2>&1 -``` - -## TODO:基础设施部署脚本 - -- [ ] PostgreSQL 主从:整理主库/从库初始化脚本、basebackup 步骤与故障切换手册。 -- [ ] Redis 哨兵/集群:补充 redis.conf/sentinel.conf 模板以及一主两从搭建命令。 -- [ ] RabbitMQ:编写单节点到镜像队列的安装脚本,记录 VHost、用户、权限、监控等操作。 -- [ ] 腾讯云 COS:整理桶创建、ACL、CDN 绑定与密钥轮换流程,并提供 coscmd/SDK 示例。 -- [ ] Hangfire 存储:确认 PostgreSQL Schema 初始化脚本,补充定期备份、清理、监控的 SOP。 - -## 6. Redis部署 - -### 6.1 Redis哨兵模式 -```bash -# redis.conf (主节点) -bind 0.0.0.0 -port 6379 -requirepass your_password -masterauth your_password - -# sentinel.conf -port 26379 -sentinel monitor mymaster 192.168.1.100 6379 2 -sentinel auth-pass mymaster your_password -sentinel down-after-milliseconds mymaster 5000 -sentinel parallel-syncs mymaster 1 -sentinel failover-timeout mymaster 10000 -``` - -### 6.2 Redis持久化配置 -```bash -# redis.conf -# RDB持久化 -save 900 1 -save 300 10 -save 60 10000 -rdbcompression yes -rdbchecksum yes -dbfilename dump.rdb - -# AOF持久化 -appendonly yes -appendfilename "appendonly.aof" -appendfsync everysec -no-appendfsync-on-rewrite no -auto-aof-rewrite-percentage 100 -auto-aof-rewrite-min-size 64mb -``` - -## 7. CI/CD配置 - -### 7.1 GitHub Actions -```yaml -# .github/workflows/deploy.yml -name: Deploy to Production - -on: - push: - branches: [ main ] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '10.0.x' - - - name: Restore dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test - run: dotnet test --no-build --verbosity normal - - - name: Publish - run: dotnet publish src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj -c Release -o ./publish - - - name: Build Docker image - run: | - docker build -t takeout-saas-api:${{ github.sha }} . - docker tag takeout-saas-api:${{ github.sha }} takeout-saas-api:latest - - - name: Push to Registry - run: | - echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - docker push takeout-saas-api:${{ github.sha }} - docker push takeout-saas-api:latest - - - name: Deploy to Server - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - cd /opt/takeout-saas - docker-compose pull - docker-compose up -d - docker system prune -f -``` - -### 7.2 GitLab CI -```yaml -# .gitlab-ci.yml -stages: - - build - - test - - deploy - -variables: - DOCKER_IMAGE: registry.example.com/takeout-saas-api - -build: - stage: build - image: mcr.microsoft.com/dotnet/sdk:10.0 - script: - - dotnet restore - - dotnet build --configuration Release - artifacts: - paths: - - src/*/bin/Release/ - -test: - stage: test - image: mcr.microsoft.com/dotnet/sdk:10.0 - script: - - dotnet test --configuration Release - -deploy: - stage: deploy - image: docker:latest - services: - - docker:dind - script: - - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA . - - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest - - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA - - docker push $DOCKER_IMAGE:latest - only: - - main -``` - -## 8. 监控告警 - -### 8.1 Prometheus配置 -```yaml -# prometheus.yml -global: - scrape_interval: 15s - evaluation_interval: 15s - -scrape_configs: - - job_name: 'takeout-api' - static_configs: - - targets: ['api:80'] - metrics_path: '/metrics' - - - job_name: 'postgres' - static_configs: - - targets: ['postgres-exporter:9187'] - - - job_name: 'redis' - static_configs: - - targets: ['redis-exporter:9121'] - - - job_name: 'node' - static_configs: - - targets: ['node-exporter:9100'] -``` - -### 8.2 应用监控指标(OpenTelemetry + Prometheus Exporter) -```csharp -// Program.cs - 指标与探针 -builder.Services.AddHealthChecks(); -builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddPrometheusExporter(); // /metrics - }); - -var app = builder.Build(); -app.MapHealthChecks("/healthz"); // 存活/就绪探针 -app.MapPrometheusScrapingEndpoint(); // 默认 /metrics -``` - -自定义业务指标(使用 `System.Diagnostics.Metrics`,由 Prometheus Exporter 暴露): -```csharp -internal static class BusinessMetrics -{ - private static readonly Meter Meter = new("TakeoutSaaS.App", "1.0.0"); - public static readonly Counter OrdersCreated = Meter.CreateCounter("orders_created_total", "个", "订单创建计数"); - public static readonly Histogram OrderProcessingSeconds = Meter.CreateHistogram("order_processing_duration_seconds", "s", "订单处理耗时"); -} -``` - -Prometheus 抓取示例:见 `deploy/prometheus/prometheus.yml`,默认拉取 `/metrics`,告警规则见 `deploy/prometheus/alert.rules.yml`。 - -### 8.3 Grafana仪表板 -```json -{ - "dashboard": { - "title": "外卖SaaS系统监控", - "panels": [ - { - "title": "API请求速率", - "targets": [ - { - "expr": "rate(http_requests_total[5m])" - } - ] - }, - { - "title": "订单创建数", - "targets": [ - { - "expr": "increase(orders_created_total[1h])" - } - ] - }, - { - "title": "数据库连接数", - "targets": [ - { - "expr": "pg_stat_activity_count" - } - ] - } - ] - } -} -``` - -### 8.4 告警规则 -```yaml -# alert.rules.yml -groups: - - name: takeout_alerts - interval: 30s - rules: - - alert: HighErrorRate - expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05 - for: 5m - labels: - severity: critical - annotations: - summary: "高错误率告警" - description: "API错误率超过5%" - - - alert: DatabaseDown - expr: up{job="postgres"} == 0 - for: 1m - labels: - severity: critical - annotations: - summary: "数据库宕机" - description: "PostgreSQL数据库不可用" - - - alert: HighMemoryUsage - expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9 - for: 5m - labels: - severity: warning - annotations: - summary: "内存使用率过高" - description: "内存使用率超过90%" -``` - -## 9. 日志管理 - -### 9.1 Serilog配置 -```json -{ - "Serilog": { - "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Elasticsearch"], - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "System": "Warning" - } - }, - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" - } - }, - { - "Name": "File", - "Args": { - "path": "logs/log-.txt", - "rollingInterval": "Day", - "retainedFileCountLimit": 30 - } - }, - { - "Name": "Elasticsearch", - "Args": { - "nodeUris": "http://elasticsearch:9200", - "indexFormat": "takeout-logs-{0:yyyy.MM.dd}", - "autoRegisterTemplate": true - } - } - ], - "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"] - } -} -``` - -### 9.2 ELK Stack部署 -```yaml -# docker-compose.elk.yml -version: '3.8' - -services: - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 - environment: - - discovery.type=single-node - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - ports: - - "9200:9200" - volumes: - - es_data:/usr/share/elasticsearch/data - - logstash: - image: docker.elastic.co/logstash/logstash:8.11.0 - volumes: - - ./logstash/pipeline:/usr/share/logstash/pipeline - ports: - - "5044:5044" - depends_on: - - elasticsearch - - kibana: - image: docker.elastic.co/kibana/kibana:8.11.0 - ports: - - "5601:5601" - environment: - ELASTICSEARCH_HOSTS: http://elasticsearch:9200 - depends_on: - - elasticsearch - -volumes: - es_data: -``` - -## 10. 安全加固 - -### 10.1 防火墙配置 -```bash -# UFW防火墙 -sudo ufw enable -sudo ufw allow 22/tcp # SSH -sudo ufw allow 80/tcp # HTTP -sudo ufw allow 443/tcp # HTTPS -sudo ufw deny 5432/tcp # 禁止外部访问数据库 -sudo ufw deny 6379/tcp # 禁止外部访问Redis -``` - -### 10.2 SSL证书(Let's Encrypt) -```bash -# 安装Certbot -sudo apt-get install certbot python3-certbot-nginx - -# 获取证书 -sudo certbot --nginx -d api.example.com - -# 自动续期 -sudo certbot renew --dry-run - -# 添加定时任务 -0 3 * * * certbot renew --quiet -``` - -### 10.3 应用安全配置 -```csharp -// Program.cs -builder.Services.AddHsts(options => -{ - options.MaxAge = TimeSpan.FromDays(365); - options.IncludeSubDomains = true; - options.Preload = true; -}); - -builder.Services.AddHttpsRedirection(options => -{ - options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect; - options.HttpsPort = 443; -}); - -// 添加安全头 -app.Use(async (context, next) => -{ - context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); - context.Response.Headers.Add("X-Frame-Options", "DENY"); - context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); - context.Response.Headers.Add("Referrer-Policy", "no-referrer"); - await next(); -}); -``` - -## 11. 性能优化 - -### 11.1 数据库连接池 -```json -{ - "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=user;Password=pass;Pooling=true;MinPoolSize=5;MaxPoolSize=100;ConnectionLifetime=300" - } -} -``` - -### 11.2 Redis连接池 -```csharp -services.AddStackExchangeRedisCache(options => -{ - options.Configuration = configuration["Redis:Configuration"]; - options.InstanceName = "TakeoutSaaS:"; -}); -``` - -### 11.3 响应压缩 -```csharp -builder.Services.AddResponseCompression(options => -{ - options.EnableForHttps = true; - options.Providers.Add(); - options.Providers.Add(); -}); -``` - -## 12. 故障恢复 - -### 12.1 数据库恢复 -```bash -# 从备份恢复 -pg_restore -h localhost -U takeout_user -d takeout_saas -v /backup/full_20240101.dump - -# PITR恢复到指定时间点 -# 1. 停止数据库 -sudo systemctl stop postgresql - -# 2. 恢复基础备份 -rm -rf /var/lib/postgresql/16/main/* -tar -xzf /backup/base_backup.tar.gz -C /var/lib/postgresql/16/main/ - -# 3. 配置recovery.conf -restore_command = 'cp /backup/wal_archive/%f %p' -recovery_target_time = '2024-01-01 12:00:00' - -# 4. 启动数据库 -sudo systemctl start postgresql -``` - -### 12.2 应用回滚 -```bash -# Docker回滚到上一个版本 -docker-compose down -docker-compose up -d --force-recreate --no-deps api - -# 或使用特定版本 -docker pull takeout-saas-api:previous-version -docker-compose up -d -``` - -## 13. 网关 TakeoutSaaS.ApiGateway 部署 - -1. **部署拓扑** - - Nginx 负责域名 `kjkj.qiyuesns.cn`(含后续 HTTPS 证书),并将所有流量反代到本机 `http://127.0.0.1:5000`。 - - .NET 网关容器(TakeoutSaaS.ApiGateway)负责 YARP 路由、限流、日志与 OpenTelemetry 埋点,向下游 49.7.179.246 的 Admin/User/Mini API 转发。 - -2. **构建与运行** - ```bash - # 构建镜像 - docker build -f src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile -t takeoutsaas/apigateway:latest . - - # 启动容器(生产环境建议挂载独立配置) - docker run -d --name takeout-gateway -p 5000:5000 ^ - -e ASPNETCORE_ENVIRONMENT=Production ^ - -v /opt/takeoutsaas/gateway/appsettings.Production.json:/app/appsettings.Production.json ^ - takeoutsaas/apigateway:latest - ``` - - `appsettings.json` 默认将 `/api/admin|mini|user/**` 指向主应用服务器(7801/7701/7901 端口)。 - - `appsettings.Development.json` 可在本地覆盖为 `localhost:5001/5002/5003`,无需改代码。 - -3. **Nginx 参考配置** - ```nginx - server { - listen 80; - server_name kjkj.qiyuesns.cn; - - location / { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } - ``` - - 后续启用 HTTPS 时,将 `listen 443 ssl` 与证书配置加入同一 `server`,其余保持不变。 - -4. **关键配置项说明** - - `Gateway:RateLimiting`:按客户端 IP 固定窗口限流(默认 300 次/60 秒),可通过配置文件调整或关闭。 - - `ReverseProxy`:集中声明路由规则(`/api/{service}/**`),后端地址变更时只需改配置即可。 - - `OpenTelemetry`:默认开启 OTLP 导出,Collector 地址通过 `OpenTelemetry:OtlpEndpoint` 指定。 - - `Serilog`:统一输出到控制台,日志采集器可以直接收集 Docker stdout。 - -5. **健康检查** - - `GET /healthz`:基础健康,用于探活或监控告警。 - - `GET /`:返回服务元信息,可作为简易诊断接口。 diff --git a/Document/06_开发规范.md b/Document/06_开发规范.md deleted file mode 100644 index f558dfb..0000000 --- a/Document/06_开发规范.md +++ /dev/null @@ -1,395 +0,0 @@ -# 外卖SaaS系统 - 开发规范 - -## 1. 代码规范 - -### 1.1 命名规范 - -#### C#命名规范 -```csharp -// 类名:PascalCase -public class OrderService { } - -// 接口:I + PascalCase -public interface IOrderRepository { } - -// 方法:PascalCase -public async Task CreateOrderAsync() { } - -// 私有字段:_camelCase -private readonly IOrderRepository _orderRepository; - -// 公共属性:PascalCase -public string OrderNo { get; set; } - -// 局部变量:camelCase -var orderTotal = 100.00m; - -// 常量:PascalCase -public const int MaxOrderItems = 50; - -// 枚举:PascalCase -public enum OrderStatus -{ - Pending = 1, - Confirmed = 2, - Completed = 3 -} -``` - -#### 数据库命名规范 -```sql --- 表名:小写,下划线分隔,复数 -orders -order_items -merchant_stores - --- 字段名:小写,下划线分隔 -order_no -created_at -total_amount - --- 索引:idx_表名_字段名 -idx_orders_merchant_id -idx_orders_created_at - --- 外键:fk_表名_引用表名 -fk_orders_merchants -``` - -### 1.2 代码组织 - -#### 项目结构 -``` -TakeoutSaaS/ -├── src/ -│ ├── TakeoutSaaS.Api/ # Web API层 -│ │ ├── Controllers/ # 控制器 -│ │ ├── Filters/ # 过滤器 -│ │ ├── Middleware/ # 中间件 -│ │ ├── Models/ # DTO模型 -│ │ └── Program.cs -│ ├── TakeoutSaaS.Application/ # 应用层 -│ │ ├── Services/ # 应用服务 -│ │ ├── DTOs/ # 数据传输对象 -│ │ ├── Interfaces/ # 服务接口 -│ │ ├── Validators/ # 验证器 -│ │ ├── Mappings/ # 对象映射 -│ │ └── Commands/ # CQRS命令 -│ │ └── Queries/ # CQRS查询 -│ ├── TakeoutSaaS.Domain/ # 领域层 -│ │ ├── Entities/ # 实体 -│ │ ├── ValueObjects/ # 值对象 -│ │ ├── Enums/ # 枚举 -│ │ ├── Events/ # 领域事件 -│ │ └── Interfaces/ # 仓储接口 -│ ├── TakeoutSaaS.Infrastructure/ # 基础设施层 -│ │ ├── Data/ # 数据访问 -│ │ │ ├── EFCore/ # EF Core实现 -│ │ │ ├── Dapper/ # Dapper实现 -│ │ │ └── Repositories/ # 仓储实现 -│ │ ├── Cache/ # 缓存实现 -│ │ ├── MessageQueue/ # 消息队列 -│ │ └── ExternalServices/ # 外部服务 -│ └── TakeoutSaaS.Shared/ # 共享层 -│ ├── Constants/ # 常量 -│ ├── Exceptions/ # 异常 -│ ├── Extensions/ # 扩展方法 -│ └── Results/ # 统一返回结果 -├── tests/ -│ ├── TakeoutSaaS.UnitTests/ # 单元测试 -│ ├── TakeoutSaaS.IntegrationTests/ # 集成测试 -│ └── TakeoutSaaS.PerformanceTests/ # 性能测试 -└── docs/ # 文档 -``` - -### 1.3 代码注释 - -```csharp -/// -/// 订单服务接口 -/// -public interface IOrderService -{ - /// - /// 创建订单 - /// - /// 订单创建请求 - /// 订单信息 - /// 业务异常 - Task CreateOrderAsync(CreateOrderRequest request); -} - -// 复杂业务逻辑添加注释 -public async Task CalculateOrderAmount(Order order) -{ - // 1. 计算菜品总金额 - var dishAmount = order.Items.Sum(x => x.Price * x.Quantity); - - // 2. 计算配送费(距离 > 3km,每公里加收2元) - var deliveryFee = CalculateDeliveryFee(order.Distance); - - // 3. 应用优惠券折扣 - var discount = await ApplyCouponDiscountAsync(order.CouponId, dishAmount); - - // 4. 计算最终金额 - return dishAmount + deliveryFee - discount; -} -``` - -### 1.4 异常处理 - -```csharp -// 自定义业务异常 -public class BusinessException : Exception -{ - public int ErrorCode { get; } - - public BusinessException(int errorCode, string message) - : base(message) - { - ErrorCode = errorCode; - } -} - -// 全局异常处理中间件 -public class ExceptionHandlingMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public async Task InvokeAsync(HttpContext context) - { - try - { - await _next(context); - } - catch (BusinessException ex) - { - _logger.LogWarning(ex, "业务异常:{Message}", ex.Message); - await HandleBusinessExceptionAsync(context, ex); - } - catch (Exception ex) - { - _logger.LogError(ex, "系统异常:{Message}", ex.Message); - await HandleSystemExceptionAsync(context, ex); - } - } - - private static Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex) - { - context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; - return context.Response.WriteAsJsonAsync(new - { - success = false, - code = ex.ErrorCode, - message = ex.Message - }); - } -} - -// 使用示例 -public async Task GetOrderAsync(Guid orderId) -{ - var order = await _orderRepository.GetByIdAsync(orderId); - if (order == null) - { - throw new BusinessException(404, "订单不存在"); - } - return order; -} -``` - -## 2. Git工作流 - -### 2.1 分支管理 - -``` -main # 主分支,生产环境代码 -├── develop # 开发分支 -│ ├── feature/order-management # 功能分支 -│ ├── feature/payment-integration # 功能分支 -│ └── bugfix/order-calculation # 修复分支 -└── hotfix/critical-bug # 紧急修复分支 -``` - -### 2.2 分支命名规范 - -- **功能分支**:`feature/功能名称`(如:`feature/order-management`) -- **修复分支**:`bugfix/问题描述`(如:`bugfix/order-calculation`) -- **紧急修复**:`hotfix/问题描述`(如:`hotfix/payment-error`) -- **发布分支**:`release/版本号`(如:`release/v1.0.0`) - -### 2.3 提交信息规范 - -```bash -# 格式:(): - -# type类型: -# feat: 新功能 -# fix: 修复bug -# docs: 文档更新 -# style: 代码格式调整 -# refactor: 重构 -# perf: 性能优化 -# test: 测试相关 -# chore: 构建/工具相关 - -# 示例 -git commit -m "feat(order): 添加订单创建功能" -git commit -m "fix(payment): 修复支付回调处理错误" -git commit -m "docs(api): 更新API文档" -git commit -m "refactor(service): 重构订单服务" -``` - -### 2.4 工作流程 - -```bash -# 1. 从develop创建功能分支 -git checkout develop -git pull origin develop -git checkout -b feature/order-management - -# 2. 开发并提交 -git add . -git commit -m "feat(order): 添加订单创建功能" - -# 3. 推送到远程 -git push origin feature/order-management - -# 4. 创建Pull Request到develop分支 - -# 5. 代码审查通过后合并 - -# 6. 删除功能分支 -git branch -d feature/order-management -git push origin --delete feature/order-management -``` - -## 3. 代码审查 - -### 3.1 审查清单 - -- [ ] 代码符合命名规范 -- [ ] 代码逻辑清晰,易于理解 -- [ ] 适当的注释和文档 -- [ ] 异常处理完善 -- [ ] 单元测试覆盖 -- [ ] 性能考虑(N+1查询、大数据量处理) -- [ ] 安全性考虑(SQL注入、XSS、权限校验) -- [ ] 日志记录完善 -- [ ] 无硬编码配置 -- [ ] 符合SOLID原则 - -### 3.2 审查重点 - -```csharp -// ❌ 不好的实践 -public class OrderService -{ - public Order CreateOrder(CreateOrderRequest request) - { - // 直接在服务层操作DbContext - var order = new Order(); - _dbContext.Orders.Add(order); - _dbContext.SaveChanges(); - - // 硬编码配置 - var deliveryFee = 5.0m; - - // 没有异常处理 - // 没有日志记录 - - return order; - } -} - -// ✅ 好的实践 -public class OrderService : IOrderService -{ - private readonly IOrderRepository _orderRepository; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IOptions _settings; - - public async Task CreateOrderAsync(CreateOrderRequest request) - { - try - { - // 参数验证 - if (request == null) - throw new ArgumentNullException(nameof(request)); - - _logger.LogInformation("创建订单:{@Request}", request); - - // 业务逻辑 - var order = new Order - { - // ... 初始化订单 - DeliveryFee = _settings.Value.DefaultDeliveryFee - }; - - // 使用仓储 - await _orderRepository.AddAsync(order); - await _unitOfWork.SaveChangesAsync(); - - _logger.LogInformation("订单创建成功:{OrderId}", order.Id); - - return _mapper.Map(order); - } - catch (Exception ex) - { - _logger.LogError(ex, "创建订单失败:{@Request}", request); - throw; - } - } -} -``` - - - -## 4. 单元测试规范 - -### 4.1 测试命名 -- 命名格式:`MethodName_Scenario_ExpectedResult` -- 测试覆盖率要求:核心业务逻辑 >= 80% - -### 4.2 测试示例 - -```csharp -[Fact] -public async Task CreateOrder_ValidRequest_ReturnsOrderDto() -{ - // Arrange - var request = new CreateOrderRequest { /* ... */ }; - - // Act - var result = await _orderService.CreateOrderAsync(request); - - // Assert - result.Should().NotBeNull(); - result.OrderNo.Should().NotBeNullOrEmpty(); -} -``` - -## 5. 性能优化规范 - -### 5.1 数据库查询优化 -- 避免N+1查询,使用Include预加载 -- 大数据量查询使用Dapper -- 合理使用索引 - -### 5.2 缓存策略 -- 商家信息:30分钟 -- 菜品信息:15分钟 -- 配置信息:1小时 -- 用户会话:2小时 - -## 6. 文档要求 - -### 6.1 代码文档 -- 所有公共API必须有XML文档注释 -- 复杂业务逻辑添加详细注释 -- README.md说明项目结构和运行方式 - -### 6.2 变更日志 -维护CHANGELOG.md记录版本变更 \ No newline at end of file diff --git a/Document/07_系统架构图.md b/Document/07_系统架构图.md deleted file mode 100644 index 3a5a350..0000000 --- a/Document/07_系统架构图.md +++ /dev/null @@ -1,321 +0,0 @@ -# 外卖SaaS系统 - 系统架构图 - -## 1. 整体架构图 - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ 客户端层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ -│ │ Web管理端 │ │ Web用户端 │ │ 小程序端(用户) │ │ -│ │ (React/Vue) │ │ (React/Vue) │ │ (微信/支付宝) │ │ -│ └──────────────┘ └──────────────┘ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ API网关层 │ -│ ┌───────────────────────────────────────────────────────────────┐ │ -│ │ Nginx / API Gateway │ │ -│ │ - 路由转发 │ │ -│ │ - 负载均衡 │ │ -│ │ - 限流熔断 │ │ -│ │ - SSL终止 │ │ -│ └───────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ 应用服务层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 租户服务 │ │ 商家服务 │ │ 菜品服务 │ │ -│ │ - 租户管理 │ │ - 商家管理 │ │ - 菜品管理 │ │ -│ │ - 权限管理 │ │ - 门店管理 │ │ - 分类管理 │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 订单服务 │ │ 配送服务 │ │ 用户服务 │ │ -│ │ - 订单管理 │ │ - 配送员管理 │ │ - 用户管理 │ │ -│ │ - 订单流转 │ │ - 任务分配 │ │ - 地址管理 │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 支付服务 │ │ 营销服务 │ │ 通知服务 │ │ -│ │ - 支付处理 │ │ - 优惠券 │ │ - 短信通知 │ │ -│ │ - 退款处理 │ │ - 活动管理 │ │ - 推送通知 │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ 基础设施层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ PostgreSQL │ │ Redis │ │ RabbitMQ │ │ -│ │ - 主数据库 │ │ - 缓存 │ │ - 消息队列 │ │ -│ │ - 主从复制 │ │ - 会话存储 │ │ - 异步任务 │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ MinIO/OSS │ │ Elasticsearch│ │ Prometheus │ │ -│ │ - 对象存储 │ │ - 日志存储 │ │ - 监控告警 │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## 2. 应用分层架构 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ (表现层) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ TakeoutSaaS.Api │ │ -│ │ - Controllers (控制器) │ │ -│ │ - Filters (过滤器) │ │ -│ │ - Middleware (中间件) │ │ -│ │ - Models (DTO模型) │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (应用层) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ TakeoutSaaS.Application │ │ -│ │ - Services (应用服务) │ │ -│ │ - DTOs (数据传输对象) │ │ -│ │ - Interfaces (服务接口) │ │ -│ │ - Validators (验证器) │ │ -│ │ - Mappings (对象映射) │ │ -│ │ - Commands/Queries (CQRS) │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Domain Layer │ -│ (领域层) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ TakeoutSaaS.Domain │ │ -│ │ - Entities (实体) │ │ -│ │ - ValueObjects (值对象) │ │ -│ │ - Enums (枚举) │ │ -│ │ - Events (领域事件) │ │ -│ │ - Interfaces (仓储接口) │ │ -│ │ - Specifications (规约) │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ (基础设施层) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ TakeoutSaaS.Infrastructure │ │ -│ │ - Data (数据访问) │ │ -│ │ - EFCore (EF Core实现) │ │ -│ │ - Dapper (Dapper实现) │ │ -│ │ - Repositories (仓储实现) │ │ -│ │ - Cache (缓存实现) │ │ -│ │ - MessageQueue (消息队列) │ │ -│ │ - ExternalServices (外部服务) │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 3. 订单处理流程图 - -``` -用户下单 → 创建订单 → 支付 → 商家接单 → 制作 → 配送 → 完成 - │ │ │ │ │ │ │ - │ │ │ │ │ │ └─→ 订单完成 - │ │ │ │ │ └─→ 配送中 - │ │ │ │ └─→ 制作中 - │ │ │ └─→ 待制作 - │ │ └─→ 待接单 - │ └─→ 待支付 - └─→ 订单创建 - -取消流程: -用户取消 ──→ 退款处理 ──→ 订单取消 -商家拒单 ──→ 退款处理 ──→ 订单取消 -超时未支付 ──→ 自动取消 -``` - -## 4. 数据流转图 - -``` -┌──────────┐ -│ 客户端 │ -└────┬─────┘ - │ HTTP Request - ▼ -┌──────────────────┐ -│ API Gateway │ -│ (Nginx) │ -└────┬─────────────┘ - │ 路由转发 - ▼ -┌──────────────────┐ -│ Web API │ -│ - 认证授权 │ -│ - 参数验证 │ -└────┬─────────────┘ - │ 调用服务 - ▼ -┌──────────────────┐ -│ Application │ -│ Service │ -│ - 业务逻辑 │ -└────┬─────────────┘ - │ 数据访问 - ▼ -┌──────────────────┐ ┌──────────┐ -│ Repository │────→│ Cache │ -│ - EF Core │ │ (Redis) │ -│ - Dapper │ └──────────┘ -└────┬─────────────┘ - │ SQL查询 - ▼ -┌──────────────────┐ -│ PostgreSQL │ -│ Database │ -└──────────────────┘ -``` - -## 5. 多租户数据隔离架构 - -``` -┌─────────────────────────────────────────────────┐ -│ 租户识别中间件 │ -│ - 从JWT Token解析租户ID │ -│ - 从HTTP Header获取租户ID │ -└─────────────────────┬───────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────┐ -│ 租户上下文 │ -│ - 当前租户ID │ -│ - 租户配置信息 │ -└─────────────────────┬───────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────┐ -│ 数据访问层 │ -│ - 自动添加租户ID过滤 │ -│ - 全局查询过滤器 │ -└─────────────────────┬───────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────┐ -│ 数据库 │ -│ 租户A数据 │ 租户B数据 │ 租户C数据 │ -│ (tenant_id = A) (tenant_id = B) (tenant_id = C)│ -└─────────────────────────────────────────────────┘ -``` - -## 6. 缓存架构 - -``` -┌──────────────┐ -│ Application │ -└──────┬───────┘ - │ - ▼ -┌──────────────────────────────────┐ -│ Cache Aside Pattern │ -│ 1. 查询缓存 │ -│ 2. 缓存未命中,查询数据库 │ -│ 3. 写入缓存 │ -└──────┬───────────────────────────┘ - │ - ├─→ L1 Cache (Memory Cache) - │ - 进程内缓存 - │ - 热点数据 - │ - └─→ L2 Cache (Redis) - - 分布式缓存 - - 会话数据 - - 共享数据 -``` - -## 7. 消息队列架构 - -``` -┌──────────────┐ -│ Producer │ -│ (订单服务) │ -└──────┬───────┘ - │ 发布事件 - ▼ -┌──────────────────┐ -│ RabbitMQ │ -│ Exchange │ -└──────┬───────────┘ - │ - ├─→ Queue: order.created - │ └─→ Consumer: 通知服务 - │ - ├─→ Queue: order.paid - │ └─→ Consumer: 库存服务 - │ - └─→ Queue: order.completed - └─→ Consumer: 统计服务 -``` - -## 8. 部署架构 - -``` -┌─────────────────────────────────────────────────┐ -│ 负载均衡器 (Nginx) │ -└─────────────┬───────────────────────────────────┘ - │ - ┌───────┴───────┐ - │ │ - ▼ ▼ -┌──────────┐ ┌──────────┐ -│ API 1 │ │ API 2 │ -│ (容器) │ │ (容器) │ -└────┬─────┘ └────┬─────┘ - │ │ - └───────┬───────┘ - │ - ┌───────┴────────┬──────────────┐ - │ │ │ - ▼ ▼ ▼ -┌──────────┐ ┌──────────┐ ┌──────────┐ -│PostgreSQL│ │ Redis │ │ RabbitMQ │ -│ 主从 │ │ 哨兵 │ │ 集群 │ -└──────────┘ └──────────┘ └──────────┘ -``` - -## 9. 监控架构 - -``` -┌──────────────────────────────────────────┐ -│ 应用程序 │ -│ - 业务指标 │ -│ - 性能指标 │ -│ - 日志输出 │ -└─────────┬────────────────────────────────┘ - │ - ┌─────┴─────┬──────────┐ - │ │ │ - ▼ ▼ ▼ -┌────────┐ ┌────────┐ ┌────────┐ -│Metrics │ │ Logs │ │Traces │ -│ │ │ │ │ │ -└───┬────┘ └───┬────┘ └───┬────┘ - │ │ │ - ▼ ▼ ▼ -┌──────────────────────────────┐ -│ Prometheus │ -│ Elasticsearch │ -│ Jaeger │ -└─────────┬────────────────────┘ - │ - ▼ -┌──────────────────────────────┐ -│ Grafana │ -│ Kibana │ -│ - 可视化仪表板 │ -│ - 告警配置 │ -└──────────────────────────────┘ -``` diff --git a/Document/08_AI精简开发规范.md b/Document/08_AI精简开发规范.md deleted file mode 100644 index 865a052..0000000 --- a/Document/08_AI精简开发规范.md +++ /dev/null @@ -1,145 +0,0 @@ -# 编程规范_FOR_AI(TakeoutSaaS) - 终极完全体 - -> **核心指令**:你是一个高级 .NET 架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。 - -## 0. AI 交互核心约束 (元规则) -1. **语言**:必须使用**中文**回复和编写注释。 -2. **文件完整性**: - * **严禁**随意删除现有代码逻辑。 - * **严禁**修改文件编码(保持 UTF-8 无 BOM)。 - * PowerShell 读取命令必须带 `-Encoding UTF8`。 -3. **Git 原子性**:每个独立的功能点或 Bug 修复完成后,必须提示用户进行 Git 提交。 -4. **无乱码承诺**:确保所有输出(控制台、日志、API响应)无乱码。 -5. **不确定的处理**:如果你通过上下文找不到某些配置(如数据库连接串格式),**请直接询问用户**,不要瞎编。 - -## 1. 技术栈详细版本 -| 组件 | 版本/选型 | 用途说明 | -| :--- | :--- | :--- | -| **Runtime** | .NET 10 | 核心运行时 | -| **API** | ASP.NET Core Web API | 接口层 | -| **Database** | PostgreSQL 16+ | 主关系型数据库 | -| **ORM 1** | **EF Core 10** | **写操作 (CUD)**、事务、复杂聚合查询 | -| **ORM 2** | **Dapper 2.1+** | **纯读操作 (R)**、复杂报表、大批量查询 | -| **Cache** | Redis 7.0+ | 分布式缓存、Session | -| **MQ** | RabbitMQ 3.12+ | 异步解耦 (MassTransit) | -| **Libs** | MediatR, Serilog, FluentValidation | CQRS, 日志, 验证 | - -## 2. 命名与风格 (严格匹配) -* **C# 代码**: - * 类/接口/方法/属性:`PascalCase` (如 `OrderService`) - * **布尔属性**:必须加 `Is` 或 `Has` 前缀 (如 `IsDeleted`, `HasPayment`) - * 私有字段:`_camelCase` (如 `_orderRepository`) - * 参数/变量:`camelCase` (如 `orderId`) -* **PostgreSQL 数据库**: - * 表名:`snake_case` + **复数** (如 `merchant_orders`) - * 列名:`snake_case` (如 `order_no`, `is_active`) - * 主键:`id` (类型 `bigint`) -* **文件规则**: - * **一个文件一个类**。文件名必须与类名完全一致。 - -## 3. 分层架构 (Clean Architecture) -**你生成的代码必须严格归类到以下目录:** -* **`src/Api`**: 仅负责路由与 DTO 转换,**禁止**包含业务逻辑。 -* **`src/Application`**: 业务编排层。必须使用 **CQRS** (`IRequestHandler`) 和 **Mediator**。 -* **`src/Domain`**: 核心领域层。包含实体、枚举、领域异常。**禁止**依赖 EF Core 等外部库。 -* **`src/Infrastructure`**: 基础设施层。实现仓储、数据库上下文、第三方服务。 - -## 4. 注释与文档 -* **强制 XML 注释**:所有 `public` 的类、方法、属性必须有 ``。 -* **步骤注释**:超过 5 行的业务逻辑,必须分步注释: - ```csharp - // 1. 验证库存 - // 2. 扣减余额 - ``` -* **Swagger**:必须开启 JWT 鉴权按钮,Request/Response 示例必须清晰。 - -## 5. 异常处理 (防御性编程) -* **禁止空 Catch**:严禁 `catch (Exception) {}`,必须记录日志或抛出。 -* **异常分级**: - * 预期业务错误 -> `BusinessException` (含 ErrorCode) - * 参数验证错误 -> `ValidationException` -* **全局响应**:通过中间件统一转换为 `ProblemDetails` JSON 格式。 - -## 6. 异步与日志 -* **全异步**:所有 I/O 操作必须 `await`。**严禁** `.Result` 或 `.Wait()`。 -* **结构化日志**: - * ❌ `_logger.LogInfo("订单 " + id + " 创建成功");` - * ✅ `_logger.LogInformation("订单 {OrderId} 创建成功", id);` -* **脱敏**:严禁打印密码、密钥、支付凭证等敏感信息。 - -## 7. 依赖注入 (DI) -* **构造函数注入**:统一使用构造函数注入。 -* **禁止项**: - * ❌ 禁止使用 `[Inject]` 属性注入。 - * ❌ 禁止使用 `ServiceLocator` (服务定位器模式)。 - * ❌ 禁止在静态类中持有 ServiceProvider。 - -## 8. 数据访问规范 (重点执行) -### 8.1 Entity Framework Core (写/事务) -1. **无跟踪查询**:只读查询**必须**加 `.AsNoTracking()`。 -2. **杜绝 N+1**:严禁在 `foreach` 循环中查询数据库。必须使用 `.Include()`。 -3. **复杂查询**:关联表超过 2 层时,考虑使用 `.AsSplitQuery()`。 - -### 8.2 Dapper (读/报表) -1. **SQL 注入防御**:**严禁**拼接 SQL 字符串。必须使用参数化查询 (`@Param`)。 -2. **字段映射**:注意 PostgreSQL (`snake_case`) 与 C# (`PascalCase`) 的映射配置。 - -## 9. 多租户与 ID 策略 -* **ID 生成**: - * **强制**使用 **雪花算法 (Snowflake ID)**。 - * 类型:C# `long` <-> DB `bigint`。 - * **禁止**使用 UUID 或 自增 INT。 -* **租户隔离**: - * 所有业务表必须包含 `tenant_id`。 - * 写入时自动填充,读取时强制过滤。 - -## 10. API 设计与序列化 (前端兼容) -* **大整数处理**: - * 所有 `long` 类型 (Snowflake ID) 在 DTO 中**必须序列化为 string**。 - * 方案:DTO 属性加 `[JsonConverter(typeof(ToStringJsonConverter))]` 或全局配置。 -* **DTO 规范**: - * 输入:`XxxRequest` - * 输出:`XxxDto` - * **禁止** Controller 直接返回 Entity。 - -## 11. 模块化与复用 -* **核心模块划分**:Identity (身份), Tenancy (租户), Dictionary (字典), Storage (存储)。 -* **公共库 (Shared)**:通用工具类、扩展方法、常量定义必须放在 `Core/Shared` 项目中,避免重复造轮子。 - -## 12. 测试规范 -* **模式**:Arrange-Act-Assert (AAA)。 -* **工具**:xUnit + Moq + FluentAssertions。 -* **覆盖率**:核心 Domain 逻辑必须 100% 覆盖;Service 层 ≥ 70%。 - -## 13. Git 工作流 -* **提交格式 (Conventional Commits)**: - * `feat`: 新功能 - * `fix`: 修复 Bug - * `refactor`: 重构 - * `docs`: 文档 - * `style`: 格式调整 -* **分支规范**:`feature/功能名`,`bugfix/问题描述`。 - -## 14. 性能优化 (显式指令) -* **投影查询**:使用 `.Select(x => new Dto { ... })` 只查询需要的字段,减少 I/O。 -* **缓存策略**:Cache-Aside 模式。数据更新后必须立即失效缓存。 -* **批量操作**: - * EF Core 10:使用 `ExecuteUpdateAsync` / `ExecuteDeleteAsync`。 - * Dapper:使用 `ExecuteAsync` 进行批量插入。 - -## 15. 安全规范 -* **SQL 注入**:已在第 8 条强制参数化。 -* **身份认证**:Admin 端使用 JWT + RBAC;小程序端使用 Session/Token。 -* **密码存储**:必须使用 PBKDF2 或 BCrypt 加盐哈希。 - -## 16. 绝对禁止事项 (AI 自检清单) -**生成代码前,请自查是否违反以下红线:** -1. [ ] **SQL 注入**:是否拼接了 SQL 字符串? -2. [ ] **架构违规**:是否在 Controller/Domain 中使用了 DbContext? -3. [ ] **数据泄露**:是否返回了 Entity 或打印了密码? -4. [ ] **同步阻塞**:是否使用了 `.Result` 或 `.Wait()`? -5. [ ] **性能陷阱**:是否在循环中查询了数据库 (N+1)? -6. [ ] **精度丢失**:Long 类型的 ID 是否转为了 String? -7. [ ] **配置硬编码**:是否直接写死了连接串或密钥? - ---- \ No newline at end of file diff --git a/Document/09_服务器文档.md b/Document/09_服务器文档.md deleted file mode 100644 index a682d88..0000000 --- a/Document/09_服务器文档.md +++ /dev/null @@ -1,79 +0,0 @@ -# 服务器文档 - -> 汇总原 12~15 号服务器记录,统一追踪账号、密码、用途与到期时间,便于统一维护。 - -## 1. 阿里云网关服务器 - -### 基础信息 -- IP: 47.94.199.87 -- 账户: root -- 密码: cJ5q2k2iW7XnMA^! -- 配置: 2 核 CPU / 2 GB 内存(阿里云轻量应用服务器) -- 地点: 北京 -- 用途: 网关 -- 到期时间: 2026-12-18 - -### 建议补充 -- 系统版本: 待补充(执行 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充 -- 备份与监控: 待补充 -- 变更记录: 待补充 - -## 2. 腾讯云主机 PostgreSQL 服务器 - -### 基础信息 -- IP: 120.53.222.17 -- 账户: ubuntu -- 密码: P3y$nJt#zaa4%fh5 -- 配置: 2 核 CPU / 4 GB 内存 -- 地点: 北京 -- 用途: 主 PostgreSQL / 数据库服务器 -- 到期时间: 2026-11-26 11:22:01 - -### 建议补充 -- 系统版本: 待补充(执行 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 数据目录: 待补充(示例 `/var/lib/postgresql`) -- 数据备份/监控: 待补充 -- 安全组/开放端口: 待补充 -- 变更记录: 待补充 - -## 3. 天翼云主机 应用服务器 - -### 基础信息 -- IP: 49.7.179.246 -- 账户: root -- 密码: 7zE&84XI6~w57W7N -- 配置: 4 核 CPU / 8 GB 内存(天翼云) -- 地点: 北京 -- 用途: 主应用服务器(承载 Admin/User/Mini API 或网关实例) -- 到期时间: 2027-10-04 17:17:57 - -### 建议补充 -- 系统版本: 待补充(执行 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 部署路径: 待补充(示例 `/opt/takeoutsaas`) -- 进程/端口: 待补充(示例 `AdminApi/UserApi/MiniApi`、`:8080`) -- 日志/监控: 待补充(Serilog 文件目录、进程监控方式) -- 安全组/开放端口: 待补充(按 API/网关暴露的 HTTP/HTTPS 端口) -- 变更记录: 待补充 - -## 4. 腾讯云 Redis/RabbitMQ 服务器 - -### 基础信息 -- IP: 49.232.6.45 -- 账户: ubuntu -- 密码: Z7NsRjT&XnWg7%7X -- 配置: 2 核 CPU / 4 GB 内存 -- 地点: 北京 -- 用途: Redis 与 RabbitMQ -- 到期时间: 2028-11-26 - -### 建议补充 -- 系统版本: 待补充(执行 `cat /etc/os-release`) -- 带宽/磁盘: 待补充 -- 安全组/开放端口: 待补充(Redis 6379,RabbitMQ 5672/15672 等) -- 数据持久化与备份: 待补充 -- 监控与告警: 待补充 -- 变更记录: 待补充 diff --git a/Document/10_设计期DbContext配置指引.md b/Document/10_设计期DbContext配置指引.md deleted file mode 100644 index 2da0fd9..0000000 --- a/Document/10_设计期DbContext配置指引.md +++ /dev/null @@ -1,131 +0,0 @@ -# 设计时 DbContext 配置指引 - -> 目的:在执行 `dotnet ef` 命令时无需硬编码数据库连接,可根据 appsettings 与环境变量自动加载。本文覆盖环境变量设置、配置目录指定等细节。 - -## 三库迁移命令 只需更改 SnowflakeIds_App 迁移关键字 -> 先生成迁移,再执行数据库更新。启动项目统一用 AdminApi 确保加载最新配置。 - -### 生成迁移 -```bash -# App 主库 -dotnet tool run dotnet-ef migrations add SnowflakeIds_App ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` - --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext - -# Identity 库 -dotnet tool run dotnet-ef migrations add SnowflakeIds_Identity ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` - --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext - -# Dictionary 库 -dotnet tool run dotnet-ef migrations add SnowflakeIds_Dictionary ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` - --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext -``` - -### 更新数据库 -```bash -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` - --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext - -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` - --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext - -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj ` - --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext -``` - -## 一、设计时工厂读取逻辑概述 -设计时工厂(`DesignTimeDbContextFactoryBase`)按下面顺序解析连接串: -1. 若设置了 `TAKEOUTSAAS_APP_CONNECTION` / `TAKEOUTSAAS_IDENTITY_CONNECTION` / `TAKEOUTSAAS_DICTIONARY_CONNECTION` 等环境变量,则优先使用。 -2. 否则查找配置文件: - - 从当前目录开始向上找到含 `TakeoutSaaS.sln` 的仓库根。 - - 依次检查 `src/Api/TakeoutSaaS.AdminApi`、`src/Api/TakeoutSaaS.UserApi`、`src/Api/TakeoutSaaS.MiniApi` 等目录,如果存在 `appsettings.json` 或 `appsettings.{Environment}.json` 则加载。 - - 若未找到,可通过环境变量 `TAKEOUTSAAS_APPSETTINGS_DIR` 指定包含 appsettings 文件的目录。 - -配置结构示例(出现在 AdminApi/MiniApi/UserApi 的 appsettings): -```json -"Database": { - "DataSources": { - "AppDatabase": { - "Write": "Host=120.53...;Database=takeout_app_db;Username=...;Password=...", - "Reads": [ - "Host=120.53...;Database=takeout_app_db;Username=...;Password=..." - ] - }, - "IdentityDatabase": { - "Write": "...", - "Reads": [ "..." ] - }, - "DictionaryDatabase": { - "Write": "...", - "Reads": [ "..." ] - } - } -} -``` -设计时工厂会根据数据源名称(`DatabaseConstants.AppDataSource` 等)读取 `Write` 连接串,实现与运行时一致。 - -## 二、环境变量配置 -### 1. Windows PowerShell -```powershell -# 指向包含 appsettings.json 的目录 -$env:TAKEOUTSAAS_APPSETTINGS_DIR = \"D:\\HAZCode\\TakeOut\\src\\Api\\TakeoutSaaS.AdminApi\" - -#(可选)覆盖 AppDatabase 连接串 -$env:TAKEOUTSAAS_APP_CONNECTION = \"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=***\" - -#(可选)覆盖 IdentityDatabase 连接串 -$env:TAKEOUTSAAS_IDENTITY_CONNECTION = \"Host=...;Database=takeout_identity_db;Username=...;Password=...\" - -#(可选)覆盖 DictionaryDatabase 连接串 -$env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=...;Database=takeout_dictionary_db;Username=...;Password=..." -``` - -### 2. Linux / macOS -```bash -export TAKEOUTSAAS_APPSETTINGS_DIR=/home/user/TakeOut/src/Api/TakeoutSaaS.AdminApi -export TAKEOUTSAAS_APP_CONNECTION=\"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=***\" -export TAKEOUTSAAS_IDENTITY_CONNECTION=\"Host=...;Database=takeout_identity_db;Username=...;Password=...\" -export TAKEOUTSAAS_DICTIONARY_CONNECTION="Host=...;Database=takeout_dictionary_db;Username=...;Password=..." -``` - -> 注意:若设置了 `TAKEOUTSAAS_APP_CONNECTION`,则无需在 appsettings 中提供 `Write` 连接串,反之亦然。不要将明文密码写入代码仓库,建议使用 Secret Manager 或部署环境的安全存储。 - -## 三、执行脚本示例 -完成上述环境变量配置后即可执行: -```powershell -# TakeoutAppDbContext(业务库) -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext - -# IdentityDbContext(身份库) -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext - -# DictionaryDbContext(字典库) -dotnet tool run dotnet-ef database update ` - --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj ` - --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext -``` - -若需迁移 Identity/Dictionary 等上下文,替换 `--context` 参数为对应类型即可。 - -## 四、常见问题 -1. **未找到 appsettings**:确保 `TAKEOUTSAAS_APPSETTINGS_DIR` 指向存在 `appsettings.json` 的目录,或将命令在 API 项目目录中执行。 -2. **密码错误**:确认远程 PostgreSQL 用户/密码是否与 appsettings 或环境变量一致,避免在 CLI 中使用默认的账号。 -3. **多环境配置**:`ASPNETCORE_ENVIRONMENT` 变量可控制加载 `appsettings.{Environment}.json`;默认是 Development。 diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md deleted file mode 100644 index dead828..0000000 --- a/Document/11_SystemTodo.md +++ /dev/null @@ -1,67 +0,0 @@ -# TODO Roadmap - -> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。 - -## 1. 配置与基础设施(高优) -- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。 -- [x] Redis 服务部署完毕并记录配置。 -- [x] RabbitMQ 服务部署完毕并记录配置。 -- [x] COS 密钥配置补录完毕。 -- [ ] OSS 密钥配置补录完毕(已忽略,待采购后再补录)。 -- [ ] SMS 平台密钥配置补录完毕(已忽略,待采购后再补录)。 -- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。 -- [x] PostgreSQL 基础实例部署完毕并记录配置。 -- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。 -- [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis). -- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。(忽略,暂时不用完成) -- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。(忽略,暂时不用完成) - -## 2. 数据与迁移(高优) -- [x] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。 -- [x] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。 -- [x] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。 - -## 3. 稳定性与质量(低优先级) -- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。 -- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ 的集成测试模板。 -- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。 - -## 4. 安全与合规 -- [x] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。 - - [x] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。 - - [x] 差距与步骤: - - [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。 - - [x] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。【已落地:RBAC1 模型 + 角色/权限管理 API;Swagger 示例后续补充】 - - [x] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。 - - [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。 - - [x] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。 - - [x] Step D 校验:Admin API 管道已在 Auth 之前使用 TenantResolution,中间件未忽略新接口;查询使用 TenantAwareDbContext + ITenantProvider 双重租户校验,暂无需调整。后续若加 Dapper 读侧需显式带 tenant 过滤。 -- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。 -- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。 -- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。 - -## 5. 观测与运维 -- [x] TraceId 贯通,Serilog 输出 Console/File(ELK 待后续配置)。 -- [x] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。 -- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。 - -## 6. 业务能力补全 -- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。 -- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。 -- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。 - -## 7. 前后台 UI 对接 -- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。 -- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。 - -## 8. CI/CD 与发布 -- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。 - - [x] `.github/workflows/ci-cd.yml` 已覆盖 Admin/Mini/User API 的变更检测、Docker Build/Push 与 SSH 部署。 - - [ ] 尚未集成静态代码扫描、安全扫描与数据库迁移自动化。 -- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。 -- [ ] 版本与发布说明模板整理并在仓库中提供示例。 - -## 9. 文档与知识库 -- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。 -- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。 -- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。 diff --git a/Document/12_BusinessTodo.md b/Document/12_BusinessTodo.md deleted file mode 100644 index 00d98dd..0000000 --- a/Document/12_BusinessTodo.md +++ /dev/null @@ -1,73 +0,0 @@ -# 里程碑待办追踪 - -> 按“小程序版模块规划”划分四个里程碑;每个里程碑只含对应范围的任务,便于分阶段推进。 - ---- -## Phase 1(当前阶段):租户/商家入驻、门店与菜品、扫码堂食、基础下单支付、预购自提、第三方配送骨架 -- [x] 管理端租户 API:注册、实名认证、套餐订阅/续费/升降配、审核流,Swagger ≥6 个端点,含审核日志。 - - 已交付:`src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs` 暴露注册、详情、实名提交、审核、订阅创建/升降配、审核日志 8 个端点;对应命令/查询位于 `src/Application/TakeoutSaaS.Application/App/Tenants`,仓储实现 `EfTenantRepository`,并写入 `TenantAuditLog` 记录。Swagger 自动收录上述接口,满足 Phase1 租户管理要求。 -- [x] 商家入驻 API:证照上传、合同管理、类目选择,驱动待审/审核/驳回/通过状态机,文件持久在 COS。 - - 已交付:`src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs` 新增证照上传/审核、合同创建与状态更新、商户审核、审核日志、类目列表等 8 个端点;应用层新增 `AddMerchantDocumentCommand`、`CreateMerchantContractCommand`、`ReviewMerchantCommand` 等 Handler;`MerchantDocument/Contract/Audit` DTO 完整返回详情,文件 URL 仍通过 `/api/admin/v1/files/upload` 上 COS。仓储实现扩展 `EfMerchantRepository` 支持文档/合同/AuditLog 持久化,`TakeoutAppDbContext` 新增 `merchant_audit_logs` 表实现状态机追踪。 -- [x] RBAC 模板:平台管理员、租户管理员、店长、店员四角色模板;API 可复制并允许租户自定义扩展。 - - 已交付:角色模板改为数据库驱动,新增 `RoleTemplate/RoleTemplatePermission` 实体与仓储接口/实现;应用层提供模板列表/详情/创建/更新/删除、按模板复制与租户批量初始化命令/查询;Admin 端 `RolesController` 暴露模板 CRUD 与复制/初始化端点(`src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs`),复制时补齐缺失权限且保留租户自定义授权;预置模板/权限种子写入 `src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.*.json`。 -- [x] 配额与套餐:TenantPackage CRUD、订阅/续费/配额校验(门店/账号/短信/配送单量),超额返回 409 并记录 TenantQuotaUsage。 - - 已交付:新增套餐仓储与命令/查询/DTO(`src/Application/TakeoutSaaS.Application/App/Tenants`),Admin 端新增 `TenantPackagesController` 提供套餐列表/详情/创建/更新/删除接口。新增配额校验命令与租户接口 `/api/admin/v1/tenants/{id}/quotas/check`,基于当前订阅套餐限额校验并占用配额,超额抛出 409 并写入 `TenantQuotaUsage`。仓储注册于 `AddAppInfrastructure`。 -- [x] 租户运营面板:欠费/到期告警、账单列表、公告通知接口,支持已读状态并在 Admin UI 展示。 - - 已交付:新增账单/公告/通知实体与仓储,Admin 端提供 `/tenants/{id}/billings`(列表/详情/创建/标记支付)、`/announcements`(列表/详情/创建/更新/删除/已读)、`/notifications`(列表/已读)端点;权限码补充 `tenant-bill:*`、`tenant-announcement:*`、`tenant-notification:*`,种子模板更新;配额/订阅告警可通过通知表承载。 -- [x] 门店管理:Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。 - - 已交付:营业时间/配送区/节假日命令、查询、验证与处理器齐全,Admin API 子路由完成 CRUD,门店能力开关(预约/排队)对外暴露,仓储读写删除均带租户过滤。 -- [x] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 - - 已交付:桌台区域/桌码 DTO、命令、查询、验证与处理器完善,支持批量生成、区域绑定/更新;Admin API 增加区域/桌码 CRUD 与二维码 ZIP 导出(QRCoder 生成 SVG 打包),仓储补齐查找、更新、删除。 -- [x] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 - - 已交付:门店员工 DTO/命令/查询/验证/处理器完成,支持创建/更新/删除/查询;排班 CRUD(默认未来 7 天)含归属与时间冲突校验;Admin API 增加员工与排班控制器及权限种子,仓储含排班查询/更新/删除。 -- [x] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。 - - 已交付:桌码上下文查询 DTO/验证/处理器完成,可按桌码返回门店名称/公告/标签与桌台信息;MiniApi 新增 `TablesController` `/context` 端点,仓储支持按桌码查询。 -- [x] 菜品建模:分类、SPU、SKU、规格/加料组、价格策略、媒资 CRUD + 上下架流程;Mini 端可拉取完整 JSON。 - - 已交付:Admin 侧补齐 SKU/规格/加料/媒资/定价替换命令、验证与端点,并新增上/下架接口与全量详情;权限种子补充 `product:publish` 与子资源读写。Mini 侧新增门店菜单接口,按门店返回分类 + 商品全量 JSON(含 SKU/规格/加料/媒资/定价),支持 `updatedAfter` 增量。 -- [x] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。 - - 已交付:库存模型补充预售/限购/并发字段与批次策略(FIFO/FEFO),新增锁定记录与幂等、过期释放;应用层提供调整/锁定/释放/扣减/批次维护命令与查询,Admin API 暴露库存与批次端点及权限种子。需后续生成迁移落库,并可按需将过期释放接入定时任务。 -- [x] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。 - - 已交付:新增自提设置与档期实体/表、并发控制,Admin 端提供自提配置与档期 CRUD 权限/接口;Mini 端提供按日期查询可用档期,包含截单与容量校验。下单限制待后续与订单流程联调。 -- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。 - - 当前:领域层与表结构已有 `ShoppingCart/CartItem/CartItemAddon`,但缺少 CQRS 命令/查询、并发锁/限购/券积分预校验以及任何 Admin/Mini 端接口。 -- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。 - - 当前:Admin 端 `OrdersController`/`PaymentsController` 仅提供基础 CRUD,未覆盖堂食/自提/配送业务流、微信/支付宝支付、优惠券/积分抵扣、订单状态机、通知链路及与库存/配送的集成,Mini 端也无下单/支付接口。 -- [ ] 桌台账单:合单/拆单、结账、电子小票、桌台释放,完成结账后恢复 Idle 并生成票据 URL。 - - 当前:无桌台账单/合单/拆单/结账或电子小票逻辑,桌台仅有基础实体定义。 -- [ ] 自配送骨架:骑手管理、取送件信息录入、费用补贴记录,Admin 端可派单并更新 DeliveryOrder。 - - 当前:`DeliveryOrder` CRUD 支持录入 `CourierName/Phone`,但缺少骑手管理、派单流程、取送件详情与补贴记录等自配送骨架。 -- [ ] 第三方配送抽象:统一下单/取消/加价/查询接口,支持达达、美团、闪送等,含回调验签与异常补偿骨架。 - - 当前:尚未提供第三方配送抽象、回调验签或补偿逻辑,配送模块仅有基础 CRUD。 -- [ ] 预购自提核销:提货码生成、手机号/二维码核销、自提柜/前台流程,超时自动取消或退款,记录操作者与时间。 - - 当前:存在 `Reservation` 实体及订单字段 `ReservationId/CheckInCode`,但未实现提货码生成、核销接口、超时取消/退款或核销人记录,未与订单支付联动。 -- [ ] 指标与日志:Prometheus 输出订单创建、支付成功率、配送回调耗时等,Grafana ≥8 个图表;关键流程日志记录 TraceId + 业务 ID。 - - 当前:Admin/Mini/User API 与网关已接入 OpenTelemetry(OTLP 与 Prometheus 导出)和 TraceId 结构化日志,但缺少订单/支付/配送等业务指标定义、Prometheus 爬取路径说明及 Grafana 图表配置。 -- [ ] 测试:Phase 1 核心 API 具备 ≥30 条自动化用例(单元 + 集成),覆盖租户→商户→下单链路。 - - 当前:仓库尚无自动化测试项目/用例,Phase 1 链路未覆盖 xUnit/Moq/FluentAssertions 的单元或集成测试。 ---- - -## Phase 2(下一阶段):拼单、优惠券与基础营销、会员积分/会员日、客服聊天、同城自配送调度、搜索 -- [ ] 拼单引擎:GroupOrder/Participant CRUD、发起/加入/成团条件、自动解散与退款、团内消息与提醒。 -- [ ] 优惠券与基础营销:模板管理、领券、核销、库存/有效期/叠加规则,基础抽奖/秒杀/满减活动。 -- [ ] 会员与积分:会员档案、等级/成长值、会员日通知;积分获取/消耗、有效期、黑名单。 -- [ ] 客服聊天:实时会话、机器人/人工切换、排队/转接、消息模板、敏感词审查、工单流转与评价。 -- [ ] 同城自配送调度:骑手智能指派、路线估时、无接触配送、费用补贴策略、调度看板。 -- [ ] 搜索:门店/菜品/活动/优惠券搜索,过滤/排序、热门/历史记录、联想与纠错。 ---- - -## Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、高阶营销、风控与补偿 -- [ ] 分销返利:AffiliatePartner/Order/Payout 管理,佣金阶梯、结算周期、税务信息、违规处理。 -- [ ] 签到打卡:CheckInCampaign/Record、连签奖励、补签、积分/券/成长值奖励、反作弊机制。 -- [ ] 预约预订:档期/资源占用、预约下单/支付、提醒/改期/取消、到店核销与履约记录。 -- [ ] 地图导航扩展:附近门店/推荐、距离/路线规划、跳转原生导航、导航请求埋点。 -- [ ] 社区:动态发布、评论、点赞、话题/标签、图片/视频审核、举报与风控,店铺口碑展示。 -- [ ] 高阶营销:秒杀/抽奖/裂变、裂变海报、爆款推荐位、多渠道投放分析。 -- [ ] 风控与审计:黑名单、频率限制、异常行为监控、审计日志、补偿与告警体系。 ---- - -## Phase 4:性能优化、缓存、运营大盘、测试与文档、上线与监控 -- [ ] 性能与缓存:热点接口缓存、慢查询治理、批处理优化、异步化改造。 -- [ ] 可靠性:幂等与重试策略、任务调度补偿、链路追踪、告警联动。 -- [ ] 运营大盘:交易/营销/履约/用户维度的细分报表、GMV/成本/毛利分析。 -- [ ] 文档与测试:完整测试矩阵、性能测试报告、上线手册、回滚方案。 -- [ ] 监控与运维:上线发布流程、灰度/回滚策略、系统稳定性指标、24x7 监控与告警。 diff --git a/Document/13_AppSeed说明.md b/Document/13_AppSeed说明.md deleted file mode 100644 index 95e2d0e..0000000 --- a/Document/13_AppSeed说明.md +++ /dev/null @@ -1,62 +0,0 @@ -# App 数据种子使用说明(App:Seed) -> 作用:在启动时自动创建默认租户与基础字典,便于本地/测试环境快速落地必备数据。由 `AppDataSeeder` 执行,支持幂等多次运行。 - -## 配置入口 -- 文件位置:`appsettings.Seed.{Environment}.json`(AdminApi 下新增独立种子文件,示例已写入 Development) -- 配置节:`App:Seed` - -示例(已写入 `src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json`): -```json -{ - "App": { - "Seed": { - "Enabled": true, - "DefaultTenant": { - "TenantId": 1000000000001, - "Code": "demo", - "Name": "Demo租户", - "ShortName": "Demo", - "ContactName": "DemoAdmin", - "ContactPhone": "13800000000" - }, - "DictionaryGroups": [ - { - "Code": "order_status", - "Name": "订单状态", - "Scope": "Business", - "Items": [ - { "Key": "pending", "Value": "待支付", "SortOrder": 10 }, - { "Key": "paid", "Value": "已支付", "SortOrder": 20 }, - { "Key": "finished", "Value": "已完成", "SortOrder": 30 } - ] - }, - { - "Code": "store_tags", - "Name": "门店标签", - "Scope": "Business", - "Items": [ - { "Key": "hot", "Value": "热门", "SortOrder": 10 }, - { "Key": "new", "Value": "新店", "SortOrder": 20 } - ] - } - ] - } - } -} -``` - -## 字段说明 -- `Enabled`: 是否启用种子 -- `DefaultTenant`: 默认租户(使用雪花 long ID;0 表示让雪花生成) -- `DictionaryGroups`: 基础字典,`Scope` 可选 `System`/`Business`,`Items` 支持幂等运行更新 - -## 运行方式 -1. 确保 Admin API 已调用 `AddAppInfrastructure`(Program.cs 已注册,会启用 `AppDataSeeder`)。 -2. 修改 `appsettings.Seed.{Environment}.json` 的 `App:Seed` 后,启动 Admin API,即会自动执行种子逻辑(幂等)。 -3. 查看日志:`AppSeed` 前缀会输出创建/更新结果。 - -## 注意事项 -- ID 必须用 long(雪花),不要再使用 Guid/自增。 -- 系统租户使用 `TenantId = 0`;业务租户请填写实际雪花 ID。 -- 字典分组编码需唯一;重复运行会按编码合并更新。 -- 生产环境请按需开启 `Enabled`,避免误写入。 diff --git a/Document/14_OpenTelemetry接入指引.md b/Document/14_OpenTelemetry接入指引.md deleted file mode 100644 index bdfcd50..0000000 --- a/Document/14_OpenTelemetry接入指引.md +++ /dev/null @@ -1,40 +0,0 @@ -# 14_OpenTelemetry 接入指引 - -> 现状:Admin/Mini/User API 已集成 OTel 埋点,可导出到 Collector/控制台/文件日志,默认关闭 OTLP 导出。 - -## 1. 依赖与版本 -- NuGet:`OpenTelemetry.Extensions.Hosting`、`OpenTelemetry.Instrumentation.AspNetCore`、`OpenTelemetry.Instrumentation.Http`、`OpenTelemetry.Instrumentation.EntityFrameworkCore`、`OpenTelemetry.Instrumentation.Runtime`、`OpenTelemetry.Exporter.OpenTelemetryProtocol`、`OpenTelemetry.Exporter.Console`。 -- 当前 EF Core instrumentation 由 NuGet 回退到 `1.10.0-beta.1`(会提示 NU1603/NU1902),待可用时统一升级到稳定版以消除告警。 - -## 2. 程序内配置(Admin/Mini/User API) -- Resource:`ServiceName` 分别为 `TakeoutSaaS.AdminApi|MiniApi|UserApi`,`ServiceInstanceId = Environment.MachineName`。 -- Tracing:开启 ASP.NET Core、HttpClient、EF Core(禁用 SQL 文本)、Runtime;采样器默认 `ParentBased + AlwaysOn`。 -- Metrics:开启 ASP.NET Core、HttpClient、Runtime。 -- Exporter: - - OTLP(可选):读取 `Otel:Endpoint`,非空时启用。 - - Console:`Otel:UseConsoleExporter`(默认 Dev 开启,Prod 关闭)。 -- 日志:Serilog 输出 Console + 文件(按天滚动,保留 7 天),模板已包含 TraceId/SpanId(通过 Enrich FromLogContext)。 - -## 3. appsettings 配置键 -```json -"Otel": { - "Endpoint": "", // 为空则不推 OTLP,例如 http://otel-collector:4317 - "Sampling": "ParentBasedAlwaysOn", - "UseConsoleExporter": true // Dev 默认 true,Prod 建议 false -} -``` -- 环境变量可覆盖:`OTEL_SERVICE_NAME`、`OTEL_EXPORTER_OTLP_ENDPOINT` 等。 - -## 4. Collector/后端接入建议 -- Collector 监听 4317/4318(gRPC/HTTP OTLP),做采样/脱敏/分流,再转发 Jaeger/Tempo/ELK/Datadog 等。 -- 生产注意:限制导出 SQL 文本(已关闭)、对敏感字段脱敏,必要时在 Collector 做 TraceIdRatioBased 采样以控量。 - -## 5. 验证步骤 -1) 开启 `Otel:UseConsoleExporter=true`,本地运行 API,观察控制台是否输出 Span/Metric。 -2) 配置 `Otel:Endpoint=http://localhost:4317` 并启动 Collector,使用 Jaeger/Tempo UI 或 `curl http://localhost:4318/v1/traces` 验证链路。 -3) 文件日志:查看 `logs/admin-api-*.log` 等,确认包含 TraceId/SpanId。 - -## 6. 后续工作 -- 待 NuGet 源更新后,升级到稳定版 OTel 包并消除 NU1603/NU1902 告警。 -- 如需采集日志到 ELK,可直接用 Filebeat/Vector 读取 `logs/*.log` 推送,无需改代码。 -- 如需控制采样率或关闭某些 instrumentation,调整 appsettings 中的 Sampling/开关后重启即可。 diff --git a/Document/15_API边界与自检清单.md b/Document/15_API边界与自检清单.md deleted file mode 100644 index 53e284a..0000000 --- a/Document/15_API边界与自检清单.md +++ /dev/null @@ -1,53 +0,0 @@ -# API 边界与自检清单 - -> 目的:明确 Admin/User/Mini 三个 API 的职责边界,避免跨端耦合。开发新接口或改动现有控制器时请对照自检,确保租户、安全、DTO、路由符合约定。 - -## 1. AdminApi(管理后台) -- **面向对象**:运营、客服、商户管理员。 -- **职责**:租户/门店/商品/订单/支付/配送/字典/权限/RBAC/审计/任务调度等后台管理与洞察。 -- **鉴权**:JWT + RBAC(`[Authorize]` + `PermissionAuthorize`),必须带租户头 `X-Tenant-Id/Code`。 -- **路由前缀**:`api/admin/v{version}/...`。 -- **DTO/约束**:仅管理字段,禁止返回 C 端敏感信息;long -> string;严禁实体直接返回。 -- **现有控制器**:`AuthController`、`DeliveriesController`、`DictionaryController`、`FilesController`、`MerchantsController`、`OrdersController`、`PaymentsController`、`PermissionsController`、`RolesController`、`StoresController`、`SystemParametersController`、`TenantPackagesController`、`TenantsController`、`TenantBillingsController`、`TenantAnnouncementsController`、`TenantNotificationsController`、`UserPermissionsController`、`HealthController`。 -- **自检清单**: - 1. 是否需要权限/租户过滤?未加则补 `[Authorize]` + 租户解析。 - 2. 是否调用了应用层 CQRS,而非在 Controller 写业务? - 3. DTO 是否按管理口径,未暴露用户端字段? - 4. 是否使用参数化/AsNoTracking/投影,避免 N+1? - 5. 路由和 Swagger 示例是否含租户/权限说明? -- **自检记录**:RolesController 新增模板列表/详情/复制/初始化端点,均已套用 `[Authorize]` + `PermissionAuthorize`、仅调用 CQRS/DTO,依赖租户头隔离。TenantPackagesController 与 TenantsController(配额校验) 均使用权限码、DTO 映射,配额校验要求携带租户头防越权。新增租户账单/公告/通知控制器,全部采用 CQRS、权限校验与租户参数,列表分页、未暴露实体。 - -## 2. UserApi(C 端用户) -- **面向对象**:App/H5 普通用户。 -- **职责**:菜单浏览、下单、支付、评价、地址、售后、订单查询、支付/配送回调(验证签名)等用户闭环。 -- **鉴权**:用户 JWT,租户隔离;幂等接口需校验。 -- **路由前缀**:`api/user/v{version}/...`。 -- **DTO/约束**:仅用户侧可见字段,屏蔽后台配置字段;long -> string。 -- **现有控制器**:当前仅 `HealthController`(业务接口待补)。 -- **自检清单**: - 1. 是否暴露给用户的纯前台功能?后台配置请放 AdminApi。 - 2. 是否做租户隔离、用户鉴权、签名/幂等校验? - 3. 响应是否脱敏且只含用户需要的字段? - 4. 是否避免跨端复用后台 DTO/命令? - 5. 回调路由是否验证签名/防重放? - -## 3. MiniApi(小程序端) -- **面向对象**:微信/小程序前端。 -- **职责**:小程序登录/刷新、当前用户档案、订阅消息、直传凭证、小程序场景特定的下单/浏览等。 -- **鉴权**:小程序登录态/Token,租户隔离;必要时区分渠道。 -- **路由前缀**:`api/mini/v{version}/...`。 -- **DTO/约束**:遵循小程序接口规范,错误码与前端对齐;long -> string。 -- **现有控制器**:`AuthController`、`MeController`、`FilesController`、`HealthController`。 -- **自检清单**: - 1. 是否为小程序特有流程(code2session、订阅消息、直传等)?通用用户接口放 UserApi。 - 2. 是否完成租户/鉴权校验,区分渠道标识? - 3. 请求/响应是否符合小程序对错误码与字段的约定? - 4. 是否避免使用后台管理 DTO/权限模型? - 5. 上传/直传接口是否限制 MIME/大小并做鉴权? - -## 4. 共通约束 -- **分层**:Controller 仅做路由/DTO 转换,业务放 Application 层 Handler。 -- **租户**:所有写/读需租户过滤;严禁跨租户访问。 -- **日志/观测**:TraceId/SpanId 已贯通;/metrics、/healthz 按服务暴露。 -- **命名**:输入 `XxxRequest`、输出 `XxxDto`;文件名与类名一致;布尔属性加 `Is/Has`。 -- **发布前检查**:运行 `dotnet build`,必要时补 Swagger 示例、单元测试(核心逻辑 100% 覆盖,服务 ≥70%)。 diff --git a/Document/Completed/后端套餐管理.md b/Document/Completed/后端套餐管理.md deleted file mode 100644 index 23c0496..0000000 --- a/Document/Completed/后端套餐管理.md +++ /dev/null @@ -1,79 +0,0 @@ -# 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/Document/README.md b/Document/README.md deleted file mode 100644 index aa08578..0000000 --- a/Document/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# 外卖SaaS系统 - 文档中心 - -欢迎查阅外卖SaaS系统的完整文档。本文档中心包含了项目的所有技术文档和开发指南。 - -## 📚 文档目录 - -### 1. [项目概述](01_项目概述.md) -- 项目简介与背景 -- 核心业务模块介绍 -- 用户角色说明 -- 系统特性 -- 技术选型 -- 项目里程碑 - -**适合人群**:项目经理、产品经理、新加入的开发人员 - ---- - -### 2. [技术架构](02_技术架构.md) -- 技术栈详解 -- 系统架构设计 -- 分层架构说明 -- 核心设计模式 -- 数据访问策略(EF Core + Dapper) -- 缓存策略 -- 消息队列应用 -- 安全设计 - -**适合人群**:架构师、技术负责人、高级开发人员 - ---- - -### 3. [数据库设计](03_数据库设计.md) -- 数据库设计原则 -- 命名规范 -- 核心表结构 - - 租户管理 - - 商家管理 - - 菜品管理 - - 订单管理 - - 配送管理 - - 支付管理 - - 营销管理 - - 系统管理 -- 索引策略 -- 数据库优化 -- 备份策略 - -**适合人群**:数据库管理员、后端开发人员 - ---- - -### 4A. [管理后台 API 设计](04A_管理后台API.md) -- 角色与权限(平台/租户/商家) -- 租户与商家管理 -- 菜品与分类管理 -- 订单流转与售后 -- 优惠券与评价管理 -- 统计报表与文件上传 - -### 4B. [小程序/用户端 API 设计](04B_小程序API.md) -- 小程序登录与用户信息 -- 商家与门店浏览 -- 菜品与分类列表 -- 购物车同步 -- 订单创建/查询/取消 -- 支付对接(微信/支付宝) -- 优惠券领取与使用、评价发布 - -**适合人群**:前端开发人员(小程序/Web用户端)、后端开发人员、接口对接人员 - ---- - -### 5. [部署运维](05_部署运维.md) -- 环境要求 -- 本地开发环境搭建 -- Docker部署 -- Nginx配置 -- 数据库部署(主从复制) -- Redis部署(哨兵模式) -- CI/CD配置 -- 监控告警(Prometheus + Grafana) -- 日志管理(ELK Stack) -- 安全加固 -- 性能优化 -- 故障恢复 - -**适合人群**:运维工程师、DevOps工程师、系统管理员 - ---- - -### 6. [开发规范](06_开发规范.md) -- 代码规范 - - 命名规范 - - 代码组织 - - 代码注释 - - 异常处理 -- Git工作流 - - 分支管理 - - 提交信息规范 -- 代码审查标准 -- 单元测试规范 -- 性能优化规范 -- 安全规范 -- 日志规范 -- 配置管理 -- API设计规范 - -**适合人群**:所有开发人员 - ---- - -### 7. [系统架构图](07_系统架构图.md) -- 整体架构图 -- 应用分层架构 -- 订单处理流程图 -- 数据流转图 -- 多租户数据隔离架构 -- 缓存架构 -- 消息队列架构 -- 部署架构 -- 监控架构 - -**适合人群**:架构师、技术负责人、所有开发人员 - ---- - -## 🚀 快速导航 - -### 我是新人,从哪里开始? -1. 先阅读 [项目概述](01_项目概述.md) 了解项目背景和业务 -2. 查看 [系统架构图](07_系统架构图.md) 理解系统整体架构 -3. 阅读 [开发规范](06_开发规范.md) 了解开发要求 -4. 参考 [部署运维](05_部署运维.md) 搭建本地开发环境 - -### 我要开发新功能 -1. 查看 [数据库设计](03_数据库设计.md) 了解数据模型 -2. 参考 [API接口设计](04_API接口设计.md) 设计接口 -3. 遵循 [开发规范](06_开发规范.md) 编写代码 -4. 参考 [技术架构](02_技术架构.md) 选择合适的技术方案 - -### 我要部署系统 -1. 阅读 [部署运维](05_部署运维.md) 了解部署流程 -2. 参考 [系统架构图](07_系统架构图.md) 理解部署架构 -3. 按照文档配置监控和日志系统 - -### 我要对接API -1. 查看 [API接口设计](04_API接口设计.md) 了解接口规范 -2. 参考接口文档进行开发和测试 - ---- - -## 📖 文档更新记录 - -### v1.0.0 (2024-01-01) -- ✅ 完成项目概述文档 -- ✅ 完成技术架构文档 -- ✅ 完成数据库设计文档 -- ✅ 完成API接口设计文档 -- ✅ 完成部署运维文档 -- ✅ 完成开发规范文档 -- ✅ 完成系统架构图文档 - ---- - -## 💡 文档贡献 - -如果您发现文档有任何问题或需要改进的地方,欢迎: -1. 提交 Issue 反馈问题 -2. 提交 Pull Request 改进文档 -3. 联系项目负责人 - ---- - -## 📞 联系方式 - -- 项目地址:https://github.com/your-org/takeout-saas -- 问题反馈:https://github.com/your-org/takeout-saas/issues -- 邮箱:dev@example.com - ---- - -## 📝 文档规范 - -本文档使用 Markdown 格式编写,遵循以下规范: -- 使用清晰的标题层级 -- 代码示例使用语法高亮 -- 重要内容使用加粗或引用 -- 保持文档简洁易读 -- 及时更新文档内容 - ---- - -**最后更新时间**:2024-01-01 -**文档版本**:v1.0.0 diff --git a/Document/infra/postgres_redis.md b/Document/infra/postgres_redis.md deleted file mode 100644 index bf6e076..0000000 --- a/Document/infra/postgres_redis.md +++ /dev/null @@ -1,101 +0,0 @@ -# PostgreSQL 与 Redis 接入手册 - -> 本文档补齐 `Document/10_TODO.md` 中“Postgres/Redis 接入文档与 IaC/脚本”的要求,统一描述连接信息、账号权限、运维流程,以及可复用的部署脚本位置。 - -## 1. 运行环境总览 - -| 组件 | 地址/端口 | 主要数据库/实例 | 说明 | -| --- | --- | --- | --- | -| PostgreSQL | `120.53.222.17:5432` | `takeout_app_db`、`takeout_identity_db`、`takeout_dictionary_db`、`takeout_hangfire_db` | 线上实例,所有业务上下文共用。 | -| Redis | `49.232.6.45:6379` | 单节点 | 业务缓存/登录限流/刷新令牌存储。 | - -> 注意:所有业务账号都只具备既有库的读写权限,无 `CREATEDB`。若需新库,需使用平台管理员账号(`postgres`)或联系 DBA。 - -## 2. 账号与库映射 - -| 数据库 | 角色 | 密码 | 用途 | -| --- | --- | --- | --- | -| `takeout_app_db` | `app_user` | `AppUser112233` | 业务域 (`TakeoutAppDbContext`) | -| `takeout_identity_db` | `identity_user` | `IdentityUser112233` | 身份域 (`IdentityDbContext`) | -| `takeout_dictionary_db` | `dictionary_user` | `DictionaryUser112233` | 字典域 (`DictionaryDbContext`) | -| `takeout_hangfire_db` | `hangfire_user` | `HangFire112233` | 后台调度/Hangfire | - -Redis 密码:`MsuMshk112233`,见 `appsettings.*.json -> Redis`。 - -## 3. 环境变量/配置注入 - -### PowerShell - -```powershell -$env:TAKEOUTSAAS_APPSETTINGS_DIR = "D:\HAZCode\TakeOut\src\Api\TakeoutSaaS.AdminApi" -$env:TAKEOUTSAAS_APP_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true" -$env:TAKEOUTSAAS_IDENTITY_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true" -$env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true" -``` - -### Bash - -```bash -export TAKEOUTSAAS_APPSETTINGS_DIR=/home/user/TakeOut/src/Api/TakeoutSaaS.AdminApi -export TAKEOUTSAAS_APP_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true" -export TAKEOUTSAAS_IDENTITY_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true" -export TAKEOUTSAAS_DICTIONARY_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true" -``` - -Redis 连接字符串直接写入 `appsettings.*.json` 即可,如: - -```jsonc -"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false" -``` - -## 4. 运维指南 - -### PostgreSQL - -1. **只读账号验证** - ```powershell - psql "host=120.53.222.17 port=5432 dbname=takeout_app_db user=app_user password=AppUser112233" - ``` -2. **备份** - ```bash - pg_dump -h 120.53.222.17 -p 5432 -U postgres -F c -d takeout_app_db -f backup/takeout_app_db_$(date +%Y%m%d).dump - pg_dumpall -h 120.53.222.17 -p 5432 -U postgres > backup/all_$(date +%Y%m%d).sql - ``` -3. **恢复** - ```bash - pg_restore -h 120.53.222.17 -p 5432 -U postgres -d takeout_app_db backup/takeout_app_db_xxx.dump - psql -h 120.53.222.17 -p 5432 -U postgres -f backup/all_yyyymmdd.sql - ``` -4. **账号/权限策略** - - `app_user` / `identity_user` / `dictionary_user` 拥有 `CONNECT`、`TEMP`、Schema `public` 的 CRUD 权限。 - - `hangfire_user` 仅能访问 `takeout_hangfire_db`,不可访问业务库。 - - 创建新表/列时,通过 EF Migration 自动添加 COMMENT。 - -### Redis - -1. **连接验证** - ```bash - redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 ping - ``` -2. **备份** - ```bash - redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 save # 触发 RDB - redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 bgsave # 后台 - ``` - RDB/AOF 文件在服务器 `redis.conf` 定义的目录(默认 `/var/lib/redis`)。 -3. **常见运维项** - - `CONFIG GET dir` / `CONFIG GET dbfilename` 可查看持久化路径。 - - `INFO memory` 监控内存;开启 `maxmemory` + `allkeys-lru` 保护。 - -## 5. IaC / 脚本 - -| 文件 | 说明 | -| --- | --- | -| `deploy/postgres/create_databases.sql` | 基于 `postgres` 管理员执行,创建四个业务库及角色、授予权限、补 COMMENT。 | -| `deploy/postgres/bootstrap.ps1` | PowerShell 包装脚本,调用 `psql` 执行上面的 SQL(默认读取 `postgres` 管理员账号)。 | -| `deploy/postgres/README.md` | 介绍如何在本地/测试环境执行 bootstrap 并校验连接。 | -| `deploy/redis/docker-compose.yml` | 可复用的 Redis 部署(Redis 7 + AOF),便于本地或测试环境一键拉起。 | -| `deploy/redis/redis.conf` | compose/裸机均可共用的配置(`requirepass`、持久化等已写好)。 | -| `deploy/redis/README.md` | 说明如何使用 compose 或将 `redis.conf` 部署到现有实例。 | - -> 线上目前为裸机安装(非容器),如需创建新环境/快速恢复,可直接运行上述脚本达到同样配置;即使在现有机器上,也可把 SQL/配置当作“最终规范”确保环境一致性。 diff --git a/Document/swagger/swagger20251215095917.json b/Document/swagger/swagger20251215095917.json deleted file mode 100644 index d1efe82..0000000 --- a/Document/swagger/swagger20251215095917.json +++ /dev/null @@ -1,19254 +0,0 @@ -{ - "openapi": "3.0.4", - "info": { - "title": "外卖SaaS - 管理后台 1.0", - "description": "管理后台 API 文档", - "version": "1.0" - }, - "paths": { - "/api/admin/v1/auth/login": { - "post": { - "tags": [ - "Auth" - ], - "summary": "登录获取 Token", - "requestBody": { - "description": "登录请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenResponseApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/auth/login/simple": { - "post": { - "tags": [ - "Auth" - ], - "summary": "免租户号登录(仅账号+密码)。", - "description": "用于前端简化登录,无需额外传递租户号。", - "requestBody": { - "description": "登录请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/AdminLoginRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenResponseApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/auth/refresh": { - "post": { - "tags": [ - "Auth" - ], - "summary": "刷新 Token", - "requestBody": { - "description": "刷新令牌请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenResponseApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/auth/profile": { - "get": { - "tags": [ - "Auth" - ], - "summary": "获取当前用户信息", - "description": "示例:\n```\nGET /api/admin/v1/auth/profile\nHeader: Authorization: Bearer \n响应:\n{\n \"success\": true,\n \"code\": 200,\n \"message\": \"操作成功\",\n \"data\": {\n \"userId\": \"900123456789012345\",\n \"account\": \"admin@tenant1\",\n \"displayName\": \"租户管理员\",\n \"tenantId\": \"100000000000000001\",\n \"merchantId\": null,\n \"roles\": [\"TenantAdmin\"],\n \"permissions\": [\"identity:permission:read\", \"merchant:read\", \"order:read\"],\n \"avatar\": \"https://cdn.example.com/avatar.png\"\n }\n}\n```", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CurrentUserProfileApiResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CurrentUserProfileApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/auth/menu": { - "get": { - "tags": [ - "Auth" - ], - "summary": "获取当前用户的菜单树(按权限过滤)。", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuNodeDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/auth/permissions/{userId}": { - "get": { - "tags": [ - "Auth" - ], - "summary": "查询指定用户的角色与权限概览(当前租户范围)。", - "description": "示例:\n```\nGET /api/admin/v1/auth/permissions/900123456789012346\nHeader: Authorization: Bearer \n响应:\n{\n \"success\": true,\n \"code\": 200,\n \"data\": {\n \"userId\": \"900123456789012346\",\n \"tenantId\": \"100000000000000001\",\n \"merchantId\": \"200000000000000001\",\n \"account\": \"ops.manager\",\n \"displayName\": \"运营经理\",\n \"roles\": [\"OpsManager\", \"Reporter\"],\n \"permissions\": [\"delivery:read\", \"order:read\", \"payment:read\"],\n \"createdAt\": \"2025-12-01T08:30:00Z\"\n }\n}\n```", - "parameters": [ - { - "name": "userId", - "in": "path", - "description": "目标用户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserPermissionDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserPermissionDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/deliveries": { - "post": { - "tags": [ - "Deliveries" - ], - "summary": "创建配送单。", - "requestBody": { - "description": "创建命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateDeliveryOrderCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateDeliveryOrderCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateDeliveryOrderCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateDeliveryOrderCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeliveryOrderDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Deliveries" - ], - "summary": "查询配送单列表。", - "parameters": [ - { - "name": "orderId", - "in": "query", - "description": "订单 ID。", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "in": "query", - "description": "配送状态。", - "schema": { - "$ref": "#/components/schemas/DeliveryStatus" - } - }, - { - "name": "page", - "in": "query", - "description": "页码。", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "description": "每页大小。", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "description": "排序字段。", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "description": "是否倒序。", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeliveryOrderDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/deliveries/{deliveryOrderId}": { - "get": { - "tags": [ - "Deliveries" - ], - "summary": "获取配送单详情。", - "parameters": [ - { - "name": "deliveryOrderId", - "in": "path", - "description": "配送单 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeliveryOrderDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "Deliveries" - ], - "summary": "更新配送单。", - "parameters": [ - { - "name": "deliveryOrderId", - "in": "path", - "description": "配送单 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "更新命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeliveryOrderCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeliveryOrderCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeliveryOrderCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeliveryOrderCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeliveryOrderDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Deliveries" - ], - "summary": "删除配送单。", - "parameters": [ - { - "name": "deliveryOrderId", - "in": "path", - "description": "配送单 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/dictionaries": { - "get": { - "tags": [ - "Dictionary" - ], - "summary": "查询字典分组。", - "parameters": [ - { - "name": "Scope", - "in": "query", - "description": "参数字典作用域。", - "schema": { - "$ref": "#/components/schemas/DictionaryScope" - } - }, - { - "name": "IncludeItems", - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryGroupDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Dictionary" - ], - "summary": "创建字典分组。", - "requestBody": { - "description": "创建分组请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryGroupRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryGroupRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryGroupRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryGroupRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryGroupDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/dictionaries/{groupId}": { - "put": { - "tags": [ - "Dictionary" - ], - "summary": "更新字典分组。", - "parameters": [ - { - "name": "groupId", - "in": "path", - "description": "分组 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "更新请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryGroupRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryGroupRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryGroupRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryGroupRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryGroupDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Dictionary" - ], - "summary": "删除字典分组。", - "parameters": [ - { - "name": "groupId", - "in": "path", - "description": "分组 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/dictionaries/{groupId}/items": { - "post": { - "tags": [ - "Dictionary" - ], - "summary": "创建字典项。", - "parameters": [ - { - "name": "groupId", - "in": "path", - "description": "分组 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "创建请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryItemRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryItemRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryItemRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateDictionaryItemRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryItemDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/dictionaries/items/{itemId}": { - "put": { - "tags": [ - "Dictionary" - ], - "summary": "更新字典项。", - "parameters": [ - { - "name": "itemId", - "in": "path", - "description": "字典项 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "更新请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryItemRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryItemRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryItemRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateDictionaryItemRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryItemDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Dictionary" - ], - "summary": "删除字典项。", - "parameters": [ - { - "name": "itemId", - "in": "path", - "description": "字典项 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/dictionaries/batch": { - "post": { - "tags": [ - "Dictionary" - ], - "summary": "批量获取字典项(命中缓存)。", - "requestBody": { - "description": "批量查询请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/DictionaryBatchQueryRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryBatchQueryRequest" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/DictionaryBatchQueryRequest" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/DictionaryBatchQueryRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StringDictionaryItemDtoIReadOnlyListIReadOnlyDictionaryApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/files/upload": { - "post": { - "tags": [ - "Files" - ], - "summary": "上传图片或文件。", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "File" - ], - "type": "object", - "properties": { - "File": { - "type": "string", - "description": "上传文件。", - "format": "binary" - }, - "Type": { - "type": "string", - "description": "上传类型。" - } - } - }, - "encoding": { - "File": { - "style": "form" - }, - "Type": { - "style": "form" - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileUploadResponseApiResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileUploadResponseApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/Health": { - "get": { - "tags": [ - "Health" - ], - "summary": "获取服务健康状态。", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/{productSkuId}": { - "get": { - "tags": [ - "Inventory" - ], - "summary": "查询库存。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "productSkuId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryItemDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/adjust": { - "post": { - "tags": [ - "Inventory" - ], - "summary": "调整库存(入库/盘点/报损)。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/AdjustInventoryCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdjustInventoryCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/AdjustInventoryCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/AdjustInventoryCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryItemDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/lock": { - "post": { - "tags": [ - "Inventory" - ], - "summary": "锁定库存(下单占用)。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/LockInventoryCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/LockInventoryCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/LockInventoryCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/LockInventoryCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryItemDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/release": { - "post": { - "tags": [ - "Inventory" - ], - "summary": "释放库存(取消订单等)。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReleaseInventoryCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReleaseInventoryCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReleaseInventoryCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReleaseInventoryCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryItemDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/deduct": { - "post": { - "tags": [ - "Inventory" - ], - "summary": "扣减库存(支付或履约成功)。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/DeductInventoryCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeductInventoryCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/DeductInventoryCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/DeductInventoryCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryItemDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/{productSkuId}/batches": { - "get": { - "tags": [ - "Inventory" - ], - "summary": "查询批次列表。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "productSkuId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryBatchDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Inventory" - ], - "summary": "新增或更新批次。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "productSkuId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpsertInventoryBatchCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertInventoryBatchCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpsertInventoryBatchCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpsertInventoryBatchCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InventoryBatchDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/inventory/locks/expire": { - "post": { - "tags": [ - "Inventory" - ], - "summary": "释放过期锁定。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Int32ApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/menus": { - "get": { - "tags": [ - "Menus" - ], - "summary": "获取当前租户的菜单列表(平铺)。", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuDefinitionDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Menus" - ], - "summary": "创建菜单。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateMenuCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMenuCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateMenuCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateMenuCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuDefinitionDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/menus/{menuId}": { - "get": { - "tags": [ - "Menus" - ], - "summary": "获取菜单详情。", - "parameters": [ - { - "name": "menuId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuDefinitionDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuDefinitionDtoApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "Menus" - ], - "summary": "更新菜单。", - "parameters": [ - { - "name": "menuId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateMenuCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMenuCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMenuCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateMenuCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuDefinitionDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MenuDefinitionDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Menus" - ], - "summary": "删除菜单。", - "parameters": [ - { - "name": "menuId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchant-categories": { - "get": { - "tags": [ - "MerchantCategories" - ], - "summary": "列出所有类目。", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantCategoryDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "MerchantCategories" - ], - "summary": "新增类目。", - "requestBody": { - "description": "创建命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCategoryCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCategoryCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCategoryCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCategoryCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantCategoryDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchant-categories/{categoryId}": { - "delete": { - "tags": [ - "MerchantCategories" - ], - "summary": "删除类目。", - "parameters": [ - { - "name": "categoryId", - "in": "path", - "description": "类目 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchant-categories/reorder": { - "post": { - "tags": [ - "MerchantCategories" - ], - "summary": "批量调整类目排序。", - "requestBody": { - "description": "排序命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReorderMerchantCategoriesCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReorderMerchantCategoriesCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReorderMerchantCategoriesCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReorderMerchantCategoriesCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants": { - "post": { - "tags": [ - "Merchants" - ], - "summary": "创建商户。", - "requestBody": { - "description": "创建命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Merchants" - ], - "summary": "查询商户列表。", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "状态筛选。", - "schema": { - "$ref": "#/components/schemas/MerchantStatus" - } - }, - { - "name": "page", - "in": "query", - "description": "页码。", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "description": "每页大小。", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "description": "排序字段。", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "description": "是否倒序。", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}": { - "put": { - "tags": [ - "Merchants" - ], - "summary": "更新商户。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "description": "商户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "更新命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Merchants" - ], - "summary": "删除商户。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "description": "商户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Merchants" - ], - "summary": "获取商户概览。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "description": "商户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/detail": { - "get": { - "tags": [ - "Merchants" - ], - "summary": "获取商户详细资料(含证照、合同)。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDetailDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/documents": { - "post": { - "tags": [ - "Merchants" - ], - "summary": "上传商户证照信息(先通过文件上传接口获取 COS 地址)。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/AddMerchantDocumentCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddMerchantDocumentCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/AddMerchantDocumentCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/AddMerchantDocumentCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDocumentDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Merchants" - ], - "summary": "商户证照列表。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDocumentDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/documents/{documentId}/review": { - "post": { - "tags": [ - "Merchants" - ], - "summary": "审核指定证照。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "documentId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantDocumentCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantDocumentCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantDocumentCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantDocumentCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDocumentDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/contracts": { - "post": { - "tags": [ - "Merchants" - ], - "summary": "新增商户合同。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantContractCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantContractCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantContractCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateMerchantContractCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantContractDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Merchants" - ], - "summary": "合同列表。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantContractDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/contracts/{contractId}/status": { - "put": { - "tags": [ - "Merchants" - ], - "summary": "更新合同状态(生效/终止等)。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "contractId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantContractStatusCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantContractStatusCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantContractStatusCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateMerchantContractStatusCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantContractDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/review": { - "post": { - "tags": [ - "Merchants" - ], - "summary": "审核商户(通过/驳回)。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReviewMerchantCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/{merchantId}/audits": { - "get": { - "tags": [ - "Merchants" - ], - "summary": "审核日志。", - "parameters": [ - { - "name": "merchantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantAuditLogDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/merchants/categories": { - "get": { - "tags": [ - "Merchants" - ], - "summary": "可选商户类目列表。", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StringIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/orders": { - "post": { - "tags": [ - "Orders" - ], - "summary": "创建订单。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateOrderCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOrderCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateOrderCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateOrderCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrderDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Orders" - ], - "summary": "查询订单列表。", - "parameters": [ - { - "name": "storeId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "in": "query", - "description": "订单状态。", - "schema": { - "$ref": "#/components/schemas/OrderStatus" - } - }, - { - "name": "paymentStatus", - "in": "query", - "description": "支付记录状态。", - "schema": { - "$ref": "#/components/schemas/PaymentStatus" - } - }, - { - "name": "orderNo", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrderDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/orders/{orderId}": { - "get": { - "tags": [ - "Orders" - ], - "summary": "获取订单详情。", - "parameters": [ - { - "name": "orderId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrderDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "Orders" - ], - "summary": "更新订单。", - "parameters": [ - { - "name": "orderId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrderCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrderCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrderCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrderCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrderDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Orders" - ], - "summary": "删除订单。", - "parameters": [ - { - "name": "orderId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/payments": { - "post": { - "tags": [ - "Payments" - ], - "summary": "创建支付记录。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreatePaymentCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreatePaymentCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreatePaymentCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreatePaymentCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Payments" - ], - "summary": "查询支付记录列表。", - "parameters": [ - { - "name": "orderId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "in": "query", - "description": "支付记录状态。", - "schema": { - "$ref": "#/components/schemas/PaymentStatus" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/payments/{paymentId}": { - "get": { - "tags": [ - "Payments" - ], - "summary": "获取支付记录详情。", - "parameters": [ - { - "name": "paymentId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "Payments" - ], - "summary": "更新支付记录。", - "parameters": [ - { - "name": "paymentId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdatePaymentCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdatePaymentCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdatePaymentCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdatePaymentCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Payments" - ], - "summary": "删除支付记录。", - "parameters": [ - { - "name": "paymentId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/permissions": { - "get": { - "tags": [ - "Permissions" - ], - "summary": "分页查询权限。", - "description": "示例:GET /api/admin/v1/permissions?keyword=order&page=1&pageSize=20", - "parameters": [ - { - "name": "Keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "SortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "SortDescending", - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionDtoPagedResultApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Permissions" - ], - "summary": "创建权限。", - "requestBody": { - "description": "创建命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreatePermissionCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreatePermissionCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreatePermissionCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreatePermissionCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/permissions/tree": { - "get": { - "tags": [ - "Permissions" - ], - "summary": "获取权限树。", - "parameters": [ - { - "name": "keyword", - "in": "query", - "description": "关键字(可选)。", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionTreeDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/permissions/{permissionId}": { - "put": { - "tags": [ - "Permissions" - ], - "summary": "更新权限。", - "parameters": [ - { - "name": "permissionId", - "in": "path", - "description": "权限 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "更新命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdatePermissionCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdatePermissionCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdatePermissionCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdatePermissionCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Permissions" - ], - "summary": "删除权限。", - "parameters": [ - { - "name": "permissionId", - "in": "path", - "description": "权限 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products": { - "post": { - "tags": [ - "Products" - ], - "summary": "创建商品。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateProductCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateProductCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateProductCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateProductCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Products" - ], - "summary": "查询商品列表。", - "parameters": [ - { - "name": "storeId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "categoryId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "in": "query", - "description": "商品状态。", - "schema": { - "$ref": "#/components/schemas/ProductStatus" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}": { - "get": { - "tags": [ - "Products" - ], - "summary": "获取商品详情。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "Products" - ], - "summary": "更新商品。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateProductCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateProductCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateProductCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateProductCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Products" - ], - "summary": "删除商品。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/detail": { - "get": { - "tags": [ - "Products" - ], - "summary": "获取商品全量详情。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDetailDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/publish": { - "post": { - "tags": [ - "Products" - ], - "summary": "上架商品。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/PublishProductCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublishProductCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/PublishProductCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/PublishProductCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/unpublish": { - "post": { - "tags": [ - "Products" - ], - "summary": "下架商品。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UnpublishProductCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnpublishProductCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UnpublishProductCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UnpublishProductCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/skus": { - "put": { - "tags": [ - "Products" - ], - "summary": "替换商品 SKU。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductSkusCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductSkusCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductSkusCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductSkusCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductSkuDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/attributes": { - "put": { - "tags": [ - "Products" - ], - "summary": "替换商品规格。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAttributesCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAttributesCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAttributesCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAttributesCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductAttributeGroupDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/addons": { - "put": { - "tags": [ - "Products" - ], - "summary": "替换商品加料。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAddonsCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAddonsCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAddonsCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductAddonsCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductAddonGroupDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/media": { - "put": { - "tags": [ - "Products" - ], - "summary": "替换商品媒资。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductMediaCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductMediaCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductMediaCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductMediaCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductMediaAssetDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/products/{productId}/pricing-rules": { - "put": { - "tags": [ - "Products" - ], - "summary": "替换商品价格策略。", - "parameters": [ - { - "name": "productId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductPricingRulesCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductPricingRulesCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductPricingRulesCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReplaceProductPricingRulesCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductPricingRuleDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/public/v1/tenant-packages": { - "get": { - "tags": [ - "PublicTenantPackages" - ], - "summary": "分页获取已启用的租户套餐。", - "parameters": [ - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/public/v1/tenants/self-register": { - "post": { - "tags": [ - "PublicTenants" - ], - "summary": "自助注册租户并生成初始管理员。", - "requestBody": { - "description": "自助注册命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/SelfRegisterTenantCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/SelfRegisterTenantCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/SelfRegisterTenantCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/SelfRegisterTenantCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SelfRegisterResultDtoApiResponse" - } - } - } - } - } - } - }, - "/api/public/v1/tenants/{tenantId}/verification": { - "post": { - "tags": [ - "PublicTenants" - ], - "summary": "自助提交或更新实名资料。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "租户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "实名资料。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantVerificationDtoApiResponse" - } - } - } - } - } - } - }, - "/api/public/v1/tenants/{tenantId}/status": { - "get": { - "tags": [ - "PublicTenants" - ], - "summary": "查询租户入住进度。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "租户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantProgressDtoApiResponse" - } - } - } - } - } - } - }, - "/api/public/v1/tenants/{tenantId}/subscriptions/initial": { - "post": { - "tags": [ - "PublicTenantSubscriptions" - ], - "summary": "初次绑定租户订阅(默认 0 个月)。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "租户 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "绑定请求。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/BindInitialTenantSubscriptionCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BindInitialTenantSubscriptionCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BindInitialTenantSubscriptionCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/BindInitialTenantSubscriptionCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantSubscriptionDtoApiResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantSubscriptionDtoApiResponse" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantSubscriptionDtoApiResponse" - } - } - } - }, - "409": { - "description": "Conflict", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantSubscriptionDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/role-templates": { - "get": { - "tags": [ - "RoleTemplates" - ], - "summary": "分页查询角色模板。", - "parameters": [ - { - "name": "isActive", - "in": "query", - "description": "是否启用筛选。", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "RoleTemplates" - ], - "summary": "创建角色模板。", - "requestBody": { - "description": "创建命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleTemplateCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleTemplateCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleTemplateCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleTemplateCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/role-templates/{templateCode}/clone": { - "post": { - "tags": [ - "RoleTemplates" - ], - "summary": "克隆角色模板。", - "parameters": [ - { - "name": "templateCode", - "in": "path", - "description": "源模板编码。", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "克隆命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CloneRoleTemplateCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CloneRoleTemplateCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CloneRoleTemplateCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CloneRoleTemplateCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/role-templates/{templateCode}": { - "get": { - "tags": [ - "RoleTemplates" - ], - "summary": "获取角色模板详情。", - "parameters": [ - { - "name": "templateCode", - "in": "path", - "description": "模板编码。", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "RoleTemplates" - ], - "summary": "更新角色模板。", - "parameters": [ - { - "name": "templateCode", - "in": "path", - "description": "模板编码。", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "更新命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleTemplateCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleTemplateCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleTemplateCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleTemplateCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleTemplateDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "RoleTemplates" - ], - "summary": "删除角色模板。", - "parameters": [ - { - "name": "templateCode", - "in": "path", - "description": "模板编码。", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/role-templates/{templateCode}/permissions": { - "get": { - "tags": [ - "RoleTemplates" - ], - "summary": "获取模板的权限列表。", - "parameters": [ - { - "name": "templateCode", - "in": "path", - "description": "模板编码。", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionTemplateDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/role-templates/init": { - "post": { - "tags": [ - "RoleTemplates" - ], - "summary": "为当前租户批量初始化预置角色模板。", - "requestBody": { - "description": "初始化命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/InitializeRoleTemplatesCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/InitializeRoleTemplatesCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/InitializeRoleTemplatesCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/InitializeRoleTemplatesCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/role-templates/{templateCode}/initialize-tenant": { - "post": { - "tags": [ - "RoleTemplates" - ], - "summary": "将单个模板初始化到当前租户。", - "parameters": [ - { - "name": "templateCode", - "in": "path", - "description": "模板编码。", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/pickup/settings": { - "get": { - "tags": [ - "StorePickup" - ], - "summary": "获取自提配置。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StorePickupSettingDtoApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "StorePickup" - ], - "summary": "更新自提配置。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpsertStorePickupSettingCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertStorePickupSettingCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpsertStorePickupSettingCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpsertStorePickupSettingCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StorePickupSettingDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/pickup/slots": { - "get": { - "tags": [ - "StorePickup" - ], - "summary": "查询档期列表。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StorePickupSlotDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "StorePickup" - ], - "summary": "创建档期。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStorePickupSlotCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStorePickupSlotCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStorePickupSlotCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStorePickupSlotCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StorePickupSlotDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/pickup/slots/{slotId}": { - "put": { - "tags": [ - "StorePickup" - ], - "summary": "更新档期。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "slotId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStorePickupSlotCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStorePickupSlotCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStorePickupSlotCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStorePickupSlotCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StorePickupSlotDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "StorePickup" - ], - "summary": "删除档期。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "slotId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores": { - "post": { - "tags": [ - "Stores" - ], - "summary": "创建门店。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Stores" - ], - "summary": "查询门店列表。", - "parameters": [ - { - "name": "merchantId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "in": "query", - "description": "门店运营状态。", - "schema": { - "$ref": "#/components/schemas/StoreStatus" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}": { - "get": { - "tags": [ - "Stores" - ], - "summary": "获取门店详情。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "Stores" - ], - "summary": "更新门店。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Stores" - ], - "summary": "删除门店。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/business-hours": { - "get": { - "tags": [ - "Stores" - ], - "summary": "查询门店营业时段。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreBusinessHourDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Stores" - ], - "summary": "新增营业时段。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreBusinessHourCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreBusinessHourCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreBusinessHourCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreBusinessHourCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreBusinessHourDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/business-hours/{businessHourId}": { - "put": { - "tags": [ - "Stores" - ], - "summary": "更新营业时段。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "businessHourId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreBusinessHourCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreBusinessHourCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreBusinessHourCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreBusinessHourCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreBusinessHourDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Stores" - ], - "summary": "删除营业时段。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "businessHourId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/delivery-zones": { - "get": { - "tags": [ - "Stores" - ], - "summary": "查询配送区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDeliveryZoneDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Stores" - ], - "summary": "新增配送区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreDeliveryZoneCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreDeliveryZoneCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreDeliveryZoneCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreDeliveryZoneCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDeliveryZoneDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/delivery-zones/{deliveryZoneId}": { - "put": { - "tags": [ - "Stores" - ], - "summary": "更新配送区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "deliveryZoneId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreDeliveryZoneCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreDeliveryZoneCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreDeliveryZoneCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreDeliveryZoneCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreDeliveryZoneDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Stores" - ], - "summary": "删除配送区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "deliveryZoneId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/holidays": { - "get": { - "tags": [ - "Stores" - ], - "summary": "查询门店节假日。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreHolidayDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "Stores" - ], - "summary": "新增节假日配置。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreHolidayCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreHolidayCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreHolidayCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreHolidayCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreHolidayDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/holidays/{holidayId}": { - "put": { - "tags": [ - "Stores" - ], - "summary": "更新节假日配置。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "holidayId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreHolidayCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreHolidayCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreHolidayCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreHolidayCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreHolidayDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Stores" - ], - "summary": "删除节假日配置。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "holidayId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/shifts": { - "get": { - "tags": [ - "StoreShifts" - ], - "summary": "查询排班(默认未来 7 天)。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "from", - "in": "query", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "to", - "in": "query", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "staffId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreEmployeeShiftDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "StoreShifts" - ], - "summary": "创建排班。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreEmployeeShiftCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreEmployeeShiftCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreEmployeeShiftCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreEmployeeShiftCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreEmployeeShiftDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/shifts/{shiftId}": { - "put": { - "tags": [ - "StoreShifts" - ], - "summary": "更新排班。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "shiftId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreEmployeeShiftCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreEmployeeShiftCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreEmployeeShiftCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreEmployeeShiftCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreEmployeeShiftDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "StoreShifts" - ], - "summary": "删除排班。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "shiftId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/staffs": { - "get": { - "tags": [ - "StoreStaffs" - ], - "summary": "查询门店员工列表。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "role", - "in": "query", - "description": "商户员工角色。", - "schema": { - "$ref": "#/components/schemas/StaffRoleType" - } - }, - { - "name": "status", - "in": "query", - "description": "员工账号状态。", - "schema": { - "$ref": "#/components/schemas/StaffStatus" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreStaffDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "StoreStaffs" - ], - "summary": "创建门店员工。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreStaffCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreStaffCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreStaffCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreStaffCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreStaffDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/staffs/{staffId}": { - "put": { - "tags": [ - "StoreStaffs" - ], - "summary": "更新门店员工。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "staffId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreStaffCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreStaffCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreStaffCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreStaffCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreStaffDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "StoreStaffs" - ], - "summary": "删除门店员工。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "staffId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/table-areas": { - "get": { - "tags": [ - "StoreTableAreas" - ], - "summary": "查询区域列表。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreTableAreaDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "StoreTableAreas" - ], - "summary": "创建区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreTableAreaCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreTableAreaCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreTableAreaCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateStoreTableAreaCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreTableAreaDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/table-areas/{areaId}": { - "put": { - "tags": [ - "StoreTableAreas" - ], - "summary": "更新区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "areaId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableAreaCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableAreaCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableAreaCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableAreaCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreTableAreaDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "StoreTableAreas" - ], - "summary": "删除区域。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "areaId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/tables": { - "get": { - "tags": [ - "StoreTables" - ], - "summary": "查询桌码列表。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "areaId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "in": "query", - "description": "桌台占用状态。", - "schema": { - "$ref": "#/components/schemas/StoreTableStatus" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreTableDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "StoreTables" - ], - "summary": "批量生成桌码。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/GenerateStoreTablesCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenerateStoreTablesCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/GenerateStoreTablesCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/GenerateStoreTablesCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreTableDtoIReadOnlyListApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/tables/{tableId}": { - "put": { - "tags": [ - "StoreTables" - ], - "summary": "更新桌码。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "tableId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateStoreTableCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StoreTableDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "StoreTables" - ], - "summary": "删除桌码。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "tableId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/stores/{storeId}/tables/export": { - "post": { - "tags": [ - "StoreTables" - ], - "summary": "导出桌码二维码 ZIP。", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ExportStoreTableQRCodesQuery" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExportStoreTableQRCodesQuery" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ExportStoreTableQRCodesQuery" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ExportStoreTableQRCodesQuery" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/system-parameters": { - "post": { - "tags": [ - "SystemParameters" - ], - "summary": "创建系统参数。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateSystemParameterCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSystemParameterCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateSystemParameterCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateSystemParameterCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemParameterDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "SystemParameters" - ], - "summary": "查询系统参数列表。", - "parameters": [ - { - "name": "keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "isEnabled", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - }, - { - "name": "sortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "sortDesc", - "in": "query", - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemParameterDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/system-parameters/{parameterId}": { - "get": { - "tags": [ - "SystemParameters" - ], - "summary": "获取系统参数详情。", - "parameters": [ - { - "name": "parameterId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemParameterDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "SystemParameters" - ], - "summary": "更新系统参数。", - "parameters": [ - { - "name": "parameterId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateSystemParameterCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSystemParameterCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSystemParameterCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateSystemParameterCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemParameterDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "SystemParameters" - ], - "summary": "删除系统参数。", - "parameters": [ - { - "name": "parameterId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObjectApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/announcements": { - "get": { - "tags": [ - "TenantAnnouncements" - ], - "summary": "分页查询公告。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "TenantId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "AnnouncementType", - "in": "query", - "description": "租户公告类型。", - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementType" - } - }, - { - "name": "IsActive", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "OnlyEffective", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoPagedResultApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "TenantAnnouncements" - ], - "summary": "创建公告。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantAnnouncementCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantAnnouncementCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantAnnouncementCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantAnnouncementCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/announcements/{announcementId}": { - "get": { - "tags": [ - "TenantAnnouncements" - ], - "summary": "公告详情。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "announcementId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "TenantAnnouncements" - ], - "summary": "更新公告。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "announcementId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantAnnouncementCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantAnnouncementCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantAnnouncementCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantAnnouncementCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "TenantAnnouncements" - ], - "summary": "删除公告。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "announcementId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/announcements/{announcementId}/read": { - "post": { - "tags": [ - "TenantAnnouncements" - ], - "summary": "标记公告已读。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "announcementId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAnnouncementDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/billings": { - "get": { - "tags": [ - "TenantBillings" - ], - "summary": "分页查询账单。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "TenantId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "Status", - "in": "query", - "description": "账单状态。", - "schema": { - "$ref": "#/components/schemas/TenantBillingStatus" - } - }, - { - "name": "From", - "in": "query", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "To", - "in": "query", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantBillingDtoPagedResultApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "TenantBillings" - ], - "summary": "创建账单。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantBillingCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantBillingCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantBillingCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantBillingCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantBillingDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/billings/{billingId}": { - "get": { - "tags": [ - "TenantBillings" - ], - "summary": "账单详情。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "billingId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantBillingDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantBillingDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/billings/{billingId}/pay": { - "post": { - "tags": [ - "TenantBillings" - ], - "summary": "标记账单已支付。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "billingId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/MarkTenantBillingPaidCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/MarkTenantBillingPaidCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/MarkTenantBillingPaidCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/MarkTenantBillingPaidCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantBillingDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantBillingDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/notifications": { - "get": { - "tags": [ - "TenantNotifications" - ], - "summary": "分页查询通知。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "TenantId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "Severity", - "in": "query", - "description": "租户通知的重要程度。", - "schema": { - "$ref": "#/components/schemas/TenantNotificationSeverity" - } - }, - { - "name": "UnreadOnly", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantNotificationDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/notifications/{notificationId}/read": { - "post": { - "tags": [ - "TenantNotifications" - ], - "summary": "标记通知已读。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "notificationId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantNotificationDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantNotificationDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenant-packages": { - "get": { - "tags": [ - "TenantPackages" - ], - "summary": "分页查询租户套餐。", - "parameters": [ - { - "name": "Keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "IsActive", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoPagedResultApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "TenantPackages" - ], - "summary": "创建套餐。", - "requestBody": { - "description": "创建命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantPackageCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantPackageCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantPackageCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantPackageCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenant-packages/{tenantPackageId}": { - "get": { - "tags": [ - "TenantPackages" - ], - "summary": "查看套餐详情。", - "parameters": [ - { - "name": "tenantPackageId", - "in": "path", - "description": "套餐 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "TenantPackages" - ], - "summary": "更新套餐。", - "parameters": [ - { - "name": "tenantPackageId", - "in": "path", - "description": "套餐 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "description": "更新命令。", - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantPackageCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantPackageCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantPackageCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateTenantPackageCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantPackageDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "TenantPackages" - ], - "summary": "删除套餐。", - "parameters": [ - { - "name": "tenantPackageId", - "in": "path", - "description": "套餐 ID。", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/roles": { - "get": { - "tags": [ - "TenantRoles" - ], - "summary": "租户角色分页。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "TenantId", - "in": "query", - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "Keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "SortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "SortDescending", - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDtoPagedResultApiResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "TenantRoles" - ], - "summary": "创建角色。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateRoleCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/roles/{roleId}": { - "get": { - "tags": [ - "TenantRoles" - ], - "summary": "角色详情(含权限)。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "roleId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDetailDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDetailDtoApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "TenantRoles" - ], - "summary": "更新角色。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "roleId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateRoleCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDtoApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoleDtoApiResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "TenantRoles" - ], - "summary": "删除角色。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "roleId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/roles/{roleId}/permissions": { - "get": { - "tags": [ - "TenantRoles" - ], - "summary": "获取角色权限列表。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "roleId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionDtoIReadOnlyListApiResponse" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PermissionDtoIReadOnlyListApiResponse" - } - } - } - } - } - }, - "put": { - "tags": [ - "TenantRoles" - ], - "summary": "覆盖角色权限。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "roleId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/BindRolePermissionsCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BindRolePermissionsCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BindRolePermissionsCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/BindRolePermissionsCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BooleanApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants": { - "post": { - "tags": [ - "Tenants" - ], - "summary": "注册租户并初始化套餐。", - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/RegisterTenantCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterTenantCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/RegisterTenantCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/RegisterTenantCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantDtoApiResponse" - } - } - } - } - } - }, - "get": { - "tags": [ - "Tenants" - ], - "summary": "分页查询租户。", - "parameters": [ - { - "name": "Status", - "in": "query", - "description": "租户服务状态。", - "schema": { - "$ref": "#/components/schemas/TenantStatus" - } - }, - { - "name": "VerificationStatus", - "in": "query", - "description": "租户实名认证状态。", - "schema": { - "$ref": "#/components/schemas/TenantVerificationStatus" - } - }, - { - "name": "Name", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "ContactName", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "ContactPhone", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "Keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}": { - "get": { - "tags": [ - "Tenants" - ], - "summary": "查看租户详情。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantDetailDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/verification": { - "post": { - "tags": [ - "Tenants" - ], - "summary": "提交或更新实名认证资料。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/SubmitTenantVerificationCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantVerificationDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/review": { - "post": { - "tags": [ - "Tenants" - ], - "summary": "审核租户。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ReviewTenantCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReviewTenantCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ReviewTenantCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ReviewTenantCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/subscriptions": { - "post": { - "tags": [ - "Tenants" - ], - "summary": "创建或续费租户订阅。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantSubscriptionCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantSubscriptionCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantSubscriptionCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CreateTenantSubscriptionCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantSubscriptionDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/subscriptions/{subscriptionId}/plan": { - "put": { - "tags": [ - "Tenants" - ], - "summary": "套餐升降配。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "subscriptionId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/ChangeTenantSubscriptionPlanCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChangeTenantSubscriptionPlanCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ChangeTenantSubscriptionPlanCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/ChangeTenantSubscriptionPlanCommand" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantSubscriptionDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/audits": { - "get": { - "tags": [ - "Tenants" - ], - "summary": "查询审核日志。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 1 - } - }, - { - "name": "pageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TenantAuditLogDtoPagedResultApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/tenants/{tenantId}/quotas/check": { - "post": { - "tags": [ - "Tenants" - ], - "summary": "配额校验并占用额度(门店/账号/短信/配送)。", - "description": "需在请求头携带 X-Tenant-Id 对应的租户。", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "content": { - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/CheckTenantQuotaCommand" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckTenantQuotaCommand" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/CheckTenantQuotaCommand" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/CheckTenantQuotaCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuotaCheckResultDtoApiResponse" - } - } - } - } - } - } - }, - "/api/admin/v1/users/permissions": { - "get": { - "tags": [ - "UserPermissions" - ], - "summary": "分页查询当前租户用户的角色与权限概览。", - "description": "示例:\n```\nGET /api/admin/v1/users/permissions?keyword=ops&page=1&pageSize=20&sortBy=createdAt&sortDescending=true\nHeader: Authorization: Bearer \n响应:\n{\n \"success\": true,\n \"code\": 200,\n \"data\": {\n \"items\": [\n {\n \"userId\": \"900123456789012346\",\n \"tenantId\": \"100000000000000001\",\n \"merchantId\": \"200000000000000001\",\n \"account\": \"ops.manager\",\n \"displayName\": \"运营经理\",\n \"roles\": [\"OpsManager\", \"Reporter\"],\n \"permissions\": [\"delivery:read\", \"order:read\", \"payment:read\"],\n \"createdAt\": \"2025-12-01T08:30:00Z\"\n }\n ],\n \"page\": 1,\n \"pageSize\": 20,\n \"totalCount\": 1,\n \"totalPages\": 1\n }\n}\n```", - "parameters": [ - { - "name": "Keyword", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "Page", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "PageSize", - "in": "query", - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "SortBy", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "SortDescending", - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserPermissionDtoPagedResultApiResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "AddMerchantDocumentCommand": { - "required": [ - "documentType", - "fileUrl", - "merchantId" - ], - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "documentType": { - "$ref": "#/components/schemas/MerchantDocumentType" - }, - "fileUrl": { - "maxLength": 512, - "minLength": 1, - "type": "string" - }, - "documentNumber": { - "maxLength": 64, - "type": "string", - "nullable": true - }, - "issuedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, - "AdjustInventoryCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "quantityDelta": { - "type": "integer", - "format": "int32" - }, - "adjustmentType": { - "$ref": "#/components/schemas/InventoryAdjustmentType" - }, - "reason": { - "type": "string", - "nullable": true - }, - "safetyStock": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "isSoldOut": { - "type": "boolean", - "nullable": true - } - }, - "additionalProperties": false - }, - "AdminLoginRequest": { - "required": [ - "account", - "password" - ], - "type": "object", - "properties": { - "account": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "password": { - "maxLength": 128, - "minLength": 1, - "type": "string" - } - }, - "additionalProperties": false - }, - "BindInitialTenantSubscriptionCommand": { - "required": [ - "tenantId", - "tenantPackageId" - ], - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "tenantPackageId": { - "type": "integer", - "format": "int64" - }, - "autoRenew": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "BindRolePermissionsCommand": { - "type": "object", - "properties": { - "roleId": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "permissionIds": { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "BooleanApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "boolean", - "description": "业务数据。" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "BusinessHourType": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "营业时段类型。", - "format": "int32" - }, - "ChangeTenantSubscriptionPlanCommand": { - "required": [ - "targetPackageId", - "tenantId", - "tenantSubscriptionId" - ], - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "tenantSubscriptionId": { - "type": "integer", - "format": "int64" - }, - "targetPackageId": { - "type": "integer", - "format": "int64" - }, - "immediate": { - "type": "boolean" - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CheckTenantQuotaCommand": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "quotaType": { - "$ref": "#/components/schemas/TenantQuotaType" - }, - "delta": { - "type": "number", - "format": "double" - } - }, - "additionalProperties": false - }, - "CloneRoleTemplateCommand": { - "type": "object", - "properties": { - "sourceTemplateCode": { - "type": "string", - "nullable": true - }, - "newTemplateCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean", - "nullable": true - }, - "permissionCodes": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ContractStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "商户合同状态。", - "format": "int32" - }, - "CreateDeliveryOrderCommand": { - "type": "object", - "properties": { - "orderId": { - "type": "integer", - "format": "int64" - }, - "provider": { - "$ref": "#/components/schemas/DeliveryProvider" - }, - "providerOrderId": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/DeliveryStatus" - }, - "deliveryFee": { - "type": "number", - "format": "double", - "nullable": true - }, - "courierName": { - "type": "string", - "nullable": true - }, - "courierPhone": { - "type": "string", - "nullable": true - }, - "dispatchedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "pickedUpAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "deliveredAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "failureReason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateDictionaryGroupRequest": { - "required": [ - "code", - "name", - "scope" - ], - "type": "object", - "properties": { - "code": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" - }, - "scope": { - "$ref": "#/components/schemas/DictionaryScope" - }, - "description": { - "maxLength": 512, - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateDictionaryItemRequest": { - "required": [ - "groupId", - "key", - "value" - ], - "type": "object", - "properties": { - "groupId": { - "type": "integer", - "format": "int64" - }, - "key": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "value": { - "maxLength": 256, - "minLength": 1, - "type": "string" - }, - "isDefault": { - "type": "boolean" - }, - "isEnabled": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "description": { - "maxLength": 512, - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateMenuCommand": { - "type": "object", - "properties": { - "parentId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "path": { - "type": "string", - "nullable": true - }, - "component": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "icon": { - "type": "string", - "nullable": true - }, - "isIframe": { - "type": "boolean" - }, - "link": { - "type": "string", - "nullable": true - }, - "keepAlive": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "requiredPermissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "metaPermissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "metaRoles": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "authList": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuAuthItemDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateMerchantCategoryCommand": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "displayOrder": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "isActive": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "CreateMerchantCommand": { - "required": [ - "brandName", - "contactPhone" - ], - "type": "object", - "properties": { - "brandName": { - "maxLength": 128, - "minLength": 1, - "type": "string" - }, - "brandAlias": { - "maxLength": 64, - "type": "string", - "nullable": true - }, - "logoUrl": { - "maxLength": 256, - "type": "string", - "nullable": true - }, - "category": { - "maxLength": 64, - "type": "string", - "nullable": true - }, - "contactPhone": { - "maxLength": 32, - "minLength": 1, - "type": "string" - }, - "contactEmail": { - "maxLength": 128, - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/MerchantStatus" - } - }, - "additionalProperties": false - }, - "CreateMerchantContractCommand": { - "required": [ - "contractNumber", - "fileUrl", - "merchantId" - ], - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "contractNumber": { - "maxLength": 64, - "minLength": 1, - "type": "string" - }, - "startDate": { - "type": "string", - "format": "date-time" - }, - "endDate": { - "type": "string", - "format": "date-time" - }, - "fileUrl": { - "maxLength": 512, - "minLength": 1, - "type": "string" - } - }, - "additionalProperties": false - }, - "CreateOrderCommand": { - "type": "object", - "properties": { - "orderNo": { - "type": "string", - "nullable": true - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "channel": { - "$ref": "#/components/schemas/OrderChannel" - }, - "deliveryType": { - "$ref": "#/components/schemas/DeliveryType" - }, - "status": { - "$ref": "#/components/schemas/OrderStatus" - }, - "paymentStatus": { - "$ref": "#/components/schemas/PaymentStatus" - }, - "customerName": { - "type": "string", - "nullable": true - }, - "customerPhone": { - "type": "string", - "nullable": true - }, - "tableNo": { - "type": "string", - "nullable": true - }, - "queueNumber": { - "type": "string", - "nullable": true - }, - "reservationId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "itemsAmount": { - "type": "number", - "format": "double" - }, - "discountAmount": { - "type": "number", - "format": "double" - }, - "payableAmount": { - "type": "number", - "format": "double" - }, - "paidAmount": { - "type": "number", - "format": "double" - }, - "paidAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "finishedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelledAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelReason": { - "type": "string", - "nullable": true - }, - "remark": { - "type": "string", - "nullable": true - }, - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderItemRequest" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "CreatePaymentCommand": { - "type": "object", - "properties": { - "orderId": { - "type": "integer", - "format": "int64" - }, - "method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "status": { - "$ref": "#/components/schemas/PaymentStatus" - }, - "amount": { - "type": "number", - "format": "double" - }, - "tradeNo": { - "type": "string", - "nullable": true - }, - "channelTransactionId": { - "type": "string", - "nullable": true - }, - "paidAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "remark": { - "type": "string", - "nullable": true - }, - "payload": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreatePermissionCommand": { - "type": "object", - "properties": { - "parentId": { - "type": "integer", - "format": "int64" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateProductCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "categoryId": { - "type": "integer", - "format": "int64" - }, - "spuCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "subtitle": { - "type": "string", - "nullable": true - }, - "unit": { - "type": "string", - "nullable": true - }, - "price": { - "type": "number", - "format": "double" - }, - "originalPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "stockQuantity": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxQuantityPerOrder": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ProductStatus" - }, - "coverImage": { - "type": "string", - "nullable": true - }, - "galleryImages": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "enableDineIn": { - "type": "boolean" - }, - "enablePickup": { - "type": "boolean" - }, - "enableDelivery": { - "type": "boolean" - }, - "isFeatured": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "CreateRoleCommand": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateRoleTemplateCommand": { - "type": "object", - "properties": { - "templateCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "permissionCodes": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateStoreBusinessHourCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "dayOfWeek": { - "$ref": "#/components/schemas/DayOfWeek" - }, - "hourType": { - "$ref": "#/components/schemas/BusinessHourType" - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "capacityLimit": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateStoreCommand": { - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "managerName": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/StoreStatus" - }, - "province": { - "type": "string", - "nullable": true - }, - "city": { - "type": "string", - "nullable": true - }, - "district": { - "type": "string", - "nullable": true - }, - "address": { - "type": "string", - "nullable": true - }, - "longitude": { - "type": "number", - "format": "double", - "nullable": true - }, - "latitude": { - "type": "number", - "format": "double", - "nullable": true - }, - "announcement": { - "type": "string", - "nullable": true - }, - "tags": { - "type": "string", - "nullable": true - }, - "deliveryRadiusKm": { - "type": "number", - "format": "double" - }, - "supportsDineIn": { - "type": "boolean" - }, - "supportsPickup": { - "type": "boolean" - }, - "supportsDelivery": { - "type": "boolean" - }, - "supportsReservation": { - "type": "boolean" - }, - "supportsQueueing": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "CreateStoreDeliveryZoneCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "zoneName": { - "type": "string", - "nullable": true - }, - "polygonGeoJson": { - "type": "string", - "nullable": true - }, - "minimumOrderAmount": { - "type": "number", - "format": "double", - "nullable": true - }, - "deliveryFee": { - "type": "number", - "format": "double", - "nullable": true - }, - "estimatedMinutes": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "CreateStoreEmployeeShiftCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "staffId": { - "type": "integer", - "format": "int64" - }, - "shiftDate": { - "type": "string", - "format": "date-time" - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "roleType": { - "$ref": "#/components/schemas/StaffRoleType" - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateStoreHolidayCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "date": { - "type": "string", - "format": "date-time" - }, - "isClosed": { - "type": "boolean" - }, - "reason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateStorePickupSlotCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "cutoffMinutes": { - "type": "integer", - "format": "int32" - }, - "capacity": { - "type": "integer", - "format": "int32" - }, - "weekdays": { - "type": "string", - "nullable": true - }, - "isEnabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "CreateStoreStaffCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "roleType": { - "$ref": "#/components/schemas/StaffRoleType" - } - }, - "additionalProperties": false - }, - "CreateStoreTableAreaCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "CreateSystemParameterCommand": { - "type": "object", - "properties": { - "key": { - "type": "string", - "nullable": true - }, - "value": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "isEnabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "CreateTenantAnnouncementCommand": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "title": { - "type": "string", - "nullable": true - }, - "content": { - "type": "string", - "nullable": true - }, - "announcementType": { - "$ref": "#/components/schemas/TenantAnnouncementType" - }, - "priority": { - "type": "integer", - "format": "int32" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time" - }, - "effectiveTo": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "isActive": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "CreateTenantBillingCommand": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "statementNo": { - "type": "string", - "nullable": true - }, - "periodStart": { - "type": "string", - "format": "date-time" - }, - "periodEnd": { - "type": "string", - "format": "date-time" - }, - "amountDue": { - "type": "number", - "format": "double" - }, - "amountPaid": { - "type": "number", - "format": "double" - }, - "status": { - "$ref": "#/components/schemas/TenantBillingStatus" - }, - "dueDate": { - "type": "string", - "format": "date-time" - }, - "lineItemsJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CreateTenantPackageCommand": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "packageType": { - "$ref": "#/components/schemas/TenantPackageType" - }, - "monthlyPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "yearlyPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "maxStoreCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxAccountCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxStorageGb": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxSmsCredits": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxDeliveryOrders": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "featurePoliciesJson": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "CreateTenantSubscriptionCommand": { - "required": [ - "tenantId", - "tenantPackageId" - ], - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "tenantPackageId": { - "type": "integer", - "format": "int64" - }, - "durationMonths": { - "type": "integer", - "format": "int32" - }, - "autoRenew": { - "type": "boolean" - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CurrentUserProfile": { - "type": "object", - "properties": { - "userId": { - "type": "integer", - "format": "int64" - }, - "account": { - "type": "string", - "nullable": true - }, - "displayName": { - "type": "string", - "nullable": true - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "permissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "avatar": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "CurrentUserProfileApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/CurrentUserProfile" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "DayOfWeek": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6 - ], - "type": "integer", - "format": "int32" - }, - "DeductInventoryCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "isPresaleOrder": { - "type": "boolean" - }, - "idempotencyKey": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "DeliveryEventDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "deliveryOrderId": { - "type": "integer", - "format": "int64" - }, - "eventType": { - "$ref": "#/components/schemas/DeliveryEventType" - }, - "message": { - "type": "string", - "nullable": true - }, - "occurredAt": { - "type": "string", - "format": "date-time" - }, - "payload": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "DeliveryEventType": { - "enum": [ - 0, - 1, - 2 - ], - "type": "integer", - "description": "配送事件类型。", - "format": "int32" - }, - "DeliveryOrderDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "provider": { - "$ref": "#/components/schemas/DeliveryProvider" - }, - "providerOrderId": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/DeliveryStatus" - }, - "deliveryFee": { - "type": "number", - "format": "double", - "nullable": true - }, - "courierName": { - "type": "string", - "nullable": true - }, - "courierPhone": { - "type": "string", - "nullable": true - }, - "dispatchedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "pickedUpAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "deliveredAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "failureReason": { - "type": "string", - "nullable": true - }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeliveryEventDto" - }, - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "DeliveryOrderDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/DeliveryOrderDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "DeliveryOrderDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeliveryOrderDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "DeliveryOrderDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/DeliveryOrderDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "DeliveryProvider": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "type": "integer", - "description": "配送服务商类型。", - "format": "int32" - }, - "DeliveryStatus": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6 - ], - "type": "integer", - "description": "配送状态。", - "format": "int32" - }, - "DeliveryType": { - "enum": [ - 0, - 1, - 2 - ], - "type": "integer", - "description": "履约/交付方式。", - "format": "int32" - }, - "DictionaryBatchQueryRequest": { - "required": [ - "codes" - ], - "type": "object", - "properties": { - "codes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "DictionaryGroupDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "scope": { - "$ref": "#/components/schemas/DictionaryScope" - }, - "description": { - "type": "string", - "nullable": true - }, - "isEnabled": { - "type": "boolean" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DictionaryItemDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "DictionaryGroupDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/DictionaryGroupDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "DictionaryGroupDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DictionaryGroupDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "DictionaryItemDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "groupId": { - "type": "integer", - "format": "int64" - }, - "key": { - "type": "string", - "nullable": true - }, - "value": { - "type": "string", - "nullable": true - }, - "isDefault": { - "type": "boolean" - }, - "isEnabled": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "DictionaryItemDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/DictionaryItemDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "DictionaryScope": { - "enum": [ - 1, - 2 - ], - "type": "integer", - "description": "参数字典作用域。", - "format": "int32" - }, - "ExportStoreTableQRCodesQuery": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "areaId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "qrContentTemplate": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "FileUploadResponse": { - "type": "object", - "properties": { - "url": { - "type": "string", - "nullable": true - }, - "fileName": { - "type": "string", - "nullable": true - }, - "fileSize": { - "type": "integer", - "format": "int64" - } - }, - "additionalProperties": false - }, - "FileUploadResponseApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/FileUploadResponse" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "GenerateStoreTablesCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "tableCodePrefix": { - "type": "string", - "nullable": true - }, - "startNumber": { - "type": "integer", - "format": "int32" - }, - "count": { - "type": "integer", - "format": "int32" - }, - "defaultCapacity": { - "type": "integer", - "format": "int32" - }, - "areaId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "tags": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "InitializeRoleTemplatesCommand": { - "type": "object", - "properties": { - "templateCodes": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "Int32ApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "integer", - "description": "业务数据。", - "format": "int32" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "InventoryAdjustmentType": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "description": "库存调整类型。", - "format": "int32" - }, - "InventoryBatchDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "batchNumber": { - "type": "string", - "nullable": true - }, - "productionDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "expireDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "remainingQuantity": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "InventoryBatchDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/InventoryBatchDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "InventoryBatchDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/InventoryBatchDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "InventoryItemDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "batchNumber": { - "type": "string", - "nullable": true - }, - "quantityOnHand": { - "type": "integer", - "format": "int32" - }, - "quantityReserved": { - "type": "integer", - "format": "int32" - }, - "safetyStock": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "location": { - "type": "string", - "nullable": true - }, - "expireDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "isPresale": { - "type": "boolean" - }, - "presaleStartTime": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "presaleEndTime": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "presaleCapacity": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "presaleLocked": { - "type": "integer", - "format": "int32" - }, - "maxQuantityPerOrder": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "isSoldOut": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "InventoryItemDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/InventoryItemDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "LockInventoryCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "isPresaleOrder": { - "type": "boolean" - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "idempotencyKey": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "MarkTenantBillingPaidCommand": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "billingId": { - "type": "integer", - "format": "int64" - }, - "amountPaid": { - "type": "number", - "format": "double" - }, - "paidAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "MediaAssetType": { - "enum": [ - 0, - 1, - 2 - ], - "type": "integer", - "description": "商品媒资类型。", - "format": "int32" - }, - "MenuAuthItemDto": { - "required": [ - "authMark", - "title" - ], - "type": "object", - "properties": { - "title": { - "type": "string", - "nullable": true - }, - "authMark": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "MenuDefinitionDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "parentId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "path": { - "type": "string", - "nullable": true - }, - "component": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "icon": { - "type": "string", - "nullable": true - }, - "isIframe": { - "type": "boolean" - }, - "link": { - "type": "string", - "nullable": true - }, - "keepAlive": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "requiredPermissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "metaPermissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "metaRoles": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "authList": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuAuthItemDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "MenuDefinitionDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MenuDefinitionDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MenuDefinitionDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuDefinitionDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MenuMetaDto": { - "required": [ - "title" - ], - "type": "object", - "properties": { - "title": { - "type": "string", - "nullable": true - }, - "icon": { - "type": "string", - "nullable": true - }, - "keepAlive": { - "type": "boolean" - }, - "isIframe": { - "type": "boolean" - }, - "link": { - "type": "string", - "nullable": true - }, - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "authList": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuAuthItemDto" - }, - "nullable": true - }, - "permissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "MenuNodeDto": { - "required": [ - "component", - "meta", - "name", - "path" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "path": { - "type": "string", - "nullable": true - }, - "component": { - "type": "string", - "nullable": true - }, - "meta": { - "$ref": "#/components/schemas/MenuMetaDto" - }, - "children": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuNodeDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "MenuNodeDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuNodeDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantAuditAction": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "type": "integer", - "description": "商户审核日志动作。", - "format": "int32" - }, - "MerchantAuditLogDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64" - }, - "action": { - "$ref": "#/components/schemas/MerchantAuditAction" - }, - "title": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "operatorName": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "MerchantAuditLogDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantAuditLogDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "MerchantAuditLogDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MerchantAuditLogDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantCategoryDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "displayOrder": { - "type": "integer", - "format": "int32" - }, - "isActive": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "MerchantCategoryDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MerchantCategoryDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantCategoryDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantCategoryDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantCategoryOrderItem": { - "required": [ - "categoryId" - ], - "type": "object", - "properties": { - "categoryId": { - "type": "integer", - "format": "int64" - }, - "displayOrder": { - "maximum": 100000, - "minimum": -1000, - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "MerchantContractDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64" - }, - "contractNumber": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ContractStatus" - }, - "startDate": { - "type": "string", - "format": "date-time" - }, - "endDate": { - "type": "string", - "format": "date-time" - }, - "fileUrl": { - "type": "string", - "nullable": true - }, - "signedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "terminatedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "terminationReason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "MerchantContractDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MerchantContractDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantContractDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantContractDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantDetailDto": { - "type": "object", - "properties": { - "merchant": { - "$ref": "#/components/schemas/MerchantDto" - }, - "documents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantDocumentDto" - }, - "nullable": true - }, - "contracts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantContractDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "MerchantDetailDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MerchantDetailDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantDocumentDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64" - }, - "documentType": { - "$ref": "#/components/schemas/MerchantDocumentType" - }, - "status": { - "$ref": "#/components/schemas/MerchantDocumentStatus" - }, - "fileUrl": { - "type": "string", - "nullable": true - }, - "documentNumber": { - "type": "string", - "nullable": true - }, - "issuedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "remarks": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "MerchantDocumentDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MerchantDocumentDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantDocumentDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantDocumentDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantDocumentStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "证照审核状态。", - "format": "int32" - }, - "MerchantDocumentType": { - "enum": [ - 0, - 1, - 2, - 99 - ], - "type": "integer", - "description": "商户证照类型。", - "format": "int32" - }, - "MerchantDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "brandName": { - "type": "string", - "nullable": true - }, - "brandAlias": { - "type": "string", - "nullable": true - }, - "logoUrl": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string", - "nullable": true - }, - "contactPhone": { - "type": "string", - "nullable": true - }, - "contactEmail": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/MerchantStatus" - }, - "joinedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "MerchantDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/MerchantDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "MerchantStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "商户入驻状态。", - "format": "int32" - }, - "ObjectApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "OrderChannel": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "type": "integer", - "description": "下单渠道。", - "format": "int32" - }, - "OrderDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "orderNo": { - "type": "string", - "nullable": true - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "channel": { - "$ref": "#/components/schemas/OrderChannel" - }, - "deliveryType": { - "$ref": "#/components/schemas/DeliveryType" - }, - "status": { - "$ref": "#/components/schemas/OrderStatus" - }, - "paymentStatus": { - "$ref": "#/components/schemas/PaymentStatus" - }, - "customerName": { - "type": "string", - "nullable": true - }, - "customerPhone": { - "type": "string", - "nullable": true - }, - "tableNo": { - "type": "string", - "nullable": true - }, - "queueNumber": { - "type": "string", - "nullable": true - }, - "reservationId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "itemsAmount": { - "type": "number", - "format": "double" - }, - "discountAmount": { - "type": "number", - "format": "double" - }, - "payableAmount": { - "type": "number", - "format": "double" - }, - "paidAmount": { - "type": "number", - "format": "double" - }, - "paidAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "finishedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelledAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelReason": { - "type": "string", - "nullable": true - }, - "remark": { - "type": "string", - "nullable": true - }, - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderItemDto" - }, - "nullable": true - }, - "statusHistory": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderStatusHistoryDto" - }, - "nullable": true - }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundRequestDto" - }, - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "OrderDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/OrderDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "OrderDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "OrderDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/OrderDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "OrderItemDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "productId": { - "type": "integer", - "format": "int64" - }, - "productName": { - "type": "string", - "nullable": true - }, - "skuName": { - "type": "string", - "nullable": true - }, - "unit": { - "type": "string", - "nullable": true - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "unitPrice": { - "type": "number", - "format": "double" - }, - "discountAmount": { - "type": "number", - "format": "double" - }, - "subTotal": { - "type": "number", - "format": "double" - }, - "attributesJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "OrderItemRequest": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "productName": { - "type": "string", - "nullable": true - }, - "skuName": { - "type": "string", - "nullable": true - }, - "unit": { - "type": "string", - "nullable": true - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "unitPrice": { - "type": "number", - "format": "double" - }, - "discountAmount": { - "type": "number", - "format": "double" - }, - "subTotal": { - "type": "number", - "format": "double" - }, - "attributesJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "OrderStatus": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "type": "integer", - "description": "订单状态。", - "format": "int32" - }, - "OrderStatusHistoryDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "status": { - "$ref": "#/components/schemas/OrderStatus" - }, - "operatorId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "notes": { - "type": "string", - "nullable": true - }, - "occurredAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "PaymentDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "status": { - "$ref": "#/components/schemas/PaymentStatus" - }, - "amount": { - "type": "number", - "format": "double" - }, - "tradeNo": { - "type": "string", - "nullable": true - }, - "channelTransactionId": { - "type": "string", - "nullable": true - }, - "paidAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "remark": { - "type": "string", - "nullable": true - }, - "payload": { - "type": "string", - "nullable": true - }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentRefundDto" - }, - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "PaymentDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/PaymentDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PaymentDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "PaymentDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/PaymentDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PaymentMethod": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "type": "integer", - "description": "支付方式。", - "format": "int32" - }, - "PaymentRefundDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "paymentRecordId": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "amount": { - "type": "number", - "format": "double" - }, - "channelRefundId": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/PaymentRefundStatus" - }, - "payload": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "PaymentRefundStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "支付退款状态。", - "format": "int32" - }, - "PaymentStatus": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "description": "支付记录状态。", - "format": "int32" - }, - "PermissionDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "parentId": { - "type": "integer", - "format": "int64" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "PermissionDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/PermissionDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PermissionDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PermissionDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "PermissionDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/PermissionDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PermissionTemplateDto": { - "type": "object", - "properties": { - "code": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "PermissionTemplateDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionTemplateDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PermissionTreeDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "parentId": { - "type": "integer", - "format": "int64" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "children": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionTreeDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "PermissionTreeDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionTreeDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "PricingRuleType": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "description": "价格策略类型。", - "format": "int32" - }, - "ProductAddonGroupDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "productId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "minSelect": { - "type": "integer", - "format": "int32" - }, - "maxSelect": { - "type": "integer", - "format": "int32" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAddonOptionDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ProductAddonGroupDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAddonGroupDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductAddonOptionDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "addonGroupId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "extraPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "ProductAttributeGroupDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "productId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "selectionType": { - "type": "integer", - "format": "int32" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAttributeOptionDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ProductAttributeGroupDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAttributeGroupDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductAttributeOptionDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "attributeGroupId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "ProductDetailDto": { - "type": "object", - "properties": { - "product": { - "$ref": "#/components/schemas/ProductDto" - }, - "skus": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductSkuDto" - }, - "nullable": true - }, - "attributeGroups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAttributeGroupDto" - }, - "nullable": true - }, - "addonGroups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAddonGroupDto" - }, - "nullable": true - }, - "pricingRules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductPricingRuleDto" - }, - "nullable": true - }, - "mediaAssets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductMediaAssetDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ProductDetailDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/ProductDetailDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "categoryId": { - "type": "integer", - "format": "int64" - }, - "spuCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "subtitle": { - "type": "string", - "nullable": true - }, - "unit": { - "type": "string", - "nullable": true - }, - "price": { - "type": "number", - "format": "double" - }, - "originalPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "stockQuantity": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxQuantityPerOrder": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ProductStatus" - }, - "coverImage": { - "type": "string", - "nullable": true - }, - "galleryImages": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "enableDineIn": { - "type": "boolean" - }, - "enablePickup": { - "type": "boolean" - }, - "enableDelivery": { - "type": "boolean" - }, - "isFeatured": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "ProductDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/ProductDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "ProductDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/ProductDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductMediaAssetDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "productId": { - "type": "integer", - "format": "int64" - }, - "mediaType": { - "$ref": "#/components/schemas/MediaAssetType" - }, - "url": { - "type": "string", - "nullable": true - }, - "caption": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "ProductMediaAssetDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductMediaAssetDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductPricingRuleDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "productId": { - "type": "integer", - "format": "int64" - }, - "ruleType": { - "$ref": "#/components/schemas/PricingRuleType" - }, - "price": { - "type": "number", - "format": "double" - }, - "conditionsJson": { - "type": "string", - "nullable": true - }, - "weekdaysJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "ProductPricingRuleDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductPricingRuleDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductSkuDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "productId": { - "type": "integer", - "format": "int64" - }, - "skuCode": { - "type": "string", - "nullable": true - }, - "barcode": { - "type": "string", - "nullable": true - }, - "price": { - "type": "number", - "format": "double" - }, - "originalPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "stockQuantity": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "weight": { - "type": "number", - "format": "double", - "nullable": true - }, - "attributesJson": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "ProductSkuDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductSkuDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "ProductStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "商品状态。", - "format": "int32" - }, - "PublishProductCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "reason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "QuotaCheckResultDto": { - "type": "object", - "properties": { - "quotaType": { - "$ref": "#/components/schemas/TenantQuotaType" - }, - "limit": { - "type": "number", - "format": "double", - "nullable": true - }, - "used": { - "type": "number", - "format": "double" - }, - "remaining": { - "type": "number", - "format": "double", - "nullable": true - } - }, - "additionalProperties": false - }, - "QuotaCheckResultDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/QuotaCheckResultDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "RefreshTokenRequest": { - "required": [ - "refreshToken" - ], - "type": "object", - "properties": { - "refreshToken": { - "maxLength": 256, - "minLength": 1, - "type": "string" - } - }, - "additionalProperties": false - }, - "RefundRequestDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "refundNo": { - "type": "string", - "nullable": true - }, - "amount": { - "type": "number", - "format": "double" - }, - "reason": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/RefundStatus" - }, - "requestedAt": { - "type": "string", - "format": "date-time" - }, - "processedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reviewNotes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "RefundStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "退款申请状态。", - "format": "int32" - }, - "RegisterTenantCommand": { - "required": [ - "code", - "name", - "tenantPackageId" - ], - "type": "object", - "properties": { - "code": { - "maxLength": 64, - "minLength": 0, - "type": "string" - }, - "name": { - "maxLength": 128, - "minLength": 0, - "type": "string" - }, - "shortName": { - "type": "string", - "nullable": true - }, - "industry": { - "type": "string", - "nullable": true - }, - "contactName": { - "type": "string", - "nullable": true - }, - "contactPhone": { - "type": "string", - "nullable": true - }, - "contactEmail": { - "type": "string", - "nullable": true - }, - "tenantPackageId": { - "type": "integer", - "format": "int64" - }, - "durationMonths": { - "type": "integer", - "format": "int32" - }, - "autoRenew": { - "type": "boolean" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, - "ReleaseInventoryCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "isPresaleOrder": { - "type": "boolean" - }, - "idempotencyKey": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "ReorderMerchantCategoriesCommand": { - "required": [ - "items" - ], - "type": "object", - "properties": { - "items": { - "minItems": 1, - "type": "array", - "items": { - "$ref": "#/components/schemas/MerchantCategoryOrderItem" - } - } - }, - "additionalProperties": false - }, - "ReplaceProductAddonsCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "addonGroups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAddonGroupDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ReplaceProductAttributesCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "attributeGroups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductAttributeGroupDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ReplaceProductMediaCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "mediaAssets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductMediaAssetDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ReplaceProductPricingRulesCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "pricingRules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductPricingRuleDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ReplaceProductSkusCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "skus": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductSkuDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "ReviewMerchantCommand": { - "required": [ - "merchantId" - ], - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "approve": { - "type": "boolean" - }, - "remarks": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "ReviewMerchantDocumentCommand": { - "required": [ - "documentId", - "merchantId" - ], - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "documentId": { - "type": "integer", - "format": "int64" - }, - "approve": { - "type": "boolean" - }, - "remarks": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "ReviewTenantCommand": { - "required": [ - "tenantId" - ], - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "approve": { - "type": "boolean" - }, - "reason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "RoleDetailDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "permissions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "RoleDetailDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/RoleDetailDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "RoleDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "code": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "RoleDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/RoleDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "RoleDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RoleDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "RoleDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RoleDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "RoleDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/RoleDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "RoleTemplateDto": { - "type": "object", - "properties": { - "templateCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "permissions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionTemplateDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "RoleTemplateDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/RoleTemplateDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "RoleTemplateDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RoleTemplateDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "SelfRegisterResultDto": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/TenantStatus" - }, - "verificationStatus": { - "$ref": "#/components/schemas/TenantVerificationStatus" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "effectiveTo": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "adminAccount": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "SelfRegisterResultDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/SelfRegisterResultDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "SelfRegisterTenantCommand": { - "required": [ - "adminAccount", - "adminPassword", - "adminPhone" - ], - "type": "object", - "properties": { - "adminAccount": { - "maxLength": 64, - "minLength": 0, - "pattern": "^[A-Za-z0-9]+$", - "type": "string" - }, - "adminDisplayName": { - "maxLength": 64, - "minLength": 0, - "type": "string", - "nullable": true - }, - "adminEmail": { - "maxLength": 128, - "minLength": 0, - "type": "string", - "format": "email", - "nullable": true - }, - "adminPhone": { - "maxLength": 32, - "minLength": 0, - "type": "string" - }, - "adminPassword": { - "maxLength": 128, - "minLength": 8, - "type": "string" - } - }, - "additionalProperties": false - }, - "StaffRoleType": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "description": "商户员工角色。", - "format": "int32" - }, - "StaffStatus": { - "enum": [ - 0, - 1, - 2 - ], - "type": "integer", - "description": "员工账号状态。", - "format": "int32" - }, - "StoreBusinessHourDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "dayOfWeek": { - "$ref": "#/components/schemas/DayOfWeek" - }, - "hourType": { - "$ref": "#/components/schemas/BusinessHourType" - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "capacityLimit": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "notes": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreBusinessHourDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreBusinessHourDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreBusinessHourDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreBusinessHourDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreDeliveryZoneDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "zoneName": { - "type": "string", - "nullable": true - }, - "polygonGeoJson": { - "type": "string", - "nullable": true - }, - "minimumOrderAmount": { - "type": "number", - "format": "double", - "nullable": true - }, - "deliveryFee": { - "type": "number", - "format": "double", - "nullable": true - }, - "estimatedMinutes": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreDeliveryZoneDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreDeliveryZoneDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreDeliveryZoneDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreDeliveryZoneDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "managerName": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/StoreStatus" - }, - "province": { - "type": "string", - "nullable": true - }, - "city": { - "type": "string", - "nullable": true - }, - "district": { - "type": "string", - "nullable": true - }, - "address": { - "type": "string", - "nullable": true - }, - "longitude": { - "type": "number", - "format": "double", - "nullable": true - }, - "latitude": { - "type": "number", - "format": "double", - "nullable": true - }, - "announcement": { - "type": "string", - "nullable": true - }, - "tags": { - "type": "string", - "nullable": true - }, - "deliveryRadiusKm": { - "type": "number", - "format": "double" - }, - "supportsDineIn": { - "type": "boolean" - }, - "supportsPickup": { - "type": "boolean" - }, - "supportsDelivery": { - "type": "boolean" - }, - "supportsReservation": { - "type": "boolean" - }, - "supportsQueueing": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "StoreDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreEmployeeShiftDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "staffId": { - "type": "integer", - "format": "int64" - }, - "shiftDate": { - "type": "string", - "format": "date-time" - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "roleType": { - "$ref": "#/components/schemas/StaffRoleType" - }, - "notes": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreEmployeeShiftDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreEmployeeShiftDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreEmployeeShiftDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreEmployeeShiftDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreHolidayDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "date": { - "type": "string", - "format": "date-time" - }, - "isClosed": { - "type": "boolean" - }, - "reason": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreHolidayDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreHolidayDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreHolidayDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreHolidayDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StorePickupSettingDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "allowToday": { - "type": "boolean" - }, - "allowDaysAhead": { - "type": "integer", - "format": "int32" - }, - "defaultCutoffMinutes": { - "type": "integer", - "format": "int32" - }, - "maxQuantityPerOrder": { - "type": "integer", - "format": "int32", - "nullable": true - } - }, - "additionalProperties": false - }, - "StorePickupSettingDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StorePickupSettingDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StorePickupSlotDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "cutoffMinutes": { - "type": "integer", - "format": "int32" - }, - "capacity": { - "type": "integer", - "format": "int32" - }, - "reservedCount": { - "type": "integer", - "format": "int32" - }, - "weekdays": { - "type": "string", - "nullable": true - }, - "isEnabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "StorePickupSlotDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StorePickupSlotDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StorePickupSlotDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StorePickupSlotDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreStaffDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "roleType": { - "$ref": "#/components/schemas/StaffRoleType" - }, - "status": { - "$ref": "#/components/schemas/StaffStatus" - } - }, - "additionalProperties": false - }, - "StoreStaffDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreStaffDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreStaffDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreStaffDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "门店运营状态。", - "format": "int32" - }, - "StoreTableAreaDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreTableAreaDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreTableAreaDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreTableAreaDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreTableAreaDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreTableDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "areaId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "tableCode": { - "type": "string", - "nullable": true - }, - "capacity": { - "type": "integer", - "format": "int32" - }, - "tags": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/StoreTableStatus" - }, - "qrCodeUrl": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "StoreTableDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/StoreTableDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreTableDtoIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StoreTableDto" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StoreTableStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "桌台占用状态。", - "format": "int32" - }, - "StringDictionaryItemDtoIReadOnlyListIReadOnlyDictionaryApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DictionaryItemDto" - }, - "nullable": true - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "StringIReadOnlyListApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "type": "array", - "items": { - "type": "string" - }, - "description": "业务数据。", - "nullable": true - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "SubmitTenantVerificationCommand": { - "required": [ - "tenantId" - ], - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "businessLicenseNumber": { - "type": "string", - "nullable": true - }, - "businessLicenseUrl": { - "type": "string", - "nullable": true - }, - "legalPersonName": { - "type": "string", - "nullable": true - }, - "legalPersonIdNumber": { - "type": "string", - "nullable": true - }, - "legalPersonIdFrontUrl": { - "type": "string", - "nullable": true - }, - "legalPersonIdBackUrl": { - "type": "string", - "nullable": true - }, - "bankAccountName": { - "type": "string", - "nullable": true - }, - "bankAccountNumber": { - "type": "string", - "nullable": true - }, - "bankName": { - "type": "string", - "nullable": true - }, - "additionalDataJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "SubscriptionStatus": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "description": "订阅状态。", - "format": "int32" - }, - "SystemParameterDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "key": { - "type": "string", - "nullable": true - }, - "value": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "isEnabled": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, - "SystemParameterDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/SystemParameterDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "SystemParameterDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SystemParameterDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "SystemParameterDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/SystemParameterDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantAnnouncementDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "title": { - "type": "string", - "nullable": true - }, - "content": { - "type": "string", - "nullable": true - }, - "announcementType": { - "$ref": "#/components/schemas/TenantAnnouncementType" - }, - "priority": { - "type": "integer", - "format": "int32" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time" - }, - "effectiveTo": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "isRead": { - "type": "boolean" - }, - "readAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, - "TenantAnnouncementDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantAnnouncementDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantAnnouncementDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TenantAnnouncementDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "TenantAnnouncementDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantAnnouncementDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantAnnouncementType": { - "enum": [ - 0, - 1, - 2 - ], - "type": "integer", - "description": "租户公告类型。", - "format": "int32" - }, - "TenantAuditAction": { - "enum": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7 - ], - "type": "integer", - "description": "租户运营审核动作。", - "format": "int32" - }, - "TenantAuditLogDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "action": { - "$ref": "#/components/schemas/TenantAuditAction" - }, - "title": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "operatorName": { - "type": "string", - "nullable": true - }, - "previousStatus": { - "$ref": "#/components/schemas/TenantStatus" - }, - "currentStatus": { - "$ref": "#/components/schemas/TenantStatus" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "TenantAuditLogDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TenantAuditLogDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "TenantAuditLogDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantAuditLogDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantBillingDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "statementNo": { - "type": "string", - "nullable": true - }, - "periodStart": { - "type": "string", - "format": "date-time" - }, - "periodEnd": { - "type": "string", - "format": "date-time" - }, - "amountDue": { - "type": "number", - "format": "double" - }, - "amountPaid": { - "type": "number", - "format": "double" - }, - "status": { - "$ref": "#/components/schemas/TenantBillingStatus" - }, - "dueDate": { - "type": "string", - "format": "date-time" - }, - "lineItemsJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "TenantBillingDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantBillingDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantBillingDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TenantBillingDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "TenantBillingDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantBillingDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantBillingStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "账单状态。", - "format": "int32" - }, - "TenantDetailDto": { - "type": "object", - "properties": { - "tenant": { - "$ref": "#/components/schemas/TenantDto" - }, - "verification": { - "$ref": "#/components/schemas/TenantVerificationDto" - }, - "subscription": { - "$ref": "#/components/schemas/TenantSubscriptionDto" - }, - "package": { - "$ref": "#/components/schemas/TenantPackageDto" - } - }, - "additionalProperties": false - }, - "TenantDetailDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantDetailDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "shortName": { - "type": "string", - "nullable": true - }, - "contactName": { - "type": "string", - "nullable": true - }, - "contactPhone": { - "type": "string", - "nullable": true - }, - "contactEmail": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/TenantStatus" - }, - "verificationStatus": { - "$ref": "#/components/schemas/TenantVerificationStatus" - }, - "currentPackageId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "effectiveFrom": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "effectiveTo": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "autoRenew": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "TenantDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TenantDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "TenantDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantNotificationChannel": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "通知推送渠道。", - "format": "int32" - }, - "TenantNotificationDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "title": { - "type": "string", - "nullable": true - }, - "message": { - "type": "string", - "nullable": true - }, - "channel": { - "$ref": "#/components/schemas/TenantNotificationChannel" - }, - "severity": { - "$ref": "#/components/schemas/TenantNotificationSeverity" - }, - "sentAt": { - "type": "string", - "format": "date-time" - }, - "readAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "metadataJson": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "TenantNotificationDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantNotificationDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantNotificationDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TenantNotificationDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "TenantNotificationDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantNotificationDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantNotificationSeverity": { - "enum": [ - 0, - 1, - 2 - ], - "type": "integer", - "description": "租户通知的重要程度。", - "format": "int32" - }, - "TenantPackageDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "packageType": { - "$ref": "#/components/schemas/TenantPackageType" - }, - "monthlyPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "yearlyPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "maxStoreCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxAccountCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxStorageGb": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxSmsCredits": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxDeliveryOrders": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "featurePoliciesJson": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "TenantPackageDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantPackageDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantPackageDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TenantPackageDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "TenantPackageDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantPackageDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantPackageType": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "套餐类型枚举。", - "format": "int32" - }, - "TenantProgressDto": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/TenantStatus" - }, - "verificationStatus": { - "$ref": "#/components/schemas/TenantVerificationStatus" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "effectiveTo": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, - "TenantProgressDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantProgressDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantQuotaType": { - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "type": "integer", - "description": "配额类型,覆盖容量及调用次数。", - "format": "int32" - }, - "TenantStatus": { - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "type": "integer", - "description": "租户服务状态。", - "format": "int32" - }, - "TenantSubscriptionDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "tenantPackageId": { - "type": "integer", - "format": "int64" - }, - "status": { - "$ref": "#/components/schemas/SubscriptionStatus" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time" - }, - "effectiveTo": { - "type": "string", - "format": "date-time" - }, - "nextBillingDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "autoRenew": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "TenantSubscriptionDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantSubscriptionDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantVerificationDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "status": { - "$ref": "#/components/schemas/TenantVerificationStatus" - }, - "businessLicenseNumber": { - "type": "string", - "nullable": true - }, - "businessLicenseUrl": { - "type": "string", - "nullable": true - }, - "legalPersonName": { - "type": "string", - "nullable": true - }, - "legalPersonIdNumber": { - "type": "string", - "nullable": true - }, - "legalPersonIdFrontUrl": { - "type": "string", - "nullable": true - }, - "legalPersonIdBackUrl": { - "type": "string", - "nullable": true - }, - "bankAccountName": { - "type": "string", - "nullable": true - }, - "bankAccountNumber": { - "type": "string", - "nullable": true - }, - "bankName": { - "type": "string", - "nullable": true - }, - "additionalDataJson": { - "type": "string", - "nullable": true - }, - "submittedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reviewedBy": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "reviewRemarks": { - "type": "string", - "nullable": true - }, - "reviewedByName": { - "type": "string", - "nullable": true - }, - "reviewedAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, - "TenantVerificationDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TenantVerificationDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "TenantVerificationStatus": { - "enum": [ - 0, - 1, - 2, - 3 - ], - "type": "integer", - "description": "租户实名认证状态。", - "format": "int32" - }, - "TokenResponse": { - "type": "object", - "properties": { - "accessToken": { - "type": "string", - "nullable": true - }, - "accessTokenExpiresAt": { - "type": "string", - "format": "date-time" - }, - "refreshToken": { - "type": "string", - "nullable": true - }, - "refreshTokenExpiresAt": { - "type": "string", - "format": "date-time" - }, - "user": { - "$ref": "#/components/schemas/CurrentUserProfile" - }, - "isNewUser": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "TokenResponseApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/TokenResponse" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "UnpublishProductCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "reason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateDeliveryOrderCommand": { - "type": "object", - "properties": { - "deliveryOrderId": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "provider": { - "$ref": "#/components/schemas/DeliveryProvider" - }, - "providerOrderId": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/DeliveryStatus" - }, - "deliveryFee": { - "type": "number", - "format": "double", - "nullable": true - }, - "courierName": { - "type": "string", - "nullable": true - }, - "courierPhone": { - "type": "string", - "nullable": true - }, - "dispatchedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "pickedUpAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "deliveredAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "failureReason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateDictionaryGroupRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" - }, - "description": { - "maxLength": 512, - "type": "string", - "nullable": true - }, - "isEnabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "UpdateDictionaryItemRequest": { - "required": [ - "value" - ], - "type": "object", - "properties": { - "value": { - "maxLength": 256, - "minLength": 1, - "type": "string" - }, - "isDefault": { - "type": "boolean" - }, - "isEnabled": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "description": { - "maxLength": 512, - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateMenuCommand": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "parentId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "path": { - "type": "string", - "nullable": true - }, - "component": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "icon": { - "type": "string", - "nullable": true - }, - "isIframe": { - "type": "boolean" - }, - "link": { - "type": "string", - "nullable": true - }, - "keepAlive": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "requiredPermissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "metaPermissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "metaRoles": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "authList": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuAuthItemDto" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateMerchantCommand": { - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "brandName": { - "type": "string", - "nullable": true - }, - "brandAlias": { - "type": "string", - "nullable": true - }, - "logoUrl": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string", - "nullable": true - }, - "contactPhone": { - "type": "string", - "nullable": true - }, - "contactEmail": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/MerchantStatus" - } - }, - "additionalProperties": false - }, - "UpdateMerchantContractStatusCommand": { - "required": [ - "contractId", - "merchantId", - "status" - ], - "type": "object", - "properties": { - "merchantId": { - "type": "integer", - "format": "int64" - }, - "contractId": { - "type": "integer", - "format": "int64" - }, - "status": { - "$ref": "#/components/schemas/ContractStatus" - }, - "signedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateOrderCommand": { - "type": "object", - "properties": { - "orderId": { - "type": "integer", - "format": "int64" - }, - "orderNo": { - "type": "string", - "nullable": true - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "channel": { - "$ref": "#/components/schemas/OrderChannel" - }, - "deliveryType": { - "$ref": "#/components/schemas/DeliveryType" - }, - "status": { - "$ref": "#/components/schemas/OrderStatus" - }, - "paymentStatus": { - "$ref": "#/components/schemas/PaymentStatus" - }, - "customerName": { - "type": "string", - "nullable": true - }, - "customerPhone": { - "type": "string", - "nullable": true - }, - "tableNo": { - "type": "string", - "nullable": true - }, - "queueNumber": { - "type": "string", - "nullable": true - }, - "reservationId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "itemsAmount": { - "type": "number", - "format": "double" - }, - "discountAmount": { - "type": "number", - "format": "double" - }, - "payableAmount": { - "type": "number", - "format": "double" - }, - "paidAmount": { - "type": "number", - "format": "double" - }, - "paidAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "finishedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelledAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "cancelReason": { - "type": "string", - "nullable": true - }, - "remark": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdatePaymentCommand": { - "type": "object", - "properties": { - "paymentId": { - "type": "integer", - "format": "int64" - }, - "orderId": { - "type": "integer", - "format": "int64" - }, - "method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "status": { - "$ref": "#/components/schemas/PaymentStatus" - }, - "amount": { - "type": "number", - "format": "double" - }, - "tradeNo": { - "type": "string", - "nullable": true - }, - "channelTransactionId": { - "type": "string", - "nullable": true - }, - "paidAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "remark": { - "type": "string", - "nullable": true - }, - "payload": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdatePermissionCommand": { - "type": "object", - "properties": { - "permissionId": { - "type": "integer", - "format": "int64" - }, - "parentId": { - "type": "integer", - "format": "int64" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "type": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateProductCommand": { - "type": "object", - "properties": { - "productId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "categoryId": { - "type": "integer", - "format": "int64" - }, - "spuCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "subtitle": { - "type": "string", - "nullable": true - }, - "unit": { - "type": "string", - "nullable": true - }, - "price": { - "type": "number", - "format": "double" - }, - "originalPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "stockQuantity": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxQuantityPerOrder": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ProductStatus" - }, - "coverImage": { - "type": "string", - "nullable": true - }, - "galleryImages": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "enableDineIn": { - "type": "boolean" - }, - "enablePickup": { - "type": "boolean" - }, - "enableDelivery": { - "type": "boolean" - }, - "isFeatured": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "UpdateRoleCommand": { - "type": "object", - "properties": { - "roleId": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateRoleTemplateCommand": { - "type": "object", - "properties": { - "templateCode": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "permissionCodes": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateStoreBusinessHourCommand": { - "type": "object", - "properties": { - "businessHourId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "dayOfWeek": { - "$ref": "#/components/schemas/DayOfWeek" - }, - "hourType": { - "$ref": "#/components/schemas/BusinessHourType" - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "capacityLimit": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateStoreCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64" - }, - "code": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "managerName": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/StoreStatus" - }, - "province": { - "type": "string", - "nullable": true - }, - "city": { - "type": "string", - "nullable": true - }, - "district": { - "type": "string", - "nullable": true - }, - "address": { - "type": "string", - "nullable": true - }, - "longitude": { - "type": "number", - "format": "double", - "nullable": true - }, - "latitude": { - "type": "number", - "format": "double", - "nullable": true - }, - "announcement": { - "type": "string", - "nullable": true - }, - "tags": { - "type": "string", - "nullable": true - }, - "deliveryRadiusKm": { - "type": "number", - "format": "double" - }, - "supportsDineIn": { - "type": "boolean" - }, - "supportsPickup": { - "type": "boolean" - }, - "supportsDelivery": { - "type": "boolean" - }, - "supportsReservation": { - "type": "boolean" - }, - "supportsQueueing": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "UpdateStoreDeliveryZoneCommand": { - "type": "object", - "properties": { - "deliveryZoneId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "zoneName": { - "type": "string", - "nullable": true - }, - "polygonGeoJson": { - "type": "string", - "nullable": true - }, - "minimumOrderAmount": { - "type": "number", - "format": "double", - "nullable": true - }, - "deliveryFee": { - "type": "number", - "format": "double", - "nullable": true - }, - "estimatedMinutes": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "UpdateStoreEmployeeShiftCommand": { - "type": "object", - "properties": { - "shiftId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "staffId": { - "type": "integer", - "format": "int64" - }, - "shiftDate": { - "type": "string", - "format": "date-time" - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "roleType": { - "$ref": "#/components/schemas/StaffRoleType" - }, - "notes": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateStoreHolidayCommand": { - "type": "object", - "properties": { - "holidayId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "date": { - "type": "string", - "format": "date-time" - }, - "isClosed": { - "type": "boolean" - }, - "reason": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, - "UpdateStorePickupSlotCommand": { - "type": "object", - "properties": { - "slotId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "startTime": { - "type": "string", - "format": "date-span" - }, - "endTime": { - "type": "string", - "format": "date-span" - }, - "cutoffMinutes": { - "type": "integer", - "format": "int32" - }, - "capacity": { - "type": "integer", - "format": "int32" - }, - "weekdays": { - "type": "string", - "nullable": true - }, - "isEnabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "UpdateStoreStaffCommand": { - "type": "object", - "properties": { - "staffId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "roleType": { - "$ref": "#/components/schemas/StaffRoleType" - }, - "status": { - "$ref": "#/components/schemas/StaffStatus" - } - }, - "additionalProperties": false - }, - "UpdateStoreTableAreaCommand": { - "type": "object", - "properties": { - "areaId": { - "type": "integer", - "format": "int64" - }, - "storeId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "UpdateStoreTableCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "tableId": { - "type": "integer", - "format": "int64" - }, - "areaId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "tableCode": { - "type": "string", - "nullable": true - }, - "capacity": { - "type": "integer", - "format": "int32" - }, - "tags": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/StoreTableStatus" - } - }, - "additionalProperties": false - }, - "UpdateSystemParameterCommand": { - "type": "object", - "properties": { - "parameterId": { - "type": "integer", - "format": "int64" - }, - "key": { - "type": "string", - "nullable": true - }, - "value": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "sortOrder": { - "type": "integer", - "format": "int32" - }, - "isEnabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "UpdateTenantAnnouncementCommand": { - "type": "object", - "properties": { - "tenantId": { - "type": "integer", - "format": "int64" - }, - "announcementId": { - "type": "integer", - "format": "int64" - }, - "title": { - "type": "string", - "nullable": true - }, - "content": { - "type": "string", - "nullable": true - }, - "announcementType": { - "$ref": "#/components/schemas/TenantAnnouncementType" - }, - "priority": { - "type": "integer", - "format": "int32" - }, - "effectiveFrom": { - "type": "string", - "format": "date-time" - }, - "effectiveTo": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "isActive": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "UpdateTenantPackageCommand": { - "type": "object", - "properties": { - "tenantPackageId": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "packageType": { - "$ref": "#/components/schemas/TenantPackageType" - }, - "monthlyPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "yearlyPrice": { - "type": "number", - "format": "double", - "nullable": true - }, - "maxStoreCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxAccountCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxStorageGb": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxSmsCredits": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "maxDeliveryOrders": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "featurePoliciesJson": { - "type": "string", - "nullable": true - }, - "isActive": { - "type": "boolean" - }, - "sortOrder": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "UpsertInventoryBatchCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "productSkuId": { - "type": "integer", - "format": "int64" - }, - "batchNumber": { - "type": "string", - "nullable": true - }, - "productionDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "expireDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "remainingQuantity": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - "UpsertStorePickupSettingCommand": { - "type": "object", - "properties": { - "storeId": { - "type": "integer", - "format": "int64" - }, - "allowToday": { - "type": "boolean" - }, - "allowDaysAhead": { - "type": "integer", - "format": "int32" - }, - "defaultCutoffMinutes": { - "type": "integer", - "format": "int32" - }, - "maxQuantityPerOrder": { - "type": "integer", - "format": "int32", - "nullable": true - } - }, - "additionalProperties": false - }, - "UserPermissionDto": { - "type": "object", - "properties": { - "userId": { - "type": "integer", - "format": "int64" - }, - "tenantId": { - "type": "integer", - "format": "int64" - }, - "merchantId": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "account": { - "type": "string", - "nullable": true - }, - "displayName": { - "type": "string", - "nullable": true - }, - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "permissions": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": false - }, - "UserPermissionDtoApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/UserPermissionDto" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - }, - "UserPermissionDtoPagedResult": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserPermissionDto" - }, - "description": "数据列表。", - "nullable": true - }, - "page": { - "type": "integer", - "description": "当前页码,从 1 开始。", - "format": "int32" - }, - "pageSize": { - "type": "integer", - "description": "每页条数。", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "description": "总条数。", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "description": "总页数。", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false, - "description": "分页结果包装,携带列表与总条数等元数据。" - }, - "UserPermissionDtoPagedResultApiResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "是否成功。" - }, - "code": { - "type": "integer", - "description": "状态/错误码(默认 200)。", - "format": "int32" - }, - "message": { - "type": "string", - "description": "提示信息。", - "nullable": true - }, - "data": { - "$ref": "#/components/schemas/UserPermissionDtoPagedResult" - }, - "errors": { - "description": "错误详情(如字段验证错误)。", - "nullable": true - }, - "traceId": { - "type": "string", - "description": "TraceId,便于链路追踪。", - "nullable": true - }, - "timestamp": { - "type": "string", - "description": "时间戳(UTC)。", - "format": "date-time" - } - }, - "additionalProperties": false, - "description": "统一的 API 返回结果包装。" - } - }, - "securitySchemes": { - "Bearer": { - "type": "http", - "description": "在下方输入Bearer Token,格式:Bearer {token}", - "scheme": "bearer", - "bearerFormat": "JWT" - } - } - }, - "security": [ - { - "Bearer": [ ] - } - ], - "tags": [ - { - "name": "Auth", - "description": "管理后台认证接口" - }, - { - "name": "Deliveries", - "description": "配送单管理。" - }, - { - "name": "Dictionary", - "description": "参数字典管理。" - }, - { - "name": "Files", - "description": "管理后台文件上传。" - }, - { - "name": "Health", - "description": "管理后台 - 健康检查。" - }, - { - "name": "Inventory", - "description": "库存管理。" - }, - { - "name": "Menus", - "description": "菜单管理(可增删改查)。" - }, - { - "name": "MerchantCategories", - "description": "商户类目管理。" - }, - { - "name": "Merchants", - "description": "商户管理。" - }, - { - "name": "Orders", - "description": "订单管理。" - }, - { - "name": "Payments", - "description": "支付记录管理。" - }, - { - "name": "Permissions", - "description": "权限管理。" - }, - { - "name": "Products", - "description": "商品管理。" - }, - { - "name": "PublicTenantPackages", - "description": "公共租户套餐查询接口。" - }, - { - "name": "PublicTenants", - "description": "公域租户自助入住接口。" - }, - { - "name": "PublicTenantSubscriptions", - "description": "公域租户订阅自助接口(需登录,无权限校验)。" - }, - { - "name": "RoleTemplates", - "description": "角色模板管理(平台蓝本)。" - }, - { - "name": "StorePickup", - "description": "门店自提管理。" - }, - { - "name": "Stores", - "description": "门店管理。" - }, - { - "name": "StoreShifts", - "description": "门店排班管理。" - }, - { - "name": "StoreStaffs", - "description": "门店员工管理。" - }, - { - "name": "StoreTableAreas", - "description": "门店桌台区域管理。" - }, - { - "name": "StoreTables", - "description": "门店桌码管理。" - }, - { - "name": "SystemParameters", - "description": "系统参数管理。" - }, - { - "name": "TenantAnnouncements", - "description": "租户公告管理。" - }, - { - "name": "TenantBillings", - "description": "租户账单管理。" - }, - { - "name": "TenantNotifications", - "description": "租户通知接口。" - }, - { - "name": "TenantPackages", - "description": "租户套餐管理。" - }, - { - "name": "TenantRoles", - "description": "租户角色管理(实例层)。" - }, - { - "name": "Tenants", - "description": "租户管理。" - }, - { - "name": "UserPermissions", - "description": "用户权限洞察接口。" - } - ] -} \ No newline at end of file diff --git a/Document/xmind_小程序版模块规划.md b/Document/xmind_小程序版模块规划.md deleted file mode 100644 index 3af2007..0000000 --- a/Document/xmind_小程序版模块规划.md +++ /dev/null @@ -1,89 +0,0 @@ -# 外卖SaaS(小程序导向)模块脑图 - -## 1. 目标 -- 聚焦小程序用户体验:高效点单、扫码堂食、同城履约、实时状态 -- 平台/租户多租户隔离,支持商家快速入驻与运营 -- 门店同城即时配送为主,第三方配送对接兜底,统一回调验签 - -## 2. 端侧 -- 小程序用户端:扫码进店/堂食点餐、店铺浏览、菜品/套餐、购物车、下单支付、拼单、预购自提、同城配送、订单跟踪、优惠券/积分/会员、营销活动、评价、地址管理、搜索、签到、预约、社区、客服聊天、地图导航 -- 管理后台(商家/店员):门店配置、桌码管理、菜品与库存、订单/拼单/自提处理、配送调度与回调、营销配置、分销配置、客服工单/会话、数据看板 -- 平台运营后台:租户/商家审核、套餐与配额、全局配置、日志审计、监控看板、分销/返利策略、活动审核 - -## 3. 核心业务模块 -- 租户与平台 - - 租户生命周期:注册、实名认证/资质审核、套餐订阅、续费/升降配、停用/注销 - - 配额与权限:门店数/账号数/存储/短信/配送单量,RBAC 角色模板,租户级配置继承与覆盖 - - 平台运营:租户监控、账单/计费/催缴、公告/通知、合规与风控(黑名单/频率限制) -- 商家与门店 - - 入驻与资质审核:证照上传、合同、店铺类目、品牌与连锁 - - 门店配置:多店管理、营业时间、休息/打烊、门店状态、区域/配送范围、到店导航信息 - - 桌码/场景:桌码生成与绑定、桌台容量/区域、桌码标签(堂食/快餐/档口) - - 门店运营:员工账号与分工(前台/后厨/配送)、交班与收银、公告/售罄提示 -- 菜品与库存 - - 菜品建模:分类/标签/排序、规格与属性(辣度/份量)、套餐与组合、加料/做法/备注 - - 价格策略:会员价、门店价、时间段价、限时折扣、区域价 - - 库存与可售:总库存/门店库存/档期库存、售罄/预售、批量导入与同步、条码/编码 - - 媒资:图片/视频、SPU/SKU 编码、营养/过敏源/溯源信息 -- 扫码点餐/堂食 - - 桌码入口:扫码识别门店/桌台、桌台上下文、预加载菜单 - - 点餐流程:购物车并发锁、加菜/催单、口味备注、拆单/并单、多人同桌分付/代付 - - 账单与核销:桌台账单合并、结账/买单、电子小票、发票抬头、桌台释放 -- 预购自提 - - 档期配置:自提时间窗/容量、预售库存、截单时间 - - 下单与支付:自提地址确认、档期校验、预售与现制、库存锁定 - - 提货核销:提货码/手机号核销、自提柜/前台、超时/取消/退款、代取人 -- 下单与支付 - - 购物车与校验:库存/限购/门店状态/配送范围/桌台状态、券和积分叠加规则 - - 支付与结算:微信/支付宝/余额/优惠券/积分抵扣、回调幂等、预授权、分账/对账 - - 售后与状态机:退单/部分退款、异常单处理、状态机(待付→待接→制作→配送/自提→完成/取消) -- 同城即时配送(门店履约) - - 自配送:骑手管理、取/送件信息、路线估时、导航、取餐码、费用与补贴 - - 第三方配送:统一抽象(下单/取消/加价/查询)、多渠道择优、回调验签、异常重试与补偿 - - 配送体验:预计送达/计价、配送进度推送、无接触配送、收货码、投诉与赔付 -- 拼单 - - 团单规则:发起/加入、成团条件(人数/金额/时间)、拼主与参团人、支付/锁单策略 - - 失败与退款:超时/人数不足自动解散、自动退款/原路退、通知 - - 履约:同单配送至拼主地址、收件信息可见范围、团内消息/提醒 -- 优惠券与营销工具 - - 券种与发放:满减/折扣/新人券/裂变券/桌码专属券/会员券,渠道(领券中心/活动/分享/桌码) - - 核销规则:适用范围(门店/品类/菜品)、叠加/互斥、最低消费、有效期、库存与风控 - - 活动组件:抽奖、分享裂变、秒杀/限时抢购、满减活动、爆款/推荐位、裂变海报 -- 会员与积分/会员营销 - - 会员体系:付费会员/积分会员、等级/成长值、会员权益(专享价/券/运费/客服) - - 积分运营:获取(消费/签到/任务/活动)、消耗(抵扣/兑换)、有效期、黑名单 - - 会员运营:会员日、续费提醒、沉睡唤醒、专属活动、分层运营与 A/B -- 客服工具 - - 会话:实时聊天、机器人/人工切换、快捷回复、排队/转接、消息模板/通知 - - 质量与风控:会话记录与审计、敏感词、评价与回访、工单流转 -- 分销返利 - - 规则:商品/类目返利、佣金阶梯、结算周期、税务信息、违规处理 - - 链路:分享链接/小程序码、点击与下单跟踪、确认收货后结算、售后扣回 -- 地图导航 - - 门店/自提点定位、距离/路况、路线规划、附近门店/推荐、跳转原生导航 -- 签到打卡 - - 规则:每日/任务签到、连签奖励、补签、积分/券/成长值奖励、反作弊 -- 预约预订 - - 档期/资源:时间段、人员/座位/设备占用、容量校验 - - 流程:预约下单/支付、提醒/改期/取消、到店核销与履约记录 -- 搜索查询 - - 内容:门店/菜品/活动/优惠券搜索,过滤/排序,热门/历史搜索 - - 体验:纠错/联想、推荐、结果埋点与转化分析 -- 用户社区 - - 社区运营:动态发布/评论/点赞、话题/标签、图片/视频审核、举报与风控 - - 互动:店铺口碑、菜品晒图、官方/商家号发布、置顶与精选 -- 数据与分析 - - 交易分析:销售/订单/客单/转化漏斗、支付与退款、品类/单品分析 - - 营销分析:券发放与核销、活动效果(拼单/秒杀/抽奖/分销)、会员留存与复购 - - 履约分析:配送时效、超时/异常、堂食与自提拆分、投诉与赔付 - - 运营大盘:租户/商家健康度、活跃度、留存、GMV、成本与毛利 -- 系统与运维 - - 安全与合规:RBAC、租户隔离、限流与风控(设备/IP/账户/店铺)、敏感词与内容安全 - - 配置与网关:字典/参数、灰度/开关、网关限流/鉴权/租户透传 - - 可靠性:任务调度(订单/拼单超时、券过期、回调补偿)、幂等与重试、健康检查/告警、日志/链路追踪、备份恢复 - -## 4. 里程碑(建议) -- Phase 1:租户/商家入驻、门店与菜品、桌码扫码堂食、基础下单支付、预购自提、第三方配送骨架 -- Phase 2:拼单、优惠券与基础营销组件、会员积分/会员日、客服聊天、同城自配送调度、搜索 -- Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、营销活动丰富(秒杀/抽奖等)、风控与审计、补偿与告警体系 -- Phase 4:性能优化与缓存、运营大盘与细分报表、测试与文档完善、上线与监控 diff --git a/Document/xmind_小程序版模块规划.xmind b/Document/xmind_小程序版模块规划.xmind deleted file mode 100644 index 9df8073cf64ee0cd98825de340684a92a7440f8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 341473 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%0DR&%fi~R%RmH52!r@qo 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 最后更新日期:2025-12-30 - -### 功能概述 - -- 系统/业务字典分组与字典项管理 -- 租户覆盖:隐藏系统项、自定义字典项、拖拽排序 -- CSV/JSON 批量导入导出 -- 两级缓存(Memory + Redis)与缓存监控指标 - -### 配置要点 - -- `ConnectionStrings:Redis`:Redis 连接字符串 -- `Database:DataSources:DictionaryDatabase`:字典库读写连接 -- `Dictionary:Cache:SlidingExpiration`:字典缓存滑动过期 -- `CacheWarmup:DictionaryCodes`:缓存预热字典编码列表 - -## 公告管理 - -> 最后更新日期:2025-12-20 - -### 功能概述 - -- 支持平台公告与租户公告统一管理(TenantId=0 代表平台公告) -- 状态机:草稿 → 已发布 → 已撤销(已发布不可编辑) -- 支持目标受众过滤与未读/已读能力 - -### 快速开始(示例流程) - -1. 创建公告(草稿) -2. 发布公告(进入 Published) -3. 应用端获取可见公告列表与未读公告 - -```mermaid -stateDiagram-v2 - [*] --> Draft - Draft --> Published: publish - Published --> Revoked: revoke - Revoked --> Published: republish -``` - -### 关键概念 - -- `Status`:Draft/Published/Revoked,已发布不可编辑 -- `RowVersion`:并发控制字段(Base64) -- `TargetType/TargetParameters`:目标受众过滤 - -### 相关文档 - -- `docs/api/announcements-api.md` -- `docs/permissions/announcement-permissions.md` -- `docs/adr/0001-announcement-status-state-machine.md` -- `docs/observability/announcement-events.md` -- `docs/migrations/announcement-status-migration.md` -- `docs/technical-debt.md` - -## 项目结构 - -``` -TakeoutSaaS/ -├── 0_Document/ # 项目文档 -│ ├── 01_项目概述.md -│ ├── 02_技术架构.md -│ ├── 03_数据库设计.md -│ ├── 04A_管理后台API.md -│ ├── 04B_小程序API.md -│ ├── 05_部署运维.md -│ └── 06_开发规范.md -├── src/ -│ ├── TakeoutSaaS.AdminApi/ # 管理后台 Web API -│ ├── TakeoutSaaS.MiniApi/ # 小程序/用户端 Web API -│ ├── TakeoutSaaS.Application/ # 应用层 -│ ├── TakeoutSaaS.Domain/ # 领域层 -│ ├── TakeoutSaaS.Infrastructure/ # 基础设施层 -│ └── TakeoutSaaS.Shared/ # 共享层 -├── tests/ -│ ├── TakeoutSaaS.UnitTests/ # 单元测试 -│ └── TakeoutSaaS.IntegrationTests/ # 集成测试 -├── docker-compose.yml # Docker编排文件 -└── README.md -``` - -## 测试说明 - -### 运行单元测试 -```bash -dotnet test tests/TakeoutSaaS.UnitTests -``` - -### 运行集成测试 -```bash -dotnet test tests/TakeoutSaaS.IntegrationTests -``` - -## 部署说明 - -### Docker部署 -```bash -# 构建镜像 -docker build -t takeout-saas-api:latest . - -# 运行容器 -docker run -d -p 8080:80 --name takeout-api takeout-saas-api:latest -``` - -详细部署文档请参考:[部署运维文档](0_Document/05_部署运维.md) - -## 文档 - -- [项目概述](0_Document/01_项目概述.md) - 系统介绍和业务说明 -- [技术架构](0_Document/02_技术架构.md) - 技术栈和架构设计 -- [数据库设计](0_Document/03_数据库设计.md) - 数据模型和表结构 -- [API接口设计](0_Document/04_API接口设计.md) - RESTful API规范 -- [部署运维](0_Document/05_部署运维.md) - 部署和运维指南 -- [开发规范](0_Document/06_开发规范.md) - 代码规范和最佳实践 - -## 开发规范 - -请遵循项目的[开发规范](0_Document/06_开发规范.md) - -## 贡献指南 - -1. Fork 本仓库 -2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 创建 Pull Request - -## 许可证 - -本项目采用 MIT 许可证 - -## 联系方式 - -- 项目地址:https://github.com/your-org/takeout-saas -- 问题反馈:https://github.com/your-org/takeout-saas/issues - -## 协作者 - -感谢所有为本项目做出贡献的开发者! - ---- - -⭐ 如果这个项目对你有帮助,请给我们一个星标! +- `TakeoutSaaS.Docs/README.md` +- `TakeoutSaaS.Docs/Document/README.md` diff --git a/TakeoutSaaS.BuildingBlocks b/TakeoutSaaS.BuildingBlocks new file mode 160000 index 0000000..bcf0a6b --- /dev/null +++ b/TakeoutSaaS.BuildingBlocks @@ -0,0 +1 @@ +Subproject commit bcf0a6bd7dbcbef19e630ca768ccb5d617373c7d diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs new file mode 160000 index 0000000..88ad710 --- /dev/null +++ b/TakeoutSaaS.Docs @@ -0,0 +1 @@ +Subproject commit 88ad71041bcec59715274c9ca568b0ca434408a1 diff --git a/TakeoutSaaS.sln b/TakeoutSaaS.sln index e765339..593dd42 100644 --- a/TakeoutSaaS.sln +++ b/TakeoutSaaS.sln @@ -11,9 +11,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.AdminApi", "src EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8D626EA8-CB54-BC41-363A-217881BEBA6E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Web", "src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj", "{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Web", "TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj", "{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Abstractions", "src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj", "{0DA03B31-E718-4424-A1F0-9989E79FFE81}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Abstractions", "TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj", "{0DA03B31-E718-4424-A1F0-9989E79FFE81}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{22BAF98C-8415-17C4-B26A-D537657BC863}" EndProject @@ -41,7 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gateway", "Gateway", "{6306 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.ApiGateway", "src\Gateway\TakeoutSaaS.ApiGateway\TakeoutSaaS.ApiGateway.csproj", "{A2620200-D487-49A7-ABAF-9B84951F81DD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Kernel", "src\Core\TakeoutSaaS.Shared.Kernel\TakeoutSaaS.Shared.Kernel.csproj", "{BBC99B58-ECA8-42C3-9070-9AA058D778D3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Kernel", "TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Kernel\TakeoutSaaS.Shared.Kernel.csproj", "{BBC99B58-ECA8-42C3-9070-9AA058D778D3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Storage", "src\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj", "{05058F44-6FB7-43AF-8648-8BF538E283EF}" EndProject diff --git a/deploy/dbhub/dbhub.toml b/deploy/dbhub/dbhub.toml deleted file mode 100644 index 814bcb7..0000000 --- a/deploy/dbhub/dbhub.toml +++ /dev/null @@ -1,15 +0,0 @@ -[[sources]] -id = "takeout_app" -dsn = "postgres://app_user:AppUser112233@120.53.222.17:5432/takeout_app_db" - -[[sources]] -id = "takeout_identity" -dsn = "postgres://identity_user:IdentityUser112233@120.53.222.17:5432/takeout_identity_db" - -[[sources]] -id = "takeout_dictionary" -dsn = "postgres://dictionary_user:DictionaryUser112233@120.53.222.17:5432/takeout_dictionary_db" - -[[sources]] -id = "takeout_hangfire" -dsn = "postgres://hangfire_user:HangFire112233@120.53.222.17:5432/takeout_hangfire_db" diff --git a/deploy/postgres/README.md b/deploy/postgres/README.md deleted file mode 100644 index 04e7222..0000000 --- a/deploy/postgres/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# PostgreSQL 部署脚本 - -本目录提供在测试/预发布环境快速拉起 PostgreSQL 的脚本,复用线上同名数据库与账号,方便迁移/恢复。 - -## 目录结构 - -- `create_databases.sql`:创建四个业务库与对应角色(可多次执行,存在则跳过)。 -- `bootstrap.ps1`:PowerShell 包装脚本,调用 `psql` 执行 SQL。 - -## 前置条件 - -1. 已安装 PostgreSQL 12+,并能以管理员身份访问(默认使用 `postgres`)。 -2. 本地已配置 `psql` 可执行命令。 - -## 使用方法 - -```powershell -cd deploy/postgres -.\bootstrap.ps1 ` - -Host 120.53.222.17 ` - -Port 5432 ` - -AdminUser postgres ` - -AdminPassword "超级管理员密码" -``` - -脚本会: - -1. 创建/更新以下角色与库: - - `app_user` / `takeout_app_db` - - `identity_user` / `takeout_identity_db` - - `dictionary_user` / `takeout_dictionary_db` - - `hangfire_user` / `takeout_hangfire_db` -2. 为库设置 COMMENT,授予 Schema `public` 的 CRUD 权限。 -3. 输出执行日志,失败时终止。 - -## 自定义 - -- 如需修改密码或新增库,编辑 `create_databases.sql` 后重新运行脚本。 -- 若在本地拉起测试库,可把 `Host` 指向 `localhost`,其余参数保持一致。 - -## 常见问题 - -| 问题 | 处理方式 | -| --- | --- | -| `psql : command not found` | 确认 PostgreSQL bin 目录已加入 PATH。 | -| `permission denied to create database` | 改用具有 `CREATEDB` 权限的管理员执行脚本。 | -| 需要删除库 | 先 `DROP DATABASE xxx`,再运行脚本重新创建。 | diff --git a/deploy/postgres/bootstrap.ps1 b/deploy/postgres/bootstrap.ps1 deleted file mode 100644 index 3ea0bea..0000000 --- a/deploy/postgres/bootstrap.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -param( - [string]$Host = "120.53.222.17", - [int]$Port = 5432, - [string]$AdminUser = "postgres", - [string]$AdminPassword = "" -) - -if (-not (Get-Command psql -ErrorAction SilentlyContinue)) { - throw "psql command not found. Add PostgreSQL bin directory to PATH." -} - -if ([string]::IsNullOrWhiteSpace($AdminPassword)) { - Write-Warning "AdminPassword not provided. You will be prompted by psql." -} - -$sqlPath = Join-Path $PSScriptRoot "create_databases.sql" -if (-not (Test-Path $sqlPath)) { - throw "Cannot find create_databases.sql under $PSScriptRoot." -} - -$env:PGPASSWORD = $AdminPassword - -$arguments = @( - "-h", $Host, - "-p", $Port, - "-U", $AdminUser, - "-f", $sqlPath -) - -Write-Host "Executing create_databases.sql on $Host:$Port as $AdminUser ..." -& psql @arguments - -if ($LASTEXITCODE -ne 0) { - throw "psql returned non-zero exit code ($LASTEXITCODE)." -} - -Write-Host "PostgreSQL databases and roles ensured successfully." diff --git a/deploy/postgres/create_databases.sql b/deploy/postgres/create_databases.sql deleted file mode 100644 index afc0817..0000000 --- a/deploy/postgres/create_databases.sql +++ /dev/null @@ -1,102 +0,0 @@ --- Reusable provisioning script for Takeout SaaS PostgreSQL databases. --- Execute with a superuser (e.g. postgres). Safe to re-run. - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN - CREATE ROLE app_user LOGIN PASSWORD 'AppUser112233'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'identity_user') THEN - CREATE ROLE identity_user LOGIN PASSWORD 'IdentityUser112233'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dictionary_user') THEN - CREATE ROLE dictionary_user LOGIN PASSWORD 'DictionaryUser112233'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hangfire_user') THEN - CREATE ROLE hangfire_user LOGIN PASSWORD 'HangFire112233'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'logs_user') THEN - CREATE ROLE logs_user LOGIN PASSWORD 'Logs112233'; - END IF; -END $$; - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_app_db') THEN - CREATE DATABASE takeout_app_db OWNER app_user ENCODING 'UTF8'; - END IF; -END $$; -COMMENT ON DATABASE takeout_app_db IS 'Takeout SaaS 业务域数据库'; - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_identity_db') THEN - CREATE DATABASE takeout_identity_db OWNER identity_user ENCODING 'UTF8'; - END IF; -END $$; -COMMENT ON DATABASE takeout_identity_db IS 'Takeout SaaS 身份域数据库'; - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_dictionary_db') THEN - CREATE DATABASE takeout_dictionary_db OWNER dictionary_user ENCODING 'UTF8'; - END IF; -END $$; -COMMENT ON DATABASE takeout_dictionary_db IS 'Takeout SaaS 字典域数据库'; - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_hangfire_db') THEN - CREATE DATABASE takeout_hangfire_db OWNER hangfire_user ENCODING 'UTF8'; - END IF; -END $$; -COMMENT ON DATABASE takeout_hangfire_db IS 'Takeout SaaS 调度/Hangfire 数据库'; - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_logs_db') THEN - CREATE DATABASE takeout_logs_db OWNER logs_user ENCODING 'UTF8'; - END IF; -END $$; -COMMENT ON DATABASE takeout_logs_db IS 'Takeout SaaS 审计/日志数据库'; - --- Ensure privileges and default schema permissions -\connect takeout_app_db -GRANT CONNECT, TEMP ON DATABASE takeout_app_db TO app_user; -GRANT USAGE ON SCHEMA public TO app_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user; -GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO app_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user; - -\connect takeout_identity_db -GRANT CONNECT, TEMP ON DATABASE takeout_identity_db TO identity_user; -GRANT USAGE ON SCHEMA public TO identity_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO identity_user; -GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO identity_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO identity_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO identity_user; - -\connect takeout_dictionary_db -GRANT CONNECT, TEMP ON DATABASE takeout_dictionary_db TO dictionary_user; -GRANT USAGE ON SCHEMA public TO dictionary_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO dictionary_user; -GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO dictionary_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dictionary_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO dictionary_user; - -\connect takeout_hangfire_db -GRANT CONNECT, TEMP ON DATABASE takeout_hangfire_db TO hangfire_user; -GRANT USAGE ON SCHEMA public TO hangfire_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO hangfire_user; -GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO hangfire_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO hangfire_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO hangfire_user; - -\connect takeout_logs_db -GRANT CONNECT, TEMP ON DATABASE takeout_logs_db TO logs_user; -GRANT USAGE ON SCHEMA public TO logs_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO logs_user; -GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO logs_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO logs_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO logs_user; diff --git a/deploy/postgres/migrate_logs_to_logs_db.sql b/deploy/postgres/migrate_logs_to_logs_db.sql deleted file mode 100644 index 95892c8..0000000 --- a/deploy/postgres/migrate_logs_to_logs_db.sql +++ /dev/null @@ -1,89 +0,0 @@ --- 日志库迁移脚本(请在 psql 中按步骤执行) - --- 1. 在日志库创建表结构(takeout_logs_db) -\connect takeout_logs_db - -CREATE TABLE IF NOT EXISTS tenant_audit_logs ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "TenantId" bigint NOT NULL, - "Action" integer NOT NULL, - "Title" character varying(128) NOT NULL, - "Description" character varying(1024), - "OperatorId" bigint, - "OperatorName" character varying(64), - "PreviousStatus" integer, - "CurrentStatus" integer, - "CreatedAt" timestamp with time zone NOT NULL, - "UpdatedAt" timestamp with time zone, - "DeletedAt" timestamp with time zone, - "CreatedBy" bigint, - "UpdatedBy" bigint, - "DeletedBy" bigint -); -CREATE INDEX IF NOT EXISTS "IX_tenant_audit_logs_TenantId" ON tenant_audit_logs ("TenantId"); - -CREATE TABLE IF NOT EXISTS merchant_audit_logs ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "MerchantId" bigint NOT NULL, - "Action" integer NOT NULL, - "Title" character varying(128) NOT NULL, - "Description" character varying(1024), - "OperatorId" bigint, - "OperatorName" character varying(64), - "CreatedAt" timestamp with time zone NOT NULL, - "UpdatedAt" timestamp with time zone, - "DeletedAt" timestamp with time zone, - "CreatedBy" bigint, - "UpdatedBy" bigint, - "DeletedBy" bigint, - "TenantId" bigint NOT NULL -); -CREATE INDEX IF NOT EXISTS "IX_merchant_audit_logs_TenantId_MerchantId" ON merchant_audit_logs ("TenantId", "MerchantId"); - -CREATE TABLE IF NOT EXISTS operation_logs ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "OperationType" character varying(64) NOT NULL, - "TargetType" character varying(64) NOT NULL, - "TargetIds" text, - "OperatorId" character varying(64), - "OperatorName" character varying(128), - "Parameters" text, - "Result" text, - "Success" boolean NOT NULL, - "CreatedAt" timestamp with time zone NOT NULL, - "UpdatedAt" timestamp with time zone, - "DeletedAt" timestamp with time zone, - "CreatedBy" bigint, - "UpdatedBy" bigint, - "DeletedBy" bigint -); -CREATE INDEX IF NOT EXISTS "IX_operation_logs_CreatedAt" ON operation_logs ("CreatedAt"); -CREATE INDEX IF NOT EXISTS "IX_operation_logs_OperationType_CreatedAt" ON operation_logs ("OperationType", "CreatedAt"); - -CREATE TABLE IF NOT EXISTS member_growth_logs ( - "Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "MemberId" bigint NOT NULL, - "ChangeValue" integer NOT NULL, - "CurrentValue" integer NOT NULL, - "Notes" character varying(256), - "OccurredAt" timestamp with time zone NOT NULL, - "CreatedAt" timestamp with time zone NOT NULL, - "UpdatedAt" timestamp with time zone, - "DeletedAt" timestamp with time zone, - "CreatedBy" bigint, - "UpdatedBy" bigint, - "DeletedBy" bigint, - "TenantId" bigint NOT NULL -); -CREATE INDEX IF NOT EXISTS "IX_member_growth_logs_TenantId_MemberId_OccurredAt" ON member_growth_logs ("TenantId", "MemberId", "OccurredAt"); - --- 2. 迁移数据(建议使用 pg_dump/pg_restore 或应用侧批量拷贝) --- 示例:pg_dump -t tenant_audit_logs -t merchant_audit_logs -t operation_logs -t member_growth_logs takeout_app_db > logs_dump.sql --- psql -d takeout_logs_db -f logs_dump.sql - --- 3. 在业务库删除旧日志表(takeout_app_db) -\connect takeout_app_db -DROP TABLE IF EXISTS tenant_audit_logs; -DROP TABLE IF EXISTS merchant_audit_logs; -DROP TABLE IF EXISTS operation_logs; -DROP TABLE IF EXISTS member_growth_logs; diff --git a/deploy/prometheus/alert.rules.yml b/deploy/prometheus/alert.rules.yml deleted file mode 100644 index a74f845..0000000 --- a/deploy/prometheus/alert.rules.yml +++ /dev/null @@ -1,34 +0,0 @@ -groups: - - name: takeoutsaas-app - interval: 30s - rules: - - alert: HighErrorRate - expr: | - sum(rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m])) - / sum(rate(http_server_request_duration_seconds_count[5m])) > 0.05 - for: 5m - labels: - severity: critical - annotations: - summary: "API 5xx 错误率过高" - description: "过去 5 分钟 5xx 占比超过 5%,请检查依赖或发布" - - - alert: HighP95Latency - expr: | - histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le, service_name)) - > 1 - for: 5m - labels: - severity: warning - annotations: - summary: "API P95 延迟过高" - description: "过去 5 分钟 P95 超过 1s,请排查热点接口或依赖" - - - alert: InstanceDown - expr: up{job=~"admin-api|mini-api|user-api"} == 0 - for: 2m - labels: - severity: critical - annotations: - summary: "实例不可达" - description: "Prometheus 抓取失败,实例处于 down 状态" diff --git a/deploy/prometheus/prometheus.yml b/deploy/prometheus/prometheus.yml deleted file mode 100644 index 3385f12..0000000 --- a/deploy/prometheus/prometheus.yml +++ /dev/null @@ -1,28 +0,0 @@ -global: - scrape_interval: 15s - evaluation_interval: 30s - -rule_files: - - alert.rules.yml - -scrape_configs: - - job_name: admin-api - metrics_path: /metrics - static_configs: - - targets: ["admin-api:8080"] - labels: - service: admin-api - - - job_name: mini-api - metrics_path: /metrics - static_configs: - - targets: ["mini-api:8080"] - labels: - service: mini-api - - - job_name: user-api - metrics_path: /metrics - static_configs: - - targets: ["user-api:8080"] - labels: - service: user-api diff --git a/deploy/redis/README.md b/deploy/redis/README.md deleted file mode 100644 index c955032..0000000 --- a/deploy/redis/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Redis 部署脚本 - -本目录提供可复用的 Redis 配置,既可在本地通过 Docker Compose 启动,也可将 `redis.conf` 拷贝到现有服务器,确保与线上一致。 - -## 1. 部署步骤 (裸机)\n\n1. 将 \\ edis.conf\\ 拷贝到服务器(例如 /etc/redis/redis.conf)。\n2. 根据需要修改数据目录(\\dir\\)和绑定地址。\n3. 使用系统服务或 \\ edis-server redis.conf\\ 启动。\n4. 确认开放端口 6379,保证通过 \\ edis-cli -h -a ping\\ 可访问。\n\n## 2. 配置说明\n\n- \\ equirepass\\ 已设置为 MsuMshk112233。\n- 启用 appendonly(AOF),并每秒 fsync。\n- \\maxmemory-policy\\ 为 allkeys-lru,适合缓存场景。\n- \\protected-mode no\\ 允许远程连接,需结合安全组或防火墙限制来源 IP。\n\n## 3. 常用命令使用 `redis.conf` - -1. 把 `redis.conf` 拷贝到服务器 `/etc/redis/redis.conf`(或自定义目录)。 -2. 修改 `dir` 指向实际数据目录。 -3. 使用系统服务或 `redis-server redis.conf` 启动。 - -关键配置已包含: - -- `requirepass`(密码) -- `protected-mode no`(允许远程连接) -- `appendonly yes` + `appendfsync everysec` -- `maxmemory-policy allkeys-lru` - -## 3. 常用命令 - -在应用或 CLI 中使用: - -```bash -redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 ping -``` - -`appsettings.*.json` 的格式:`"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false"` - -## 4. 备份 - -- RDB 文件:`dump.rdb` -- AOF 文件:`appendonly.aof` - -通过 `redis-cli -a save` 或 `bgsave` 触发。确保备份目录已纳入快照/对象存储。 - diff --git a/deploy/redis/redis.conf b/deploy/redis/redis.conf deleted file mode 100644 index 5d20bb0..0000000 --- a/deploy/redis/redis.conf +++ /dev/null @@ -1,25 +0,0 @@ -bind 0.0.0.0 -port 6379 -protected-mode no - -requirepass MsuMshk112233 - -timeout 0 -tcp-keepalive 300 - -daemonize no - -loglevel notice -databases 16 - -save 900 1 -save 300 10 -save 60 10000 - -appendonly yes -appendfilename "appendonly.aof" -appendfsync everysec - -dir /data - -maxmemory-policy allkeys-lru diff --git a/scripts/build-adminapi-forlinux.sh b/scripts/build-adminapi-forlinux.sh deleted file mode 100755 index cfadd23..0000000 --- a/scripts/build-adminapi-forlinux.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# 用法:在 Linux 终端执行本脚本,自动构建并重启 AdminApi 容器。 -# 前置:已安装并运行 Docker。 -set -euo pipefail -# 0. 遇到异常时输出错误信息,方便查看 -trap 'echo "发生错误:${BASH_COMMAND}" >&2' ERR -# 1. 基本变量(脚本位于 repo_root/scripts,下移一层再上跳到仓库根) -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd "${script_dir}/.." && pwd)" -image_name='takeout.api.admin:dev' -container_name='takeout.api.admin' -dockerfile_path="${repo_root}/src/Api/TakeoutSaaS.AdminApi/Dockerfile" -echo "工作目录:${repo_root}" -# 2. 停止并删除旧容器 -if docker ps -a --format '{{.Names}}' | grep -qx "${container_name}"; then - echo "发现旧容器,正在移除:${container_name}" - docker stop "${container_name}" >/dev/null - docker rm "${container_name}" >/dev/null -fi -# 3. 删除旧镜像 -if docker images --format '{{.Repository}}:{{.Tag}}' | grep -qx "${image_name}"; then - echo "发现旧镜像,正在移除:${image_name}" - docker rmi "${image_name}" >/dev/null -fi -# 4. 构建最新镜像(使用仓库根作为上下文) -echo "开始构建镜像:${image_name}" -docker build -f "${dockerfile_path}" -t "${image_name}" "${repo_root}" -# 5. 运行新容器并映射端口 -echo "运行新容器:${container_name} (端口映射 7801:7801,环境 Development)" -docker run -d --name "${container_name}" -e ASPNETCORE_ENVIRONMENT=Development -p 7801:7801 "${image_name}" -echo "完成。镜像:${image_name},容器:${container_name}。Swagger 访问:http://localhost:7801/swagger" -# 6. 交互式终端下暂停,方便查看输出 -if [ -t 0 ]; then - read -r -p "按回车关闭窗口" _ -fi diff --git a/scripts/build-adminapi.ps1 b/scripts/build-adminapi.ps1 deleted file mode 100644 index 29265bd..0000000 --- a/scripts/build-adminapi.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -<# -用法:在 PowerShell 中执行本脚本,自动构建并重启 AdminApi 容器。 -如果要放到桌面双击运行,可将本文件复制到桌面后右键“使用 PowerShell 运行”。 -前置:已安装并运行 Docker Desktop。 -#> - -# 遇到异常时停住窗口,方便查看错误 -trap { - Write-Host "发生错误:" $_ -ForegroundColor Red - Read-Host "按回车关闭窗口" - exit 1 -} - -$ErrorActionPreference = 'Stop' - -# 1. 基本变量(脚本位于 repo_root/scripts,下移一层再上跳到仓库根) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$repoRoot = Split-Path -Parent $scriptDir -$imageName = 'takeout.api.admin:dev' -$containerName = 'takeout.api.admin' -$dockerfilePath = Join-Path $repoRoot 'src/Api/TakeoutSaaS.AdminApi/Dockerfile' - -Write-Host "工作目录:$repoRoot" - -# 2. 停止并删除旧容器 -if ((docker ps -a --format '{{.Names}}') -contains $containerName) { - Write-Host "发现旧容器,正在移除:$containerName" - docker stop $containerName | Out-Null - docker rm $containerName | Out-Null -} - -# 3. 删除旧镜像 -if ((docker images --format '{{.Repository}}:{{.Tag}}') -contains $imageName) { - Write-Host "发现旧镜像,正在移除:$imageName" - docker rmi $imageName | Out-Null -} - -# 4. 构建最新镜像(使用仓库根作为上下文) -Write-Host "开始构建镜像:$imageName" -docker build -f $dockerfilePath -t $imageName $repoRoot - -# 5. 运行新容器并映射端口 -Write-Host "运行新容器:$containerName (端口映射 7801:7801,环境 Development)" -docker run -d --name $containerName -e ASPNETCORE_ENVIRONMENT=Development -p 7801:7801 $imageName - -Write-Host "完成。镜像:$imageName,容器:$containerName。Swagger 访问:http://localhost:7801/swagger" diff --git a/src/Api/TakeoutSaaS.AdminApi/Dockerfile b/src/Api/TakeoutSaaS.AdminApi/Dockerfile index 27f5733..72754e9 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Dockerfile +++ b/src/Api/TakeoutSaaS.AdminApi/Dockerfile @@ -1,8 +1,33 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src + +# Copy only what's needed for restore first, so `dotnet restore` can be cached. +COPY ["Directory.Build.props", "./"] +COPY ["TakeoutSaaS.sln", "./"] +COPY ["stylecop.json", "./"] +COPY ["src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj", "src/Api/TakeoutSaaS.AdminApi/"] +COPY ["src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj", "src/Application/TakeoutSaaS.Application/"] +COPY ["TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj", "TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Abstractions/"] +COPY ["TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj", "TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Kernel/"] +COPY ["TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj", "TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Web/"] +COPY ["src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj", "src/Domain/TakeoutSaaS.Domain/"] +COPY ["src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj", "src/Infrastructure/TakeoutSaaS.Infrastructure/"] +COPY ["src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj", "src/Modules/TakeoutSaaS.Module.Authorization/"] +COPY ["src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj", "src/Modules/TakeoutSaaS.Module.Delivery/"] +COPY ["src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj", "src/Modules/TakeoutSaaS.Module.Dictionary/"] +COPY ["src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj", "src/Modules/TakeoutSaaS.Module.Messaging/"] +COPY ["src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj", "src/Modules/TakeoutSaaS.Module.Scheduler/"] +COPY ["src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj", "src/Modules/TakeoutSaaS.Module.Sms/"] +COPY ["src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj", "src/Modules/TakeoutSaaS.Module.Storage/"] +COPY ["src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj", "src/Modules/TakeoutSaaS.Module.Tenancy/"] + +RUN dotnet restore "src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj" + +# Copy the rest of the source after restore for best cache reuse. COPY . . -RUN dotnet restore src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj -RUN dotnet publish src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj -c Release -o /app/publish + +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish --no-restore FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index 59d35a0..212123a 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index 22d02129478fcd3c292f68dda33b37c4bd976ee6..653f6a017e443cb8945826651688af1fcb3dff6a 100644 GIT binary patch delta 106 zcmaE({7Yqn2sg7HgWhCDUTI|~hEj%1h8%_zAeqOI&fo-O=QAWTWHS^q!~k&-L-J-p WZecdChD1SGRs0&DS|-a2o&^Bntrryl delta 28 kcmeyR@1lmGw# diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index e6d50e9faf26fb9355619e4c0d873dceafc56b37..ed127bdbdd8294cd7f70943bbbf006bf6b52629c 100644 GIT binary patch delta 108 zcmX>gcTRqT3Kz2;gWhCDZfRvFhEj%1h8%_zAeqOI&fo-O=QAWTWHS^q!~k&-L-J-t Yu2>dUpazB*kQP<^TA-RHSMnVP0P6e~UH||9 delta 28 kcmX>ne?V@73Kz38L;hq%9{I@z{Ct~daA~nj=HWjC0DB4u5dZ)H diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index 9d43afd90df02d300240c407ac0ffb039223e193..cf2034e104d6983cc1c5408fbe178e963f8e4d20 100644 GIT binary patch delta 56 zcmaDOwn2OY6DPACgWlvuc4=iNhEj%1h8%_zAeqOI&fo-O=QAWTWHS^q!~k&-L-OYT H9Jx#YfMpGq delta 24 fcmdlW{zhyA6DPYfLq0 -/// 数据源名称常量,统一配置键与使用说明。 -///

-public static class DatabaseConstants -{ - /// - /// 默认业务库(AppDatabase). - /// - public const string AppDataSource = "AppDatabase"; - - /// - /// 身份认证库(IdentityDatabase)。 - /// - public const string IdentityDataSource = "IdentityDatabase"; - - /// - /// 字典库(DictionaryDatabase)。 - /// - public const string DictionaryDataSource = "DictionaryDatabase"; - - /// - /// 日志库(LogsDatabase)。 - /// - public const string LogsDataSource = "LogsDatabase"; -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs deleted file mode 100644 index 3214155..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Constants/ErrorCodes.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Constants; - -/// -/// 统一错误码常量。 -/// -public static class ErrorCodes -{ - /// - /// 请求参数错误。 - /// - public const int BadRequest = 400; - - /// - /// 未授权访问。 - /// - public const int Unauthorized = 401; - - /// - /// 权限不足。 - /// - public const int Forbidden = 403; - - /// - /// 资源未找到。 - /// - public const int NotFound = 404; - - /// - /// 资源冲突。 - /// - public const int Conflict = 409; - - /// - /// 校验失败。 - /// - public const int ValidationFailed = 422; - - /// - /// 服务器内部错误。 - /// - public const int InternalServerError = 500; - - /// - /// 业务自定义错误(10000+)。 - /// - public const int BusinessError = 10001; -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs deleted file mode 100644 index 175f9f6..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Data/DatabaseConnectionRole.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Data; - -/// -/// 数据库连接角色,用于区分主写与从读连接。 -/// -public enum DatabaseConnectionRole -{ - /// - /// 主写连接,用于写入或强一致读。 - /// - Write = 1, - - /// - /// 从读连接,用于只读查询或报表。 - /// - Read = 2 -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs deleted file mode 100644 index 0423468..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Data/IDapperExecutor.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Data; - -namespace TakeoutSaaS.Shared.Abstractions.Data; - -/// -/// Dapper 查询/命令执行器抽象,封装连接获取与读写路由。 -/// -public interface IDapperExecutor -{ - /// - /// 使用指定数据源与读写角色执行异步查询,并返回结果。 - /// - /// 查询结果类型。 - /// 逻辑数据源名称。 - /// 连接角色(读/写)。 - /// 查询委托,提供已打开的连接和取消标记。 - /// 取消标记。 - /// 查询结果。 - Task QueryAsync( - string dataSourceName, - DatabaseConnectionRole role, - Func> query, - CancellationToken cancellationToken = default); - - /// - /// 使用指定数据源与读写角色执行异步命令。 - /// - /// 逻辑数据源名称。 - /// 连接角色(读/写)。 - /// 命令委托,提供已打开的连接和取消标记。 - /// 取消标记。 - /// 异步执行任务。 - Task ExecuteAsync( - string dataSourceName, - DatabaseConnectionRole role, - Func command, - CancellationToken cancellationToken = default); - - /// - /// 获取指定数据源及角色的默认命令超时时间(秒)。 - /// - /// 逻辑数据源名称。 - /// 连接角色,默认读取从库。 - /// 命令超时时间(秒)。 - int GetDefaultCommandTimeoutSeconds( - string dataSourceName, - DatabaseConnectionRole role = DatabaseConnectionRole.Read); -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs deleted file mode 100644 index ae8284b..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Diagnostics/TraceContext.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Diagnostics; - -/// -/// 轻量级 TraceId/SpanId 上下文,便于跨层访问当前请求的追踪标识。 -/// -public static class TraceContext -{ - private static readonly AsyncLocal TraceIdHolder = new(); - private static readonly AsyncLocal SpanIdHolder = new(); - - /// - /// 当前请求的 TraceId。 - /// - public static string? TraceId - { - get => TraceIdHolder.Value; - set => TraceIdHolder.Value = value; - } - - /// - /// 当前请求的 SpanId。 - /// - public static string? SpanId - { - get => SpanIdHolder.Value; - set => SpanIdHolder.Value = value; - } - - /// - /// 清理 TraceId,避免 AsyncLocal 污染其它请求。 - /// - public static void Clear() - { - TraceIdHolder.Value = null; - SpanIdHolder.Value = null; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs deleted file mode 100644 index 6dbcf9c..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/AuditableEntityBase.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Entities; - -/// -/// 审计实体基类:提供创建、更新时间以及软删除时间。 -/// -public abstract class AuditableEntityBase : EntityBase, IAuditableEntity -{ - /// - /// 创建时间(UTC)。 - /// - public DateTime CreatedAt { get; set; } - - /// - /// 最近一次更新时间(UTC),从未更新时为 null。 - /// - public DateTime? UpdatedAt { get; set; } - - /// - /// 软删除时间(UTC),未删除时为 null。 - /// - public DateTime? DeletedAt { get; set; } - - /// - /// 创建人用户标识,匿名或系统操作时为 null。 - /// - public long? CreatedBy { get; set; } - - /// - /// 最后更新人用户标识,匿名或系统操作时为 null。 - /// - public long? UpdatedBy { get; set; } - - /// - /// 删除人用户标识(软删除),未删除时为 null。 - /// - public long? DeletedBy { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs deleted file mode 100644 index e1e2aa4..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/EntityBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Entities; - -/// -/// 实体基类,统一提供主键标识。 -/// -public abstract class EntityBase -{ - /// - /// 实体唯一标识。 - /// - public long Id { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs deleted file mode 100644 index 844ad54..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IAuditableEntity.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Entities; - -/// -/// 审计字段接口:提供创建、更新、删除时间与操作者标识。 -/// -public interface IAuditableEntity : ISoftDeleteEntity -{ - /// - /// 创建时间(UTC)。 - /// - DateTime CreatedAt { get; set; } - - /// - /// 更新时间(UTC),未更新时为 null。 - /// - DateTime? UpdatedAt { get; set; } - - /// - /// 删除时间(UTC),未删除时为 null。 - /// - new DateTime? DeletedAt { get; set; } - - /// - /// 创建人用户标识,匿名或系统操作时为 null。 - /// - long? CreatedBy { get; set; } - - /// - /// 最后更新人用户标识,匿名或系统操作时为 null。 - /// - long? UpdatedBy { get; set; } - - /// - /// 删除人用户标识(软删除),未删除时为 null。 - /// - long? DeletedBy { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs deleted file mode 100644 index 1a0fecd..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/IMultiTenantEntity.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Entities; - -/// -/// 多租户实体约定:所有持久化实体须包含租户标识字段。 -/// -public interface IMultiTenantEntity -{ - /// - /// 所属租户 ID。 - /// - long TenantId { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs deleted file mode 100644 index 4bc3fba..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/ISoftDeleteEntity.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Entities; - -/// -/// 软删除实体约定:提供可空的删除时间戳以支持全局过滤。 -/// -public interface ISoftDeleteEntity -{ - /// - /// 删除时间(UTC),未删除时为 null。 - /// - DateTime? DeletedAt { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs deleted file mode 100644 index df6417e..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Entities/MultiTenantEntityBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Entities; - -/// -/// 多租户审计实体基类:提供租户标识、审计字段与软删除标记。 -/// -public abstract class MultiTenantEntityBase : AuditableEntityBase, IMultiTenantEntity -{ - /// - /// 所属租户 ID。 - /// - public long TenantId { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs deleted file mode 100644 index b14dc4a..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/BusinessException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Exceptions; - -/// -/// 业务异常(用于可预期的业务校验错误)。 -/// -public class BusinessException(int errorCode, string message) : Exception(message) -{ - /// - /// 业务错误码。 - /// - public int ErrorCode { get; } = errorCode; -} - diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs deleted file mode 100644 index f95bf0b..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Exceptions/ValidationException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Exceptions; - -/// -/// 验证异常(用于聚合验证错误信息)。 -/// -public class ValidationException(IDictionary errors) : Exception("一个或多个验证错误") -{ - /// - /// 字段/属性的错误集合。 - /// - public IDictionary Errors { get; } = errors; -} - diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs deleted file mode 100644 index ce8dff4..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Ids; - -/// -/// 雪花 ID 生成器接口。 -/// -public interface IIdGenerator -{ - /// - /// 生成下一个唯一长整型 ID。 - /// - /// 雪花 ID。 - long NextId(); -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs deleted file mode 100644 index 6d9b40f..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IdGeneratorOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Shared.Abstractions.Ids; - -/// -/// 雪花 ID 生成器配置。 -/// -public sealed class IdGeneratorOptions -{ - /// - /// 配置节名称。 - /// - public const string SectionName = "IdGenerator"; - - /// - /// 工作节点标识,0-31。 - /// - [Range(0, 31)] - public int WorkerId { get; set; } - - /// - /// 机房标识,0-31。 - /// - [Range(0, 31)] - public int DatacenterId { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs deleted file mode 100644 index 04f400c..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.NonGeneric.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Results; - -/// -/// 非泛型便捷封装。 -/// -public static class ApiResponse -{ - /// - /// 仅返回成功消息(无数据)。 - /// - /// 提示信息。 - /// 封装后的成功响应。 - public static ApiResponse Success(string? message = "操作成功") - => ApiResponse.Ok(message: message); - - /// - /// 成功且携带数据。 - /// - /// 业务数据。 - /// 提示信息。 - /// 封装后的成功响应。 - public static ApiResponse Ok(object? data, string? message = "操作成功") - => data is null ? ApiResponse.Ok(message: message) : ApiResponse.Ok(data, message); - - /// - /// 错误返回。 - /// - /// 错误码。 - /// 错误提示。 - /// 封装后的失败响应。 - public static ApiResponse Failure(int code, string message) - => ApiResponse.Error(code, message); - - /// - /// 错误返回(附带详情)。 - /// - /// 错误码。 - /// 错误提示。 - /// 错误详情。 - /// 封装后的失败响应。 - public static ApiResponse Error(int code, string message, object? errors = null) - => ApiResponse.Error(code, message, errors); -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs deleted file mode 100644 index f89a1b7..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Diagnostics; -using TakeoutSaaS.Shared.Abstractions.Diagnostics; - -namespace TakeoutSaaS.Shared.Abstractions.Results; - -/// -/// 统一的 API 返回结果包装。 -/// -/// 数据载荷类型。 -public sealed record ApiResponse -{ - /// - /// 是否成功。 - /// - public bool Success { get; init; } - - /// - /// 状态/错误码(默认 200)。 - /// - public int Code { get; init; } = 200; - - /// - /// 提示信息。 - /// - public string? Message { get; init; } - - /// - /// 业务数据。 - /// - public T? Data { get; init; } - - /// - /// 错误详情(如字段验证错误)。 - /// - public object? Errors { get; init; } - - /// - /// TraceId,便于链路追踪。 - /// - public string TraceId { get; init; } = string.Empty; - - /// - /// 时间戳(UTC)。 - /// - public DateTime Timestamp { get; init; } = DateTime.UtcNow; - - /// - /// 成功返回。 - /// - /// 业务数据。 - /// 提示信息。 - /// 封装后的成功响应。 - public static ApiResponse Ok(T data, string? message = "操作成功") - => Create(true, 200, message, data); - - /// - /// 无数据的成功返回。 - /// - /// 提示信息。 - /// 封装后的成功响应。 - public static ApiResponse Ok(string? message = "操作成功") - => Create(true, 200, message, default); - - /// - /// 兼容旧名称:成功结果。 - /// - /// 业务数据。 - /// 提示信息。 - /// 封装后的成功响应。 - public static ApiResponse SuccessResult(T data, string? message = "操作成功") - => Ok(data, message); - - /// - /// 错误返回。 - /// - /// 错误码。 - /// 错误提示。 - /// 错误详情。 - /// 封装后的失败响应。 - public static ApiResponse Error(int code, string message, object? errors = null) - => Create(false, code, message, default, errors); - - /// - /// 兼容旧名称:失败结果。 - /// - /// 错误码。 - /// 错误提示。 - /// 封装后的失败响应。 - public static ApiResponse Failure(int code, string message) - => Error(code, message); - - /// - /// 附加错误详情。 - /// - /// 错误详情。 - /// 包含错误详情的新响应。 - public ApiResponse WithErrors(object? errors) - => this with { Errors = errors }; - - private static ApiResponse Create(bool success, int code, string? message, T? data, object? errors = null) - => new() - { - Success = success, - Code = code, - Message = message, - Data = data, - Errors = errors, - TraceId = ResolveTraceId(), - Timestamp = DateTime.UtcNow - }; - - /// - /// 解析当前 TraceId。 - /// - /// 当前有效的 TraceId。 - private static string ResolveTraceId() - { - if (!string.IsNullOrWhiteSpace(TraceContext.TraceId)) - { - return TraceContext.TraceId; - } - - if (!string.IsNullOrWhiteSpace(TraceContext.TraceId)) - { - return TraceContext.TraceId; - } - - if (Activity.Current?.Id is { } id && !string.IsNullOrWhiteSpace(id)) - { - return id; - } - - return IdFallbackGenerator.Instance.NextId().ToString(); - } -} - -/// -/// 作为 TraceId 缺失时的本地雪花 ID 备用生成器。 -/// -internal sealed class IdFallbackGenerator -{ - /// - /// 延迟初始化的单例实例承载。 - /// - private static readonly Lazy Lazy = new(() => new IdFallbackGenerator()); - - /// - /// 获取备用雪花生成器单例。 - /// - public static IdFallbackGenerator Instance => Lazy.Value; - - private readonly object _sync = new(); - private long _lastTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - private long _sequence; - - private IdFallbackGenerator() - { - } - - /// - /// 生成雪花风格的本地备用 ID。 - /// - /// 本地生成的雪花 ID。 - public long NextId() - { - lock (_sync) - { - var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - if (timestamp == _lastTimestamp) - { - _sequence = (_sequence + 1) & 4095; - if (_sequence == 0) - { - timestamp = WaitNextMillis(_lastTimestamp); - } - } - else - { - _sequence = 0; - } - - _lastTimestamp = timestamp; - return ((timestamp - 1577836800000L) << 22) | _sequence; - } - } - - /// - /// 等待到下一个毫秒以避免序列冲突。 - /// - /// 上一毫秒的时间戳。 - /// 下一个时间戳(毫秒)。 - private static long WaitNextMillis(long lastTimestamp) - { - var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - while (timestamp <= lastTimestamp) - { - Thread.SpinWait(100); - timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - } - - return timestamp; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs deleted file mode 100644 index 4bc4c7e..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Results/PagedResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Results; - -/// -/// 分页结果包装,携带列表与总条数等元数据。 -/// -/// 数据类型。 -/// -/// 初始化分页结果。 -/// -public sealed class PagedResult(IReadOnlyList items, int page, int pageSize, int totalCount) -{ - /// - /// 数据列表。 - /// - public IReadOnlyList Items { get; } = items; - - /// - /// 当前页码,从 1 开始。 - /// - public int Page { get; } = page; - - /// - /// 每页条数。 - /// - public int PageSize { get; } = pageSize; - - /// - /// 总条数。 - /// - public int TotalCount { get; } = totalCount; - - /// - /// 总页数。 - /// - public int TotalPages { get; } = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs deleted file mode 100644 index ba0d7e2..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Security/ICurrentUserAccessor.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Security; - -/// -/// 当前用户访问器:提供与当前请求相关的用户标识信息。 -/// -public interface ICurrentUserAccessor -{ - /// - /// 当前用户 ID,未登录时为 Guid.Empty。 - /// - long UserId { get; } - - /// - /// 是否已登录。 - /// - bool IsAuthenticated { get; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs deleted file mode 100644 index b7f82a0..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Serialization/SnowflakeIdJsonConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace TakeoutSaaS.Shared.Abstractions.Serialization; - -/// -/// 将 long 类型的雪花 ID 以字符串形式序列化/反序列化,避免前端精度丢失。 -/// -public sealed class SnowflakeIdJsonConverter : JsonConverter -{ - /// - public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.Number => reader.GetInt64(), - JsonTokenType.String when long.TryParse(reader.GetString(), out var value) => value, - JsonTokenType.Null => 0, - _ => throw new JsonException("无法解析雪花 ID") - }; - } - - /// - public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) - { - writer.WriteStringValue(value == 0 ? "0" : value.ToString()); - } -} - -/// -/// 可空雪花 ID 转换器。 -/// -public sealed class NullableSnowflakeIdJsonConverter : JsonConverter -{ - /// - public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.Number => reader.GetInt64(), - JsonTokenType.String when long.TryParse(reader.GetString(), out var value) => value, - JsonTokenType.Null => null, - _ => throw new JsonException("无法解析雪花 ID") - }; - } - - /// - public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.HasValue ? value.Value.ToString() : null); - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj deleted file mode 100644 index 43219c2..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net10.0 - enable - enable - true - 1591 - - - - - - diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs deleted file mode 100644 index c05f027..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantContextAccessor.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Tenancy; - -/// -/// 租户上下文访问器:用于在请求生命周期内读写当前租户上下文。 -/// -public interface ITenantContextAccessor -{ - /// - /// 获取或设置当前租户上下文。 - /// - TenantContext? Current { get; set; } -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs deleted file mode 100644 index 76358db..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/ITenantProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Tenancy; - -/// -/// 租户提供者:用于在各层读取当前请求绑定的租户 ID。 -/// -public interface ITenantProvider -{ - /// - /// 获取当前租户 ID,未解析时返回 0。 - /// - /// 当前请求绑定的租户 ID,未解析时为 0。 - long GetCurrentTenantId(); -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs deleted file mode 100644 index d5638fa..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantConstants.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Tenancy; - -/// -/// 多租户相关通用常量。 -/// -public static class TenantConstants -{ - /// - /// HttpContext.Items 中租户上下文的键名。 - /// - public const string HttpContextItemKey = "__tenant_context"; -} diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs b/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs deleted file mode 100644 index 8ba5da6..0000000 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/Tenancy/TenantContext.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace TakeoutSaaS.Shared.Abstractions.Tenancy; - -/// -/// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。 -/// -/// -/// 初始化租户上下文。 -/// -/// 租户 ID -/// 租户编码(可选) -/// 解析来源 -public sealed class TenantContext(long tenantId, string? tenantCode, string source) -{ - /// - /// 未解析到租户时的默认上下文。 - /// - public static TenantContext Empty { get; } = new(0, null, "unresolved"); - - /// - /// 当前租户 ID,未解析时为 Guid.Empty。 - /// - public long TenantId { get; } = tenantId; - - /// - /// 当前租户编码(例如子域名或业务编码),可为空。 - /// - public string? TenantCode { get; } = tenantCode; - - /// - /// 租户解析来源(Header、Host、Token 等)。 - /// - public string Source { get; } = source; - - /// - /// 是否已成功解析到租户。 - /// - public bool IsResolved => TenantId != 0; -} diff --git a/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs b/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs deleted file mode 100644 index 862edb9..0000000 --- a/src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Diagnostics; -using System.Security.Cryptography; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Shared.Kernel.Ids; - -/// -/// 基于雪花算法的长整型 ID 生成器。 -/// -/// -/// 初始化生成器。 -/// -/// 工作节点 ID。 -/// 机房 ID。 -public sealed class SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0) : IIdGenerator -{ - private const long Twepoch = 1577836800000L; // 2020-01-01 UTC - private const int WorkerIdBits = 5; - private const int DatacenterIdBits = 5; - private const int SequenceBits = 12; - - private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits); - private const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits); - - private const int WorkerIdShift = SequenceBits; - private const int DatacenterIdShift = SequenceBits + WorkerIdBits; - private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; - private const long SequenceMask = -1L ^ (-1L << SequenceBits); - - private readonly long _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId)); - private readonly long _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId)); - private long _lastTimestamp = -1L; - private long _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask); - private readonly object _syncRoot = new(); - - - /// - public long NextId() - { - lock (_syncRoot) - { - var timestamp = CurrentTimeMillis(); - - if (timestamp < _lastTimestamp) - { - // 时钟回拨时等待到下一毫秒。 - var wait = _lastTimestamp - timestamp; - Thread.Sleep(TimeSpan.FromMilliseconds(wait)); - timestamp = CurrentTimeMillis(); - if (timestamp < _lastTimestamp) - { - throw new InvalidOperationException($"系统时钟回拨 {_lastTimestamp - timestamp} 毫秒,无法生成 ID。"); - } - } - - if (_lastTimestamp == timestamp) - { - _sequence = (_sequence + 1) & SequenceMask; - if (_sequence == 0) - { - timestamp = WaitNextMillis(_lastTimestamp); - } - } - else - { - _sequence = 0; - } - - _lastTimestamp = timestamp; - - var id = ((timestamp - Twepoch) << TimestampLeftShift) - | (_datacenterId << DatacenterIdShift) - | (_workerId << WorkerIdShift) - | _sequence; - - Debug.Assert(id > 0); - return id; - } - } - - private static long WaitNextMillis(long lastTimestamp) - { - var timestamp = CurrentTimeMillis(); - while (timestamp <= lastTimestamp) - { - Thread.SpinWait(50); - timestamp = CurrentTimeMillis(); - } - - return timestamp; - } - - private static long CurrentTimeMillis() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - private static long Normalize(long value, long max, string name) - { - if (value < 0 || value > max) - { - throw new ArgumentOutOfRangeException(name, value, $"取值范围 0~{max}"); - } - - return value; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj b/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj deleted file mode 100644 index db63687..0000000 --- a/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - net10.0 - enable - enable - - - - - - - - - diff --git a/src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs b/src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs deleted file mode 100644 index af1240e..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Api/BaseApiController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace TakeoutSaaS.Shared.Web.Api; - -/// -/// API 基类控制器: -/// - 统一应用 [ApiController] 和默认响应类型 -/// - 作为所有 API 控制器的基类,便于复用过滤器/中间件特性 -/// -[ApiController] -[Produces("application/json")] -public abstract class BaseApiController : ControllerBase -{ -} - diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs deleted file mode 100644 index 2c25aaa..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using TakeoutSaaS.Shared.Web.Middleware; - -namespace TakeoutSaaS.Shared.Web.Extensions; - -/// -/// Web 应用中间件扩展。 -/// -public static class ApplicationBuilderExtensions -{ - /// - /// 按规范启用 TraceId、请求日志、异常映射与安全响应头。 - /// - public static IApplicationBuilder UseSharedWebCore(this IApplicationBuilder app) - { - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - return app; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 842728d..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Asp.Versioning; -using Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Web.Filters; -using TakeoutSaaS.Shared.Web.Security; -namespace TakeoutSaaS.Shared.Web.Extensions; - -/// -/// Shared.Web 服务注册扩展。 -/// -public static class ServiceCollectionExtensions -{ - /// - /// 注册控制器、模型验证、API 版本化等基础能力。 - /// - public static IServiceCollection AddSharedWebCore(this IServiceCollection services) - { - // 1. 注册基础上下文与当前用户访问器 - services.AddHttpContextAccessor(); - services.AddEndpointsApiExplorer(); - services.AddScoped(); - // 2. 注册控制器与全局过滤器 - services - .AddControllers(options => - { - options.Filters.Add(); - options.Filters.Add(); - }) - .AddNewtonsoftJson(); - // 3. 配置模型验证行为 - services.Configure(options => - { - options.SuppressModelStateInvalidFilter = true; - }); - // 4. 配置 API 版本化 - var apiVersioningBuilder = services.AddApiVersioning(options => - { - options.AssumeDefaultVersionWhenUnspecified = true; - options.DefaultApiVersion = new ApiVersion(1, 0); - options.ReportApiVersions = true; - }); - // 5. 注册版本化 Api Explorer - apiVersioningBuilder.AddApiExplorer(setup => - { - setup.GroupNameFormat = "'v'VVV"; - setup.SubstituteApiVersionInUrl = true; - }); - // 6. 返回服务集合 - return services; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs b/src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs deleted file mode 100644 index 30c6c7c..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Filters/ApiResponseResultFilter.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Shared.Web.Filters; - -/// -/// ApiResponse 结果过滤器:自动将 ApiResponse 转换为对应的 HTTP 状态码。 -/// 使用此过滤器后,控制器可以直接返回 ApiResponse<T>,无需再包一层 Ok() 或 Unauthorized()。 -/// -public sealed class ApiResponseResultFilter : IAsyncResultFilter -{ - /// - /// 执行结果过滤,将 ApiResponse 映射为对应 HTTP 状态码。 - /// - /// 结果执行上下文。 - /// 后续委托。 - /// 异步任务。 - public Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) - { - // 1. 仅处理 ObjectResult - // 只处理 ObjectResult 类型的结果 - if (context.Result is not ObjectResult objectResult) - { - return next(); - } - - // 2. 结果为空直接跳过 - var value = objectResult.Value; - if (value == null) - { - return next(); - } - - // 3. 确认类型为 ApiResponse - // 检查是否是 ApiResponse 类型 - var valueType = value.GetType(); - if (!IsApiResponseType(valueType)) - { - return next(); - } - - // 4. 读取 Success 与 Code - // 使用反射获取 Success 和 Code 属性 - // 注意:由于已通过 IsApiResponseType 检查,属性名是固定的 - const string successPropertyName = "Success"; - const string codePropertyName = "Code"; - var successProperty = valueType.GetProperty(successPropertyName); - var codeProperty = valueType.GetProperty(codePropertyName); - - if (successProperty == null || codeProperty == null) - { - return next(); - } - - var success = (bool)(successProperty.GetValue(value) ?? false); - var code = (int)(codeProperty.GetValue(value) ?? 200); - - // 5. 映射 HTTP 状态码 - // 根据 Success 和 Code 设置 HTTP 状态码 - var statusCode = success ? MapSuccessCode(code) : MapErrorCode(code); - - // 6. 回写状态码 - // 更新 ObjectResult 的状态码 - objectResult.StatusCode = statusCode; - - return next(); - } - - private static bool IsApiResponseType(Type type) - { - // 检查是否是 ApiResponse 类型 - if (type.IsGenericType) - { - var genericTypeDefinition = type.GetGenericTypeDefinition(); - return genericTypeDefinition == typeof(ApiResponse<>); - } - - return false; - } - - private static int MapSuccessCode(int code) - { - // 成功情况下,通常返回 200 - // 但也可以根据业务码返回其他成功状态码(如 201 Created) - return code switch - { - 200 => StatusCodes.Status200OK, - 201 => StatusCodes.Status201Created, - 204 => StatusCodes.Status204NoContent, - _ => StatusCodes.Status200OK - }; - } - - private static int MapErrorCode(int code) - { - // 根据业务错误码映射到 HTTP 状态码 - return code switch - { - ErrorCodes.BadRequest => StatusCodes.Status400BadRequest, - ErrorCodes.Unauthorized => StatusCodes.Status401Unauthorized, - ErrorCodes.Forbidden => StatusCodes.Status403Forbidden, - ErrorCodes.NotFound => StatusCodes.Status404NotFound, - ErrorCodes.Conflict => StatusCodes.Status409Conflict, - ErrorCodes.ValidationFailed => StatusCodes.Status422UnprocessableEntity, - ErrorCodes.InternalServerError => StatusCodes.Status500InternalServerError, - // 业务错误码(10000+)统一返回 422 - >= 10000 => StatusCodes.Status422UnprocessableEntity, - // 默认返回 400 - _ => StatusCodes.Status400BadRequest - }; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs b/src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs deleted file mode 100644 index f128bea..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Filters/ValidateModelAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Shared.Web.Filters; - -/// -/// 模型验证过滤器:将模型验证错误统一为ApiResponse输出 -/// -public sealed class ValidateModelAttribute : ActionFilterAttribute -{ - /// - /// 在 Action 执行前拦截模型验证错误。 - /// - /// 执行上下文。 - public override void OnActionExecuting(ActionExecutingContext context) - { - // 1. 模型验证未通过则返回 422 - if (!context.ModelState.IsValid) - { - var errors = context.ModelState - .Where(kv => kv.Value?.Errors.Count > 0) - .ToDictionary( - kv => kv.Key, - kv => kv.Value!.Errors.Select(e => string.IsNullOrWhiteSpace(e.ErrorMessage) ? "Invalid" : e.ErrorMessage).ToArray() - ); - - var response = ApiResponse.Error(ErrorCodes.ValidationFailed, "一个或多个验证错误", errors); - context.Result = new UnprocessableEntityObjectResult(response); - } - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs deleted file mode 100644 index 3f545e3..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/CorrelationIdMiddleware.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using System.Diagnostics; -using TakeoutSaaS.Shared.Abstractions.Diagnostics; -using TakeoutSaaS.Shared.Abstractions.Ids; - -namespace TakeoutSaaS.Shared.Web.Middleware; - -/// -/// 统一 TraceId/CorrelationId,贯穿日志与响应。 -/// -public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger logger, IIdGenerator idGenerator) -{ - private const string TraceHeader = "X-Trace-Id"; - private const string SpanHeader = "X-Span-Id"; - private const string RequestHeader = "X-Request-Id"; - - /// - /// 管道入口,确保 TraceId/SpanId 贯穿请求。 - /// - /// HTTP 上下文。 - public async Task InvokeAsync(HttpContext context) - { - // 1. 确保活动存在并启动 - var ownsActivity = Activity.Current is null; - var activity = Activity.Current ?? new Activity("TakeoutSaaS.Request"); - - if (activity.Id is null) - { - activity.SetIdFormat(ActivityIdFormat.W3C); - activity.Start(); - } - - // 2. 生成/解析 TraceId、SpanId - var traceId = activity.TraceId.ToString(); - var spanId = activity.SpanId.ToString(); - - if (string.IsNullOrWhiteSpace(traceId)) - { - traceId = ResolveTraceId(context); - } - - // 3. 写入上下文与响应头 - context.TraceIdentifier = traceId; - TraceContext.TraceId = traceId; - TraceContext.SpanId = spanId; - - context.Response.OnStarting(() => - { - context.Response.Headers[TraceHeader] = traceId; - context.Response.Headers[SpanHeader] = spanId; - return Task.CompletedTask; - }); - - // 4. 带 Scope 调用后续中间件 - using (logger.BeginScope(new Dictionary - { - ["TraceId"] = traceId, - ["SpanId"] = spanId - })) - { - try - { - await next(context); - } - finally - { - // 5. 清理上下文与活动 - TraceContext.Clear(); - if (ownsActivity) - { - activity.Stop(); - } - } - } - } - - private string ResolveTraceId(HttpContext context) - { - if (TryGetHeader(context, TraceHeader, out var traceId)) - { - return traceId; - } - - if (TryGetHeader(context, RequestHeader, out var requestId)) - { - return requestId; - } - - return idGenerator.NextId().ToString(); - } - - private static bool TryGetHeader(HttpContext context, string headerName, out string value) - { - if (context.Request.Headers.TryGetValue(headerName, out var values)) - { - var headerValue = values.ToString(); - if (!string.IsNullOrWhiteSpace(headerValue)) - { - value = headerValue; - return true; - } - } - - value = string.Empty; - return false; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs deleted file mode 100644 index 6af83c7..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs +++ /dev/null @@ -1,139 +0,0 @@ -using FluentValidation.Results; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Collections.Generic; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Results; -using FluentValidationException = FluentValidation.ValidationException; -using SharedValidationException = TakeoutSaaS.Shared.Abstractions.Exceptions.ValidationException; - -namespace TakeoutSaaS.Shared.Web.Middleware; - -/// -/// 全局异常处理中间件,将异常统一映射为 ApiResponse。 -/// -public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger, IHostEnvironment environment) -{ - private static readonly HashSet AllowedHttpErrorCodes = new() - { - ErrorCodes.BadRequest, - ErrorCodes.Unauthorized, - ErrorCodes.Forbidden, - ErrorCodes.NotFound, - ErrorCodes.Conflict, - ErrorCodes.ValidationFailed - }; - - private static readonly JsonSerializerOptions SerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - /// - /// 中间件入口,捕获并统一处理异常。 - /// - /// HTTP 上下文。 - public async Task InvokeAsync(HttpContext context) - { - try - { - await next(context); - } - catch (Exception ex) - { - // 1. 记录异常 - logger.LogError(ex, "未处理异常:{Message}", ex.Message); - // 2. 返回统一错误响应 - await HandleExceptionAsync(context, ex); - } - } - - private Task HandleExceptionAsync(HttpContext context, Exception exception) - { - // 1. 构建错误响应与状态码 - var (statusCode, response) = BuildErrorResponse(exception); - - if (environment.IsDevelopment()) - { - // 2. 开发环境附加细节 - response = response with - { - Message = exception.Message, - Errors = new - { - response.Errors, - detail = exception.ToString() - } - }; - } - - // 3. 写入响应 - context.Response.StatusCode = statusCode; - context.Response.ContentType = "application/json"; - return context.Response.WriteAsJsonAsync(response, SerializerOptions); - } - - private static (int StatusCode, ApiResponse Response) BuildErrorResponse(Exception exception) - { - return exception switch - { - DbUpdateConcurrencyException => ( - StatusCodes.Status409Conflict, - ApiResponse.Error( - ErrorCodes.Conflict, - "数据已被他人修改,请刷新后重试", - new Dictionary - { - ["RowVersion"] = ["数据已被他人修改,请刷新后重试"] - })), - UnauthorizedAccessException => ( - StatusCodes.Status403Forbidden, - ApiResponse.Error(ErrorCodes.Forbidden, "无权访问该资源")), - SharedValidationException validationException => ( - StatusCodes.Status422UnprocessableEntity, - ApiResponse.Error(ErrorCodes.ValidationFailed, "请求参数验证失败", validationException.Errors)), - FluentValidationException fluentValidationException => ( - StatusCodes.Status422UnprocessableEntity, - ApiResponse.Error( - ErrorCodes.ValidationFailed, - "请求参数验证失败", - NormalizeValidationErrors(fluentValidationException.Errors))), - BusinessException businessException => ( - // 1. 仅当业务错误码在白名单且位于 400-499 时透传,否则回退 400 - AllowedHttpErrorCodes.Contains(businessException.ErrorCode) && businessException.ErrorCode is >= 400 and < 500 - ? businessException.ErrorCode - : StatusCodes.Status400BadRequest, - ApiResponse.Error(businessException.ErrorCode, businessException.Message)), - _ => ( - StatusCodes.Status500InternalServerError, - ApiResponse.Error(ErrorCodes.InternalServerError, "服务器开小差啦,请稍后再试")) - }; - } - - private static IDictionary NormalizeValidationErrors(IEnumerable failures) - { - var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var failure in failures) - { - var key = string.IsNullOrWhiteSpace(failure.PropertyName) ? "request" : failure.PropertyName; - if (!result.TryGetValue(key, out var list)) - { - list = new List(); - result[key] = list; - } - - if (!string.IsNullOrWhiteSpace(failure.ErrorMessage)) - { - list.Add(failure.ErrorMessage); - } - } - - return result.ToDictionary(pair => pair.Key, pair => pair.Value.Distinct().ToArray(), StringComparer.OrdinalIgnoreCase); - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs deleted file mode 100644 index bd7a983..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using System.Diagnostics; -using TakeoutSaaS.Shared.Abstractions.Diagnostics; - -namespace TakeoutSaaS.Shared.Web.Middleware; - -/// -/// 基础请求日志(方法、路径、耗时、状态码、TraceId)。 -/// -public sealed class RequestLoggingMiddleware(RequestDelegate next, ILogger logger) -{ - /// - /// 记录请求日志并调用后续中间件。 - /// - /// HTTP 上下文。 - public async Task InvokeAsync(HttpContext context) - { - // 1. 启动计时 - var stopwatch = Stopwatch.StartNew(); - try - { - await next(context); - } - finally - { - // 2. 结束计时并输出日志 - stopwatch.Stop(); - var traceId = TraceContext.TraceId ?? context.TraceIdentifier; - var spanId = TraceContext.SpanId ?? Activity.Current?.SpanId.ToString() ?? string.Empty; - logger.LogInformation( - "HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId} SpanId:{SpanId}", - context.Request.Method, - context.Request.Path, - context.Response.StatusCode, - stopwatch.Elapsed.TotalMilliseconds, - traceId, - spanId); - } - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs b/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs deleted file mode 100644 index dbc8773..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace TakeoutSaaS.Shared.Web.Middleware; - -/// -/// 安全响应头中间件 -/// -public sealed class SecurityHeadersMiddleware(RequestDelegate next) -{ - /// - /// 设置基础安全响应头。 - /// - /// HTTP 上下文。 - public async Task InvokeAsync(HttpContext context) - { - // 1. 写入安全响应头 - var headers = context.Response.Headers; - headers["X-Content-Type-Options"] = "nosniff"; - headers["X-Frame-Options"] = "DENY"; - headers["X-XSS-Protection"] = "1; mode=block"; - headers["Referrer-Policy"] = "no-referrer"; - // 2. 继续后续管道 - await next(context); - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs deleted file mode 100644 index 630789b..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Security/ClaimsPrincipalExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Security.Claims; - -namespace TakeoutSaaS.Shared.Web.Security; - -/// -/// ClaimsPrincipal 便捷扩展 -/// -public static class ClaimsPrincipalExtensions -{ - /// - /// 获取当前用户 Id(不存在时返回 0)。 - /// - public static long GetUserId(this ClaimsPrincipal? principal) - { - if (principal == null) - { - return 0; - } - - var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier) - ?? principal.FindFirstValue("sub"); - - return long.TryParse(identifier, out var userId) - ? userId - : 0; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs deleted file mode 100644 index bc43ea8..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Security/HttpContextCurrentUserAccessor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Security.Claims; -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Shared.Web.Security; - -/// -/// 基于 HttpContext 的当前用户访问器。 -/// -/// -/// 初始化访问器。 -/// -public sealed class HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) : ICurrentUserAccessor -{ - /// - public long UserId - { - get - { - var principal = httpContextAccessor.HttpContext?.User; - if (principal == null || !principal.Identity?.IsAuthenticated == true) - { - return 0; - } - - var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier) - ?? principal.FindFirstValue("sub"); - - return long.TryParse(identifier, out var id) ? id : 0; - } - } - - /// - public bool IsAuthenticated => UserId != 0; -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs deleted file mode 100644 index 12832c9..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Security/TenantHttpContextExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Shared.Web.Security; - -/// -/// HttpContext 租户扩展方法。 -/// -public static class TenantHttpContextExtensions -{ - /// - /// 获取 HttpContext.Items 中缓存的租户上下文。 - /// - /// 当前 HttpContext - /// 租户上下文,若不存在则返回 null - public static TenantContext? GetTenantContext(this HttpContext? context) - { - if (context == null) - { - return null; - } - - if (context.Items.TryGetValue(TenantConstants.HttpContextItemKey, out var value) && - value is TenantContext tenantContext) - { - return tenantContext; - } - - return null; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs deleted file mode 100644 index cfa1753..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; -namespace TakeoutSaaS.Shared.Web.Swagger; - -/// -/// 根据 API 版本动态注册 Swagger 文档。 -/// -internal sealed class ConfigureSwaggerOptions( - IApiVersionDescriptionProvider provider, - IOptions settings) : IConfigureOptions -{ - private readonly SwaggerDocumentSettings _settings = settings.Value; - - /// - /// 根据 API 版本生成 Swagger 文档配置。 - /// - public void Configure(SwaggerGenOptions options) - { - // 1. 为每个 API 版本注册文档 - foreach (var description in provider.ApiVersionDescriptions) - { - var info = new OpenApiInfo - { - Title = $"{_settings.Title} {description.ApiVersion}", - Version = description.ApiVersion.ToString(), - Description = description.IsDeprecated - ? $"{_settings.Description}(该版本已弃用)" - : _settings.Description - }; - options.SwaggerGeneratorOptions.SwaggerDocs[description.GroupName] = info; - } - - // 2. 配置 JWT 授权信息 - if (_settings.EnableAuthorization) - { - const string bearerSchemeName = "Bearer"; - var scheme = new OpenApiSecurityScheme - { - Name = "Authorization", - Description = "在下方输入Bearer Token,格式:Bearer {token}", - In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, - Scheme = "bearer", - BearerFormat = "JWT" - }; - options.AddSecurityDefinition(bearerSchemeName, scheme); - options.AddSecurityRequirement(document => - { - var requirement = new OpenApiSecurityRequirement - { - { new OpenApiSecuritySchemeReference(bearerSchemeName, document, null), new List() } - }; - return requirement; - }); - } - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs deleted file mode 100644 index f0410e0..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerDocumentSettings.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace TakeoutSaaS.Shared.Web.Swagger; - -/// -/// Swagger 文档配置。 -/// -public class SwaggerDocumentSettings -{ - /// - /// 文档标题。 - /// - public string Title { get; set; } = "TakeoutSaaS API"; - - /// - /// 描述信息。 - /// - public string? Description { get; set; } - - /// - /// 是否启用 JWT Authorize 按钮。 - /// - public bool EnableAuthorization { get; set; } = true; -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs deleted file mode 100644 index 0cfe43b..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.IO; -using Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Swashbuckle.AspNetCore.Annotations; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace TakeoutSaaS.Shared.Web.Swagger; - -/// -/// Swagger 注册/启用扩展。 -/// -public static class SwaggerExtensions -{ - /// - /// 注入统一的 Swagger 服务。 - /// - public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action? configure = null) - { - // 1. 注册 Swagger 并加载 XML 注释以展示中文文档 - services.AddSwaggerGen(options => - { - var basePath = AppContext.BaseDirectory; - var xmlFiles = Directory.GetFiles(basePath, "*.xml"); - foreach (var xml in xmlFiles) - { - options.IncludeXmlComments(xml, true); - } - options.EnableAnnotations(); - }); - services.AddSingleton(_ => - { - var settings = new SwaggerDocumentSettings(); - configure?.Invoke(settings); - return settings; - }); - services.AddSingleton>(provider => - new ConfigureSwaggerOptions( - provider.GetRequiredService(), - Options.Create(provider.GetRequiredService()))); - return services; - } - - /// - /// 开发环境启用 Swagger UI(自动注册所有版本)。 - /// - public static IApplicationBuilder UseSharedSwagger(this IApplicationBuilder app) - { - var provider = app.ApplicationServices.GetRequiredService(); - var settings = app.ApplicationServices.GetRequiredService(); - const string routePrefix = "api/docs"; - const string legacyRoutePrefix = "swagger"; - // 1. 注册 Swagger 中间件(新旧入口同时支持) - app.UseSwagger(options => { options.RouteTemplate = $"{routePrefix}/{{documentName}}/swagger.json"; }); - app.UseSwagger(options => { options.RouteTemplate = $"{legacyRoutePrefix}/{{documentName}}/swagger.json"; }); - app.UseSwaggerUI(options => - { - options.RoutePrefix = routePrefix; - foreach (var description in provider.ApiVersionDescriptions) - { - // 3. 使用相对路径适配反向代理/网关前缀 - options.SwaggerEndpoint( - $"./{description.GroupName}/swagger.json", - $"{settings.Title} {description.ApiVersion}"); - } - // 2. 显示请求耗时 - options.DisplayRequestDuration(); - }); - app.UseSwaggerUI(options => - { - options.RoutePrefix = legacyRoutePrefix; - foreach (var description in provider.ApiVersionDescriptions) - { - // 3. 使用相对路径适配反向代理/网关前缀 - options.SwaggerEndpoint( - $"./{description.GroupName}/swagger.json", - $"{settings.Title} {description.ApiVersion}"); - } - // 2. 显示请求耗时 - options.DisplayRequestDuration(); - }); - return app; - } -} diff --git a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj deleted file mode 100644 index ddb24c5..0000000 --- a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - net10.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj index fc99d63..9ef8b2e 100644 --- a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj +++ b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj @@ -7,7 +7,7 @@ 1591 - + diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index 931f2ccfa3441a5b23fff5f0dd880731e4dfd0e2..a9fa7f187de27854b44f72582dd1403af328a001 100644 GIT binary patch delta 108 zcmeBBnV`Di1~0Q7gWlx(ywb`}45bX23^@!bKr)XZoxusn&Syww$Yv;JhymgvhUCcy S`HUxX@a-W*&*p=C?#uwg Y=K$3t1JxD-brmxdF(hy1WLeD!0GHtqivR!s delta 12 Tcmeyty@h)N56k8NmKBTuAD9F) diff --git a/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj b/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj index 8001b38..def0807 100644 --- a/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj +++ b/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj index 8cb6277..ec2e881 100644 --- a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj +++ b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj @@ -5,7 +5,7 @@ enable - + diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj index 1350a29..1bc196e 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj +++ b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj index ad73a3f..47e609a 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj index 7025f49..90460da 100644 --- a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj +++ b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj index e5ec475bb63bb015a8531f83251a2c8d13746b1b..16354c59ffb2c7dc490e9b76970cf446b9258898 100644 GIT binary patch delta 56 zcmZ1=^h9Vw1v|4IgWlwNc4=iNhEj%1h8%_zAeqOI&fo-O=QAWTWHS^q!~k&-L-OWQ H_SK94jqVMZ delta 12 TcmaDNv_NP>1^ebb>?;@nBP#^= diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj index db28803..fdb3727 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj @@ -5,7 +5,7 @@ enable - + diff --git a/商品模块_API设计_v1.md b/商品模块_API设计_v1.md deleted file mode 100644 index d6a1150..0000000 --- a/商品模块_API设计_v1.md +++ /dev/null @@ -1,426 +0,0 @@ -# 商品模块 API 设计(v1) -> 企业级/混合模式版 - -## 0. 目标与范围 -- 面向商业化外卖 SaaS,覆盖“总部主库 + 门店私库 + 多维度经营 + 可配置开关 + 可扩展集成”。 -- 本文仅讨论 API 设计与契约,不涉及实现细节。 -- 默认对齐项目现有约束:多租户、CQRS、统一响应、Snowflake ID、JWT/RBAC。 - -## 1. 通用规范 -### 1.1 路由与版本 -- AdminApi:`/api/admin/v1/...` -- MiniApi:`/api/mini/v1/...` -- UserApi:`/api/user/v1/...` - -### 1.2 鉴权与租户 -- Header:`Authorization: Bearer {token}` -- 租户:`X-Tenant-Id` 或 `X-Tenant-Code`(必填,除白名单路径) -- 角色权限:AdminApi 必须绑定 `PermissionAuthorize` 权限码 - -### 1.3 响应格式 -统一使用 `ApiResponse`,与现有 `Shared.Web` 约定一致。 - -### 1.4 ID 与并发 -- 所有 `long` 类型 ID 在 API 中 **序列化为 string**。 -- 更新类接口需携带 `rowVersion`(Base64)或 `If-Match` 以并发控制。 - -### 1.5 幂等与限流 -- 创建、批量变更、导入等写接口支持 `Idempotency-Key`。 -- 面向公网端启用限流策略,读接口优先缓存。 - -### 1.6 分页与排序 -统一参数:`page`、`pageSize`、`sortBy`、`sortOrder`(`asc|desc`)。 - -## 2. 产品原则(10 年外卖 SaaS 视角) -- 品牌一致性优先:总部主库保证品牌统一,门店仅允许“可控范围内的微调”。 -- 经营灵活性必备:门店私有商品与局部覆盖是应对“城市、商圈、人力”的关键。 -- C 端效率优先:类目不超过 2 级,菜单渲染优先走聚合与缓存。 -- 扩展优先:渠道/场景/时段/三方同步都必须可开关,避免“为少数租户拖累成本”。 - -## 3. 功能域拆分(可开关) -### 3.1 核心域 -- 公共商品库(Master Library,总部商品) -- 门店私有库(Store Library,本地特色) -- 引用/下发机制(Push & Pull) -- 类目管理(2 级以内 + 时段可见) -- 商品(Product/SPU)与规格 SKU -- 场景/渠道/时段维度可见性 -- 计价与打包费策略 -- 库存视图与沽清(含每日重置) - -### 3.2 可选域 -- 加料/口味(Addon/Modifier) -- 套餐/组合(Bundle/N 选 M) -- 称重计价与时价 -- 后厨生产(KDS/打印标签/台位) -- 三方平台同步(美团/饿了么/抖音) -- 审核流与定时上架 -- 多语言(I18n) -- 评分/销量统计视图(Stats) - -## 4. 核心架构:公共商品库 + 门店私有库 + Push/Pull -### 4.1 公共商品库(Tenant/Master Library) -- 定义:总部创建的标准化商品。 -- 作用:维护品牌统一形象(名称、图、描述、营养、后厨分类)。 -- 管控:总部可锁定核心字段,门店仅可引用,不可篡改。 - -### 4.2 门店私有库(Store Private Library) -- 定义:门店为本地市场创建的特色商品。 -- 作用:一店一策(开业活动、地域限定等)。 -- 权限:仅本门店可见,总部可审计但默认不干预。 - -### 4.3 引用与下发(Push & Pull) -- 总部推送(Push):支持“静默上架”或“待门店确认”。 -- 门店拉取(Pull):门店经理从公共库勾选引入到本店经营列表。 -- 门店引用后允许“局部覆盖”,但不破坏主库锁定字段。 - -### 4.4 混合视图标识 -- API 输出 `libraryType` + `masterProductId`,便于后台列表用标签区分来源。 -- `lockedFields` 返回总部锁定字段,避免门店误操作。 - -## 5. 维度管理:类目、场景、渠道与时段 -### 5.1 类目(2 级以内) -- 类目支持“生效时段”,如早餐类目 10:00 后隐藏。 -- 类目可绑定“场景”,如堂食专属类目。 - -### 5.2 场景(履约场景) -- 堂食(DineIn)、外卖(Delivery)、自提(Pickup)。 -- 外卖场景强制打包费规则;堂食可免打包费。 - -### 5.3 渠道(流量入口) -- 微信小程序、POS 点餐、美团、饿了么、抖音等。 -- 支持“渠道隔离”:显示顺序、价格、上下架状态可独立配置。 - -### 5.4 维度优先级(建议) -门店覆盖 > 渠道配置 > 时段配置 > 商品基础配置。 - -## 6. 核心业务规则 -### 6.1 覆盖机制(Override Rule) -- 门店可对价格、场景、上架/沽清做覆盖。 -- 被锁定字段不允许覆盖;如需调整须总部解锁或走审核。 - -### 6.2 计价与打包费 -- 计价模式:固定单价、按克计价(称重菜)、时价(随行就市)。 -- 打包费支持按 SKU 设置,且可按场景配置(堂食可为 0)。 -- 打包费支持单单封顶(不超过 X 元)。 - -### 6.3 库存与自动重置 -- 支持门店级“每日自动恢复初始库存”(默认凌晨执行)。 -- 沽清为临时状态,不影响主库与其他门店。 - -### 6.4 规格、加料与套餐 -- SKU 影响价格与库存。 -- 加料支持“收费加料 + 免费属性”,可配置选配上限/下限。 -- 动态套餐支持“N 选 M”,并要求库存穿透: - - 套餐内关键单品沽清时,套餐自动联动下架。 - -### 6.5 生产与后厨(KDS/打印) -- 商品可绑定“打印标签 + 后厨台位”。 -- 订单下发需标示场景,以区分堂食/外卖/自提出餐逻辑。 - -### 6.6 三方平台同步 -- 内置 Mapping 机制,支持“商品—平台商品”映射。 -- 价格/沽清变动触发事件总线,异步同步到平台接口。 - -## 7. 权限与审计 -- 总部运营:管理主库、类目、全局规则、价格上限、审核流。 -- 门店经理:门店私有商品、门店覆盖、今日沽清。 -- 字段级审计日志:记录“谁在何时修改了哪个门店商品的价格/状态”。 -- 推送审计:记录主库变更的下发范围与结果。 - -## 8. 功能开关(租户级) -用于“商业化套餐可选启用”。建议 AdminApi 提供读取能力,写入由套餐/配置管理模块控制。 - -示例结构: -```json -{ - "enableMasterLibrary": true, - "enableStoreLibrary": true, - "enablePushPull": true, - "enableVariant": true, - "enableAddon": true, - "enableBundle": true, - "enableSceneFilter": true, - "enableChannelIsolation": true, - "enableChannelPrice": true, - "enableTimePrice": true, - "enableStoreOverride": true, - "enablePricingWeight": true, - "enablePricingMarket": true, - "enablePackagingFee": true, - "enablePackagingFeeCap": true, - "enableDailyStockReset": true, - "enableInventory": true, - "enableApproval": false, - "enableScheduledPublish": true, - "enableKds": true, - "enableThirdPartySync": true, - "enableMultiLanguage": false, - "enableNutritionInfo": false -} -``` - -## 9. 关键 DTO(摘要) -> 字段命名遵循现有规范,布尔值使用 `Is/Has` 前缀。 - -### 9.1 CategoryDto -| 字段 | 说明 | -| --- | --- | -| id | 类目 ID(string) | -| parentId | 父级类目 ID | -| name | 类目名称 | -| sortOrder | 排序 | -| isEnabled | 是否启用 | -| availableScenes | 生效场景 | -| availableTimeRanges | 生效时段 | -| createdAt | 创建时间 | - -### 9.2 ProductDto(SPU) -| 字段 | 说明 | -| --- | --- | -| id | 商品 ID(string) | -| libraryType | Master/Store | -| masterProductId | 引用的主库商品 ID | -| storeId | 归属门店 | -| name | 商品名称 | -| categoryId | 类目 ID | -| unit | 单位 | -| tags | 标签 | -| coverImageUrl | 封面图 | -| imageUrls | 轮播图 | -| isEnabled | 是否启用 | -| isPublished | 是否上架 | -| hasSku | 是否包含 SKU | -| hasAddon | 是否包含加料 | -| pricingMode | Fixed/Weight/Market | -| basePrice | 基础价格 | -| packagingFee | 打包费(基础) | -| packagingFeeCap | 打包费封顶 | -| availableScenes | 生效场景 | -| availableChannels | 生效渠道 | -| lockedFields | 被总部锁定字段 | -| rowVersion | 并发字段 | - -### 9.3 SkuDto -| 字段 | 说明 | -| --- | --- | -| id | SKU ID(string) | -| productId | 商品 ID | -| specValues | 规格值列表 | -| price | 价格 | -| stock | 库存 | -| isEnabled | 是否启用 | -| rowVersion | 并发字段 | - -### 9.4 StoreProductOverrideDto -| 字段 | 说明 | -| --- | --- | -| storeId | 门店 ID | -| productId | 商品 ID | -| overridePrice | 覆盖价格 | -| overrideScenes | 覆盖场景 | -| isSoldOut | 是否沽清 | -| isApproved | 是否已审核 | -| overrideReason | 覆盖原因 | - -### 9.5 AddonGroupDto(可选) -| 字段 | 说明 | -| --- | --- | -| id | 组 ID | -| name | 组名 | -| minSelected | 最少选择 | -| maxSelected | 最多选择 | -| isRequired | 是否必选 | -| items | 加料项列表 | - -### 9.6 ChannelSettingDto(可选) -| 字段 | 说明 | -| --- | --- | -| channelCode | 渠道编码 | -| price | 渠道价格 | -| sortOrder | 渠道排序 | -| isEnabled | 是否启用 | - -### 9.7 ProductionProfileDto(可选) -| 字段 | 说明 | -| --- | --- | -| kitchenStationId | 后厨台位 | -| printTagId | 打印标签 | - -## 10. AdminApi(管理端)接口清单 -### 10.1 公共商品库(总部) -- `GET /api/admin/v1/master-products` -- `GET /api/admin/v1/master-products/{id}` -- `POST /api/admin/v1/master-products` -- `PUT /api/admin/v1/master-products/{id}` -- `PUT /api/admin/v1/master-products/{id}/lock-fields` -- `PUT /api/admin/v1/master-products/{id}/publish` -- `PUT /api/admin/v1/master-products/{id}/unpublish` - -### 10.2 门店私有库与经营商品 -- `GET /api/admin/v1/stores/{storeId}/products` -- `POST /api/admin/v1/stores/{storeId}/products`(创建门店私有商品) -- `POST /api/admin/v1/stores/{storeId}/products/pull`(从主库拉取) -- `PUT /api/admin/v1/stores/{storeId}/products/{id}` -- `PUT /api/admin/v1/stores/{storeId}/products/{id}/override` -- `PUT /api/admin/v1/stores/{storeId}/products/{id}/publish` -- `PUT /api/admin/v1/stores/{storeId}/products/{id}/unpublish` -- `PUT /api/admin/v1/stores/{storeId}/products/{id}/sold-out` - -### 10.3 总部推送(Push) -- `POST /api/admin/v1/master-products/{id}/push`(指定门店) -- `GET /api/admin/v1/master-products/{id}/push-tasks` -- `POST /api/admin/v1/push-tasks/{taskId}/retry` - -### 10.4 类目 -- `GET /api/admin/v1/categories` -- `POST /api/admin/v1/categories` -- `PUT /api/admin/v1/categories/{id}` -- `DELETE /api/admin/v1/categories/{id}` -- `PUT /api/admin/v1/categories/{id}/enable` -- `PUT /api/admin/v1/categories/{id}/disable` -- `PUT /api/admin/v1/categories/sort`(批量排序) -- `PUT /api/admin/v1/categories/{id}/schedule`(类目时段) -- `PUT /api/admin/v1/categories/{id}/scenes`(类目场景) - -### 10.5 规格与 SKU -- `GET /api/admin/v1/products/{id}/spec-groups` -- `PUT /api/admin/v1/products/{id}/spec-groups` -- `GET /api/admin/v1/products/{id}/skus` -- `POST /api/admin/v1/products/{id}/skus` -- `PUT /api/admin/v1/skus/{id}` -- `PUT /api/admin/v1/skus/{id}/enable` -- `PUT /api/admin/v1/skus/{id}/disable` -- `PUT /api/admin/v1/skus/{id}/price` -- `PUT /api/admin/v1/skus/{id}/stock` -- `PUT /api/admin/v1/skus/{id}/pricing-mode` -- `PUT /api/admin/v1/skus/{id}/packaging-fee` -- `PUT /api/admin/v1/skus/{id}/inventory-policy` - -### 10.6 场景/渠道/时段 -- `PUT /api/admin/v1/products/{id}/scenes` -- `PUT /api/admin/v1/products/{id}/channels` -- `PUT /api/admin/v1/products/{id}/time-slots` -- `PUT /api/admin/v1/products/{id}/channel-mappings` -- `POST /api/admin/v1/products/{id}/channel-sync` - -### 10.7 加料/口味(可选) -- `GET /api/admin/v1/products/{id}/addon-groups` -- `PUT /api/admin/v1/products/{id}/addon-groups` -- `PUT /api/admin/v1/addon-groups/{id}/items` -- `PUT /api/admin/v1/addon-groups/{id}/enable` -- `PUT /api/admin/v1/addon-groups/{id}/disable` - -### 10.8 套餐/组合(可选) -- `GET /api/admin/v1/bundles` -- `POST /api/admin/v1/bundles` -- `PUT /api/admin/v1/bundles/{id}` -- `PUT /api/admin/v1/bundles/{id}/publish` -- `PUT /api/admin/v1/bundles/{id}/unpublish` -- `PUT /api/admin/v1/bundles/{id}/items` -- `PUT /api/admin/v1/bundles/{id}/rules`(N 选 M) - -### 10.9 后厨生产(可选) -- `PUT /api/admin/v1/products/{id}/production-profile` -- `PUT /api/admin/v1/skus/{id}/production-profile` - -### 10.10 导入导出与索引 -- `POST /api/admin/v1/products/import` -- `GET /api/admin/v1/products/import/{taskId}` -- `GET /api/admin/v1/products/export` -- `POST /api/admin/v1/products/reindex` - -### 10.11 审计与日志 -- `GET /api/admin/v1/products/{id}/audit-logs` -- `GET /api/admin/v1/stores/{storeId}/products/{id}/override-logs` -- `GET /api/admin/v1/master-products/{id}/push-logs` - -### 10.12 功能开关读取 -- `GET /api/admin/v1/products/features` - -## 11. MiniApi(小程序端)接口清单 -- `GET /api/mini/v1/categories?scene=Delivery&channel=WeChatMiniProgram` -- `GET /api/mini/v1/menus/{storeId}?scene=Delivery&channel=WeChatMiniProgram` -- `GET /api/mini/v1/products?storeId=...&scene=Delivery&channel=WeChatMiniProgram` -- `GET /api/mini/v1/products/{id}?scene=Delivery&channel=WeChatMiniProgram` -- `GET /api/mini/v1/products/hot?storeId=...` -- `GET /api/mini/v1/products/recommended?storeId=...` -- `POST /api/mini/v1/products/price-estimate` -- `POST /api/mini/v1/products/checkout-validate` -- `POST /api/mini/v1/products/snapshots`(订单服务调用) - -## 12. UserApi(C 端用户)接口清单 -- `GET /api/user/v1/categories?scene=Delivery&channel=H5` -- `GET /api/user/v1/menus/{storeId}?scene=Delivery&channel=H5` -- `GET /api/user/v1/products?storeId=...&scene=Delivery&channel=H5` -- `GET /api/user/v1/products/{id}?scene=Delivery&channel=H5` -- `GET /api/user/v1/products/hot?storeId=...` -- `GET /api/user/v1/products/recommended?storeId=...` -- `POST /api/user/v1/products/price-estimate` -- `POST /api/user/v1/products/checkout-validate` - -## 13. 事件与扩展点 -采用 Outbox 模式输出领域事件,便于搜索索引、缓存失效、推荐计算与三方同步。 -- `MasterProductCreated` -- `MasterProductUpdated` -- `MasterProductPushed` -- `StoreProductPulled` -- `StoreProductOverridden` -- `ProductPriceChanged` -- `ProductAvailabilityChanged` -- `ProductSoldOutChanged` -- `SkuStockChanged` -- `ProductChannelSyncRequested` - -## 14. 示例(关键请求) -### 14.1 商品创建(总部主库) -```json -{ - "name": "黄金鸡排饭", - "categoryId": "1782328933492367360", - "unit": "份", - "coverImageUrl": "https://cdn/xxx.jpg", - "imageUrls": ["https://cdn/xxx1.jpg", "https://cdn/xxx2.jpg"], - "pricingMode": "Fixed", - "basePrice": 19.9, - "isEnabled": true -} -``` - -### 14.2 门店覆盖(价格 + 场景) -```json -{ - "overridePrice": 21.9, - "overrideScenes": ["Delivery", "Pickup"], - "isSoldOut": false, - "overrideReason": "外卖平台佣金调整" -} -``` - -### 14.3 结算校验(Mini/User) -```json -{ - "storeId": "1782328933492367000", - "scene": "Delivery", - "channel": "WeChatMiniProgram", - "items": [ - { - "productId": "1782328933492367360", - "skuId": "1782328933492367400", - "quantity": 2, - "addonItemIds": ["1782328933492367501", "1782328933492367502"] - } - ] -} -``` - -## 15. 依赖说明 -- 文件上传:复用 Storage 模块(FilesController)获取 URL。 -- 库存:优先对接 Inventory 模块,商品侧仅提供视图与校验。 -- 订单:下单时生成商品快照,避免历史价格漂移。 -- 后厨:KDS/打印由生产模块承接,商品仅配置绑定信息。 -- 三方同步:由集成服务监听事件并进行异步同步与重试。 -- 权限码:`product.read`、`product.write`、`product.publish`、`product.import` 等(待统一权限表配置)。 - ---- -**待确认**:渠道编码标准、称重计价精度与四舍五入规则、库存每日重置默认时间、Push 是否强制门店确认。