refactor: 抽离 Docs/BuildingBlocks 子模块

This commit is contained in:
root
2026-01-29 03:07:01 +00:00
parent 725f89ae24
commit c5e2501e1a
97 changed files with 57 additions and 27026 deletions

6
.gitmodules vendored Normal file
View 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

View File

@@ -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" />

View File

@@ -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个月
- 测试与修复
- 文档完善
- 部署上线
- 运维监控

View File

@@ -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服务接口
- ValidatorsFluentValidation验证器
- MappingsAutoMapper配置
- Commands/QueriesCQRS命令和查询
#### 2.2.3 领域层 (Domain Layer)
- **TakeoutSaaS.Domain**:领域模型
- Entities实体类
- ValueObjects值对象
- Enums枚举
- Events领域事件
- Interfaces仓储接口
- Specifications规约模式
#### 2.2.4 基础设施层 (Infrastructure Layer)
- **TakeoutSaaS.Infrastructure**:基础设施实现
- Data数据访问
- EFCoreEF Core DbContext和配置
- DapperDapper查询实现
- Repositories仓储实现
- Migrations数据库迁移
- CacheRedis缓存实现
- MessageQueueRabbitMQ实现
- 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跨域配置

View File

@@ -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文件纳入版本控制
- 生产环境变更需要审核

View File

@@ -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": "5010",
"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`:支付成功通知

File diff suppressed because it is too large Load Diff

View File

@@ -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记录版本变更

View File

@@ -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 │
│ - 可视化仪表板 │
│ - 告警配置 │
└──────────────────────────────┘
```

View File

@@ -1,145 +0,0 @@
# 编程规范_FOR_AITakeoutSaaS - 终极完全体
> **核心指令**:你是一个高级 .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. [ ] **配置硬编码**:是否直接写死了连接串或密钥?
---

View File

@@ -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 6379RabbitMQ 5672/15672 等)
- 数据持久化与备份: 待补充
- 监控与告警: 待补充
- 变更记录: 待补充

View File

@@ -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。

View File

@@ -1,67 +0,0 @@
# TODO Roadmap
> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。
## 1. 配置与基础设施(高优)
- [x] Development/Production 数据库连接与 Secret 落地Staging 暂不需要)。
- [x] Redis 服务部署完毕并记录配置。
- [x] RabbitMQ 服务部署完毕并记录配置。
- [x] COS 密钥配置补录完毕。
- [ ] OSS 密钥配置补录完毕(已忽略,待采购后再补录)。
- [ ] SMS 平台密钥配置补录完毕(已忽略,待采购后再补录)。
- [x] WeChat Mini 程序密钥配置补录完毕AppIDwx30f91e6afe79f405AppSecret64324a7f604245301066ba7c3add488e已同步到 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、TenantAwareDbContextJWT 已写入 roles/permissions/tenant_idJwtTokenServicePermissionAuthorize 已在 Admin API 使用CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询Swagger 缺少示例与多租户示例。
- [x] 差距与步骤:
- [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤TenantAwareDbContext 或 Dapper 参数化)。
- [x] 输出可读的角色/权限列表(基于现有种子/配置的只读查询。【已落地RBAC1 模型 + 角色/权限管理 APISwagger 示例后续补充】
- [x] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions展示 Bearer 示例与租户 Header 示例。
- [ ] 若用 Dapper 读侧SQL 必须参数化并显式过滤 tenant_id。
- [x] 计划顺序Step A 设计应用层洞察 DTO/QueryStep B Admin API 只读端点Authorize/PermissionAuthorizeStep 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/FileELK 待后续配置)。
- [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 完整记录。
- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。
- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。

View File

@@ -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] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIPPOST /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 与网关已接入 OpenTelemetryOTLP 与 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 监控与告警。

View File

@@ -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 ID0 表示让雪花生成)
- `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`,避免误写入。

View File

@@ -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 默认 trueProd 建议 false
}
```
- 环境变量可覆盖:`OTEL_SERVICE_NAME``OTEL_EXPORTER_OTLP_ENDPOINT` 等。
## 4. Collector/后端接入建议
- Collector 监听 4317/4318gRPC/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/开关后重启即可。

