feat: migrate snowflake ids and refresh migrations
This commit is contained in:
150
AGENTS.md
Normal file
150
AGENTS.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Repository expectations
|
||||
# 编程规范_FOR_AI(TakeoutSaaS) - 终极完全体
|
||||
|
||||
> **核心指令**:你是一个高级 .NET 架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
|
||||
|
||||
## 0. AI 交互核心约束 (元规则)
|
||||
1. **语言**:必须使用**中文**回复和编写注释。
|
||||
2. **文件完整性**:
|
||||
* **严禁**随意删除现有代码逻辑。
|
||||
* **严禁**修改文件编码(保持 UTF-8 无 BOM)。
|
||||
* PowerShell 读取命令必须带 `-Encoding UTF8`。
|
||||
3. **Git 原子性**:每个独立的功能点或 Bug 修复完成后,必须提示用户进行 Git 提交。
|
||||
4. **无乱码承诺**:确保所有输出(控制台、日志、API响应)无乱码。
|
||||
5. **不确定的处理**:如果你通过上下文找不到某些配置(如数据库连接串格式),**请直接询问用户**,不要瞎编。
|
||||
|
||||
## 1. 技术栈详细版本
|
||||
| 组件 | 版本/选型 | 用途说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Runtime** | .NET 10 | 核心运行时 |
|
||||
| **API** | ASP.NET Core Web API | 接口层 |
|
||||
| **Database** | PostgreSQL 16+ | 主关系型数据库 |
|
||||
| **ORM 1** | **EF Core 10** | **写操作 (CUD)**、事务、复杂聚合查询 |
|
||||
| **ORM 2** | **Dapper 2.1+** | **纯读操作 (R)**、复杂报表、大批量查询 |
|
||||
| **Cache** | Redis 7.0+ | 分布式缓存、Session |
|
||||
| **MQ** | RabbitMQ 3.12+ | 异步解耦 (MassTransit) |
|
||||
| **Libs** | MediatR, Serilog, FluentValidation | CQRS, 日志, 验证 |
|
||||
|
||||
## 2. 命名与风格 (严格匹配)
|
||||
* **C# 代码**:
|
||||
* 类/接口/方法/属性:`PascalCase` (如 `OrderService`)
|
||||
* **布尔属性**:必须加 `Is` 或 `Has` 前缀 (如 `IsDeleted`, `HasPayment`)
|
||||
* 私有字段:`_camelCase` (如 `_orderRepository`)
|
||||
* 参数/变量:`camelCase` (如 `orderId`)
|
||||
* **PostgreSQL 数据库**:
|
||||
* 表名:`snake_case` + **复数** (如 `merchant_orders`)
|
||||
* 列名:`snake_case` (如 `order_no`, `is_active`)
|
||||
* 主键:`id` (类型 `bigint`)
|
||||
* **文件规则**:
|
||||
* **一个文件一个类**。文件名必须与类名完全一致。
|
||||
|
||||
## 3. 分层架构 (Clean Architecture)
|
||||
**你生成的代码必须严格归类到以下目录:**
|
||||
* **`src/Api`**: 仅负责路由与 DTO 转换,**禁止**包含业务逻辑。
|
||||
* **`src/Application`**: 业务编排层。必须使用 **CQRS** (`IRequestHandler`) 和 **Mediator**。
|
||||
* **`src/Domain`**: 核心领域层。包含实体、枚举、领域异常。**禁止**依赖 EF Core 等外部库。
|
||||
* **`src/Infrastructure`**: 基础设施层。实现仓储、数据库上下文、第三方服务。
|
||||
|
||||
## 4. 注释与文档
|
||||
* **强制 XML 注释**:所有 `public` 的类、方法、属性必须有 `<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. [ ] **配置硬编码**:是否直接写死了连接串或密钥?
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Working agreements
|
||||
- 严格遵循上述技术栈和命名规范。
|
||||
@@ -2,6 +2,48 @@
|
||||
|
||||
> 目的:在执行 `dotnet ef` 命令时无需硬编码数据库连接,可根据 appsettings 与环境变量自动加载。本文覆盖环境变量设置、配置目录指定等细节。
|
||||
|
||||
## 三库迁移命令 只需更改 SnowflakeIds_App 迁移关键字
|
||||
> 先生成迁移,再执行数据库更新。启动项目统一用 AdminApi 确保加载最新配置。
|
||||
|
||||
### 生成迁移
|
||||
```bash
|
||||
# App 主库
|
||||
dotnet tool run dotnet-ef migrations add SnowflakeIds_App `
|
||||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||||
--startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj `
|
||||
--context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext
|
||||
|
||||
# Identity 库
|
||||
dotnet tool run dotnet-ef migrations add SnowflakeIds_Identity `
|
||||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||||
--startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj `
|
||||
--context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext
|
||||
|
||||
# Dictionary 库
|
||||
dotnet tool run dotnet-ef migrations add SnowflakeIds_Dictionary `
|
||||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||||
--startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj `
|
||||
--context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext
|
||||
```
|
||||
|
||||
### 更新数据库
|
||||
```bash
|
||||
dotnet tool run dotnet-ef database update `
|
||||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||||
--startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj `
|
||||
--context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext
|
||||
|
||||
dotnet tool run dotnet-ef database update `
|
||||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||||
--startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj `
|
||||
--context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext
|
||||
|
||||
dotnet tool run dotnet-ef database update `
|
||||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||||
--startup-project src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj `
|
||||
--context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext
|
||||
```
|
||||
|
||||
## 一、设计时工厂读取逻辑概述
|
||||
设计时工厂(`DesignTimeDbContextFactoryBase<T>`)按下面顺序解析连接串:
|
||||
1. 若设置了 `TAKEOUTSAAS_APP_CONNECTION` / `TAKEOUTSAAS_IDENTITY_CONNECTION` / `TAKEOUTSAAS_DICTIONARY_CONNECTION` 等环境变量,则优先使用。
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
# System-Level TODOs
|
||||
# TODO Roadmap
|
||||
|
||||
> Infrastructure / platform backlog migrated from the former 11th document. Unless noted explicitly, all checklist items remain pending.
|
||||
> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成。
|
||||
|
||||
## 1. 閰嶇疆涓庡熀纭€璁炬柦锛堥珮浼橈級
|
||||
- [x] Development/Production 鏁版嵁搴撹繛鎺ヤ笌 Secret 钀藉湴锛圫taging 鏆備笉闇€瑕侊級銆?
|
||||
- [x] Redis 鏈嶅姟閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€?
|
||||
- [x] RabbitMQ 鏈嶅姟閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€?
|
||||
- [x] COS 瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯銆?
|
||||
- [ ] OSS 瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛堝緟閲囪喘锛夈€?
|
||||
- [ ] SMS 骞冲彴瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛堝緟閲囪喘锛夈€?
|
||||
- [x] WeChat Mini 绋嬪簭瀵嗛挜閰嶇疆琛ュ綍瀹屾瘯锛圓ppID锛歸x30f91e6afe79f405锛孉ppSecret锛?4324a7f604245301066ba7c3add488e锛屽凡鍚屾鍒?admin/mini 閰嶇疆骞剁櫥璁版洿鏂颁汉锛夈€?
|
||||
- [x] PostgreSQL 鍩虹瀹炰緥閮ㄧ讲瀹屾瘯骞惰褰曢厤缃€?
|
||||
- [x] Postgres/Redis 鎺ュ叆鏂囨。 + IaC/鑴氭湰琛ラ綈锛堣 Document/infra/postgres_redis.md 涓?deploy/postgres|redis锛夈€?
|
||||
## 1. 配置与基础设施(高优)
|
||||
- [x] Development/Production 数据库连接与 Secret 落地(Staging 暂不需要)。
|
||||
- [x] Redis 服务部署完毕并记录配置。
|
||||
- [x] RabbitMQ 服务部署完毕并记录配置。
|
||||
- [x] COS 密钥配置补录完毕。
|
||||
- [ ] OSS 密钥配置补录完毕(已忽略,待采购后再补录)。
|
||||
- [ ] SMS 平台密钥配置补录完毕(已忽略,待采购后再补录)。
|
||||
- [x] WeChat Mini 程序密钥配置补录完毕(AppID:wx30f91e6afe79f405,AppSecret:64324a7f604245301066ba7c3add488e,已同步到 admin/mini 配置并登记更新人)。
|
||||
- [x] PostgreSQL 基础实例部署完毕并记录配置。
|
||||
- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis)。
|
||||
- [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis).
|
||||
- [ ] admin/mini/user/gateway 缃戝叧鍩熷悕銆佽瘉涔︺€丆ORS 鍒楄〃鏁寸悊瀹屾垚銆?
|
||||
- [ ] Hangfire Dashboard 鍚敤骞舵柊澧?Admin 瑙掕壊楠岃瘉/缃戝叧鐧藉悕鍗曘€?
|
||||
- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。(忽略,暂时不用完成)
|
||||
- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。(忽略,暂时不用完成)
|
||||
|
||||
## 2. 鏁版嵁涓庤縼绉伙紙楂樹紭锛?
|
||||
- [x] App/Identity/Dictionary/Hangfire 鍥涗釜 DbContext 鍧囩敓鎴愬垵濮?Migration 骞舵垚鍔?update database銆?
|
||||
- [ ] 鍟嗘埛/闂ㄥ簵/鍟嗗搧/璁㈠崟/鏀粯/閰嶉€佺瓑瀹炰綋涓庝粨鍌ㄥ疄鐜板畬鎴愶紝鎻愪緵 CRUD + 鏌ヨ銆?
|
||||
- [ ] 绯荤粺鍙傛暟銆侀粯璁ょ鎴枫€佺鐞嗗憳璐﹀彿銆佸熀纭€瀛楀吀鐨勭瀛愯剼鏈彲閲嶅鎵ц銆?
|
||||
## 2. 数据与迁移(高优)
|
||||
- [x] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。
|
||||
- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。
|
||||
- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。
|
||||
|
||||
## 3. 绋冲畾鎬т笌璐ㄩ噺
|
||||
- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 鐨?xUnit+FluentAssertions 鍗曞厓娴嬭瘯妗嗘灦鎼缓銆?
|
||||
- [ ] WebApplicationFactory + Testcontainers 鎷夎捣 Postgres/Redis/RabbitMQ/MinIO 鐨勯泦鎴愭祴璇曟ā鏉裤€?
|
||||
- [ ] .editorconfig銆?globalconfig銆丷oslyn 鍒嗘瀽鍣ㄩ厤缃粨搴撻€氱敤瑙勫垯骞跺惎鐢?CI 妫€鏌ャ€?
|
||||
## 3. 稳定性与质量
|
||||
- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。
|
||||
- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。
|
||||
- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。
|
||||
|
||||
## 4. 瀹夊叏涓庡悎瑙?
|
||||
- [ ] RBAC 鏉冮檺銆佺鎴烽殧绂汇€佺敤鎴?鏉冮檺娲炲療 API 瀹屾暣婕旂ず骞跺湪 Swagger 涓彁渚涚ず渚嬨€?
|
||||
- [ ] 鐧诲綍/鍒锋柊娴佺▼澧炲姞 IP 鏍¢獙銆佺鎴烽殧绂汇€侀獙璇佺爜/棰戠巼闄愬埗銆?
|
||||
- [ ] 鐧诲綍/鏉冮檺/鏁忔劅鎿嶄綔鏃ュ織鍙拷婧紝鎻愪緵鏌ヨ鎺ュ彛鎴?Kibana Saved Search銆?
|
||||
- [ ] Secret Store/KeyVault/KMS 绠$悊鏁忔劅閰嶇疆锛岀姝㈠瘑閽ュ啓鍏?Git/鏁版嵁搴撴槑鏂囥€?
|
||||
## 4. 安全与合规
|
||||
- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。
|
||||
- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。
|
||||
- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。
|
||||
- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。
|
||||
|
||||
## 5. 瑙傛祴涓庤繍缁?
|
||||
- [ ] TraceId 璐€氾紝骞跺湪 Serilog 涓緭鍑?Console/File/ELK 涓夌鐩爣銆?
|
||||
- [ ] Prometheus exporter 鏆撮湶鍏抽敭鎸囨爣锛?health 鎺㈤拡涓庡憡璀﹁鍒欏悓姝ユ帹閫併€?
|
||||
- [ ] PostgreSQL 鍏ㄩ噺/澧為噺澶囦唤鑴氭湰鍙婁竴娆$湡瀹炴仮澶嶆紨缁冩姤鍛娿€?
|
||||
## 5. 观测与运维
|
||||
- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。
|
||||
- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。
|
||||
- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。
|
||||
|
||||
## 6. 涓氬姟鑳藉姏琛ュ叏
|
||||
- [ ] 鍟嗘埛/闂ㄥ簵/鑿滃搧 API 瀹屾垚骞跺湪 MQ 涓姇閫掍笂鏋?鏀粯鎴愬姛浜嬩欢銆?
|
||||
- [ ] 閰嶉€佸鎺?API 鏀寔涓嬪崟/鍙栨秷/鏌ヨ骞跺畬鎴愮鍚嶉獙绛句腑闂翠欢銆?
|
||||
- [ ] 灏忕▼搴忕鍟嗗搧娴忚銆佷笅鍗曘€佹敮浠樸€佽瘎浠枫€佸浘鐗囩洿浼犵瓑 API 鍙棴鐜窇閫氥€?
|
||||
## 6. 业务能力补全
|
||||
- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。
|
||||
- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。
|
||||
- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。
|
||||
|
||||
## 7. 鍓嶅悗鍙?UI 瀵规帴
|
||||
- [ ] Admin UI 閫氳繃 OpenAPI 鐢熸垚鎴栨墜鍐欑晫闈紝鎺ュ叆 Hangfire Dashboard/MQ 鐩戞帶鍙妯″紡銆?
|
||||
- [ ] 灏忕▼搴忕瀹屾垚鐧诲綍銆佽彍鍗曟祻瑙堛€佷笅鍗曘€佹敮浠樸€佺墿娴佽建杩广€佺礌鏉愮洿浼犻棴鐜€?
|
||||
## 7. 前后台 UI 对接
|
||||
- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。
|
||||
- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。
|
||||
|
||||
## 8. CI/CD 涓庡彂甯?
|
||||
- [ ] CI/CD 娴佹按绾胯鐩栨瀯寤恒€佸彂甯冦€侀潤鎬佹壂鎻忋€佹暟鎹簱杩佺Щ銆?
|
||||
- [ ] Dev/Staging/Prod 澶氱幆澧冮厤缃煩闃?+ 鍩虹璁炬柦 IaC 鑴氭湰銆?
|
||||
- [ ] 鐗堟湰涓庡彂甯冭鏄庢ā鏉挎暣鐞嗗苟鍦ㄤ粨搴撲腑鎻愪緵绀轰緥銆?
|
||||
## 8. CI/CD 与发布
|
||||
- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。
|
||||
- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。
|
||||
- [ ] 版本与发布说明模板整理并在仓库中提供示例。
|
||||
|
||||
## 9. 鏂囨。涓庣煡璇嗗簱
|
||||
- [ ] 鎺ュ彛鏂囨。銆侀鍩熸ā鍨嬨€佸叧閿害鏉熶娇鐢?Markdown 鎴?API Portal 瀹屾暣璁板綍銆?
|
||||
- [ ] 杩愯鎵嬪唽鍖呭惈閮ㄧ讲姝ラ銆佽祫婧愭嫇鎵戙€佹晠闅滄帓鏌ユ墜鍐屻€?
|
||||
- [ ] 瀹夊叏鍚堣妯℃澘瑕嗙洊鏁版嵁鍒嗙骇銆佸瘑閽ョ鐞嗐€佸璁℃祦绋嬪苟褰㈡垚鍙鐢ㄨ〃鏍笺€
|
||||
## 9. 文档与知识库
|
||||
- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。
|
||||
- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。
|
||||
- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
# Business-Level TODOs
|
||||
# 里程碑待办追踪
|
||||
|
||||
> Product & business capability roadmap grouped by milestones; each phase only tracks the scoped backlog to enable staged delivery.
|
||||
> 按“小程序版模块规划”划分四个里程碑;每个里程碑只含对应范围的任务,便于分阶段推进。
|
||||
|
||||
---
|
||||
## Phase 1锛堝綋鍓嶉樁娈碉級锛氱鎴?鍟嗗鍏ラ┗銆侀棬搴椾笌鑿滃搧銆佹壂鐮佸爞椋熴€佸熀纭€涓嬪崟鏀粯銆侀璐嚜鎻愩€佺涓夋柟閰嶉€侀鏋?
|
||||
- [ ] 绠$悊绔鎴?API锛氭敞鍐屻€佸疄鍚嶈璇併€佸椁愯闃?缁垂/鍗囬檷閰嶃€佸鏍告祦锛孲wagger 鈮? 涓鐐癸紝鍚鏍告棩蹇椼€?
|
||||
- [ ] 鍟嗗鍏ラ┗ API锛氳瘉鐓т笂浼犮€佸悎鍚岀鐞嗐€佺被鐩€夋嫨锛岄┍鍔ㄥ緟瀹?瀹℃牳/椹冲洖/閫氳繃鐘舵€佹満锛屾枃浠舵寔涔呭湪 COS銆?
|
||||
- [ ] RBAC 妯℃澘锛氬钩鍙扮鐞嗗憳銆佺鎴风鐞嗗憳銆佸簵闀裤€佸簵鍛樺洓瑙掕壊妯℃澘锛汚PI 鍙鍒跺苟鍏佽绉熸埛鑷畾涔夋墿灞曘€?
|
||||
- [ ] 閰嶉涓庡椁愶細TenantPackage CRUD銆佽闃?缁垂/閰嶉鏍¢獙锛堥棬搴?璐﹀彿/鐭俊/閰嶉€佸崟閲忥級锛岃秴棰濊繑鍥?409 骞惰褰?TenantQuotaUsage銆?
|
||||
- [ ] 绉熸埛杩愯惀闈㈡澘锛氭瑺璐?鍒版湡鍛婅銆佽处鍗曞垪琛ㄣ€佸叕鍛婇€氱煡鎺ュ彛锛屾敮鎸佸凡璇荤姸鎬佸苟鍦?Admin UI 灞曠ず銆?
|
||||
- [ ] 闂ㄥ簵绠$悊锛歋tore/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 瀹屾暣锛屽惈 GeoJSON 閰嶉€佽寖鍥村強鑳藉姏寮€鍏炽€?
|
||||
- [ ] 妗岀爜绠$悊锛氭壒閲忕敓鎴愭鐮併€佺粦瀹氬尯鍩?瀹归噺銆佸鍑轰簩缁寸爜 ZIP锛圥OST /api/admin/stores/{id}/tables 鍙笅杞斤級銆?
|
||||
- [ ] 鍛樺伐鎺掔彮锛氬垱寤哄憳宸ャ€佺粦瀹氶棬搴楄鑹层€佺淮鎶?StoreEmployeeShift锛屽彲鏌ヨ鏈潵 7 鏃ユ帓鐝€?
|
||||
- [ ] 妗岀爜鎵爜鍏ュ彛锛歁ini 绔В鏋愪簩缁寸爜锛孏ET /api/mini/tables/{code}/context 杩斿洖闂ㄥ簵銆佹鍙般€佸叕鍛娿€?
|
||||
- [ ] 鑿滃搧寤烘ā锛氬垎绫汇€丼PU銆丼KU銆佽鏍?鍔犳枡缁勩€佷环鏍肩瓥鐣ャ€佸獟璧?CRUD + 涓婁笅鏋舵祦绋嬶紱Mini 绔彲鎷夊彇瀹屾暣 JSON銆?
|
||||
- [ ] 搴撳瓨浣撶郴锛歋KU 搴撳瓨銆佹壒娆°€佽皟鏁淬€佸敭缃勭鐞嗭紝鏀寔棰勫敭/妗f湡閿佸畾骞跺湪璁㈠崟涓墸鍑?閲婃斁銆?
|
||||
- [ ] 鑷彁妗f湡锛氶棬搴楅厤缃嚜鎻愭椂闂寸獥銆佸閲忋€佹埅鍗曟椂闂达紱Mini 绔嵁姝ら檺鍒朵笅鍗曟椂闂淬€?
|
||||
- [ ] 璐墿杞︽湇鍔★細ShoppingCart/CartItem/CartItemAddon API 鏀寔骞跺彂閿併€侀檺璐€佸埜/绉垎棰勬牎楠岋紝淇濊瘉骞跺彂鏃犺剰鏁版嵁銆?
|
||||
- [ ] 璁㈠崟涓庢敮浠橈細鍫傞/鑷彁/閰嶉€佷笅鍗曘€佸井淇?鏀粯瀹濇敮浠樸€佷紭鎯犲埜/绉垎鎶垫墸銆佽鍗曠姸鎬佹満涓庨€氱煡閾捐矾榻愬叏銆?
|
||||
- [ ] 妗屽彴璐﹀崟锛氬悎鍗?鎷嗗崟銆佺粨璐︺€佺數瀛愬皬绁ㄣ€佹鍙伴噴鏀撅紝瀹屾垚缁撹处鍚庢仮澶?Idle 骞剁敓鎴愮エ鎹?URL銆?
|
||||
- [ ] 鑷厤閫侀鏋讹細楠戞墜绠$悊銆佸彇閫佷欢淇℃伅褰曞叆銆佽垂鐢ㄨˉ璐磋褰曪紝Admin 绔彲娲惧崟骞舵洿鏂?DeliveryOrder銆?
|
||||
- [ ] 绗笁鏂归厤閫佹娊璞★細缁熶竴涓嬪崟/鍙栨秷/鍔犱环/鏌ヨ鎺ュ彛锛屾敮鎸佽揪杈俱€佺編鍥€侀棯閫佺瓑锛屽惈鍥炶皟楠岀涓庡紓甯歌ˉ鍋块鏋躲€?
|
||||
- [ ] 棰勮喘鑷彁鏍搁攢锛氭彁璐х爜鐢熸垚銆佹墜鏈哄彿/浜岀淮鐮佹牳閿€銆佽嚜鎻愭煖/鍓嶅彴娴佺▼锛岃秴鏃惰嚜鍔ㄥ彇娑堟垨閫€娆撅紝璁板綍鎿嶄綔鑰呬笌鏃堕棿銆?
|
||||
- [ ] 鎸囨爣涓庢棩蹇楋細Prometheus 杈撳嚭璁㈠崟鍒涘缓銆佹敮浠樻垚鍔熺巼銆侀厤閫佸洖璋冭€楁椂绛夛紝Grafana 鈮? 涓浘琛紱鍏抽敭娴佺▼鏃ュ織璁板綍 TraceId + 涓氬姟 ID銆?
|
||||
- [ ] 娴嬭瘯锛歅hase 1 鏍稿績 API 鍏峰 鈮?0 鏉¤嚜鍔ㄥ寲鐢ㄤ緥锛堝崟鍏?+ 闆嗘垚锛夛紝瑕嗙洊绉熸埛鈫掑晢鎴封啋涓嬪崟閾捐矾銆?
|
||||
|
||||
## Phase 1(当前阶段):租户/商家入驻、门店与菜品、扫码堂食、基础下单支付、预购自提、第三方配送骨架
|
||||
- [ ] 管理端租户 API:注册、实名认证、套餐订阅/续费/升降配、审核流,Swagger ≥6 个端点,含审核日志。
|
||||
- [ ] 商家入驻 API:证照上传、合同管理、类目选择,驱动待审/审核/驳回/通过状态机,文件持久在 COS。
|
||||
- [ ] RBAC 模板:平台管理员、租户管理员、店长、店员四角色模板;API 可复制并允许租户自定义扩展。
|
||||
- [ ] 配额与套餐:TenantPackage CRUD、订阅/续费/配额校验(门店/账号/短信/配送单量),超额返回 409 并记录 TenantQuotaUsage。
|
||||
- [ ] 租户运营面板:欠费/到期告警、账单列表、公告通知接口,支持已读状态并在 Admin UI 展示。
|
||||
- [ ] 门店管理:Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。
|
||||
- [ ] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。
|
||||
- [ ] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。
|
||||
- [ ] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。
|
||||
- [ ] 菜品建模:分类、SPU、SKU、规格/加料组、价格策略、媒资 CRUD + 上下架流程;Mini 端可拉取完整 JSON。
|
||||
- [ ] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。
|
||||
- [ ] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。
|
||||
- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。
|
||||
- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。
|
||||
- [ ] 桌台账单:合单/拆单、结账、电子小票、桌台释放,完成结账后恢复 Idle 并生成票据 URL。
|
||||
- [ ] 自配送骨架:骑手管理、取送件信息录入、费用补贴记录,Admin 端可派单并更新 DeliveryOrder。
|
||||
- [ ] 第三方配送抽象:统一下单/取消/加价/查询接口,支持达达、美团、闪送等,含回调验签与异常补偿骨架。
|
||||
- [ ] 预购自提核销:提货码生成、手机号/二维码核销、自提柜/前台流程,超时自动取消或退款,记录操作者与时间。
|
||||
- [ ] 指标与日志:Prometheus 输出订单创建、支付成功率、配送回调耗时等,Grafana ≥8 个图表;关键流程日志记录 TraceId + 业务 ID。
|
||||
- [ ] 测试:Phase 1 核心 API 具备 ≥30 条自动化用例(单元 + 集成),覆盖租户→商户→下单链路。
|
||||
---
|
||||
## Phase 2锛堜笅涓€闃舵锛夛細鎷煎崟銆佷紭鎯犲埜涓庡熀纭€钀ラ攢銆佷細鍛樼Н鍒?浼氬憳鏃ャ€佸鏈嶈亰澶┿€佸悓鍩庤嚜閰嶉€佽皟搴︺€佹悳绱?
|
||||
- [ ] 鎷煎崟寮曟搸锛欸roupOrder/Participant CRUD銆佸彂璧?鍔犲叆/鎴愬洟鏉′欢銆佽嚜鍔ㄨВ鏁d笌閫€娆俱€佸洟鍐呮秷鎭笌鎻愰啋銆?
|
||||
- [ ] 浼樻儬鍒镐笌鍩虹钀ラ攢锛氭ā鏉跨鐞嗐€侀鍒搞€佹牳閿€銆佸簱瀛?鏈夋晥鏈?鍙犲姞瑙勫垯锛屽熀纭€鎶藉/绉掓潃/婊″噺娲诲姩銆?
|
||||
- [ ] 浼氬憳涓庣Н鍒嗭細浼氬憳妗f銆佺瓑绾?鎴愰暱鍊笺€佷細鍛樻棩閫氱煡锛涚Н鍒嗚幏鍙?娑堣€椼€佹湁鏁堟湡銆侀粦鍚嶅崟銆?
|
||||
- [ ] 瀹㈡湇鑱婂ぉ锛氬疄鏃朵細璇濄€佹満鍣ㄤ汉/浜哄伐鍒囨崲銆佹帓闃?杞帴銆佹秷鎭ā鏉裤€佹晱鎰熻瘝瀹℃煡銆佸伐鍗曟祦杞笌璇勪环銆?
|
||||
- [ ] 鍚屽煄鑷厤閫佽皟搴︼細楠戞墜鏅鸿兘鎸囨淳銆佽矾绾夸及鏃躲€佹棤鎺ヨЕ閰嶉€併€佽垂鐢ㄨˉ璐寸瓥鐣ャ€佽皟搴︾湅鏉裤€?
|
||||
- [ ] 鎼滅储锛氶棬搴?鑿滃搧/娲诲姩/浼樻儬鍒告悳绱紝杩囨护/鎺掑簭銆佺儹闂?鍘嗗彶璁板綍銆佽仈鎯充笌绾犻敊銆?
|
||||
|
||||
## Phase 2(下一阶段):拼单、优惠券与基础营销、会员积分/会员日、客服聊天、同城自配送调度、搜索
|
||||
- [ ] 拼单引擎:GroupOrder/Participant CRUD、发起/加入/成团条件、自动解散与退款、团内消息与提醒。
|
||||
- [ ] 优惠券与基础营销:模板管理、领券、核销、库存/有效期/叠加规则,基础抽奖/秒杀/满减活动。
|
||||
- [ ] 会员与积分:会员档案、等级/成长值、会员日通知;积分获取/消耗、有效期、黑名单。
|
||||
- [ ] 客服聊天:实时会话、机器人/人工切换、排队/转接、消息模板、敏感词审查、工单流转与评价。
|
||||
- [ ] 同城自配送调度:骑手智能指派、路线估时、无接触配送、费用补贴策略、调度看板。
|
||||
- [ ] 搜索:门店/菜品/活动/优惠券搜索,过滤/排序、热门/历史记录、联想与纠错。
|
||||
---
|
||||
## Phase 3锛氬垎閿€杩斿埄銆佺鍒版墦鍗°€侀绾﹂璁€佸湴鍥惧鑸€佺ぞ鍖恒€侀珮闃惰惀閿€銆侀鎺т笌琛ュ伩
|
||||
- [ ] 鍒嗛攢杩斿埄锛欰ffiliatePartner/Order/Payout 绠$悊锛屼剑閲戦樁姊€佺粨绠楀懆鏈熴€佺◣鍔′俊鎭€佽繚瑙勫鐞嗐€?
|
||||
- [ ] 绛惧埌鎵撳崱锛欳heckInCampaign/Record銆佽繛绛惧鍔便€佽ˉ绛俱€佺Н鍒?鍒?鎴愰暱鍊煎鍔便€佸弽浣滃紛鏈哄埗銆?
|
||||
- [ ] 棰勭害棰勮锛氭。鏈?璧勬簮鍗犵敤銆侀绾︿笅鍗?鏀粯銆佹彁閱?鏀规湡/鍙栨秷銆佸埌搴楁牳閿€涓庡饱绾﹁褰曘€?
|
||||
- [ ] 鍦板浘瀵艰埅鎵╁睍锛氶檮杩戦棬搴?鎺ㄨ崘銆佽窛绂?璺嚎瑙勫垝銆佽烦杞師鐢熷鑸€佸鑸姹傚煁鐐广€?
|
||||
- [ ] 绀惧尯锛氬姩鎬佸彂甯冦€佽瘎璁恒€佺偣璧炪€佽瘽棰?鏍囩銆佸浘鐗?瑙嗛瀹℃牳銆佷妇鎶ヤ笌椋庢帶锛屽簵閾哄彛纰戝睍绀恒€?
|
||||
- [ ] 楂橀樁钀ラ攢锛氱鏉€/鎶藉/瑁傚彉銆佽鍙樻捣鎶ャ€佺垎娆炬帹鑽愪綅銆佸娓犻亾鎶曟斁鍒嗘瀽銆?
|
||||
- [ ] 椋庢帶涓庡璁★細榛戝悕鍗曘€侀鐜囬檺鍒躲€佸紓甯歌涓虹洃鎺с€佸璁℃棩蹇椼€佽ˉ鍋夸笌鍛婅浣撶郴銆?
|
||||
|
||||
## Phase 3:分销返利、签到打卡、预约预订、地图导航、社区、高阶营销、风控与补偿
|
||||
- [ ] 分销返利:AffiliatePartner/Order/Payout 管理,佣金阶梯、结算周期、税务信息、违规处理。
|
||||
- [ ] 签到打卡:CheckInCampaign/Record、连签奖励、补签、积分/券/成长值奖励、反作弊机制。
|
||||
- [ ] 预约预订:档期/资源占用、预约下单/支付、提醒/改期/取消、到店核销与履约记录。
|
||||
- [ ] 地图导航扩展:附近门店/推荐、距离/路线规划、跳转原生导航、导航请求埋点。
|
||||
- [ ] 社区:动态发布、评论、点赞、话题/标签、图片/视频审核、举报与风控,店铺口碑展示。
|
||||
- [ ] 高阶营销:秒杀/抽奖/裂变、裂变海报、爆款推荐位、多渠道投放分析。
|
||||
- [ ] 风控与审计:黑名单、频率限制、异常行为监控、审计日志、补偿与告警体系。
|
||||
---
|
||||
## Phase 4锛氭€ц兘浼樺寲銆佺紦瀛樸€佽繍钀ュぇ鐩樸€佹祴璇曚笌鏂囨。銆佷笂绾夸笌鐩戞帶
|
||||
- [ ] 鎬ц兘涓庣紦瀛橈細鐑偣鎺ュ彛缂撳瓨銆佹參鏌ヨ娌荤悊銆佹壒澶勭悊浼樺寲銆佸紓姝ュ寲鏀归€犮€?
|
||||
- [ ] 鍙潬鎬э細骞傜瓑涓庨噸璇曠瓥鐣ャ€佷换鍔¤皟搴﹁ˉ鍋裤€侀摼璺拷韪€佸憡璀﹁仈鍔ㄣ€?
|
||||
- [ ] 杩愯惀澶х洏锛氫氦鏄?钀ラ攢/灞ョ害/鐢ㄦ埛缁村害鐨勭粏鍒嗘姤琛ㄣ€丟MV/鎴愭湰/姣涘埄鍒嗘瀽銆?
|
||||
- [ ] 鏂囨。涓庢祴璇曪細瀹屾暣娴嬭瘯鐭╅樀銆佹€ц兘娴嬭瘯鎶ュ憡銆佷笂绾挎墜鍐屻€佸洖婊氭柟妗堛€?
|
||||
- [ ] 鐩戞帶涓庤繍缁达細涓婄嚎鍙戝竷娴佺▼銆佺伆搴?鍥炴粴绛栫暐銆佺郴缁熺ǔ瀹氭€ф寚鏍囥€?4x7 鐩戞帶涓庡憡璀︺€?
|
||||
|
||||
## Phase 4:性能优化、缓存、运营大盘、测试与文档、上线与监控
|
||||
- [ ] 性能与缓存:热点接口缓存、慢查询治理、批处理优化、异步化改造。
|
||||
- [ ] 可靠性:幂等与重试策略、任务调度补偿、链路追踪、告警联动。
|
||||
- [ ] 运营大盘:交易/营销/履约/用户维度的细分报表、GMV/成本/毛利分析。
|
||||
- [ ] 文档与测试:完整测试矩阵、性能测试报告、上线手册、回滚方案。
|
||||
- [ ] 监控与运维:上线发布流程、灰度/回滚策略、系统稳定性指标、24x7 监控与告警。
|
||||
52
Document/13_AppSeed说明.md
Normal file
52
Document/13_AppSeed说明.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# App 数据种子使用说明(App:Seed)
|
||||
|
||||
> 作用:在启动时自动创建默认租户与基础字典,便于本地/测试环境快速落地必备数据。由 `AppDataSeeder` 执行,支持幂等多次运行。
|
||||
|
||||
## 配置入口
|
||||
- 文件位置:`appsettings.{Environment}.json`(示例已写入 AdminApi 的 Development 配置)。
|
||||
- 配置节:`App:Seed`。
|
||||
|
||||
示例(已写入 `src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json`):
|
||||
```json
|
||||
"App": {
|
||||
"Seed": {
|
||||
"Enabled": true,
|
||||
"DefaultTenant": {
|
||||
"TenantId": 1000000000001,
|
||||
"Code": "demo",
|
||||
"Name": "Demo租户",
|
||||
"ShortName": "Demo",
|
||||
"ContactName": "DemoAdmin",
|
||||
"ContactPhone": "13800000000"
|
||||
},
|
||||
"DictionaryGroups": [
|
||||
{
|
||||
"Code": "order_status",
|
||||
"Name": "订单状态",
|
||||
"Scope": "Business",
|
||||
"Items": [
|
||||
{ "Key": "pending", "Value": "待支付", "SortOrder": 10 },
|
||||
{ "Key": "paid", "Value": "已支付", "SortOrder": 20 },
|
||||
{ "Key": "finished", "Value": "已完成", "SortOrder": 30 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `Enabled`: 是否启用种子。
|
||||
- `DefaultTenant`: 默认租户(使用雪花 long ID,0 表示让雪花生成)。
|
||||
- `DictionaryGroups`: 基础字典,`Scope` 可选 `System`/`Business`,`Items` 支持重复执行更新。
|
||||
|
||||
## 运行方式
|
||||
1. 确保 Admin API 已调用 `AddAppInfrastructure`(已在 Program.cs 中注册,会启动 `AppDataSeeder`)。
|
||||
2. 修改 `appsettings.{Environment}.json` 的 `App:Seed` 后,启动 Admin API,即会自动执行种子逻辑(幂等)。
|
||||
3. 查看日志:`AppSeed` 前缀会输出创建/更新结果。
|
||||
|
||||
## 注意事项
|
||||
- ID 必须为 long(雪花),不要再使用 Guid/自增。
|
||||
- 系统租户使用 `TenantId = 0`;业务租户请填写实际雪花 ID。
|
||||
- 字典分组编码需唯一;重复运行会按编码合并更新。
|
||||
- 生产环境请按需开启 `Enabled`,避免误写入。
|
||||
@@ -67,7 +67,7 @@ public sealed class AuthController : BaseApiController
|
||||
public async Task<ApiResponse<CurrentUserProfile>> GetProfile(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == Guid.Empty)
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
|
||||
}
|
||||
|
||||
@@ -55,10 +55,10 @@ public sealed class DictionaryController : BaseApiController
|
||||
/// <summary>
|
||||
/// 更新字典分组。
|
||||
/// </summary>
|
||||
[HttpPut("{groupId:guid}")]
|
||||
[HttpPut("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(Guid groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var group = await _dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken);
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(group);
|
||||
@@ -67,10 +67,10 @@ public sealed class DictionaryController : BaseApiController
|
||||
/// <summary>
|
||||
/// 删除字典分组。
|
||||
/// </summary>
|
||||
[HttpDelete("{groupId:guid}")]
|
||||
[HttpDelete("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteGroup(Guid groupId, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<object>> DeleteGroup(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken);
|
||||
return ApiResponse.Success();
|
||||
@@ -79,10 +79,10 @@ public sealed class DictionaryController : BaseApiController
|
||||
/// <summary>
|
||||
/// 创建字典项。
|
||||
/// </summary>
|
||||
[HttpPost("{groupId:guid}/items")]
|
||||
[HttpPost("{groupId:long}/items")]
|
||||
[PermissionAuthorize("dictionary:item:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> CreateItem(Guid groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<DictionaryItemDto>> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.GroupId = groupId;
|
||||
var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken);
|
||||
@@ -92,10 +92,10 @@ public sealed class DictionaryController : BaseApiController
|
||||
/// <summary>
|
||||
/// 更新字典项。
|
||||
/// </summary>
|
||||
[HttpPut("items/{itemId:guid}")]
|
||||
[HttpPut("items/{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(Guid itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await _dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(item);
|
||||
@@ -104,10 +104,10 @@ public sealed class DictionaryController : BaseApiController
|
||||
/// <summary>
|
||||
/// 删除字典项。
|
||||
/// </summary>
|
||||
[HttpDelete("items/{itemId:guid}")]
|
||||
[HttpDelete("items/{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteItem(Guid itemId, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<object>> DeleteItem(long itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken);
|
||||
return ApiResponse.Success();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 商户管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/merchants")]
|
||||
public sealed class MerchantsController : BaseApiController
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化控制器。
|
||||
/// </summary>
|
||||
public MerchantsController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建商户。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("merchant:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantDto>> Create([FromBody] CreateMerchantCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<MerchantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询商户列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantDto>>> List([FromQuery] MerchantStatus? status, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _mediator.Send(new SearchMerchantsQuery { Status = status }, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MerchantDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商户详情。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<MerchantDto>> Detail(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
|
||||
: ApiResponse<MerchantDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using TakeoutSaaS.Application.App.Extensions;
|
||||
using TakeoutSaaS.Application.Identity.Extensions;
|
||||
using TakeoutSaaS.Application.Messaging.Extensions;
|
||||
using TakeoutSaaS.Application.Sms.Extensions;
|
||||
using TakeoutSaaS.Application.Storage.Extensions;
|
||||
using TakeoutSaaS.Infrastructure.App.Extensions;
|
||||
using TakeoutSaaS.Infrastructure.Identity.Extensions;
|
||||
using TakeoutSaaS.Module.Authorization.Extensions;
|
||||
using TakeoutSaaS.Module.Dictionary.Extensions;
|
||||
@@ -40,6 +42,8 @@ builder.Services.AddSharedSwagger(options =>
|
||||
});
|
||||
builder.Services.AddIdentityApplication();
|
||||
builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true);
|
||||
builder.Services.AddAppInfrastructure(builder.Configuration);
|
||||
builder.Services.AddAppApplication();
|
||||
builder.Services.AddJwtAuthentication(builder.Configuration);
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddPermissionAuthorization();
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />
|
||||
|
||||
@@ -149,5 +149,39 @@
|
||||
"WorkerCount": 5,
|
||||
"DashboardEnabled": false,
|
||||
"DashboardPath": "/hangfire"
|
||||
},
|
||||
"App": {
|
||||
"Seed": {
|
||||
"Enabled": true,
|
||||
"DefaultTenant": {
|
||||
"TenantId": 1000000000001,
|
||||
"Code": "demo",
|
||||
"Name": "Demo租户",
|
||||
"ShortName": "Demo",
|
||||
"ContactName": "DemoAdmin",
|
||||
"ContactPhone": "13800000000"
|
||||
},
|
||||
"DictionaryGroups": [
|
||||
{
|
||||
"Code": "order_status",
|
||||
"Name": "订单状态",
|
||||
"Scope": "Business",
|
||||
"Items": [
|
||||
{ "Key": "pending", "Value": "待支付", "SortOrder": 10 },
|
||||
{ "Key": "paid", "Value": "已支付", "SortOrder": 20 },
|
||||
{ "Key": "finished", "Value": "已完成", "SortOrder": 30 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"Code": "store_tags",
|
||||
"Name": "门店标签",
|
||||
"Scope": "Business",
|
||||
"Items": [
|
||||
{ "Key": "hot", "Value": "热门", "SortOrder": 10 },
|
||||
{ "Key": "new", "Value": "新店", "SortOrder": 20 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class MeController : BaseApiController
|
||||
public async Task<ApiResponse<CurrentUserProfile>> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == Guid.Empty)
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 业务应用层服务注册。
|
||||
/// </summary>
|
||||
public static class AppApplicationServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册业务应用层(MediatR 处理器等)。
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合。</param>
|
||||
/// <returns>服务集合。</returns>
|
||||
public static IServiceCollection AddAppApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddMediatR(Assembly.GetExecutingAssembly());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建商户命令。
|
||||
/// </summary>
|
||||
public sealed class CreateMerchantCommand : IRequest<MerchantDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 品牌名称。
|
||||
/// </summary>
|
||||
[Required, MaxLength(128)]
|
||||
public string BrandName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 品牌简称。
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? BrandAlias { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品牌 Logo。
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
public string? LogoUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品类。
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? Category { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
[Required, MaxLength(32)]
|
||||
public string ContactPhone { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 联系邮箱。
|
||||
/// </summary>
|
||||
[MaxLength(128)]
|
||||
public string? ContactEmail { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态,可用于直接设为审核通过等场景。
|
||||
/// </summary>
|
||||
public MerchantStatus Status { get; init; } = MerchantStatus.Pending;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 商户 DTO。
|
||||
/// </summary>
|
||||
public sealed class MerchantDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品牌名称。
|
||||
/// </summary>
|
||||
public string BrandName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 品牌简称。
|
||||
/// </summary>
|
||||
public string? BrandAlias { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品牌 Logo。
|
||||
/// </summary>
|
||||
public string? LogoUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品类。
|
||||
/// </summary>
|
||||
public string? Category { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string ContactPhone { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 联系邮箱。
|
||||
/// </summary>
|
||||
public string? ContactEmail { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 入驻状态。
|
||||
/// </summary>
|
||||
public MerchantStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 入驻时间。
|
||||
/// </summary>
|
||||
public DateTime? JoinedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建商户命令处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRepository, ILogger<CreateMerchantCommandHandler> logger)
|
||||
: IRequestHandler<CreateMerchantCommand, MerchantDto>
|
||||
{
|
||||
private readonly IMerchantRepository _merchantRepository = merchantRepository;
|
||||
private readonly ILogger<CreateMerchantCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MerchantDto> Handle(CreateMerchantCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var merchant = new Merchant
|
||||
{
|
||||
BrandName = request.BrandName.Trim(),
|
||||
BrandAlias = request.BrandAlias?.Trim(),
|
||||
LogoUrl = request.LogoUrl?.Trim(),
|
||||
Category = request.Category?.Trim(),
|
||||
ContactPhone = request.ContactPhone.Trim(),
|
||||
ContactEmail = request.ContactEmail?.Trim(),
|
||||
Status = request.Status,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _merchantRepository.AddMerchantAsync(merchant, cancellationToken);
|
||||
await _merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("创建商户 {MerchantId} - {BrandName}", merchant.Id, merchant.BrandName);
|
||||
return MapToDto(merchant);
|
||||
}
|
||||
|
||||
private static MerchantDto MapToDto(Merchant merchant) => new()
|
||||
{
|
||||
Id = merchant.Id,
|
||||
TenantId = merchant.TenantId,
|
||||
BrandName = merchant.BrandName,
|
||||
BrandAlias = merchant.BrandAlias,
|
||||
LogoUrl = merchant.LogoUrl,
|
||||
Category = merchant.Category,
|
||||
ContactPhone = merchant.ContactPhone,
|
||||
ContactEmail = merchant.ContactEmail,
|
||||
Status = merchant.Status,
|
||||
JoinedAt = merchant.JoinedAt
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 获取商户详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepository, ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetMerchantByIdQuery, MerchantDto?>
|
||||
{
|
||||
private readonly IMerchantRepository _merchantRepository = merchantRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MerchantDto?> Handle(GetMerchantByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
|
||||
if (merchant == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MerchantDto
|
||||
{
|
||||
Id = merchant.Id,
|
||||
TenantId = merchant.TenantId,
|
||||
BrandName = merchant.BrandName,
|
||||
BrandAlias = merchant.BrandAlias,
|
||||
LogoUrl = merchant.LogoUrl,
|
||||
Category = merchant.Category,
|
||||
ContactPhone = merchant.ContactPhone,
|
||||
ContactEmail = merchant.ContactEmail,
|
||||
Status = merchant.Status,
|
||||
JoinedAt = merchant.JoinedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 商户列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchMerchantsQueryHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SearchMerchantsQuery, IReadOnlyList<MerchantDto>>
|
||||
{
|
||||
private readonly IMerchantRepository _merchantRepository = merchantRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MerchantDto>> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken);
|
||||
|
||||
return merchants
|
||||
.Select(merchant => new MerchantDto
|
||||
{
|
||||
Id = merchant.Id,
|
||||
TenantId = merchant.TenantId,
|
||||
BrandName = merchant.BrandName,
|
||||
BrandAlias = merchant.BrandAlias,
|
||||
LogoUrl = merchant.LogoUrl,
|
||||
Category = merchant.Category,
|
||||
ContactPhone = merchant.ContactPhone,
|
||||
ContactEmail = merchant.ContactEmail,
|
||||
Status = merchant.Status,
|
||||
JoinedAt = merchant.JoinedAt
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 按 ID 获取商户。
|
||||
/// </summary>
|
||||
public sealed class GetMerchantByIdQuery : IRequest<MerchantDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
/// </summary>
|
||||
public long MerchantId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 搜索商户列表。
|
||||
/// </summary>
|
||||
public sealed class SearchMerchantsQuery : IRequest<IReadOnlyList<MerchantDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 按状态过滤。
|
||||
/// </summary>
|
||||
public MerchantStatus? Status { get; init; }
|
||||
}
|
||||
@@ -10,17 +10,17 @@ public interface IDictionaryAppService
|
||||
{
|
||||
Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<DictionaryGroupDto> UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default);
|
||||
Task<DictionaryGroupDto> UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default);
|
||||
Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<DictionaryItemDto> CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<DictionaryItemDto> UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default);
|
||||
Task<DictionaryItemDto> UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default);
|
||||
Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ public interface IDictionaryCache
|
||||
/// <summary>
|
||||
/// 获取缓存。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(Guid tenantId, string code, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 写入缓存。
|
||||
/// </summary>
|
||||
Task SetAsync(Guid tenantId, string code, IReadOnlyList<DictionaryItemDto> items, CancellationToken cancellationToken = default);
|
||||
Task SetAsync(long tenantId, string code, IReadOnlyList<DictionaryItemDto> items, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 移除缓存。
|
||||
/// </summary>
|
||||
Task RemoveAsync(Guid tenantId, string code, CancellationToken cancellationToken = default);
|
||||
Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
@@ -11,7 +13,8 @@ public sealed class CreateDictionaryItemRequest
|
||||
/// 所属分组 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid GroupId { get; set; }
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典项键。
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.Dictionary.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,7 +10,8 @@ namespace TakeoutSaaS.Application.Dictionary.Models;
|
||||
/// </summary>
|
||||
public sealed class DictionaryGroupDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.Dictionary.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,9 +8,11 @@ namespace TakeoutSaaS.Application.Dictionary.Models;
|
||||
/// </summary>
|
||||
public sealed class DictionaryItemDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
public Guid GroupId { get; init; }
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long GroupId { get; init; }
|
||||
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
var group = new DictionaryGroup
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = 0,
|
||||
TenantId = targetTenant,
|
||||
Code = normalizedCode,
|
||||
Name = request.Name.Trim(),
|
||||
@@ -62,7 +62,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return MapGroup(group, includeItems: false);
|
||||
}
|
||||
|
||||
public async Task<DictionaryGroupDto> UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
|
||||
public async Task<DictionaryGroupDto> UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var group = await RequireGroupAsync(groupId, cancellationToken);
|
||||
EnsureScopePermission(group.Scope);
|
||||
@@ -77,7 +77,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return MapGroup(group, includeItems: false);
|
||||
}
|
||||
|
||||
public async Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var group = await RequireGroupAsync(groupId, cancellationToken);
|
||||
EnsureScopePermission(group.Scope);
|
||||
@@ -120,7 +120,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
var item = new DictionaryItem
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = 0,
|
||||
TenantId = group.TenantId,
|
||||
GroupId = group.Id,
|
||||
Key = request.Key.Trim(),
|
||||
@@ -138,7 +138,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return MapItem(item);
|
||||
}
|
||||
|
||||
public async Task<DictionaryItemDto> UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default)
|
||||
public async Task<DictionaryItemDto> UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await RequireItemAsync(itemId, cancellationToken);
|
||||
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
|
||||
@@ -156,7 +156,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return MapItem(item);
|
||||
}
|
||||
|
||||
public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await RequireItemAsync(itemId, cancellationToken);
|
||||
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
|
||||
@@ -186,8 +186,8 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
foreach (var code in normalizedCodes)
|
||||
{
|
||||
var systemItems = await GetOrLoadCacheAsync(Guid.Empty, code, cancellationToken);
|
||||
if (tenantId == Guid.Empty)
|
||||
var systemItems = await GetOrLoadCacheAsync(0, code, cancellationToken);
|
||||
if (tenantId == 0)
|
||||
{
|
||||
result[code] = systemItems;
|
||||
continue;
|
||||
@@ -200,7 +200,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<DictionaryGroup> RequireGroupAsync(Guid groupId, CancellationToken cancellationToken)
|
||||
private async Task<DictionaryGroup> RequireGroupAsync(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken);
|
||||
if (group == null)
|
||||
@@ -211,7 +211,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return group;
|
||||
}
|
||||
|
||||
private async Task<DictionaryItem> RequireItemAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
private async Task<DictionaryItem> RequireItemAsync(long itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await _repository.FindItemByIdAsync(itemId, cancellationToken);
|
||||
if (item == null)
|
||||
@@ -222,16 +222,16 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
return item;
|
||||
}
|
||||
|
||||
private Guid ResolveTargetTenant(DictionaryScope scope)
|
||||
private long ResolveTargetTenant(DictionaryScope scope)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
if (scope == DictionaryScope.System)
|
||||
{
|
||||
EnsurePlatformTenant(tenantId);
|
||||
return Guid.Empty;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (tenantId == Guid.Empty)
|
||||
if (tenantId == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户");
|
||||
}
|
||||
@@ -241,28 +241,28 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant();
|
||||
|
||||
private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, Guid tenantId)
|
||||
private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, long tenantId)
|
||||
{
|
||||
if (requestedScope.HasValue)
|
||||
{
|
||||
return requestedScope.Value;
|
||||
}
|
||||
|
||||
return tenantId == Guid.Empty ? DictionaryScope.System : DictionaryScope.Business;
|
||||
return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business;
|
||||
}
|
||||
|
||||
private void EnsureScopePermission(DictionaryScope scope)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
if (scope == DictionaryScope.System && tenantId != Guid.Empty)
|
||||
if (scope == DictionaryScope.System && tenantId != 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePlatformTenant(Guid tenantId)
|
||||
private void EnsurePlatformTenant(long tenantId)
|
||||
{
|
||||
if (tenantId != Guid.Empty)
|
||||
if (tenantId != 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
}
|
||||
@@ -279,7 +279,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
// 系统参数更新需要逐租户重新合并,由调用方在下一次请求时重新加载
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(Guid tenantId, string code, CancellationToken cancellationToken)
|
||||
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken)
|
||||
{
|
||||
var cached = await _cache.GetAsync(tenantId, code, cancellationToken);
|
||||
if (cached != null)
|
||||
|
||||
@@ -12,5 +12,5 @@ public interface IAdminAuthService
|
||||
{
|
||||
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
|
||||
Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ public interface IMiniAuthService
|
||||
{
|
||||
Task<TokenResponse> LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default);
|
||||
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
|
||||
Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Abstractions;
|
||||
/// </summary>
|
||||
public interface IRefreshTokenStore
|
||||
{
|
||||
Task<RefreshTokenDescriptor> IssueAsync(Guid userId, DateTime expiresAt, CancellationToken cancellationToken = default);
|
||||
Task<RefreshTokenDescriptor> IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default);
|
||||
Task<RefreshTokenDescriptor?> GetAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||
Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public sealed class CurrentUserProfile
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid UserId { get; init; }
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录账号。
|
||||
@@ -23,12 +23,12 @@ public sealed class CurrentUserProfile
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public Guid TenantId { get; init; }
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属商户 ID(平台管理员为空)。
|
||||
/// </summary>
|
||||
public Guid? MerchantId { get; init; }
|
||||
public long? MerchantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合。
|
||||
|
||||
@@ -9,6 +9,6 @@ namespace TakeoutSaaS.Application.Identity.Models;
|
||||
/// <param name="Revoked">是否已撤销</param>
|
||||
public sealed record RefreshTokenDescriptor(
|
||||
string Token,
|
||||
Guid UserId,
|
||||
long UserId,
|
||||
DateTime ExpiresAt,
|
||||
bool Revoked);
|
||||
|
||||
@@ -77,7 +77,7 @@ public sealed class AdminAuthService(
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>用户档案</returns>
|
||||
/// <exception cref="BusinessException">用户不存在时抛出</exception>
|
||||
public async Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class MiniAuthService(
|
||||
|
||||
// 3. 获取当前租户 ID(多租户支持)
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (tenantId == Guid.Empty)
|
||||
if (tenantId == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public sealed class MiniAuthService(
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>用户档案</returns>
|
||||
/// <exception cref="BusinessException">用户不存在时抛出</exception>
|
||||
public async Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
@@ -113,7 +113,7 @@ public sealed class MiniAuthService(
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>用户实体和是否为新用户的元组</returns>
|
||||
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken)
|
||||
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 检查用户是否已存在
|
||||
var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken);
|
||||
|
||||
@@ -8,7 +8,7 @@ public sealed class OrderCreatedEvent
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; init; }
|
||||
public long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单编号。
|
||||
@@ -23,7 +23,7 @@ public sealed class OrderCreatedEvent
|
||||
/// <summary>
|
||||
/// 所属租户。
|
||||
/// </summary>
|
||||
public Guid TenantId { get; init; }
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
|
||||
@@ -8,7 +8,7 @@ public sealed class PaymentSucceededEvent
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; init; }
|
||||
public long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付流水号。
|
||||
@@ -23,7 +23,7 @@ public sealed class PaymentSucceededEvent
|
||||
/// <summary>
|
||||
/// 所属租户。
|
||||
/// </summary>
|
||||
public Guid TenantId { get; init; }
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付时间(UTC)。
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class VerificationCodeService(
|
||||
var codeOptions = codeOptionsMonitor.CurrentValue;
|
||||
var templateCode = ResolveTemplate(request.Scene, smsOptions);
|
||||
var phone = NormalizePhoneNumber(request.PhoneNumber);
|
||||
var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N");
|
||||
var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString();
|
||||
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
|
||||
var cooldownKey = $"{cacheKey}:cooldown";
|
||||
|
||||
@@ -90,7 +90,7 @@ public sealed class VerificationCodeService(
|
||||
|
||||
var codeOptions = codeOptionsMonitor.CurrentValue;
|
||||
var phone = NormalizePhoneNumber(request.PhoneNumber);
|
||||
var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N");
|
||||
var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString();
|
||||
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
|
||||
|
||||
var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -212,7 +212,7 @@ public sealed class FileStorageService(
|
||||
private string BuildObjectKey(UploadFileType type, string extension)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var tenantSegment = tenantId == Guid.Empty ? "platform" : tenantId.ToString("N");
|
||||
var tenantSegment = tenantId == 0 ? "platform" : tenantId.ToString();
|
||||
var folder = type.ToFolderName();
|
||||
var now = DateTime.UtcNow;
|
||||
var fileName = $"{Guid.NewGuid():N}{extension}";
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj" />
|
||||
|
||||
@@ -23,15 +23,15 @@ public abstract class AuditableEntityBase : EntityBase, IAuditableEntity
|
||||
/// <summary>
|
||||
/// 创建人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
public Guid? CreatedBy { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除人用户标识(软删除),未删除时为 null。
|
||||
/// </summary>
|
||||
public Guid? DeletedBy { get; set; }
|
||||
public long? DeletedBy { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ public abstract class EntityBase
|
||||
/// <summary>
|
||||
/// 实体唯一标识。
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
@@ -23,15 +23,15 @@ public interface IAuditableEntity : ISoftDeleteEntity
|
||||
/// <summary>
|
||||
/// 创建人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
Guid? CreatedBy { get; set; }
|
||||
long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人用户标识,匿名或系统操作时为 null。
|
||||
/// </summary>
|
||||
Guid? UpdatedBy { get; set; }
|
||||
long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除人用户标识(软删除),未删除时为 null。
|
||||
/// </summary>
|
||||
Guid? DeletedBy { get; set; }
|
||||
long? DeletedBy { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ public interface IMultiTenantEntity
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
Guid TenantId { get; set; }
|
||||
long TenantId { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ public abstract class MultiTenantEntityBase : AuditableEntityBase, IMultiTenantE
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public Guid TenantId { get; set; }
|
||||
public long TenantId { get; set; }
|
||||
}
|
||||
|
||||
13
src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs
Normal file
13
src/Core/TakeoutSaaS.Shared.Abstractions/Ids/IIdGenerator.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
/// <summary>
|
||||
/// 雪花 ID 生成器接口。
|
||||
/// </summary>
|
||||
public interface IIdGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 生成下一个唯一长整型 ID。
|
||||
/// </summary>
|
||||
/// <returns>雪花 ID。</returns>
|
||||
long NextId();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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; }
|
||||
}
|
||||
@@ -99,6 +99,65 @@ public sealed record ApiResponse<T>
|
||||
return TraceContext.TraceId;
|
||||
}
|
||||
|
||||
return Activity.Current?.Id ?? Guid.NewGuid().ToString("N");
|
||||
if (!string.IsNullOrWhiteSpace(TraceContext.TraceId))
|
||||
{
|
||||
return TraceContext.TraceId;
|
||||
}
|
||||
|
||||
if (Activity.Current?.Id is { } id && !string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
return IdFallbackGenerator.Instance.NextId().ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class IdFallbackGenerator
|
||||
{
|
||||
private static readonly Lazy<IdFallbackGenerator> Lazy = new(() => new IdFallbackGenerator());
|
||||
public static IdFallbackGenerator Instance => Lazy.Value;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private long _lastTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
private long _sequence;
|
||||
|
||||
private IdFallbackGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
public long NextId()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (timestamp == _lastTimestamp)
|
||||
{
|
||||
_sequence = (_sequence + 1) & 4095;
|
||||
if (_sequence == 0)
|
||||
{
|
||||
timestamp = WaitNextMillis(_lastTimestamp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_sequence = 0;
|
||||
}
|
||||
|
||||
_lastTimestamp = timestamp;
|
||||
return ((timestamp - 1577836800000L) << 22) | _sequence;
|
||||
}
|
||||
}
|
||||
|
||||
private static long WaitNextMillis(long lastTimestamp)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
while (timestamp <= lastTimestamp)
|
||||
{
|
||||
Thread.SpinWait(100);
|
||||
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public interface ICurrentUserAccessor
|
||||
/// <summary>
|
||||
/// 当前用户 ID,未登录时为 Guid.Empty。
|
||||
/// </summary>
|
||||
Guid UserId { get; }
|
||||
long UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已登录。
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,5 @@ public interface ITenantProvider
|
||||
/// <summary>
|
||||
/// 获取当前租户 ID,未解析时返回 Guid.Empty。
|
||||
/// </summary>
|
||||
Guid GetCurrentTenantId();
|
||||
long GetCurrentTenantId();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public sealed class TenantContext
|
||||
/// <summary>
|
||||
/// 未解析到租户时的默认上下文。
|
||||
/// </summary>
|
||||
public static TenantContext Empty { get; } = new(Guid.Empty, null, "unresolved");
|
||||
public static TenantContext Empty { get; } = new(0, null, "unresolved");
|
||||
|
||||
/// <summary>
|
||||
/// 初始化租户上下文。
|
||||
@@ -16,7 +16,7 @@ public sealed class TenantContext
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="tenantCode">租户编码(可选)</param>
|
||||
/// <param name="source">解析来源</param>
|
||||
public TenantContext(Guid tenantId, string? tenantCode, string source)
|
||||
public TenantContext(long tenantId, string? tenantCode, string source)
|
||||
{
|
||||
TenantId = tenantId;
|
||||
TenantCode = tenantCode;
|
||||
@@ -26,7 +26,7 @@ public sealed class TenantContext
|
||||
/// <summary>
|
||||
/// 当前租户 ID,未解析时为 Guid.Empty。
|
||||
/// </summary>
|
||||
public Guid TenantId { get; }
|
||||
public long TenantId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前租户编码(例如子域名或业务编码),可为空。
|
||||
@@ -41,5 +41,5 @@ public sealed class TenantContext
|
||||
/// <summary>
|
||||
/// 是否已成功解析到租户。
|
||||
/// </summary>
|
||||
public bool IsResolved => TenantId != Guid.Empty;
|
||||
public bool IsResolved => TenantId != 0;
|
||||
}
|
||||
|
||||
111
src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs
Normal file
111
src/Core/TakeoutSaaS.Shared.Kernel/Ids/SnowflakeIdGenerator.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Kernel.Ids;
|
||||
|
||||
/// <summary>
|
||||
/// 基于雪花算法的长整型 ID 生成器。
|
||||
/// </summary>
|
||||
public sealed class SnowflakeIdGenerator : IIdGenerator
|
||||
{
|
||||
private const long Twepoch = 1577836800000L; // 2020-01-01 UTC
|
||||
private const int WorkerIdBits = 5;
|
||||
private const int DatacenterIdBits = 5;
|
||||
private const int SequenceBits = 12;
|
||||
|
||||
private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits);
|
||||
private const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits);
|
||||
|
||||
private const int WorkerIdShift = SequenceBits;
|
||||
private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
|
||||
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
|
||||
private const long SequenceMask = -1L ^ (-1L << SequenceBits);
|
||||
|
||||
private readonly long _workerId;
|
||||
private readonly long _datacenterId;
|
||||
private long _lastTimestamp = -1L;
|
||||
private long _sequence;
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化生成器。
|
||||
/// </summary>
|
||||
/// <param name="workerId">工作节点 ID。</param>
|
||||
/// <param name="datacenterId">机房 ID。</param>
|
||||
public SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0)
|
||||
{
|
||||
_workerId = Normalize(workerId, MaxWorkerId, nameof(workerId));
|
||||
_datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId));
|
||||
_sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
|
||||
@@ -17,11 +18,13 @@ public sealed class CorrelationIdMiddleware
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<CorrelationIdMiddleware> _logger;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
|
||||
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
|
||||
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_idGenerator = idGenerator;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
@@ -52,7 +55,7 @@ public sealed class CorrelationIdMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveTraceId(HttpContext context)
|
||||
private string ResolveTraceId(HttpContext context)
|
||||
{
|
||||
if (TryGetHeader(context, TraceHeader, out var traceId))
|
||||
{
|
||||
@@ -64,7 +67,7 @@ public sealed class CorrelationIdMiddleware
|
||||
return requestId;
|
||||
}
|
||||
|
||||
return Guid.NewGuid().ToString("N");
|
||||
return _idGenerator.NextId().ToString();
|
||||
}
|
||||
|
||||
private static bool TryGetHeader(HttpContext context, string headerName, out string value)
|
||||
|
||||
@@ -9,20 +9,20 @@ namespace TakeoutSaaS.Shared.Web.Security;
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户 Id(不存在时返回 Guid.Empty)
|
||||
/// 获取当前用户 Id(不存在时返回 0)。
|
||||
/// </summary>
|
||||
public static Guid GetUserId(this ClaimsPrincipal? principal)
|
||||
public static long GetUserId(this ClaimsPrincipal? principal)
|
||||
{
|
||||
if (principal == null)
|
||||
{
|
||||
return Guid.Empty;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? principal.FindFirstValue("sub");
|
||||
|
||||
return Guid.TryParse(identifier, out var userId)
|
||||
return long.TryParse(identifier, out var userId)
|
||||
? userId
|
||||
: Guid.Empty;
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,23 +20,23 @@ public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid UserId
|
||||
public long UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var principal = _httpContextAccessor.HttpContext?.User;
|
||||
if (principal == null || !principal.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return Guid.Empty;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? principal.FindFirstValue("sub");
|
||||
|
||||
return Guid.TryParse(identifier, out var id) ? id : Guid.Empty;
|
||||
return long.TryParse(identifier, out var id) ? id : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAuthenticated => UserId != Guid.Empty;
|
||||
public bool IsAuthenticated => UserId != 0;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class MetricAlertRule : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 关联指标。
|
||||
/// </summary>
|
||||
public Guid MetricDefinitionId { get; set; }
|
||||
public long MetricDefinitionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发条件 JSON。
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class MetricSnapshot : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 指标定义 ID。
|
||||
/// </summary>
|
||||
public Guid MetricDefinitionId { get; set; }
|
||||
public long MetricDefinitionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 维度键(JSON)。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class Coupon : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 模板标识。
|
||||
/// </summary>
|
||||
public Guid CouponTemplateId { get; set; }
|
||||
public long CouponTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 券码或序列号。
|
||||
@@ -21,12 +21,12 @@ public sealed class Coupon : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 归属用户。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单 ID(已使用时记录)。
|
||||
/// </summary>
|
||||
public Guid? OrderId { get; set; }
|
||||
public long? OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class ChatMessage : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 会话标识。
|
||||
/// </summary>
|
||||
public Guid ChatSessionId { get; set; }
|
||||
public long ChatSessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送方类型。
|
||||
@@ -21,7 +21,7 @@ public sealed class ChatMessage : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 发送方用户 ID。
|
||||
/// </summary>
|
||||
public Guid? SenderUserId { get; set; }
|
||||
public long? SenderUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息内容。
|
||||
|
||||
@@ -16,17 +16,17 @@ public sealed class ChatSession : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 顾客用户 ID。
|
||||
/// </summary>
|
||||
public Guid CustomerUserId { get; set; }
|
||||
public long CustomerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前客服员工 ID。
|
||||
/// </summary>
|
||||
public Guid? AgentUserId { get; set; }
|
||||
public long? AgentUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属门店(可空为平台)。
|
||||
/// </summary>
|
||||
public Guid? StoreId { get; set; }
|
||||
public long? StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话状态。
|
||||
|
||||
@@ -16,12 +16,12 @@ public sealed class SupportTicket : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 客户用户 ID。
|
||||
/// </summary>
|
||||
public Guid CustomerUserId { get; set; }
|
||||
public long CustomerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单(如有)。
|
||||
/// </summary>
|
||||
public Guid? OrderId { get; set; }
|
||||
public long? OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单主题。
|
||||
@@ -46,7 +46,7 @@ public sealed class SupportTicket : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 指派的客服。
|
||||
/// </summary>
|
||||
public Guid? AssignedAgentId { get; set; }
|
||||
public long? AssignedAgentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关闭时间。
|
||||
|
||||
@@ -10,12 +10,12 @@ public sealed class TicketComment : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 工单标识。
|
||||
/// </summary>
|
||||
public Guid SupportTicketId { get; set; }
|
||||
public long SupportTicketId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评论人 ID。
|
||||
/// </summary>
|
||||
public Guid? AuthorUserId { get; set; }
|
||||
public long? AuthorUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评论内容。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class DeliveryEvent : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 配送单标识。
|
||||
/// </summary>
|
||||
public Guid DeliveryOrderId { get; set; }
|
||||
public long DeliveryOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 事件类型。
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace TakeoutSaaS.Domain.Deliveries.Entities;
|
||||
/// </summary>
|
||||
public sealed class DeliveryOrder : MultiTenantEntityBase
|
||||
{
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配送服务商。
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Deliveries.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Deliveries.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 配送聚合仓储契约。
|
||||
/// </summary>
|
||||
public interface IDeliveryRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 依据标识获取配送单。
|
||||
/// </summary>
|
||||
Task<DeliveryOrder?> FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 依据订单标识获取配送单。
|
||||
/// </summary>
|
||||
Task<DeliveryOrder?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取配送事件轨迹。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<DeliveryEvent>> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增配送单。
|
||||
/// </summary>
|
||||
Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增配送事件。
|
||||
/// </summary>
|
||||
Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public sealed class DictionaryItem : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 关联分组 ID。
|
||||
/// </summary>
|
||||
public Guid GroupId { get; set; }
|
||||
public long GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典项键。
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface IDictionaryRepository
|
||||
/// <summary>
|
||||
/// 依据 ID 获取分组。
|
||||
/// </summary>
|
||||
Task<DictionaryGroup?> FindGroupByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<DictionaryGroup?> FindGroupByIdAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 依据编码获取分组。
|
||||
@@ -39,17 +39,17 @@ public interface IDictionaryRepository
|
||||
/// <summary>
|
||||
/// 依据 ID 获取字典项。
|
||||
/// </summary>
|
||||
Task<DictionaryItem?> FindItemByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<DictionaryItem?> FindItemByIdAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某分组下的所有字典项。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按分组编码集合获取字典项(可包含系统参数)。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, Guid tenantId, bool includeSystem, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增字典项。
|
||||
|
||||
@@ -11,17 +11,17 @@ public sealed class AffiliateOrder : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 推广人标识。
|
||||
/// </summary>
|
||||
public Guid AffiliatePartnerId { get; set; }
|
||||
public long AffiliatePartnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联订单。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid BuyerUserId { get; set; }
|
||||
public long BuyerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单金额。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class AffiliatePartner : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 用户 ID(如绑定平台账号)。
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 昵称或渠道名称。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class AffiliatePayout : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 合作伙伴标识。
|
||||
/// </summary>
|
||||
public Guid AffiliatePartnerId { get; set; }
|
||||
public long AffiliatePartnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结算周期描述。
|
||||
|
||||
@@ -10,12 +10,12 @@ public sealed class CheckInRecord : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 活动标识。
|
||||
/// </summary>
|
||||
public Guid CheckInCampaignId { get; set; }
|
||||
public long CheckInCampaignId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户标识。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到日期(本地)。
|
||||
|
||||
@@ -10,12 +10,12 @@ public sealed class CommunityComment : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 动态标识。
|
||||
/// </summary>
|
||||
public Guid PostId { get; set; }
|
||||
public long PostId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评论人。
|
||||
/// </summary>
|
||||
public Guid AuthorUserId { get; set; }
|
||||
public long AuthorUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评论内容。
|
||||
@@ -25,7 +25,7 @@ public sealed class CommunityComment : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 父级评论 ID。
|
||||
/// </summary>
|
||||
public Guid? ParentId { get; set; }
|
||||
public long? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class CommunityPost : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 作者用户 ID。
|
||||
/// </summary>
|
||||
public Guid AuthorUserId { get; set; }
|
||||
public long AuthorUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
|
||||
@@ -11,12 +11,12 @@ public sealed class CommunityReaction : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 动态 ID。
|
||||
/// </summary>
|
||||
public Guid PostId { get; set; }
|
||||
public long PostId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 反应类型。
|
||||
|
||||
@@ -11,12 +11,12 @@ public sealed class GroupOrder : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联商品或套餐。
|
||||
/// </summary>
|
||||
public Guid ProductId { get; set; }
|
||||
public long ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 拼单编号。
|
||||
@@ -26,7 +26,7 @@ public sealed class GroupOrder : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 团长用户 ID。
|
||||
/// </summary>
|
||||
public Guid LeaderUserId { get; set; }
|
||||
public long LeaderUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成团需要的人数。
|
||||
|
||||
@@ -11,17 +11,17 @@ public sealed class GroupParticipant : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 拼单活动标识。
|
||||
/// </summary>
|
||||
public Guid GroupOrderId { get; set; }
|
||||
public long GroupOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对应订单标识。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户标识。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参与状态。
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class IdentityUser : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 所属商户(平台管理员为空)。
|
||||
/// </summary>
|
||||
public Guid? MerchantId { get; set; }
|
||||
public long? MerchantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合。
|
||||
|
||||
@@ -18,5 +18,5 @@ public interface IIdentityUserRepository
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户。
|
||||
/// </summary>
|
||||
Task<IdentityUser?> FindByIdAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public interface IMiniUserRepository
|
||||
/// <param name="id">用户 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>小程序用户,如果不存在则返回 null</returns>
|
||||
Task<MiniUser?> FindByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 创建或更新小程序用户(如果 OpenId 已存在则更新,否则创建)。
|
||||
@@ -33,5 +33,5 @@ public interface IMiniUserRepository
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>创建或更新后的小程序用户</returns>
|
||||
Task<MiniUser> CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default);
|
||||
Task<MiniUser> CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class InventoryAdjustment : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 对应的库存记录标识。
|
||||
/// </summary>
|
||||
public Guid InventoryItemId { get; set; }
|
||||
public long InventoryItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 调整类型。
|
||||
@@ -31,7 +31,7 @@ public sealed class InventoryAdjustment : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 操作人标识。
|
||||
/// </summary>
|
||||
public Guid? OperatorId { get; set; }
|
||||
public long? OperatorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生时间。
|
||||
|
||||
@@ -10,12 +10,12 @@ public sealed class InventoryBatch : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 标识。
|
||||
/// </summary>
|
||||
public Guid ProductSkuId { get; set; }
|
||||
public long ProductSkuId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 批次编号。
|
||||
|
||||
@@ -10,12 +10,12 @@ public sealed class InventoryItem : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 标识。
|
||||
/// </summary>
|
||||
public Guid ProductSkuId { get; set; }
|
||||
public long ProductSkuId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 批次编号,可为空表示混批。
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class MemberGrowthLog : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 会员标识。
|
||||
/// </summary>
|
||||
public Guid MemberId { get; set; }
|
||||
public long MemberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变动数量。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class MemberPointLedger : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 会员标识。
|
||||
/// </summary>
|
||||
public Guid MemberId { get; set; }
|
||||
public long MemberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变动数量,可为负值。
|
||||
@@ -31,7 +31,7 @@ public sealed class MemberPointLedger : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 来源 ID(订单、活动等)。
|
||||
/// </summary>
|
||||
public Guid? SourceId { get; set; }
|
||||
public long? SourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生时间。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class MemberProfile : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 用户标识。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
@@ -31,7 +31,7 @@ public sealed class MemberProfile : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 当前会员等级 ID。
|
||||
/// </summary>
|
||||
public Guid? MemberTierId { get; set; }
|
||||
public long? MemberTierId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员状态。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class MerchantContract : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 所属商户标识。
|
||||
/// </summary>
|
||||
public Guid MerchantId { get; set; }
|
||||
public long MerchantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同编号。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class MerchantDocument : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 所属商户标识。
|
||||
/// </summary>
|
||||
public Guid MerchantId { get; set; }
|
||||
public long MerchantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证照类型。
|
||||
|
||||
@@ -11,12 +11,12 @@ public sealed class MerchantStaff : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 所属商户标识。
|
||||
/// </summary>
|
||||
public Guid MerchantId { get; set; }
|
||||
public long MerchantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选的关联门店 ID。
|
||||
/// </summary>
|
||||
public Guid? StoreId { get; set; }
|
||||
public long? StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工姓名。
|
||||
@@ -36,7 +36,7 @@ public sealed class MerchantStaff : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 登录账号 ID(指向统一身份体系)。
|
||||
/// </summary>
|
||||
public Guid? IdentityUserId { get; set; }
|
||||
public long? IdentityUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工角色类型。
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 商户聚合仓储契约,提供基础 CRUD 与查询能力。
|
||||
/// </summary>
|
||||
public interface IMerchantRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 依据标识获取商户。
|
||||
/// </summary>
|
||||
Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按状态筛选商户列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定商户的员工列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MerchantStaff>> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定商户的合同列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MerchantContract>> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定商户的资质文件列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MerchantDocument>> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增商户主体。
|
||||
/// </summary>
|
||||
Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增商户员工。
|
||||
/// </summary>
|
||||
Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增商户合同。
|
||||
/// </summary>
|
||||
Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增商户资质文件。
|
||||
/// </summary>
|
||||
Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public sealed class MapLocation : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 关联门店 ID,可空表示独立 POI。
|
||||
/// </summary>
|
||||
public Guid? StoreId { get; set; }
|
||||
public long? StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
|
||||
@@ -11,12 +11,12 @@ public sealed class NavigationRequest : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源通道(小程序、H5 等)。
|
||||
|
||||
@@ -11,17 +11,17 @@ public sealed class CartItem : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 所属购物车标识。
|
||||
/// </summary>
|
||||
public Guid ShoppingCartId { get; set; }
|
||||
public long ShoppingCartId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品或 SKU 标识。
|
||||
/// </summary>
|
||||
public Guid ProductId { get; set; }
|
||||
public long ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 标识。
|
||||
/// </summary>
|
||||
public Guid? ProductSkuId { get; set; }
|
||||
public long? ProductSkuId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品名称快照。
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class CartItemAddon : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 所属购物车条目。
|
||||
/// </summary>
|
||||
public Guid CartItemId { get; set; }
|
||||
public long CartItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选项名称。
|
||||
@@ -25,5 +25,5 @@ public sealed class CartItemAddon : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 选项 ID(可对应 ProductAddonOption)。
|
||||
/// </summary>
|
||||
public Guid? OptionId { get; set; }
|
||||
public long? OptionId { get; set; }
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ public sealed class CheckoutSession : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 用户标识。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话 Token。
|
||||
|
||||
@@ -11,12 +11,12 @@ public sealed class ShoppingCart : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 用户标识。
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 购物车状态,包含正常/锁定。
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class Order : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 门店。
|
||||
/// </summary>
|
||||
public Guid StoreId { get; set; }
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下单渠道。
|
||||
@@ -62,7 +62,7 @@ public sealed class Order : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 预约 ID。
|
||||
/// </summary>
|
||||
public Guid? ReservationId { get; set; }
|
||||
public long? ReservationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品总额。
|
||||
|
||||
@@ -10,12 +10,12 @@ public sealed class OrderItem : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 订单 ID。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品 ID。
|
||||
/// </summary>
|
||||
public Guid ProductId { get; set; }
|
||||
public long ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品名称。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class OrderStatusHistory : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 订单标识。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变更后的状态。
|
||||
@@ -21,7 +21,7 @@ public sealed class OrderStatusHistory : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 操作人标识(可为空表示系统)。
|
||||
/// </summary>
|
||||
public Guid? OperatorId { get; set; }
|
||||
public long? OperatorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息。
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class RefundRequest : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 关联订单标识。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 退款单号。
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Orders.Entities;
|
||||
using TakeoutSaaS.Domain.Orders.Enums;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Orders.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 订单聚合仓储契约。
|
||||
/// </summary>
|
||||
public interface IOrderRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 依据标识获取订单。
|
||||
/// </summary>
|
||||
Task<Order?> FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 依据订单号获取订单。
|
||||
/// </summary>
|
||||
Task<Order?> FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按状态筛选订单列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取订单明细行。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取订单状态流转记录。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取订单退款申请。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增订单。
|
||||
/// </summary>
|
||||
Task AddOrderAsync(Order order, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增订单明细。
|
||||
/// </summary>
|
||||
Task AddItemsAsync(IEnumerable<OrderItem> items, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增订单状态记录。
|
||||
/// </summary>
|
||||
Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增退款申请。
|
||||
/// </summary>
|
||||
Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public sealed class PaymentRecord : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 关联订单。
|
||||
/// </summary>
|
||||
public Guid OrderId { get; set; }
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user