refactor: 抽离 Docs/BuildingBlocks 子模块
This commit is contained in:
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -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
|
||||
@@ -10,7 +10,7 @@
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(MSBuildProjectName)' == 'TakeoutSaaS.Infrastructure'">
|
||||
<PackageReference Include="MassTransit" Version="8.2.5" />
|
||||
|
||||
@@ -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个月)
|
||||
- 测试与修复
|
||||
- 文档完善
|
||||
- 部署上线
|
||||
- 运维监控
|
||||
|
||||
@@ -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<Order> GetOrderWithDetailsAsync(Guid orderId)
|
||||
{
|
||||
return await _dbContext.Orders
|
||||
.Include(o => o.OrderItems)
|
||||
.Include(o => o.Customer)
|
||||
.FirstOrDefaultAsync(o => o.Id == orderId);
|
||||
}
|
||||
|
||||
// Dapper - 高性能统计查询
|
||||
public async Task<OrderStatistics> 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<OrderStatistics>(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跨域配置
|
||||
|
||||
@@ -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文件纳入版本控制
|
||||
- 生产环境变更需要审核
|
||||
|
||||
@@ -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: <binary>
|
||||
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`:支付成功通知
|
||||
|
||||
1058
Document/05_部署运维.md
1058
Document/05_部署运维.md
File diff suppressed because it is too large
Load Diff
@@ -1,395 +0,0 @@
|
||||
# 外卖SaaS系统 - 开发规范
|
||||
|
||||
## 1. 代码规范
|
||||
|
||||
### 1.1 命名规范
|
||||
|
||||
#### C#命名规范
|
||||
```csharp
|
||||
// 类名:PascalCase
|
||||
public class OrderService { }
|
||||
|
||||
// 接口:I + PascalCase
|
||||
public interface IOrderRepository { }
|
||||
|
||||
// 方法:PascalCase
|
||||
public async Task<Order> 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
|
||||
/// <summary>
|
||||
/// 订单服务接口
|
||||
/// </summary>
|
||||
public interface IOrderService
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建订单
|
||||
/// </summary>
|
||||
/// <param name="request">订单创建请求</param>
|
||||
/// <returns>订单信息</returns>
|
||||
/// <exception cref="BusinessException">业务异常</exception>
|
||||
Task<OrderDto> CreateOrderAsync(CreateOrderRequest request);
|
||||
}
|
||||
|
||||
// 复杂业务逻辑添加注释
|
||||
public async Task<decimal> 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<ExceptionHandlingMiddleware> _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<Order> 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>(<scope>): <subject>
|
||||
|
||||
# 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<OrderService> _logger;
|
||||
private readonly IOptions<OrderSettings> _settings;
|
||||
|
||||
public async Task<OrderDto> 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<OrderDto>(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记录版本变更
|
||||
@@ -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 │
|
||||
│ - 可视化仪表板 │
|
||||
│ - 告警配置 │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
@@ -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` 的类、方法、属性必须有 `<summary>`。
|
||||
* **步骤注释**:超过 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. [ ] **配置硬编码**:是否直接写死了连接串或密钥?
|
||||
|
||||
---
|
||||
@@ -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 等)
|
||||
- 数据持久化与备份: 待补充
|
||||
- 监控与告警: 待补充
|
||||
- 变更记录: 待补充
|
||||
@@ -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<T>`)按下面顺序解析连接串:
|
||||
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。
|
||||
@@ -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 完整记录。
|
||||
- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。
|
||||
- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。
|
||||
@@ -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 监控与告警。
|
||||
@@ -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`,避免误写入。
|
||||
@@ -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/开关后重启即可。
|
||||
@@ -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%)。
|
||||
@@ -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. 操作审计:谁改了套餐、何时改、改了什么(合规必备)
|
||||
@@ -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
|
||||
@@ -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/配置当作“最终规范”确保环境一致性。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:性能优化与缓存、运营大盘与细分报表、测试与文档完善、上线与监控
|
||||
Binary file not shown.
232
README.md
232
README.md
@@ -1,231 +1,19 @@
|
||||
# 外卖SaaS系统 (TakeoutSaaS)
|
||||
# TakeoutSaaS.TenantApi
|
||||
|
||||
## 项目简介
|
||||
本仓库为 TakeoutSaaS 的租户侧 API 代码仓库(MiniApi/UserApi 等)。
|
||||
|
||||
外卖SaaS系统是一个基于.NET 10的多租户外卖管理平台,为中小型餐饮企业提供完整的外卖业务解决方案。系统采用现代化的技术栈,支持商家管理、菜品管理、订单处理、配送管理、支付集成等核心功能。
|
||||
## 子模块
|
||||
|
||||
### 核心特性
|
||||
- `TakeoutSaaS.BuildingBlocks/`:共享基础组件(Shared/Building Blocks)
|
||||
- `TakeoutSaaS.Docs/`:全部文档 + 运维文件 + 脚本
|
||||
|
||||
- **多租户架构**:支持多租户数据隔离,SaaS模式运营
|
||||
- **商家管理**:完善的商家入驻、门店管理、菜品管理功能
|
||||
- **订单管理**:订单全生命周期管理,实时状态跟踪
|
||||
- **配送管理**:配送任务、路线规划、第三方配送对接
|
||||
- **支付集成**:支持微信、支付宝等多种支付方式
|
||||
- **营销功能**:优惠券、满减活动、会员积分
|
||||
- **字典管理**:系统字典、租户覆盖、批量导入导出、缓存监控
|
||||
- **数据分析**:实时数据统计、经营报表、趋势分析
|
||||
- **安全可靠**:JWT认证、权限控制、数据加密
|
||||
## 初始化
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端技术
|
||||
- **.NET 10**:最新的.NET平台
|
||||
- **ASP.NET Core Web API**:RESTful API服务
|
||||
- **Entity Framework Core 10**:最新ORM框架
|
||||
- **Dapper 2.1+**:高性能数据访问
|
||||
- **PostgreSQL 16+**:主数据库
|
||||
- **Redis 7.0+**:分布式缓存
|
||||
- **RabbitMQ 3.12+**:消息队列
|
||||
|
||||
### 开发框架
|
||||
- **AutoMapper**:对象映射
|
||||
- **FluentValidation**:数据验证
|
||||
- **Serilog**:结构化日志
|
||||
- **MediatR**:CQRS模式
|
||||
- **Hangfire**:后台任务
|
||||
- **Swagger**:API文档
|
||||
|
||||
## 运行条件
|
||||
|
||||
### 开发环境要求
|
||||
* .NET SDK 10.0 或更高版本
|
||||
* PostgreSQL 16+
|
||||
* Redis 7.0+
|
||||
* RabbitMQ 3.12+(可选)
|
||||
* Docker Desktop(推荐,用于容器化开发)
|
||||
|
||||
### 推荐IDE
|
||||
* Visual Studio 2022
|
||||
* JetBrains Rider
|
||||
* Visual Studio Code
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
```bash
|
||||
git clone https://github.com/your-org/takeout-saas.git
|
||||
cd takeout-saas
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### 2. 使用Docker Compose启动依赖服务(推荐)
|
||||
```bash
|
||||
# 启动PostgreSQL、Redis、RabbitMQ等服务
|
||||
docker-compose up -d
|
||||
## 文档入口
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 3. 配置数据库连接
|
||||
编辑 `src/TakeoutSaaS.Api/appsettings.Development.json`
|
||||
|
||||
### 4. 执行数据库迁移
|
||||
```bash
|
||||
cd src/TakeoutSaaS.Api
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
### 5. 运行项目
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
访问 API 文档:
|
||||
- 管理后台 AdminApi Swagger:http://localhost:5001/api/docs
|
||||
- 小程序/用户端 MiniApi Swagger:http://localhost:5002/api/docs
|
||||
|
||||
## 字典管理
|
||||
|
||||
> 最后更新日期: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`
|
||||
|
||||
1
TakeoutSaaS.BuildingBlocks
Submodule
1
TakeoutSaaS.BuildingBlocks
Submodule
Submodule TakeoutSaaS.BuildingBlocks added at bcf0a6bd7d
1
TakeoutSaaS.Docs
Submodule
1
TakeoutSaaS.Docs
Submodule
Submodule TakeoutSaaS.Docs added at 88ad71041b
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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`,再运行脚本重新创建。 |
|
||||
@@ -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."
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 状态"
|
||||
@@ -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
|
||||
@@ -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 <host> -a <pwd> 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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,27 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// 数据源名称常量,统一配置键与使用说明。
|
||||
/// </summary>
|
||||
public static class DatabaseConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认业务库(AppDatabase).
|
||||
/// </summary>
|
||||
public const string AppDataSource = "AppDatabase";
|
||||
|
||||
/// <summary>
|
||||
/// 身份认证库(IdentityDatabase)。
|
||||
/// </summary>
|
||||
public const string IdentityDataSource = "IdentityDatabase";
|
||||
|
||||
/// <summary>
|
||||
/// 字典库(DictionaryDatabase)。
|
||||
/// </summary>
|
||||
public const string DictionaryDataSource = "DictionaryDatabase";
|
||||
|
||||
/// <summary>
|
||||
/// 日志库(LogsDatabase)。
|
||||
/// </summary>
|
||||
public const string LogsDataSource = "LogsDatabase";
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// 统一错误码常量。
|
||||
/// </summary>
|
||||
public static class ErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求参数错误。
|
||||
/// </summary>
|
||||
public const int BadRequest = 400;
|
||||
|
||||
/// <summary>
|
||||
/// 未授权访问。
|
||||
/// </summary>
|
||||
public const int Unauthorized = 401;
|
||||
|
||||
/// <summary>
|
||||
/// 权限不足。
|
||||
/// </summary>
|
||||
public const int Forbidden = 403;
|
||||
|
||||
/// <summary>
|
||||
/// 资源未找到。
|
||||
/// </summary>
|
||||
public const int NotFound = 404;
|
||||
|
||||
/// <summary>
|
||||
/// 资源冲突。
|
||||
/// </summary>
|
||||
public const int Conflict = 409;
|
||||
|
||||
/// <summary>
|
||||
/// 校验失败。
|
||||
/// </summary>
|
||||
public const int ValidationFailed = 422;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器内部错误。
|
||||
/// </summary>
|
||||
public const int InternalServerError = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 业务自定义错误(10000+)。
|
||||
/// </summary>
|
||||
public const int BusinessError = 10001;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库连接角色,用于区分主写与从读连接。
|
||||
/// </summary>
|
||||
public enum DatabaseConnectionRole
|
||||
{
|
||||
/// <summary>
|
||||
/// 主写连接,用于写入或强一致读。
|
||||
/// </summary>
|
||||
Write = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 从读连接,用于只读查询或报表。
|
||||
/// </summary>
|
||||
Read = 2
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Data;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Dapper 查询/命令执行器抽象,封装连接获取与读写路由。
|
||||
/// </summary>
|
||||
public interface IDapperExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用指定数据源与读写角色执行异步查询,并返回结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">查询结果类型。</typeparam>
|
||||
/// <param name="dataSourceName">逻辑数据源名称。</param>
|
||||
/// <param name="role">连接角色(读/写)。</param>
|
||||
/// <param name="query">查询委托,提供已打开的连接和取消标记。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>查询结果。</returns>
|
||||
Task<TResult> QueryAsync<TResult>(
|
||||
string dataSourceName,
|
||||
DatabaseConnectionRole role,
|
||||
Func<IDbConnection, CancellationToken, Task<TResult>> query,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定数据源与读写角色执行异步命令。
|
||||
/// </summary>
|
||||
/// <param name="dataSourceName">逻辑数据源名称。</param>
|
||||
/// <param name="role">连接角色(读/写)。</param>
|
||||
/// <param name="command">命令委托,提供已打开的连接和取消标记。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步执行任务。</returns>
|
||||
Task ExecuteAsync(
|
||||
string dataSourceName,
|
||||
DatabaseConnectionRole role,
|
||||
Func<IDbConnection, CancellationToken, Task> command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定数据源及角色的默认命令超时时间(秒)。
|
||||
/// </summary>
|
||||
/// <param name="dataSourceName">逻辑数据源名称。</param>
|
||||
/// <param name="role">连接角色,默认读取从库。</param>
|
||||
/// <returns>命令超时时间(秒)。</returns>
|
||||
int GetDefaultCommandTimeoutSeconds(
|
||||
string dataSourceName,
|
||||
DatabaseConnectionRole role = DatabaseConnectionRole.Read);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// 轻量级 TraceId/SpanId 上下文,便于跨层访问当前请求的追踪标识。
|
||||
/// </summary>
|
||||
public static class TraceContext
|
||||
{
|
||||
private static readonly AsyncLocal<string?> TraceIdHolder = new();
|
||||
private static readonly AsyncLocal<string?> SpanIdHolder = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求的 TraceId。
|
||||
/// </summary>
|
||||
public static string? TraceId
|
||||
{
|
||||
get => TraceIdHolder.Value;
|
||||
set => TraceIdHolder.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求的 SpanId。
|
||||
/// </summary>
|
||||
public static string? SpanId
|
||||
{
|
||||
get => SpanIdHolder.Value;
|
||||
set => SpanIdHolder.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理 TraceId,避免 AsyncLocal 污染其它请求。
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
TraceIdHolder.Value = null;
|
||||
SpanIdHolder.Value = null;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 审计实体基类:提供创建、更新时间以及软删除时间。
|
||||
/// </summary>
|
||||
public abstract class AuditableEntityBase : EntityBase, IAuditableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次更新时间(UTC),从未更新时为 null。
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 软删除时间(UTC),未删除时为 null。
|
||||
/// </summary>
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
public long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除人用户标识(软删除),未删除时为 null。
|
||||
/// </summary>
|
||||
public long? DeletedBy { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 实体基类,统一提供主键标识。
|
||||
/// </summary>
|
||||
public abstract class EntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 实体唯一标识。
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 审计字段接口:提供创建、更新、删除时间与操作者标识。
|
||||
/// </summary>
|
||||
public interface IAuditableEntity : ISoftDeleteEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间(UTC),未更新时为 null。
|
||||
/// </summary>
|
||||
DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除时间(UTC),未删除时为 null。
|
||||
/// </summary>
|
||||
new DateTime? DeletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除人用户标识(软删除),未删除时为 null。
|
||||
/// </summary>
|
||||
long? DeletedBy { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 多租户实体约定:所有持久化实体须包含租户标识字段。
|
||||
/// </summary>
|
||||
public interface IMultiTenantEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
long TenantId { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 软删除实体约定:提供可空的删除时间戳以支持全局过滤。
|
||||
/// </summary>
|
||||
public interface ISoftDeleteEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 删除时间(UTC),未删除时为 null。
|
||||
/// </summary>
|
||||
DateTime? DeletedAt { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 多租户审计实体基类:提供租户标识、审计字段与软删除标记。
|
||||
/// </summary>
|
||||
public abstract class MultiTenantEntityBase : AuditableEntityBase, IMultiTenantEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public long TenantId { get; set; }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 业务异常(用于可预期的业务校验错误)。
|
||||
/// </summary>
|
||||
public class BusinessException(int errorCode, string message) : Exception(message)
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务错误码。
|
||||
/// </summary>
|
||||
public int ErrorCode { get; } = errorCode;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 验证异常(用于聚合验证错误信息)。
|
||||
/// </summary>
|
||||
public class ValidationException(IDictionary<string, string[]> errors) : Exception("一个或多个验证错误")
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段/属性的错误集合。
|
||||
/// </summary>
|
||||
public IDictionary<string, string[]> Errors { get; } = errors;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
/// <summary>
|
||||
/// 雪花 ID 生成器接口。
|
||||
/// </summary>
|
||||
public interface IIdGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 生成下一个唯一长整型 ID。
|
||||
/// </summary>
|
||||
/// <returns>雪花 ID。</returns>
|
||||
long NextId();
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
/// <summary>
|
||||
/// 雪花 ID 生成器配置。
|
||||
/// </summary>
|
||||
public sealed class IdGeneratorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置节名称。
|
||||
/// </summary>
|
||||
public const string SectionName = "IdGenerator";
|
||||
|
||||
/// <summary>
|
||||
/// 工作节点标识,0-31。
|
||||
/// </summary>
|
||||
[Range(0, 31)]
|
||||
public int WorkerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 机房标识,0-31。
|
||||
/// </summary>
|
||||
[Range(0, 31)]
|
||||
public int DatacenterId { get; set; }
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
/// <summary>
|
||||
/// 非泛型便捷封装。
|
||||
/// </summary>
|
||||
public static class ApiResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 仅返回成功消息(无数据)。
|
||||
/// </summary>
|
||||
/// <param name="message">提示信息。</param>
|
||||
/// <returns>封装后的成功响应。</returns>
|
||||
public static ApiResponse<object> Success(string? message = "操作成功")
|
||||
=> ApiResponse<object>.Ok(message: message);
|
||||
|
||||
/// <summary>
|
||||
/// 成功且携带数据。
|
||||
/// </summary>
|
||||
/// <param name="data">业务数据。</param>
|
||||
/// <param name="message">提示信息。</param>
|
||||
/// <returns>封装后的成功响应。</returns>
|
||||
public static ApiResponse<object> Ok(object? data, string? message = "操作成功")
|
||||
=> data is null ? ApiResponse<object>.Ok(message: message) : ApiResponse<object>.Ok(data, message);
|
||||
|
||||
/// <summary>
|
||||
/// 错误返回。
|
||||
/// </summary>
|
||||
/// <param name="code">错误码。</param>
|
||||
/// <param name="message">错误提示。</param>
|
||||
/// <returns>封装后的失败响应。</returns>
|
||||
public static ApiResponse<object> Failure(int code, string message)
|
||||
=> ApiResponse<object>.Error(code, message);
|
||||
|
||||
/// <summary>
|
||||
/// 错误返回(附带详情)。
|
||||
/// </summary>
|
||||
/// <param name="code">错误码。</param>
|
||||
/// <param name="message">错误提示。</param>
|
||||
/// <param name="errors">错误详情。</param>
|
||||
/// <returns>封装后的失败响应。</returns>
|
||||
public static ApiResponse<object> Error(int code, string message, object? errors = null)
|
||||
=> ApiResponse<object>.Error(code, message, errors);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
/// <summary>
|
||||
/// 统一的 API 返回结果包装。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据载荷类型。</typeparam>
|
||||
public sealed record ApiResponse<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否成功。
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态/错误码(默认 200)。
|
||||
/// </summary>
|
||||
public int Code { get; init; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// 提示信息。
|
||||
/// </summary>
|
||||
public string? Message { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务数据。
|
||||
/// </summary>
|
||||
public T? Data { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误详情(如字段验证错误)。
|
||||
/// </summary>
|
||||
public object? Errors { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// TraceId,便于链路追踪。
|
||||
/// </summary>
|
||||
public string TraceId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 时间戳(UTC)。
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 成功返回。
|
||||
/// </summary>
|
||||
/// <param name="data">业务数据。</param>
|
||||
/// <param name="message">提示信息。</param>
|
||||
/// <returns>封装后的成功响应。</returns>
|
||||
public static ApiResponse<T> Ok(T data, string? message = "操作成功")
|
||||
=> Create(true, 200, message, data);
|
||||
|
||||
/// <summary>
|
||||
/// 无数据的成功返回。
|
||||
/// </summary>
|
||||
/// <param name="message">提示信息。</param>
|
||||
/// <returns>封装后的成功响应。</returns>
|
||||
public static ApiResponse<T> Ok(string? message = "操作成功")
|
||||
=> Create(true, 200, message, default);
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧名称:成功结果。
|
||||
/// </summary>
|
||||
/// <param name="data">业务数据。</param>
|
||||
/// <param name="message">提示信息。</param>
|
||||
/// <returns>封装后的成功响应。</returns>
|
||||
public static ApiResponse<T> SuccessResult(T data, string? message = "操作成功")
|
||||
=> Ok(data, message);
|
||||
|
||||
/// <summary>
|
||||
/// 错误返回。
|
||||
/// </summary>
|
||||
/// <param name="code">错误码。</param>
|
||||
/// <param name="message">错误提示。</param>
|
||||
/// <param name="errors">错误详情。</param>
|
||||
/// <returns>封装后的失败响应。</returns>
|
||||
public static ApiResponse<T> Error(int code, string message, object? errors = null)
|
||||
=> Create(false, code, message, default, errors);
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧名称:失败结果。
|
||||
/// </summary>
|
||||
/// <param name="code">错误码。</param>
|
||||
/// <param name="message">错误提示。</param>
|
||||
/// <returns>封装后的失败响应。</returns>
|
||||
public static ApiResponse<T> Failure(int code, string message)
|
||||
=> Error(code, message);
|
||||
|
||||
/// <summary>
|
||||
/// 附加错误详情。
|
||||
/// </summary>
|
||||
/// <param name="errors">错误详情。</param>
|
||||
/// <returns>包含错误详情的新响应。</returns>
|
||||
public ApiResponse<T> WithErrors(object? errors)
|
||||
=> this with { Errors = errors };
|
||||
|
||||
private static ApiResponse<T> 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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 解析当前 TraceId。
|
||||
/// </summary>
|
||||
/// <returns>当前有效的 TraceId。</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作为 TraceId 缺失时的本地雪花 ID 备用生成器。
|
||||
/// </summary>
|
||||
internal sealed class IdFallbackGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 延迟初始化的单例实例承载。
|
||||
/// </summary>
|
||||
private static readonly Lazy<IdFallbackGenerator> Lazy = new(() => new IdFallbackGenerator());
|
||||
|
||||
/// <summary>
|
||||
/// 获取备用雪花生成器单例。
|
||||
/// </summary>
|
||||
public static IdFallbackGenerator Instance => Lazy.Value;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private long _lastTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
private long _sequence;
|
||||
|
||||
private IdFallbackGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成雪花风格的本地备用 ID。
|
||||
/// </summary>
|
||||
/// <returns>本地生成的雪花 ID。</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待到下一个毫秒以避免序列冲突。
|
||||
/// </summary>
|
||||
/// <param name="lastTimestamp">上一毫秒的时间戳。</param>
|
||||
/// <returns>下一个时间戳(毫秒)。</returns>
|
||||
private static long WaitNextMillis(long lastTimestamp)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
while (timestamp <= lastTimestamp)
|
||||
{
|
||||
Thread.SpinWait(100);
|
||||
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
/// <summary>
|
||||
/// 分页结果包装,携带列表与总条数等元数据。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型。</typeparam>
|
||||
/// <remarks>
|
||||
/// 初始化分页结果。
|
||||
/// </remarks>
|
||||
public sealed class PagedResult<T>(IReadOnlyList<T> items, int page, int pageSize, int totalCount)
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<T> Items { get; } = items;
|
||||
|
||||
/// <summary>
|
||||
/// 当前页码,从 1 开始。
|
||||
/// </summary>
|
||||
public int Page { get; } = page;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; } = pageSize;
|
||||
|
||||
/// <summary>
|
||||
/// 总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; } = totalCount;
|
||||
|
||||
/// <summary>
|
||||
/// 总页数。
|
||||
/// </summary>
|
||||
public int TotalPages { get; } = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户访问器:提供与当前请求相关的用户标识信息。
|
||||
/// </summary>
|
||||
public interface ICurrentUserAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前用户 ID,未登录时为 Guid.Empty。
|
||||
/// </summary>
|
||||
long UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已登录。
|
||||
/// </summary>
|
||||
bool IsAuthenticated { get; }
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// 将 long 类型的雪花 ID 以字符串形式序列化/反序列化,避免前端精度丢失。
|
||||
/// </summary>
|
||||
public sealed class SnowflakeIdJsonConverter : JsonConverter<long>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value == 0 ? "0" : value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可空雪花 ID 转换器。
|
||||
/// </summary>
|
||||
public sealed class NullableSnowflakeIdJsonConverter : JsonConverter<long?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.HasValue ? value.Value.ToString() : null);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 租户上下文访问器:用于在请求生命周期内读写当前租户上下文。
|
||||
/// </summary>
|
||||
public interface ITenantContextAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置当前租户上下文。
|
||||
/// </summary>
|
||||
TenantContext? Current { get; set; }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 租户提供者:用于在各层读取当前请求绑定的租户 ID。
|
||||
/// </summary>
|
||||
public interface ITenantProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前租户 ID,未解析时返回 0。
|
||||
/// </summary>
|
||||
/// <returns>当前请求绑定的租户 ID,未解析时为 0。</returns>
|
||||
long GetCurrentTenantId();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 多租户相关通用常量。
|
||||
/// </summary>
|
||||
public static class TenantConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpContext.Items 中租户上下文的键名。
|
||||
/// </summary>
|
||||
public const string HttpContextItemKey = "__tenant_context";
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 初始化租户上下文。
|
||||
/// </remarks>
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="tenantCode">租户编码(可选)</param>
|
||||
/// <param name="source">解析来源</param>
|
||||
public sealed class TenantContext(long tenantId, string? tenantCode, string source)
|
||||
{
|
||||
/// <summary>
|
||||
/// 未解析到租户时的默认上下文。
|
||||
/// </summary>
|
||||
public static TenantContext Empty { get; } = new(0, null, "unresolved");
|
||||
|
||||
/// <summary>
|
||||
/// 当前租户 ID,未解析时为 Guid.Empty。
|
||||
/// </summary>
|
||||
public long TenantId { get; } = tenantId;
|
||||
|
||||
/// <summary>
|
||||
/// 当前租户编码(例如子域名或业务编码),可为空。
|
||||
/// </summary>
|
||||
public string? TenantCode { get; } = tenantCode;
|
||||
|
||||
/// <summary>
|
||||
/// 租户解析来源(Header、Host、Token 等)。
|
||||
/// </summary>
|
||||
public string Source { get; } = source;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已成功解析到租户。
|
||||
/// </summary>
|
||||
public bool IsResolved => TenantId != 0;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Kernel.Ids;
|
||||
|
||||
/// <summary>
|
||||
/// 基于雪花算法的长整型 ID 生成器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 初始化生成器。
|
||||
/// </remarks>
|
||||
/// <param name="workerId">工作节点 ID。</param>
|
||||
/// <param name="datacenterId">机房 ID。</param>
|
||||
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();
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
/// <summary>
|
||||
/// API 基类控制器:
|
||||
/// - 统一应用 [ApiController] 和默认响应类型
|
||||
/// - 作为所有 API 控制器的基类,便于复用过滤器/中间件特性
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
public abstract class BaseApiController : ControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using TakeoutSaaS.Shared.Web.Middleware;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Web 应用中间件扩展。
|
||||
/// </summary>
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 按规范启用 TraceId、请求日志、异常映射与安全响应头。
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseSharedWebCore(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Shared.Web 服务注册扩展。
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册控制器、模型验证、API 版本化等基础能力。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharedWebCore(this IServiceCollection services)
|
||||
{
|
||||
// 1. 注册基础上下文与当前用户访问器
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddScoped<ICurrentUserAccessor, HttpContextCurrentUserAccessor>();
|
||||
// 2. 注册控制器与全局过滤器
|
||||
services
|
||||
.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<ValidateModelAttribute>();
|
||||
options.Filters.Add<ApiResponseResultFilter>();
|
||||
})
|
||||
.AddNewtonsoftJson();
|
||||
// 3. 配置模型验证行为
|
||||
services.Configure<ApiBehaviorOptions>(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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// ApiResponse 结果过滤器:自动将 ApiResponse 转换为对应的 HTTP 状态码。
|
||||
/// 使用此过滤器后,控制器可以直接返回 ApiResponse<T>,无需再包一层 Ok() 或 Unauthorized()。
|
||||
/// </summary>
|
||||
public sealed class ApiResponseResultFilter : IAsyncResultFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行结果过滤,将 ApiResponse 映射为对应 HTTP 状态码。
|
||||
/// </summary>
|
||||
/// <param name="context">结果执行上下文。</param>
|
||||
/// <param name="next">后续委托。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
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<T>
|
||||
// 检查是否是 ApiResponse<T> 类型
|
||||
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<T> 类型
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 模型验证过滤器:将模型验证错误统一为ApiResponse输出
|
||||
/// </summary>
|
||||
public sealed class ValidateModelAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 在 Action 执行前拦截模型验证错误。
|
||||
/// </summary>
|
||||
/// <param name="context">执行上下文。</param>
|
||||
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<object>.Error(ErrorCodes.ValidationFailed, "一个或多个验证错误", errors);
|
||||
context.Result = new UnprocessableEntityObjectResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 统一 TraceId/CorrelationId,贯穿日志与响应。
|
||||
/// </summary>
|
||||
public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
|
||||
{
|
||||
private const string TraceHeader = "X-Trace-Id";
|
||||
private const string SpanHeader = "X-Span-Id";
|
||||
private const string RequestHeader = "X-Request-Id";
|
||||
|
||||
/// <summary>
|
||||
/// 管道入口,确保 TraceId/SpanId 贯穿请求。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
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<string, object>
|
||||
{
|
||||
["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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 全局异常处理中间件,将异常统一映射为 ApiResponse。
|
||||
/// </summary>
|
||||
public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
|
||||
{
|
||||
private static readonly HashSet<int> 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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 中间件入口,捕获并统一处理异常。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
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<object> Response) BuildErrorResponse(Exception exception)
|
||||
{
|
||||
return exception switch
|
||||
{
|
||||
DbUpdateConcurrencyException => (
|
||||
StatusCodes.Status409Conflict,
|
||||
ApiResponse<object>.Error(
|
||||
ErrorCodes.Conflict,
|
||||
"数据已被他人修改,请刷新后重试",
|
||||
new Dictionary<string, string[]>
|
||||
{
|
||||
["RowVersion"] = ["数据已被他人修改,请刷新后重试"]
|
||||
})),
|
||||
UnauthorizedAccessException => (
|
||||
StatusCodes.Status403Forbidden,
|
||||
ApiResponse<object>.Error(ErrorCodes.Forbidden, "无权访问该资源")),
|
||||
SharedValidationException validationException => (
|
||||
StatusCodes.Status422UnprocessableEntity,
|
||||
ApiResponse<object>.Error(ErrorCodes.ValidationFailed, "请求参数验证失败", validationException.Errors)),
|
||||
FluentValidationException fluentValidationException => (
|
||||
StatusCodes.Status422UnprocessableEntity,
|
||||
ApiResponse<object>.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<object>.Error(businessException.ErrorCode, businessException.Message)),
|
||||
_ => (
|
||||
StatusCodes.Status500InternalServerError,
|
||||
ApiResponse<object>.Error(ErrorCodes.InternalServerError, "服务器开小差啦,请稍后再试"))
|
||||
};
|
||||
}
|
||||
|
||||
private static IDictionary<string, string[]> NormalizeValidationErrors(IEnumerable<ValidationFailure> failures)
|
||||
{
|
||||
var result = new Dictionary<string, List<string>>(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<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 基础请求日志(方法、路径、耗时、状态码、TraceId)。
|
||||
/// </summary>
|
||||
public sealed class RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录请求日志并调用后续中间件。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// 安全响应头中间件
|
||||
/// </summary>
|
||||
public sealed class SecurityHeadersMiddleware(RequestDelegate next)
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置基础安全响应头。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Security;
|
||||
|
||||
/// <summary>
|
||||
/// ClaimsPrincipal 便捷扩展
|
||||
/// </summary>
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户 Id(不存在时返回 0)。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Security.Claims;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 基于 HttpContext 的当前用户访问器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 初始化访问器。
|
||||
/// </remarks>
|
||||
public sealed class HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) : ICurrentUserAccessor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAuthenticated => UserId != 0;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Security;
|
||||
|
||||
/// <summary>
|
||||
/// HttpContext 租户扩展方法。
|
||||
/// </summary>
|
||||
public static class TenantHttpContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 HttpContext.Items 中缓存的租户上下文。
|
||||
/// </summary>
|
||||
/// <param name="context">当前 HttpContext</param>
|
||||
/// <returns>租户上下文,若不存在则返回 null</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 根据 API 版本动态注册 Swagger 文档。
|
||||
/// </summary>
|
||||
internal sealed class ConfigureSwaggerOptions(
|
||||
IApiVersionDescriptionProvider provider,
|
||||
IOptions<SwaggerDocumentSettings> settings) : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly SwaggerDocumentSettings _settings = settings.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 根据 API 版本生成 Swagger 文档配置。
|
||||
/// </summary>
|
||||
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<string>() }
|
||||
};
|
||||
return requirement;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace TakeoutSaaS.Shared.Web.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Swagger 文档配置。
|
||||
/// </summary>
|
||||
public class SwaggerDocumentSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// 文档标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "TakeoutSaaS API";
|
||||
|
||||
/// <summary>
|
||||
/// 描述信息。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用 JWT Authorize 按钮。
|
||||
/// </summary>
|
||||
public bool EnableAuthorization { get; set; } = true;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Swagger 注册/启用扩展。
|
||||
/// </summary>
|
||||
public static class SwaggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注入统一的 Swagger 服务。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action<SwaggerDocumentSettings>? 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<IConfigureOptions<SwaggerGenOptions>>(provider =>
|
||||
new ConfigureSwaggerOptions(
|
||||
provider.GetRequiredService<IApiVersionDescriptionProvider>(),
|
||||
Options.Create(provider.GetRequiredService<SwaggerDocumentSettings>())));
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开发环境启用 Swagger UI(自动注册所有版本)。
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseSharedSwagger(this IApplicationBuilder app)
|
||||
{
|
||||
var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||
var settings = app.ApplicationServices.GetRequiredService<SwaggerDocumentSettings>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -7,7 +7,7 @@
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -8,7 +8,7 @@
|
||||
<None Include="..\..\..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
|
||||
Binary file not shown.
@@ -5,7 +5,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
||||
426
商品模块_API设计_v1.md
426
商品模块_API设计_v1.md
@@ -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<T>`,与现有 `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 是否强制门店确认。
|
||||
Reference in New Issue
Block a user