View File

@@ -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. UserApiC 端用户)
- **面向对象**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%)。

View File

@@ -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. 操作审计:谁改了套餐、何时改、改了什么(合规必备)

View File

@@ -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

View File

@@ -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

View File

@@ -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性能优化与缓存、运营大盘与细分报表、测试与文档完善、上线与监控

232
README.md
View File

@@ -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 Swaggerhttp://localhost:5001/api/docs
- 小程序/用户端 MiniApi Swaggerhttp://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.Docs Submodule

Submodule TakeoutSaaS.Docs added at 88ad71041b

View File

@@ -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

View File

@@ -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"

View File

@@ -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`,再运行脚本重新创建。 |

View File

@@ -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."

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 状态"

View File

@@ -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

View File

@@ -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- 启用 appendonlyAOF并每秒 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`

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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" />

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -1,17 +0,0 @@
namespace TakeoutSaaS.Shared.Abstractions.Data;
/// <summary>
/// 数据库连接角色,用于区分主写与从读连接。
/// </summary>
public enum DatabaseConnectionRole
{
/// <summary>
/// 主写连接,用于写入或强一致读。
/// </summary>
Write = 1,
/// <summary>
/// 从读连接,用于只读查询或报表。
/// </summary>
Read = 2
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -1,12 +0,0 @@
namespace TakeoutSaaS.Shared.Abstractions.Entities;
/// <summary>
/// 实体基类,统一提供主键标识。
/// </summary>
public abstract class EntityBase
{
/// <summary>
/// 实体唯一标识。
/// </summary>
public long Id { get; set; }
}

View File

@@ -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; }
}

View File

@@ -1,12 +0,0 @@
namespace TakeoutSaaS.Shared.Abstractions.Entities;
/// <summary>
/// 多租户实体约定:所有持久化实体须包含租户标识字段。
/// </summary>
public interface IMultiTenantEntity
{
/// <summary>
/// 所属租户 ID。
/// </summary>
long TenantId { get; set; }
}

View File

@@ -1,12 +0,0 @@
namespace TakeoutSaaS.Shared.Abstractions.Entities;
/// <summary>
/// 软删除实体约定:提供可空的删除时间戳以支持全局过滤。
/// </summary>
public interface ISoftDeleteEntity
{
/// <summary>
/// 删除时间UTC未删除时为 null。
/// </summary>
DateTime? DeletedAt { get; set; }
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,13 +0,0 @@
namespace TakeoutSaaS.Shared.Abstractions.Ids;
/// <summary>
/// 雪花 ID 生成器接口。
/// </summary>
public interface IIdGenerator
{
/// <summary>
/// 生成下一个唯一长整型 ID。
/// </summary>
/// <returns>雪花 ID。</returns>
long NextId();
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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; }
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -1,12 +0,0 @@
namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
/// <summary>
/// 租户上下文访问器:用于在请求生命周期内读写当前租户上下文。
/// </summary>
public interface ITenantContextAccessor
{
/// <summary>
/// 获取或设置当前租户上下文。
/// </summary>
TenantContext? Current { get; set; }
}

View File

@@ -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();
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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
{
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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&lt;T&gt;,无需再包一层 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
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
});
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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 | 类目 IDstring |
| parentId | 父级类目 ID |
| name | 类目名称 |
| sortOrder | 排序 |
| isEnabled | 是否启用 |
| availableScenes | 生效场景 |
| availableTimeRanges | 生效时段 |
| createdAt | 创建时间 |
### 9.2 ProductDtoSPU
| 字段 | 说明 |
| --- | --- |
| id | 商品 IDstring |
| 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 IDstring |
| 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. UserApiC 端用户)接口清单
- `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 是否强制门店确认。